use crate::face::Face;
use crate::shaper::{shape_run_with_font, PositionedGlyph};
use crate::shaping::arabic::{compute_forms, script_of, Script};
use crate::shaping::arabic_pf::presentation_form;
use crate::shaping::indic::{
cluster_boundaries_with, reorder_cluster_with, script_indic_tags, IndicCategory, ReorderRules,
BENGALI_RULES, DEVANAGARI_RULES, GUJARATI_RULES, GURMUKHI_RULES, KANNADA_RULES, KHMER_RULES,
MALAYALAM_RULES, ORIYA_RULES, SINHALA_RULES, TAMIL_RULES, TELUGU_RULES, THAI_RULES,
};
use crate::style::Style;
use crate::Error;
#[derive(Debug)]
pub struct FaceChain {
faces: Vec<Face>,
}
impl FaceChain {
pub fn new(primary: Face) -> Self {
Self {
faces: vec![primary],
}
}
#[must_use]
pub fn push_fallback(mut self, face: Face) -> Self {
self.faces.push(face);
self
}
pub fn len(&self) -> usize {
self.faces.len()
}
pub fn is_empty(&self) -> bool {
self.faces.is_empty()
}
pub fn face(&self, idx: u16) -> &Face {
&self.faces[idx as usize]
}
pub fn primary(&self) -> &Face {
&self.faces[0]
}
pub fn face_mut(&mut self, idx: usize) -> &mut Face {
&mut self.faces[idx]
}
pub fn set_variation_coords(&mut self, coords: &[f32]) -> Result<(), Error> {
self.faces[0].set_variation_coords(coords)
}
pub fn named_instances(&self, face_index: usize) -> Vec<crate::NamedInstance> {
self.faces
.get(face_index)
.map(|f| f.named_instances())
.unwrap_or_default()
}
pub fn variation_axes(&self, face_index: usize) -> Vec<crate::VariationAxis> {
self.faces
.get(face_index)
.map(|f| f.variation_axes())
.unwrap_or_default()
}
pub fn shape(&self, text: &str, size_px: f32) -> Result<Vec<PositionedGlyph>, Error> {
self.shape_styled(text, size_px, Style::REGULAR)
}
pub fn shape_styled(
&self,
text: &str,
size_px: f32,
_style: Style,
) -> Result<Vec<PositionedGlyph>, Error> {
if text.is_empty() || size_px <= 0.0 {
return Ok(Vec::new());
}
let assigned = self.assign_codepoints(text)?;
if assigned.is_empty() {
return Ok(Vec::new());
}
let mut out: Vec<PositionedGlyph> = Vec::with_capacity(assigned.len());
let mut run_start = 0usize;
while run_start < assigned.len() {
let face_idx = assigned[run_start].0;
let mut run_end = run_start + 1;
while run_end < assigned.len() && assigned[run_end].0 == face_idx {
run_end += 1;
}
let gids: Vec<u16> = assigned[run_start..run_end].iter().map(|p| p.1).collect();
let face = &self.faces[face_idx as usize];
let mut run_glyphs =
face.with_font(|font| shape_run_with_font(font, &gids, size_px, face_idx))?;
out.append(&mut run_glyphs);
run_start = run_end;
}
Ok(out)
}
fn assign_codepoints(&self, text: &str) -> Result<Vec<(u16, u16)>, Error> {
let chars: Vec<char> = text.chars().collect();
let arabic_shaped = apply_arabic_joining(&chars);
let (shaped_chars, reph_marks, cluster_spans) = apply_indic_reorder(&arabic_shaped);
let mut out: Vec<(u16, u16)> = Vec::with_capacity(shaped_chars.len());
for (orig_idx, ch) in shaped_chars.iter().enumerate() {
let mut found: Option<(u16, u16)> = None;
for (idx, face) in self.faces.iter().enumerate() {
let g = face.with_font(|font| font.glyph_index(*ch))?;
match g {
Some(gid) if gid != 0 => {
found = Some((idx as u16, gid));
break;
}
_ => continue,
}
}
if found.is_none() {
let orig = if orig_idx < chars.len() && *ch != chars[orig_idx] {
Some(chars[orig_idx])
} else if !chars.contains(ch) {
None
} else {
None
};
if let Some(orig) = orig {
for (idx, face) in self.faces.iter().enumerate() {
let g = face.with_font(|font| font.glyph_index(orig))?;
if let Some(gid) = g {
if gid != 0 {
found = Some((idx as u16, gid));
break;
}
}
}
}
}
out.push(found.unwrap_or((0, 0)));
}
let (out, dropped_halants) = self.apply_reph_substitutions(out, &reph_marks)?;
let adjusted_spans = adjust_cluster_spans(&cluster_spans, &dropped_halants, &shaped_chars);
let out = self.apply_cluster_position_substitutions(out, &adjusted_spans, &shaped_chars)?;
Ok(out)
}
fn apply_reph_substitutions(
&self,
glyphs: Vec<(u16, u16)>,
marks: &[RephMark],
) -> Result<RephSubstResult, Error> {
if marks.is_empty() {
return Ok((glyphs, Vec::new()));
}
let mut out = glyphs;
let mut dropped: Vec<usize> = Vec::new();
for mark in marks.iter().rev() {
if mark.ra_idx >= out.len() || mark.halant_idx >= out.len() {
continue;
}
let (ra_face_idx, ra_gid) = out[mark.ra_idx];
if ra_gid == 0 {
continue;
}
let face = &self.faces[ra_face_idx as usize];
let (modern, legacy) = match script_indic_tags(mark.script) {
Some(p) => p,
None => continue,
};
let new_ra = face.with_font(|font| {
let mut substitute: Option<u16> = None;
for tag in [modern, legacy] {
let features = font.gsub_features_for_script(tag, None);
for feat in features {
if feat.tag == *b"rphf" {
for &lookup_idx in &feat.lookup_indices {
if let Some(g) = font.gsub_apply_lookup_type_1(lookup_idx, ra_gid) {
substitute = Some(g);
break;
}
}
if substitute.is_some() {
break;
}
}
}
if substitute.is_some() {
break;
}
}
substitute
})?;
if let Some(reph_gid) = new_ra {
out[mark.ra_idx] = (ra_face_idx, reph_gid);
if mark.halant_idx < out.len() {
out.remove(mark.halant_idx);
dropped.push(mark.halant_idx);
}
}
}
Ok((out, dropped))
}
fn apply_cluster_position_substitutions(
&self,
glyphs: Vec<(u16, u16)>,
spans: &[ClusterSpan],
chars: &[char],
) -> Result<Vec<(u16, u16)>, Error> {
if spans.is_empty() {
return Ok(glyphs);
}
let mut out = glyphs;
for span in spans {
let end = span.end.min(out.len());
if span.start >= end {
continue;
}
let (modern, legacy) = match script_indic_tags(span.script) {
Some(p) => p,
None => continue,
};
let chars_end = span.end.min(chars.len());
let cat_of =
|i: usize| -> IndicCategory { indic_category_for_script(span.script, chars[i]) };
let mut pos = span.start;
while pos < chars_end {
let here = cat_of(pos);
if here == IndicCategory::Consonant && pos + 1 < chars_end {
let next = cat_of(pos + 1);
if next == IndicCategory::Halant {
let glyph_idx = pos.min(out.len().saturating_sub(1));
let after_halant = pos + 2;
if after_halant < chars_end && glyph_idx < out.len() {
self.try_apply_single_subst(
&mut out, glyph_idx, modern, legacy, b"half",
)?;
}
if after_halant < chars_end
&& cat_of(after_halant) == IndicCategory::Consonant
&& after_halant < out.len()
{
for tag in [b"pref", b"blwf", b"abvf", b"pstf"] {
if self.try_apply_single_subst(
&mut out,
after_halant,
modern,
legacy,
tag,
)? {
break;
}
}
}
}
}
pos += 1;
}
for tag in [b"pres", b"psts", b"abvs", b"blws"] {
for idx in span.start..end {
if idx < out.len() {
self.try_apply_single_subst(&mut out, idx, modern, legacy, tag)?;
}
}
}
}
Ok(out)
}
fn try_apply_single_subst(
&self,
out: &mut [(u16, u16)],
glyph_idx: usize,
modern: [u8; 4],
legacy: [u8; 4],
feature_tag: &[u8; 4],
) -> Result<bool, Error> {
let (face_idx, gid) = out[glyph_idx];
if gid == 0 {
return Ok(false);
}
let face = &self.faces[face_idx as usize];
let new_gid = face.with_font(|font| {
for tag in [modern, legacy] {
let features = font.gsub_features_for_script(tag, None);
for feat in features {
if &feat.tag == feature_tag {
for &lookup_idx in &feat.lookup_indices {
if let Some(g) = font.gsub_apply_lookup_type_1(lookup_idx, gid) {
return Some(g);
}
}
}
}
}
None
})?;
if let Some(g) = new_gid {
out[glyph_idx] = (face_idx, g);
Ok(true)
} else {
Ok(false)
}
}
}
type RephSubstResult = (Vec<(u16, u16)>, Vec<usize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RephMark {
ra_idx: usize,
halant_idx: usize,
script: Script,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ClusterSpan {
start: usize,
end: usize,
script: Script,
}
fn apply_indic_reorder(chars: &[char]) -> (Vec<char>, Vec<RephMark>, Vec<ClusterSpan>) {
if chars.is_empty() {
return (Vec::new(), Vec::new(), Vec::new());
}
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut reph_marks: Vec<RephMark> = Vec::new();
let mut spans: Vec<ClusterSpan> = Vec::new();
let mut i = 0;
while i < chars.len() {
let run_script = script_of(chars[i]);
let rules = match indic_rules_for_script(run_script) {
Some(r) => r,
None => {
out.push(chars[i]);
i += 1;
continue;
}
};
let run_start = i;
while i < chars.len() && script_of(chars[i]) == run_script {
i += 1;
}
let run = &chars[run_start..i];
let bounds = cluster_boundaries_with(run, rules.category);
for (s, e) in bounds {
let cluster = &run[s..e];
let (reordered, flags) = reorder_cluster_with(cluster, rules);
let cluster_start = out.len();
if flags.has_reph {
let ra_offset = if flags.pre_base_reordered { 1 } else { 0 };
let ra_idx = cluster_start + ra_offset;
let halant_idx = ra_idx + 1;
reph_marks.push(RephMark {
ra_idx,
halant_idx,
script: run_script,
});
}
out.extend_from_slice(&reordered);
spans.push(ClusterSpan {
start: cluster_start,
end: out.len(),
script: run_script,
});
}
}
(out, reph_marks, spans)
}
fn adjust_cluster_spans(
spans: &[ClusterSpan],
dropped: &[usize],
chars: &[char],
) -> Vec<ClusterSpan> {
if dropped.is_empty() {
return spans.to_vec();
}
let drops_before = |idx: usize| -> usize { dropped.iter().filter(|&&d| d < idx).count() };
spans
.iter()
.map(|s| {
let new_start = s.start.saturating_sub(drops_before(s.start));
let new_end = s.end.saturating_sub(drops_before(s.end));
ClusterSpan {
start: new_start.min(chars.len()),
end: new_end.min(chars.len()),
script: s.script,
}
})
.collect()
}
fn indic_category_for_script(script: Script, ch: char) -> IndicCategory {
use crate::shaping::indic;
match script {
Script::Devanagari => indic::devanagari_category(ch),
Script::Bengali => indic::bengali_category(ch),
Script::Tamil => indic::tamil_category(ch),
Script::Gurmukhi => indic::gurmukhi_category(ch),
Script::Gujarati => indic::gujarati_category(ch),
Script::Telugu => indic::telugu_category(ch),
Script::Kannada => indic::kannada_category(ch),
Script::Malayalam => indic::malayalam_category(ch),
Script::Oriya => indic::oriya_category(ch),
Script::Sinhala => indic::sinhala_category(ch),
Script::Khmer => indic::khmer_category(ch),
Script::Thai => indic::thai_category(ch),
_ => IndicCategory::Other,
}
}
fn indic_rules_for_script(script: Script) -> Option<&'static ReorderRules> {
match script {
Script::Devanagari => Some(&DEVANAGARI_RULES),
Script::Bengali => Some(&BENGALI_RULES),
Script::Tamil => Some(&TAMIL_RULES),
Script::Gurmukhi => Some(&GURMUKHI_RULES),
Script::Gujarati => Some(&GUJARATI_RULES),
Script::Telugu => Some(&TELUGU_RULES),
Script::Kannada => Some(&KANNADA_RULES),
Script::Malayalam => Some(&MALAYALAM_RULES),
Script::Oriya => Some(&ORIYA_RULES),
Script::Sinhala => Some(&SINHALA_RULES),
Script::Khmer => Some(&KHMER_RULES),
Script::Thai => Some(&THAI_RULES),
_ => None,
}
}
fn apply_arabic_joining(chars: &[char]) -> Vec<char> {
if chars.is_empty() {
return Vec::new();
}
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut i = 0;
while i < chars.len() {
let run_start = i;
while i < chars.len() && script_of(chars[i]) == Script::Arabic {
i += 1;
}
if i > run_start {
let run = &chars[run_start..i];
let forms = compute_forms(run);
for (k, &ch) in run.iter().enumerate() {
let translated = presentation_form(ch, forms[k]).unwrap_or(ch);
out.push(translated);
}
}
if i < chars.len() && script_of(chars[i]) != Script::Arabic {
out.push(chars[i]);
i += 1;
}
}
out
}
#[cfg(test)]
#[allow(non_snake_case)] mod tests {
use super::{apply_arabic_joining, apply_indic_reorder, ClusterSpan};
use crate::shaping::arabic::Script;
#[test]
fn ascii_passes_through_unchanged() {
let chars: Vec<char> = "Hello".chars().collect();
assert_eq!(apply_arabic_joining(&chars), chars);
}
#[test]
fn devanagari_pre_base_matra_moves_to_front_of_cluster() {
let chars = vec!['\u{0915}', '\u{093F}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}']);
assert!(marks.is_empty(), "no reph in this cluster");
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].start, 0);
assert_eq!(spans[0].end, 2);
assert_eq!(spans[0].script, Script::Devanagari);
}
#[test]
fn devanagari_two_clusters_each_reorder_independently() {
let chars = vec!['\u{0915}', '\u{093F}', '\u{0915}', '\u{093F}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{093F}', '\u{0915}']);
assert_eq!(spans.len(), 2);
assert_eq!((spans[0].start, spans[0].end), (0, 2));
assert_eq!((spans[1].start, spans[1].end), (2, 4));
}
#[test]
fn devanagari_conjunct_reorder_keeps_halant_chain_intact() {
let chars = vec!['\u{0915}', '\u{094D}', '\u{0937}', '\u{093F}'];
let (out, _, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{094D}', '\u{0937}']);
}
#[test]
fn ascii_passes_through_indic_reorder_unchanged() {
let chars: Vec<char> = "Hello".chars().collect();
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, chars);
assert!(marks.is_empty());
assert!(spans.is_empty());
}
#[test]
fn mixed_latin_and_devanagari_reorders_only_devanagari_clusters() {
let chars = vec!['A', '\u{0915}', '\u{093F}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['A', '\u{093F}', '\u{0915}']);
assert_eq!(spans.len(), 1);
assert_eq!((spans[0].start, spans[0].end), (1, 3));
}
#[test]
fn devanagari_reph_emits_reph_mark_at_correct_index() {
let chars = vec!['\u{0930}', '\u{094D}', '\u{0915}'];
let (out, marks, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0930}', '\u{094D}', '\u{0915}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].ra_idx, 0);
assert_eq!(marks[0].halant_idx, 1);
assert_eq!(marks[0].script, Script::Devanagari);
}
#[test]
fn devanagari_reph_with_pre_base_matra_shifts_reph_mark_by_one() {
let chars = vec!['\u{0930}', '\u{094D}', '\u{0915}', '\u{093F}'];
let (out, marks, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0930}', '\u{094D}', '\u{0915}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].ra_idx, 1);
assert_eq!(marks[0].halant_idx, 2);
}
#[test]
fn bengali_pre_base_matra_e_moves_to_front_of_cluster() {
let chars = vec!['\u{0995}', '\u{09C7}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{09C7}', '\u{0995}']);
assert!(marks.is_empty());
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Bengali);
}
#[test]
fn bengali_reph_emits_reph_mark_with_bengali_script_tag() {
let chars = vec!['\u{09B0}', '\u{09CD}', '\u{0995}'];
let (out, marks, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{09B0}', '\u{09CD}', '\u{0995}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].script, Script::Bengali);
}
#[test]
fn tamil_pre_base_matra_e_moves_to_front_of_cluster() {
let chars = vec!['\u{0B95}', '\u{0BC6}'];
let (out, marks, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0BC6}', '\u{0B95}']);
assert!(marks.is_empty());
}
#[test]
fn tamil_RA_plus_halant_does_NOT_emit_reph_mark() {
let chars = vec!['\u{0BB0}', '\u{0BCD}', '\u{0B95}'];
let (_out, marks, _) = apply_indic_reorder(&chars);
assert!(marks.is_empty(), "Tamil never sets the reph flag");
}
#[test]
fn mixed_devanagari_and_bengali_runs_segment_independently() {
let chars = vec!['\u{0915}', '\u{093F}', '\u{0995}', '\u{09BF}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{09BF}', '\u{0995}']);
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].script, Script::Devanagari);
assert_eq!(spans[1].script, Script::Bengali);
}
#[test]
fn gurmukhi_cluster_reorder_emits_span_with_gurmukhi_script() {
let chars = vec!['\u{0A15}', '\u{0A3F}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0A3F}', '\u{0A15}']);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Gurmukhi);
}
#[test]
fn gujarati_cluster_reorder_emits_span_with_gujarati_script() {
let chars = vec!['\u{0A95}', '\u{0ABF}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0ABF}', '\u{0A95}']);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Gujarati);
}
#[test]
fn telugu_pre_base_matra_e_reorders_with_telugu_span() {
let chars = vec!['\u{0C15}', '\u{0C46}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0C46}', '\u{0C15}']);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Telugu);
}
#[test]
fn kannada_reph_emits_reph_mark_with_kannada_script_tag() {
let chars = vec!['\u{0CB0}', '\u{0CCD}', '\u{0C95}'];
let (_out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].script, Script::Kannada);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Kannada);
}
#[test]
fn malayalam_RA_plus_halant_does_NOT_emit_reph_mark() {
let chars = vec!['\u{0D30}', '\u{0D4D}', '\u{0D15}'];
let (_out, marks, spans) = apply_indic_reorder(&chars);
assert!(marks.is_empty());
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Malayalam);
}
#[test]
fn oriya_pre_base_matra_e_reorders_with_oriya_span() {
let chars = vec!['\u{0B15}', '\u{0B47}'];
let (out, _, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0B47}', '\u{0B15}']);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Oriya);
}
#[test]
fn malayalam_chillu_starts_new_cluster_from_following_consonant() {
let chars = vec!['\u{0D7A}', '\u{0D15}'];
let (_out, _marks, spans) = apply_indic_reorder(&chars);
assert_eq!(spans.len(), 2);
}
#[test]
fn sinhala_pre_base_matra_reorders_with_sinhala_span() {
let chars = vec!['\u{0D9A}', '\u{0DD9}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0DD9}', '\u{0D9A}']);
assert!(marks.is_empty(), "Sinhala has no reph");
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Sinhala);
}
#[test]
fn sinhala_RA_plus_al_lakuna_does_NOT_emit_reph_mark() {
let chars = vec!['\u{0DBB}', '\u{0DCA}', '\u{0D9A}'];
let (_out, marks, spans) = apply_indic_reorder(&chars);
assert!(marks.is_empty());
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Sinhala);
}
#[test]
fn khmer_pre_base_matra_reorders_with_khmer_span() {
let chars = vec!['\u{1780}', '\u{17C1}'];
let (out, _marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{17C1}', '\u{1780}']);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].script, Script::Khmer);
}
#[test]
fn khmer_coeng_keeps_subjoined_chain_in_one_cluster_span() {
let chars = vec!['\u{1780}', '\u{17D2}', '\u{1781}', '\u{17D2}', '\u{1782}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, chars); assert!(marks.is_empty());
assert_eq!(spans.len(), 1);
assert_eq!((spans[0].start, spans[0].end), (0, 5));
assert_eq!(spans[0].script, Script::Khmer);
}
#[test]
fn thai_no_reorder_preserves_storage_order() {
let chars = vec!['\u{0E40}', '\u{0E01}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, chars);
assert!(marks.is_empty());
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].script, Script::Thai);
assert_eq!(spans[1].script, Script::Thai);
}
#[test]
fn thai_consonant_with_above_vowel_and_tone_emits_one_span() {
let chars = vec!['\u{0E01}', '\u{0E34}', '\u{0E49}'];
let (out, marks, spans) = apply_indic_reorder(&chars);
assert_eq!(out, chars);
assert!(marks.is_empty());
assert_eq!(spans.len(), 1);
assert_eq!((spans[0].start, spans[0].end), (0, 3));
assert_eq!(spans[0].script, Script::Thai);
}
#[test]
fn mixed_devanagari_and_thai_segments_at_script_boundary() {
let chars = vec!['\u{0915}', '\u{0E01}'];
let (_out, marks, spans) = apply_indic_reorder(&chars);
assert!(marks.is_empty());
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].script, Script::Devanagari);
assert_eq!(spans[1].script, Script::Thai);
}
#[test]
fn adjust_cluster_spans_shifts_subsequent_spans_after_drop() {
use super::adjust_cluster_spans;
let chars = vec!['a'; 10];
let spans = vec![
ClusterSpan {
start: 0,
end: 3,
script: Script::Devanagari,
},
ClusterSpan {
start: 3,
end: 6,
script: Script::Devanagari,
},
];
let dropped = vec![1usize];
let adjusted = adjust_cluster_spans(&spans, &dropped, &chars);
assert_eq!((adjusted[0].start, adjusted[0].end), (0, 2));
assert_eq!((adjusted[1].start, adjusted[1].end), (2, 5));
}
#[test]
fn arabic_run_translates_to_presentation_forms() {
let chars = vec!['\u{0628}', '\u{0628}', '\u{0628}'];
let out = apply_arabic_joining(&chars);
assert_eq!(out, vec!['\u{FE91}', '\u{FE92}', '\u{FE90}']);
}
#[test]
fn arabic_run_with_ascii_separator() {
let chars = vec!['\u{0628}', '\u{0628}', ' ', '\u{0628}', '\u{0628}'];
let out = apply_arabic_joining(&chars);
assert_eq!(
out,
vec!['\u{FE91}', '\u{FE90}', ' ', '\u{FE91}', '\u{FE90}']
);
}
}