// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
let use_feature_tags : Array[@common.Tag] = [
@common.Tag::from_chars('l', 'o', 'c', 'l'),
@common.Tag::from_chars('c', 'c', 'm', 'p'),
@common.Tag::from_chars('n', 'u', 'k', 't'),
@common.Tag::from_chars('a', 'k', 'h', 'n'),
@common.Tag::from_chars('r', 'p', 'h', 'f'),
@common.Tag::from_chars('p', 'r', 'e', 'f'),
@common.Tag::from_chars('r', 'k', 'r', 'f'),
@common.Tag::from_chars('a', 'b', 'v', 'f'),
@common.Tag::from_chars('b', 'l', 'w', 'f'),
@common.Tag::from_chars('h', 'a', 'l', 'f'),
@common.Tag::from_chars('p', 's', 't', 'f'),
@common.Tag::from_chars('v', 'a', 't', 'u'),
@common.Tag::from_chars('c', 'j', 'c', 't'),
@common.Tag::from_chars('i', 's', 'o', 'l'),
@common.Tag::from_chars('i', 'n', 'i', 't'),
@common.Tag::from_chars('m', 'e', 'd', 'i'),
@common.Tag::from_chars('f', 'i', 'n', 'a'),
@common.Tag::from_chars('a', 'b', 'v', 's'),
@common.Tag::from_chars('b', 'l', 'w', 's'),
@common.Tag::from_chars('h', 'a', 'l', 'n'),
@common.Tag::from_chars('p', 'r', 'e', 's'),
@common.Tag::from_chars('p', 's', 't', 's'),
]
let use_feature_rphf : Int = 4
let use_feature_pref : Int = 5
let use_feature_isol : Int = 13
let use_feature_init : Int = 14
let use_feature_medi : Int = 15
let use_feature_fina : Int = 16
let use_form_isol : Int = 0
let use_form_init : Int = 1
let use_form_medi : Int = 2
let use_form_fina : Int = 3
fn use_has_arabic_joining(script : @common.Script) -> Bool {
script == @common.script_adlam ||
script == @common.script_arabic ||
script == @common.script_chorasmian ||
script == @common.script_hanifi_rohingya ||
script == @common.script_mandaic ||
script == @common.script_manichaean ||
script == @common.script_mongolian ||
script == @common.script_nko ||
script == @common.script_old_uyghur ||
script == @common.script_phags_pa ||
script == @common.script_psalter_pahlavi ||
script == @common.script_sogdian ||
script == @common.script_syriac
}
///|
let use_cat_r : Int = 18
///|
let use_cat_h : Int = 12
///|
let use_cat_hvm : Int = 53
///|
let use_cat_is : Int = 44
///|
let use_cat_vpre : Int = 22
///|
let use_cat_vmpre : Int = 23
///|
let use_cat_fabv : Int = 24
///|
let use_cat_fblw : Int = 25
///|
let use_cat_fpst : Int = 26
///|
let use_cat_fmabv : Int = 45
///|
let use_cat_fmblw : Int = 46
///|
let use_cat_fmpst : Int = 47
///|
let use_cat_mabv : Int = 27
///|
let use_cat_mblw : Int = 28
///|
let use_cat_mpre : Int = 30
///|
let use_cat_mpst : Int = 29
///|
let use_cat_vabv : Int = 33
///|
let use_cat_vblw : Int = 34
///|
let use_cat_vpst : Int = 35
///|
let use_cat_vmabv : Int = 37
///|
let use_cat_vmblw : Int = 38
///|
let use_cat_vmpst : Int = 39
fn use_category(codepoint : UInt) -> Int {
use_get_category(codepoint).reinterpret_as_int()
}
fn is_halant_use(category : Int) -> Bool {
category == use_cat_h || category == use_cat_hvm || category == use_cat_is
}
fn is_post_base_use(category : Int) -> Bool {
category == use_cat_fabv ||
category == use_cat_fblw ||
category == use_cat_fpst ||
category == use_cat_fmabv ||
category == use_cat_fmblw ||
category == use_cat_fmpst ||
category == use_cat_mabv ||
category == use_cat_mblw ||
category == use_cat_mpst ||
category == use_cat_mpre ||
category == use_cat_vabv ||
category == use_cat_vblw ||
category == use_cat_vpst ||
category == use_cat_vpre ||
category == use_cat_vmabv ||
category == use_cat_vmblw ||
category == use_cat_vmpst ||
category == use_cat_vmpre
}
fn move_index_use(
buffer : Buffer,
categories : Array[Int],
from : Int,
to : Int,
) -> Array[Int] {
if from == to || from < 0 || to < 0 || from >= buffer.infos.length() || to >= buffer.infos.length() {
return categories
}
let info = buffer.infos[from]
let pos = buffer.positions[from]
let cat = categories[from]
let next_categories = categories
if from < to {
let mut i = from
while i < to {
buffer.infos[i] = buffer.infos[i + 1]
buffer.positions[i] = buffer.positions[i + 1]
next_categories[i] = next_categories[i + 1]
i = i + 1
}
buffer.infos[to] = info
buffer.positions[to] = pos
next_categories[to] = cat
} else {
let mut i = from
while i > to {
buffer.infos[i] = buffer.infos[i - 1]
buffer.positions[i] = buffer.positions[i - 1]
next_categories[i] = next_categories[i - 1]
i = i - 1
}
buffer.infos[to] = info
buffer.positions[to] = pos
next_categories[to] = cat
}
next_categories
}
fn reorder_syllable_use(
buffer : Buffer,
categories : Array[Int],
start : Int,
end : Int,
) -> Array[Int] {
if start < 0 || end > buffer.infos.length() || start >= end {
return categories
}
let mut next_categories = categories
if next_categories[start] == use_cat_r && end - start > 1 {
let mut target = end - 1
for i in (start + 1).. Unit {
if categories.length() != buffer.infos.length() || syllables.length() != buffer.infos.length() {
return ()
}
let mut current_categories = categories
let mut i = 0
while i < syllables.length() {
let syllable = syllables[i]
let mut j = i + 1
while j < syllables.length() && syllables[j] == syllable {
j = j + 1
}
let syllable_type = syllable & 0x0f
let allowed = syllable_type == use_syllable_type_virama_terminated_cluster ||
syllable_type == use_syllable_type_sakot_terminated_cluster ||
syllable_type == use_syllable_type_standard_cluster ||
syllable_type == use_syllable_type_symbol_cluster ||
syllable_type == use_syllable_type_broken_cluster
if allowed {
current_categories = reorder_syllable_use(buffer, current_categories, i, j)
}
i = j
}
}
fn set_use_form_masks(
masks : Array[UInt],
isol_bit : UInt,
init_bit : UInt,
medi_bit : UInt,
fina_bit : UInt,
start : Int,
end : Int,
form : Int,
) -> Unit {
if start < 0 || end > masks.length() || start >= end {
return ()
}
let clear_bits = 0xFFFFFFFFU - (isol_bit | init_bit | medi_bit | fina_bit)
for i in start.. (Array[UInt], Bool) {
if lookup_offsets.is_empty() || glyphs.is_empty() {
return (glyphs, false)
}
let gsub = match gsub {
None => return (glyphs, false)
Some(value) => value
}
let clusters = Array::make(glyphs.length(), 0)
match gsub.apply_with_lookups(
glyphs,
clusters,
lookup_offsets[:],
masks=masks,
lookup_mask=1U,
) {
Err(_) => (glyphs, false)
Ok((next, _, changed)) => (next, changed)
}
}
fn update_use_categories_for_reph_pref(
categories : Array[Int],
syllables : Array[Int],
glyphs : Array[UInt],
gsub : @ot_tables.GsubTable?,
rphf_lookups : Array[Int],
pref_lookups : Array[Int],
) -> Unit {
if categories.length() != syllables.length() || categories.length() != glyphs.length() {
return ()
}
let mut i = 0
while i < syllables.length() {
let syllable = syllables[i]
let mut j = i + 1
while j < syllables.length() && syllables[j] == syllable {
j = j + 1
}
let len = j - i
if len > 0 && !rphf_lookups.is_empty() {
let limit = if categories[i] == use_cat_r { 1 } else { if len > 3 { 3 } else { len } }
let masks = Array::make(len, 0U)
for k in 0.. 0 && !pref_lookups.is_empty() {
let sub_glyphs : Array[UInt] = []
for k in i..= i && target < j {
categories[target] = use_cat_vpre
}
}
}
i = j
}
}
fn apply_use_arabic_form_masks(
masks : Array[UInt],
actions : Array[Int],
isol_bit : UInt,
init_bit : UInt,
medi_bit : UInt,
fina_bit : UInt,
) -> Bool {
if actions.is_empty() || actions.length() != masks.length() {
return false
}
for i in 0.. Array[UInt] {
let len = categories.length()
let masks = Array::make(len, 0U)
let rphf_bit = 1U << use_feature_rphf
let isol_bit = 1U << use_feature_isol
let init_bit = 1U << use_feature_init
let medi_bit = 1U << use_feature_medi
let fina_bit = 1U << use_feature_fina
let applied_arabic = apply_use_arabic_form_masks(
masks,
arabic_actions,
isol_bit,
init_bit,
medi_bit,
fina_bit,
)
let mut last_start = -1
let mut last_form = -1
let mut i = 0
while i < syllables.length() {
let syllable = syllables[i]
let mut j = i + 1
while j < syllables.length() && syllables[j] == syllable {
j = j + 1
}
let syllable_type = syllable & 0x0f
let joinable =
syllable_type == use_syllable_type_virama_terminated_cluster ||
syllable_type == use_syllable_type_sakot_terminated_cluster ||
syllable_type == use_syllable_type_standard_cluster ||
syllable_type == use_syllable_type_number_joiner_terminated_cluster ||
syllable_type == use_syllable_type_numeral_cluster ||
syllable_type == use_syllable_type_symbol_cluster ||
syllable_type == use_syllable_type_broken_cluster
if joinable && !applied_arabic {
let mut join = false
if last_form == use_form_fina || last_form == use_form_isol {
join = true
}
if join && last_start >= 0 {
let prev_form = if last_form == use_form_fina {
use_form_medi
} else {
use_form_init
}
set_use_form_masks(
masks,
isol_bit,
init_bit,
medi_bit,
fina_bit,
last_start,
i,
prev_form,
)
}
let current_form = if join { use_form_fina } else { use_form_isol }
set_use_form_masks(
masks,
isol_bit,
init_bit,
medi_bit,
fina_bit,
i,
j,
current_form,
)
last_start = i
last_form = current_form
} else if !applied_arabic {
last_start = -1
last_form = -1
}
let limit = if categories[i] == use_cat_r { 1 } else { j - i }
let rphf_limit = if limit > 3 { 3 } else { limit }
for k in i..<(i + rphf_limit) {
masks[k] = masks[k] | rphf_bit
}
i = j
}
masks
}
///|
pub fn Buffer::shape_use(
self : Buffer,
font : @font.Font,
script_tag? : @common.Tag = self.script.to_tag(),
) -> Result[Unit, ShapeError] {
let gsub = match font.gsub() {
Err(err) => return Err(Font(err))
Ok(value) => value
}
let script_tag = script_tag
if gsub is Some(gsub) {
let dflt_tag = @common.Tag::from_chars('D', 'F', 'L', 'T')
let latn_tag = @common.Tag::from_chars('l', 'a', 't', 'n')
if !buffer_layout_has_script(gsub.layout(), script_tag) &&
(buffer_layout_has_script(gsub.layout(), dflt_tag) ||
buffer_layout_has_script(gsub.layout(), latn_tag)) {
return self.shape_ot_with_options(
font,
fallback_position=false,
zero_width_marks=ZeroWidthMarks::ByGdefEarly,
)
}
}
preprocess_vowel_constraints(self)
let categories : Array[UInt] = []
let categories_i : Array[Int] = []
let marks : Array[Bool] = []
for info in self.infos {
let category = use_get_category(info.codepoint)
categories.push(category)
categories_i.push(category.reinterpret_as_int())
marks.push(@unicode.is_mark(info.codepoint))
}
let syllables = find_syllables_use(categories, marks)
match insert_dotted_circle_for_syllables_with_repha(
font,
self,
syllables,
use_syllable_type_broken_cluster,
categories_i,
use_cat_r,
) {
Err(err) => return Err(err)
Ok(_) => ()
}
let categories_after_u : Array[UInt] = []
let categories_after_i : Array[Int] = []
let marks_after : Array[Bool] = []
for info in self.infos {
let category = use_category(info.codepoint)
categories_after_i.push(category)
categories_after_u.push(category.reinterpret_as_uint())
marks_after.push(@unicode.is_mark(info.codepoint))
}
let syllables_after = find_syllables_use(categories_after_u, marks_after)
let mut rphf_lookups : Array[Int] = []
let mut pref_lookups : Array[Int] = []
if gsub is Some(gsub) {
let lang_tag = language_to_ot_tag(self.language)
rphf_lookups = match gsub.layout().select_lookup_offsets_for_feature(
script_tag,
use_feature_tags[use_feature_rphf],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
pref_lookups = match gsub.layout().select_lookup_offsets_for_feature(
script_tag,
use_feature_tags[use_feature_pref],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
}
if !rphf_lookups.is_empty() || !pref_lookups.is_empty() {
let glyphs_for_reorder : Array[UInt] = []
for info in self.infos {
let glyph = match font.glyph_for_codepoint(info.codepoint) {
Err(err) => return Err(Font(err))
Ok(None) => 0U
Ok(Some(value)) => value
}
glyphs_for_reorder.push(glyph)
}
update_use_categories_for_reph_pref(
categories_after_i,
syllables_after,
glyphs_for_reorder,
gsub,
rphf_lookups,
pref_lookups,
)
}
reorder_use_syllables(self, categories_after_i, syllables_after)
let reordered_categories : Array[Int] = []
for info in self.infos {
reordered_categories.push(use_category(info.codepoint))
}
let mut arabic_actions : Array[Int] = []
if use_has_arabic_joining(self.script) {
arabic_actions = arabic_joining_actions(self)
if self.script == @common.script_mongolian {
apply_mongolian_variation_selectors(arabic_actions, self.infos)
}
}
let glyph_masks = build_use_feature_masks(
reordered_categories,
syllables_after,
arabic_actions=arabic_actions,
)
let (unicode_mark_flags, combining_classes) = collect_mark_fallback_data(self.infos)
match self.shape_basic(font) {
Err(err) => return Err(err)
Ok(_) => ()
}
if gsub is Some(gsub) {
match apply_gsub_features_for_syllables(
font,
self,
gsub,
use_feature_tags,
syllables_after,
glyph_masks=glyph_masks,
script_tag=script_tag,
) {
Err(err) => return Err(err)
Ok(_) => ()
}
}
apply_gpos(
font,
self,
zero_width_marks=ZeroWidthMarks::ByGdefEarly,
fallback_position=false,
unicode_mark_flags=unicode_mark_flags,
combining_classes=combining_classes,
)
}