fontbe/features/
marks.rs

1//! Generates a [FeaRsMarks] datastructure to be fed to fea-rs
2
3use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4
5use fea_rs::{
6    compile::{FeatureProvider, LookupId, PendingLookup},
7    GlyphSet,
8};
9use fontdrasil::{
10    orchestration::{Access, AccessBuilder, Work},
11    types::GlyphName,
12};
13
14use ordered_float::OrderedFloat;
15use smol_str::SmolStr;
16use write_fonts::{
17    read::collections::IntSet,
18    tables::{
19        gdef::GlyphClassDef,
20        gpos::builders::{
21            AnchorBuilder, CursivePosBuilder, MarkToBaseBuilder, MarkToLigBuilder,
22            MarkToMarkBuilder,
23        },
24        layout::{builders::CaretValueBuilder, LookupFlag},
25    },
26    types::{GlyphId16, Tag},
27};
28
29use crate::{
30    error::Error,
31    features::properties::ScriptDirection,
32    orchestration::{
33        AnyWorkId, BeWork, Context, FeaFirstPassOutput, FeaRsMarks, MarkLookups, WorkId,
34    },
35};
36use fontir::{
37    ir::{self, Anchor, AnchorKind, GlyphAnchors, GlyphOrder, StaticMetadata},
38    orchestration::WorkId as FeWorkId,
39    variations::DeltaError,
40};
41
42use super::{
43    ot_tags::{INDIC_SCRIPTS, USE_SCRIPTS},
44    properties::UnicodeShortName,
45};
46
47#[derive(Debug)]
48struct MarkWork {}
49
50pub fn create_mark_work() -> Box<BeWork> {
51    Box::new(MarkWork {})
52}
53
54const MARK: Tag = Tag::new(b"mark");
55const MKMK: Tag = Tag::new(b"mkmk");
56const CURS: Tag = Tag::new(b"curs");
57const ABVM: Tag = Tag::new(b"abvm");
58const BLWM: Tag = Tag::new(b"blwm");
59/// The canonical name shared for a given mark/base pair, e.g. `top` for `top`/`_top`
60type GroupName = SmolStr;
61
62struct MarkLookupBuilder<'a> {
63    // extracted from public.openTypeCatgories/GlyphData.xml or FEA
64    gdef_classes: HashMap<GlyphId16, GlyphClassDef>,
65    // pruned, only the anchors we are using
66    anchor_lists: BTreeMap<GlyphId16, Vec<&'a ir::Anchor>>,
67    glyph_order: &'a GlyphOrder,
68    static_metadata: &'a StaticMetadata,
69    fea_first_pass: &'a FeaFirstPassOutput,
70    // unicode names of scripts declared in FEA
71    mark_glyphs: BTreeSet<GlyphId16>,
72    lig_carets: BTreeMap<GlyphId16, Vec<CaretValueBuilder>>,
73    char_map: HashMap<u32, GlyphId16>,
74}
75
76/// Abstract over the difference in anchor shape between mark2lig and mark2base/mark2mark
77#[derive(Debug, Clone, PartialEq)]
78enum BaseOrLigAnchors<T> {
79    Base(T),
80    Ligature(Vec<Option<T>>),
81}
82
83/// The bases and marks in a particular group, e.g. "top" or "bottom"
84#[derive(Default, Debug, Clone, PartialEq)]
85struct MarkGroup<'a> {
86    bases: Vec<(GlyphId16, BaseOrLigAnchors<&'a ir::Anchor>)>,
87    marks: Vec<(GlyphId16, &'a ir::Anchor)>,
88    // if `true`, we will make a mark filter set from the marks in this group
89    // (only true for mkmk)
90    filter_glyphs: bool,
91}
92
93impl MarkGroup<'_> {
94    //https://github.com/googlefonts/ufo2ft/blob/5a606b7884bb6da594e3cc56a169e5c3d5fa267c/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L796
95    fn make_filter_glyph_set(&self, filter_glyphs: &IntSet<GlyphId16>) -> Option<GlyphSet> {
96        let all_marks = self
97            .marks
98            .iter()
99            .map(|(gid, _)| *gid)
100            .collect::<HashSet<_>>();
101        self.filter_glyphs.then(|| {
102            self.marks
103                .iter()
104                .filter_map(|(gid, _)| filter_glyphs.contains(*gid).then_some(*gid))
105                .chain(
106                    self.bases
107                        .iter()
108                        .filter_map(|(gid, _)| (!all_marks.contains(gid)).then_some(*gid)),
109                )
110                .collect()
111        })
112    }
113
114    fn only_using_glyphs(&self, include: &IntSet<GlyphId16>) -> Option<MarkGroup<'_>> {
115        let bases = self
116            .bases
117            .iter()
118            .filter(|(gid, _)| include.contains(*gid))
119            .map(|(gid, anchors)| (*gid, anchors.clone()))
120            .collect::<Vec<_>>();
121        if bases.is_empty() || self.marks.is_empty() {
122            None
123        } else {
124            Some(MarkGroup {
125                bases,
126                marks: self.marks.clone(),
127                filter_glyphs: self.filter_glyphs,
128            })
129        }
130    }
131}
132
133// a trait to abstract over three very similar builders
134trait MarkAttachmentBuilder: Default {
135    fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder);
136    fn add_base(
137        &mut self,
138        gid: GlyphId16,
139        group: &GroupName,
140        anchor: BaseOrLigAnchors<AnchorBuilder>,
141    );
142}
143
144impl MarkAttachmentBuilder for MarkToBaseBuilder {
145    fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
146        let _ = self.insert_mark(gid, group, anchor);
147    }
148
149    fn add_base(
150        &mut self,
151        gid: GlyphId16,
152        group: &GroupName,
153        anchor: BaseOrLigAnchors<AnchorBuilder>,
154    ) {
155        match anchor {
156            BaseOrLigAnchors::Base(anchor) => self.insert_base(gid, group, anchor),
157            BaseOrLigAnchors::Ligature(_) => panic!("lig anchors in mark2base builder"),
158        }
159    }
160}
161
162impl MarkAttachmentBuilder for MarkToMarkBuilder {
163    fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
164        let _ = self.insert_mark1(gid, group, anchor);
165    }
166
167    fn add_base(
168        &mut self,
169        gid: GlyphId16,
170        group: &GroupName,
171        anchor: BaseOrLigAnchors<AnchorBuilder>,
172    ) {
173        match anchor {
174            BaseOrLigAnchors::Base(anchor) => self.insert_mark2(gid, group, anchor),
175            BaseOrLigAnchors::Ligature(_) => panic!("lig anchors in mark2mark to builder"),
176        }
177    }
178}
179
180impl MarkAttachmentBuilder for MarkToLigBuilder {
181    fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
182        let _ = self.insert_mark(gid, group, anchor);
183    }
184
185    fn add_base(
186        &mut self,
187        gid: GlyphId16,
188        group: &GroupName,
189        anchors: BaseOrLigAnchors<AnchorBuilder>,
190    ) {
191        match anchors {
192            BaseOrLigAnchors::Ligature(anchors) => self.insert_ligature(gid, group, anchors),
193            BaseOrLigAnchors::Base(_) => panic!("base anchors passed to mark2lig builder"),
194        }
195    }
196}
197
198impl<'a> MarkLookupBuilder<'a> {
199    fn new(
200        anchors: Vec<&'a GlyphAnchors>,
201        glyph_order: &'a GlyphOrder,
202        static_metadata: &'a StaticMetadata,
203        fea_first_pass: &'a FeaFirstPassOutput,
204        char_map: HashMap<u32, GlyphId16>,
205    ) -> Result<Self, Error> {
206        let gdef_classes = super::get_gdef_classes(static_metadata, fea_first_pass, glyph_order);
207        let lig_carets = get_ligature_carets(glyph_order, static_metadata, &anchors)?;
208        // first we want to narrow our input down to only anchors that are participating.
209        // in pythonland this is https://github.com/googlefonts/ufo2ft/blob/8e9e6eb66a/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L380
210        let mut pruned = BTreeMap::new();
211        let mut base_groups = HashSet::new();
212        let mut mark_groups = HashSet::new();
213        let include = gdef_classes
214            .iter()
215            .filter_map(|(name, cls)| {
216                matches!(
217                    *cls,
218                    GlyphClassDef::Base | GlyphClassDef::Mark | GlyphClassDef::Ligature
219                )
220                .then_some(name)
221            })
222            .collect::<HashSet<_>>();
223        for anchors in anchors {
224            // skip anchors for non-export glyphs
225            if !glyph_order.contains(&anchors.glyph_name) {
226                continue;
227            }
228            let gid = glyph_order.glyph_id(&anchors.glyph_name).unwrap();
229            // skip glyphs that are not mark/lig/base, if we have any defined categories
230            if !include.is_empty() && !include.contains(&gid) {
231                continue;
232            }
233            for anchor in &anchors.anchors {
234                match &anchor.kind {
235                    AnchorKind::Base(group)
236                    | AnchorKind::Ligature {
237                        group_name: group, ..
238                    } => {
239                        base_groups.insert(group);
240                    }
241                    AnchorKind::Mark(group) => {
242                        mark_groups.insert(group);
243                    }
244                    AnchorKind::ComponentMarker(_)
245                    | AnchorKind::CursiveEntry
246                    | AnchorKind::CursiveExit
247                    | AnchorKind::Caret(_)
248                    | AnchorKind::VCaret(_) => (),
249                }
250                pruned.entry(gid).or_insert(Vec::new()).push(anchor);
251            }
252        }
253
254        let used_groups = base_groups
255            .intersection(&mark_groups)
256            .collect::<HashSet<_>>();
257        // we don't care about marks with no corresponding bases, & vice-versa see:
258        // <https://github.com/googlefonts/ufo2ft/blob/6787e37e63530/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L359>
259        pruned.retain(|_, anchors| {
260            anchors.retain(|anchor| {
261                anchor.is_cursive()
262                    || anchor
263                        .mark_group_name()
264                        .map(|group| used_groups.contains(&group))
265                        .unwrap_or_else(|| anchor.is_component_marker())
266            });
267            !anchors.is_empty()
268        });
269
270        let mark_glyphs = find_mark_glyphs(&pruned, &gdef_classes);
271        Ok(Self {
272            anchor_lists: pruned,
273            glyph_order,
274            static_metadata,
275            fea_first_pass,
276            gdef_classes,
277            mark_glyphs,
278            lig_carets,
279            char_map,
280        })
281    }
282
283    // corresponds to _makeFeatures in python
284    fn build(&self) -> Result<FeaRsMarks, Error> {
285        let mark_base_groups = self.make_mark_to_base_groups();
286        let mark_mark_groups = self.make_mark_to_mark_groups();
287        let mark_lig_groups = self.make_mark_to_liga_groups();
288
289        let (abvm_glyphs, non_abvm_glyphs) = self.split_mark_and_abvm_blwm_glyphs()?;
290
291        let todo = super::feature_writer_todo_list(
292            &[MARK, MKMK, ABVM, BLWM, CURS],
293            &self.fea_first_pass.ast,
294        );
295
296        let mut mark_mkmk = self.make_lookups(
297            &mark_base_groups,
298            &mark_mark_groups,
299            &mark_lig_groups,
300            &non_abvm_glyphs,
301            |_| true,
302        )?;
303        if !todo.contains(&MARK) {
304            mark_mkmk.mark_base.clear();
305            mark_mkmk.mark_lig.clear();
306        }
307        if !todo.contains(&MKMK) {
308            mark_mkmk.mark_mark.clear();
309        }
310
311        let curs = todo
312            .contains(&CURS)
313            .then(|| self.make_cursive_lookups())
314            .transpose()?
315            .unwrap_or_default();
316        let (abvm, blwm) = if !abvm_glyphs.is_empty() {
317            let abvm = todo
318                .contains(&ABVM)
319                .then(|| {
320                    self.make_lookups(
321                        &mark_base_groups,
322                        &mark_mark_groups,
323                        &mark_lig_groups,
324                        &abvm_glyphs,
325                        is_above_mark,
326                    )
327                })
328                .transpose()?;
329            let blwm = todo
330                .contains(&BLWM)
331                .then(|| {
332                    self.make_lookups(
333                        &mark_base_groups,
334                        &mark_mark_groups,
335                        &mark_lig_groups,
336                        &abvm_glyphs,
337                        is_below_mark,
338                    )
339                })
340                .transpose()?;
341            (abvm.unwrap_or_default(), blwm.unwrap_or_default())
342        } else {
343            Default::default()
344        };
345
346        Ok(FeaRsMarks {
347            glyphmap: self.glyph_order.names().cloned().collect(),
348            mark_mkmk,
349            abvm,
350            blwm,
351            curs,
352            lig_carets: self.lig_carets.clone(),
353        })
354    }
355
356    fn make_lookups(
357        &self,
358        mark_base_groups: &BTreeMap<GroupName, MarkGroup>,
359        mark_mark_groups: &BTreeMap<GroupName, MarkGroup>,
360        mark_lig_groups: &BTreeMap<GroupName, MarkGroup>,
361        include_glyphs: &IntSet<GlyphId16>,
362        marks_filter: impl Fn(&GroupName) -> bool,
363    ) -> Result<MarkLookups, Error> {
364        let mark_base = self.make_lookups_type::<MarkToBaseBuilder>(
365            mark_base_groups,
366            include_glyphs,
367            &marks_filter,
368        )?;
369        let mark_mark = self.make_lookups_type::<MarkToMarkBuilder>(
370            mark_mark_groups,
371            include_glyphs,
372            &marks_filter,
373        )?;
374        let mark_lig = self.make_lookups_type::<MarkToLigBuilder>(
375            mark_lig_groups,
376            include_glyphs,
377            &marks_filter,
378        )?;
379        Ok(MarkLookups {
380            mark_base,
381            mark_mark,
382            mark_lig,
383        })
384    }
385
386    // code shared between mark2base and mark2mark
387    fn make_lookups_type<T: MarkAttachmentBuilder>(
388        &self,
389        groups: &BTreeMap<GroupName, MarkGroup>,
390        include_glyphs: &IntSet<GlyphId16>,
391        // filters based on the name of an anchor!
392        marks_filter: &impl Fn(&GroupName) -> bool,
393    ) -> Result<Vec<PendingLookup<T>>, Error> {
394        let mut result = Vec::with_capacity(groups.len());
395        for (name, group) in groups {
396            if !marks_filter(name) {
397                continue;
398            }
399
400            // reduce the group to only include glyphs used in this feature
401            let Some(group) = group.only_using_glyphs(include_glyphs) else {
402                continue;
403            };
404
405            let mut builder = T::default();
406            let filter_set = group.make_filter_glyph_set(include_glyphs);
407            let mut flags = LookupFlag::empty();
408            if filter_set.is_some() {
409                flags |= LookupFlag::USE_MARK_FILTERING_SET;
410            }
411            for (mark_gid, anchor) in &group.marks {
412                let anchor = resolve_anchor_once(anchor, self.static_metadata)
413                    .map_err(|e| self.convert_delta_error(e, *mark_gid))?;
414                builder.add_mark(*mark_gid, name, anchor);
415            }
416
417            for (base_gid, anchor) in &group.bases {
418                if !include_glyphs.contains(*base_gid) {
419                    continue;
420                }
421                let anchor = resolve_anchor(anchor, self.static_metadata)
422                    .map_err(|e| self.convert_delta_error(e, *base_gid))?;
423                builder.add_base(*base_gid, name, anchor);
424            }
425            result.push(PendingLookup::new(vec![builder], flags, filter_set));
426        }
427        Ok(result)
428    }
429
430    fn make_mark_to_base_groups(&self) -> BTreeMap<GroupName, MarkGroup<'a>> {
431        let mut groups = BTreeMap::<_, MarkGroup>::new();
432        for (gid, anchors) in &self.anchor_lists {
433            let is_mark = self.mark_glyphs.contains(gid);
434            // if we have explicit gdef classes and this is not an expilcit base
435            let is_not_base = !self.gdef_classes.is_empty()
436                && (self.gdef_classes.get(gid)) != Some(&GlyphClassDef::Base);
437
438            let treat_as_base = !(is_mark | is_not_base);
439            for anchor in anchors {
440                match &anchor.kind {
441                    ir::AnchorKind::Base(group) if treat_as_base => groups
442                        .entry(group.clone())
443                        .or_default()
444                        .bases
445                        .push((*gid, BaseOrLigAnchors::Base(anchor))),
446                    ir::AnchorKind::Mark(group) if is_mark => groups
447                        .entry(group.clone())
448                        .or_default()
449                        .marks
450                        .push((*gid, anchor)),
451                    _ => continue,
452                }
453            }
454        }
455        groups
456    }
457
458    fn make_mark_to_mark_groups(&self) -> BTreeMap<GroupName, MarkGroup<'a>> {
459        // first find the set of glyphs that are marks, i.e. have any mark attachment point.
460        let (mark_glyphs, mark_anchors): (BTreeSet<_>, BTreeSet<_>) = self
461            .anchor_lists
462            .iter()
463            .filter(|(name, _)| self.mark_glyphs.contains(*name))
464            .flat_map(|(name, anchors)| {
465                anchors.iter().filter_map(move |anchor| {
466                    if let AnchorKind::Mark(group) = &anchor.kind {
467                        Some((name, group))
468                    } else {
469                        None
470                    }
471                })
472            })
473            .unzip();
474
475        // then iterate again, looking for glyphs that we have identified as marks,
476        // but which also have participating base anchors
477        let mut result = BTreeMap::<GroupName, MarkGroup>::new();
478        for (gid, glyph_anchors) in &self.anchor_lists {
479            if !mark_glyphs.contains(gid) {
480                continue;
481            }
482
483            for anchor in glyph_anchors {
484                if let AnchorKind::Base(group_name) = &anchor.kind {
485                    // only if this anchor is a base, AND we have a mark in the same group
486                    if mark_anchors.contains(group_name) {
487                        let group = result.entry(group_name.clone()).or_default();
488                        group.filter_glyphs = true;
489                        group.bases.push((*gid, BaseOrLigAnchors::Base(anchor)))
490                    }
491                }
492            }
493        }
494
495        // then add the anchors for the mark glyphs, if they exist
496        for mark in mark_glyphs {
497            let anchors = self.anchor_lists.get(mark).unwrap();
498            for anchor in anchors {
499                if let AnchorKind::Mark(group) = &anchor.kind {
500                    // only add mark glyph if there is at least one base
501                    if let Some(group) = result.get_mut(group) {
502                        group.marks.push((*mark, anchor))
503                    }
504                }
505            }
506        }
507        result
508    }
509
510    fn make_mark_to_liga_groups(&self) -> BTreeMap<GroupName, MarkGroup<'_>> {
511        let mut groups = BTreeMap::<_, MarkGroup>::new();
512        let mut liga_anchor_groups = HashSet::new();
513
514        // a temporary buffer, reused for each glyph
515        let mut component_groups = HashMap::new();
516
517        // first do a pass to build up the ligature anchors and track the set
518        // of mark classes used
519        for (gid, anchors) in &self.anchor_lists {
520            // skip anything that is definitely not a ligature glyph
521            let might_be_liga = self.gdef_classes.is_empty()
522                || (self.gdef_classes.get(gid) == Some(&GlyphClassDef::Ligature));
523
524            if !might_be_liga {
525                continue;
526            }
527
528            // skip any glyphs that don't have a ligature anchor
529            let Some(max_index) = anchors.iter().filter_map(|a| a.ligature_index()).max() else {
530                continue;
531            };
532
533            for anchor in anchors {
534                let AnchorKind::Ligature { group_name, index } = &anchor.kind else {
535                    continue;
536                };
537                liga_anchor_groups.insert(group_name);
538                let component_anchors = component_groups
539                    .entry(group_name.clone())
540                    .or_insert_with(|| vec![None; max_index]);
541                component_anchors[*index - 1] = Some(*anchor);
542            }
543
544            for (group_name, anchors) in component_groups.drain() {
545                groups
546                    .entry(group_name)
547                    .or_default()
548                    .bases
549                    .push((*gid, BaseOrLigAnchors::Ligature(anchors)));
550            }
551        }
552
553        // then we do another pass to add the marks in the used classes
554        for (gid, anchors) in &self.anchor_lists {
555            if !self.mark_glyphs.contains(gid) {
556                continue;
557            }
558            for anchor in anchors {
559                if let AnchorKind::Mark(group) = &anchor.kind {
560                    if liga_anchor_groups.contains(group) {
561                        groups
562                            .entry(group.to_owned())
563                            .or_default()
564                            .marks
565                            .push((*gid, anchor));
566                    }
567                }
568            }
569        }
570        groups
571    }
572
573    //https://github.com/googlefonts/ufo2ft/blob/98e8916a86/Lib/ufo2ft/featureWriters/cursFeatureWriter.py#L40
574    fn make_cursive_lookups(&self) -> Result<Vec<PendingLookup<CursivePosBuilder>>, Error> {
575        let dir_glyphs = super::properties::glyphs_by_script_direction(
576            &self.char_map,
577            self.fea_first_pass.gsub().as_ref(),
578        )?;
579
580        let mut ltr_builder = CursivePosBuilder::default();
581        let mut rtl_builder = CursivePosBuilder::default();
582
583        let ltr_glyphs = dir_glyphs.get(&ScriptDirection::LeftToRight);
584        for (gid, anchors) in &self.anchor_lists {
585            //TODO: support non-standard entry anchor names, see
586            // https://github.com/googlefonts/ufo2ft/blob/98e8916a86/Lib/ufo2ft/featureWriters/cursFeatureWriter.py#L22
587            let (entry, exit) = self.get_entry_and_exit(*gid, anchors)?;
588            if entry.is_none() && exit.is_none() {
589                continue;
590            }
591
592            // LTR only if explicit member of group, else RTL:
593            // https://github.com/googlefonts/ufo2ft/blob/98e8916a8/Lib/ufo2ft/featureWriters/cursFeatureWriter.py#L76
594            if ltr_glyphs
595                .map(|glyphs| glyphs.contains(*gid))
596                .unwrap_or(false)
597            {
598                ltr_builder.insert(*gid, entry, exit);
599            } else {
600                rtl_builder.insert(*gid, entry, exit);
601            }
602        }
603
604        Ok([
605            (ltr_builder, LookupFlag::empty()),
606            (rtl_builder, LookupFlag::RIGHT_TO_LEFT),
607        ]
608        .into_iter()
609        .filter_map(|(builder, flags)| {
610            (!builder.is_empty())
611                .then(|| PendingLookup::new(vec![builder], LookupFlag::IGNORE_MARKS | flags, None))
612        })
613        .collect())
614    }
615
616    // if either 'entry' or 'exit' is present, will return `Some`
617    fn get_entry_and_exit(
618        &self,
619        gid: GlyphId16,
620        anchors: &[&Anchor],
621    ) -> Result<(Option<AnchorBuilder>, Option<AnchorBuilder>), Error> {
622        let mut entry = None;
623        let mut exit = None;
624
625        for anchor in anchors {
626            match anchor.kind {
627                AnchorKind::CursiveEntry => {
628                    entry = Some(
629                        resolve_anchor_once(anchor, self.static_metadata)
630                            .map_err(|e| self.convert_delta_error(e, gid))?,
631                    );
632                }
633                AnchorKind::CursiveExit => {
634                    exit = Some(
635                        resolve_anchor_once(anchor, self.static_metadata)
636                            .map_err(|e| self.convert_delta_error(e, gid))?,
637                    );
638                }
639                _ => (),
640            }
641        }
642        Ok((entry, exit))
643    }
644
645    fn convert_delta_error(&self, err: DeltaError, gid: GlyphId16) -> Error {
646        let name = self.glyph_order.glyph_name(gid.into()).cloned().unwrap();
647        Error::AnchorDeltaError(name, err)
648    }
649
650    //https://github.com/googlefonts/ufo2ft/blob/5a606b7884bb6da/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L1119
651    //
652    // returns two sets: glyphs used in abvm/blwm, and glyphs used in mark
653    fn split_mark_and_abvm_blwm_glyphs(
654        &self,
655    ) -> Result<(IntSet<GlyphId16>, IntSet<GlyphId16>), Error> {
656        let scripts_using_abvm = scripts_using_abvm();
657        let fea_scripts = super::get_script_language_systems(&self.fea_first_pass.ast)
658            .into_keys()
659            .collect::<HashSet<_>>();
660        let filtered_scripts_using_abvm = (!fea_scripts.is_empty()).then(|| {
661            scripts_using_abvm
662                .intersection(&fea_scripts)
663                .copied()
664                .collect::<HashSet<_>>()
665        });
666        // if we had fea scripts, this is abvm scripts that are also in fea;
667        // otherwise it is all abvm scripts
668        let maybe_filtered = filtered_scripts_using_abvm
669            .as_ref()
670            .unwrap_or(&scripts_using_abvm);
671
672        // this returns Option<bool> to replicate the ternary logic of
673        // https://github.com/googlefonts/ufo2ft/blob/16ed156bd6a8b9bc/Lib/ufo2ft/util.py#L360
674        let unicode_is_abvm = |uv: u32| -> Option<bool> {
675            let mut saw_abvm = false;
676            for script in super::properties::scripts_for_codepoint(uv) {
677                if script == super::properties::COMMON_SCRIPT {
678                    return None;
679                }
680                // attn: this uses the _filtered_ abvm scripts:
681                saw_abvm |= maybe_filtered.contains(&script);
682            }
683            Some(saw_abvm)
684        };
685
686        // note that it's possible for a glyph to pass both these tests!
687        let unicode_is_non_abvm = |uv: u32| -> Option<bool> {
688            Some(
689                super::properties::scripts_for_codepoint(uv)
690                    // but this uses the unfiltered ones!
691                    .any(|script| !scripts_using_abvm.contains(&script)),
692            )
693        };
694
695        if scripts_using_abvm.is_empty()
696            || !self
697                .char_map
698                .keys()
699                .copied()
700                .any(|uv| unicode_is_abvm(uv).unwrap_or(false))
701        {
702            // no abvm scripts: everything is a mark
703            return Ok((
704                Default::default(),
705                self.glyph_order.iter().map(|(gid, _)| gid).collect(),
706            ));
707        }
708
709        let gsub = self.fea_first_pass.gsub();
710        let abvm_glyphs = super::properties::glyphs_matching_predicate(
711            &self.char_map,
712            unicode_is_abvm,
713            gsub.as_ref(),
714        )?;
715
716        let mut non_abvm_glyphs = super::properties::glyphs_matching_predicate(
717            &self.char_map,
718            unicode_is_non_abvm,
719            gsub.as_ref(),
720        )?;
721        // https://github.com/googlefonts/ufo2ft/blob/5a606b7884bb6da/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L1156
722        // TK: there's another bug here I think!? we can't trust char map, need
723        // to union with glyph set.
724        non_abvm_glyphs.extend(
725            self.glyph_order
726                .iter()
727                .map(|(gid, _)| gid)
728                .filter(|gid| !abvm_glyphs.contains(*gid)),
729        );
730        Ok((abvm_glyphs, non_abvm_glyphs))
731    }
732}
733
734// matching current fonttools behaviour, we treat treat every non-bottom as a top:
735// https://github.com/googlefonts/ufo2ft/blob/5a606b7884bb6da5/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L998
736fn is_above_mark(anchor_name: &GroupName) -> bool {
737    !is_below_mark(anchor_name)
738}
739
740fn is_below_mark(anchor_name: &GroupName) -> bool {
741    anchor_name.starts_with("bottom") || anchor_name == "nukta"
742}
743
744impl Work<Context, AnyWorkId, Error> for MarkWork {
745    fn id(&self) -> AnyWorkId {
746        WorkId::Marks.into()
747    }
748
749    fn read_access(&self) -> Access<AnyWorkId> {
750        AccessBuilder::new()
751            .variant(FeWorkId::StaticMetadata)
752            .variant(FeWorkId::GlyphOrder)
753            .variant(WorkId::FeaturesAst)
754            .variant(FeWorkId::ALL_ANCHORS)
755            .build()
756    }
757
758    /// Generate mark data structures.
759    fn exec(&self, context: &Context) -> Result<(), Error> {
760        let static_metadata = context.ir.static_metadata.get();
761        let glyph_order = context.ir.glyph_order.get();
762        let raw_anchors = context.ir.anchors.all();
763        let fea_first_pass = context.fea_ast.get();
764
765        let anchors = raw_anchors
766            .iter()
767            .map(|(_, anchors)| anchors.as_ref())
768            .collect::<Vec<_>>();
769
770        let glyphs = glyph_order
771            .names()
772            .map(|glyphname| context.ir.get_glyph(glyphname.clone()))
773            .collect::<Vec<_>>();
774
775        let char_map = glyphs
776            .iter()
777            .flat_map(|g| {
778                let id = glyph_order.glyph_id(&g.name).unwrap();
779                g.codepoints.iter().map(move |cp| (*cp, id))
780            })
781            .collect::<HashMap<_, _>>();
782
783        // this code is roughly equivalent to what in pythonland happens in
784        // setContext: https://github.com/googlefonts/ufo2ft/blob/8e9e6eb66a/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L322
785        let ctx = MarkLookupBuilder::new(
786            anchors,
787            &glyph_order,
788            &static_metadata,
789            &fea_first_pass,
790            char_map,
791        )?;
792        let all_marks = ctx.build()?;
793
794        context.fea_rs_marks.set(all_marks);
795
796        Ok(())
797    }
798}
799
800// in py this is set during _groupMarkGlyphsByAnchor; we try to match that logic
801// https://github.com/googlefonts/ufo2ft/blob/8e9e6eb66/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L412
802fn find_mark_glyphs(
803    anchors: &BTreeMap<GlyphId16, Vec<&Anchor>>,
804    gdef_classes: &HashMap<GlyphId16, GlyphClassDef>,
805) -> BTreeSet<GlyphId16> {
806    anchors
807        .iter()
808        .filter(|(name, anchors)| {
809            // if we have some classes, and this is not in the mark class:
810            // not a mark glyph.
811            if !gdef_classes.is_empty() && gdef_classes.get(*name) != Some(&GlyphClassDef::Mark) {
812                return false;
813            }
814            // if we have no classes, or this is in the mark class,
815            // then we just look for the presence of a mark anchor.
816            anchors.iter().any(|a| a.is_mark())
817        })
818        .map(|(name, _)| name.to_owned())
819        .collect()
820}
821
822fn resolve_anchor(
823    anchor: &BaseOrLigAnchors<&ir::Anchor>,
824    static_metadata: &StaticMetadata,
825) -> Result<BaseOrLigAnchors<AnchorBuilder>, DeltaError> {
826    match anchor {
827        BaseOrLigAnchors::Base(anchor) => {
828            resolve_anchor_once(anchor, static_metadata).map(BaseOrLigAnchors::Base)
829        }
830        BaseOrLigAnchors::Ligature(anchors) => anchors
831            .iter()
832            .map(|a| {
833                a.map(|a| resolve_anchor_once(a, static_metadata))
834                    .transpose()
835            })
836            .collect::<Result<_, _>>()
837            .map(BaseOrLigAnchors::Ligature),
838    }
839}
840
841fn resolve_anchor_once(
842    anchor: &ir::Anchor,
843    static_metadata: &StaticMetadata,
844) -> Result<AnchorBuilder, DeltaError> {
845    let (x_values, y_values): (Vec<_>, Vec<_>) = anchor
846        .positions
847        .iter()
848        .map(|(loc, pt)| {
849            (
850                (loc.clone(), OrderedFloat::from(pt.x)),
851                (loc.clone(), OrderedFloat::from(pt.y)),
852            )
853        })
854        .unzip();
855
856    let (x_default, x_deltas) = crate::features::resolve_variable_metric(
857        static_metadata,
858        // If I do map(|(pos, value)| (pos, value)) to destructure the tuple and
859        // convert &(T, V) => (&T, &V) as the values parameter expects, then
860        // clippy complains about the seemingly no-op identity map:
861        // https://rust-lang.github.io/rust-clippy/master/index.html#/map_identity
862        x_values.iter().map(|item| (&item.0, &item.1)),
863    )?;
864    let (y_default, y_deltas) = crate::features::resolve_variable_metric(
865        static_metadata,
866        y_values.iter().map(|item| (&item.0, &item.1)),
867    )?;
868
869    let mut anchor = AnchorBuilder::new(x_default, y_default);
870    if x_deltas.iter().any(|v| v.1 != 0) {
871        anchor = anchor.with_x_device(x_deltas);
872    }
873    if y_deltas.iter().any(|v| v.1 != 0) {
874        anchor = anchor.with_y_device(y_deltas);
875    }
876
877    Ok(anchor)
878}
879
880// equivalent to
881// <https://github.com/googlefonts/ufo2ft/blob/bb79cae53f/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py#L56>
882// the LigCaretList in GDEF is not universally used, but is used by at least CoreText
883// and LibreOffice.
884fn get_ligature_carets(
885    glyph_order: &GlyphOrder,
886    static_metadata: &StaticMetadata,
887    anchors: &[&GlyphAnchors],
888) -> Result<BTreeMap<GlyphId16, Vec<CaretValueBuilder>>, Error> {
889    let mut out = BTreeMap::new();
890
891    for glyph_anchor in anchors {
892        let Some(gid) = glyph_order.glyph_id(&glyph_anchor.glyph_name) else {
893            continue;
894        };
895        let carets = glyph_anchor
896            .anchors
897            .iter()
898            .filter_map(|anchor| {
899                make_caret_value(anchor, static_metadata, &glyph_anchor.glyph_name).transpose()
900            })
901            .collect::<Result<Vec<_>, _>>()?;
902        if !carets.is_empty() {
903            out.insert(gid, carets);
904        }
905    }
906    Ok(out)
907}
908
909fn make_caret_value(
910    anchor: &ir::Anchor,
911    static_metadata: &StaticMetadata,
912    glyph_name: &GlyphName,
913) -> Result<Option<CaretValueBuilder>, Error> {
914    if !matches!(anchor.kind, AnchorKind::Caret(_) | AnchorKind::VCaret(_)) {
915        return Ok(None);
916    }
917    let is_vertical = matches!(anchor.kind, AnchorKind::VCaret(_));
918
919    let values = anchor
920        .positions
921        .iter()
922        .map(|(loc, pt)| {
923            let pos = if is_vertical { pt.y } else { pt.x };
924            (loc.clone(), OrderedFloat::from(pos))
925        })
926        .collect::<Vec<_>>();
927
928    let (default, mut deltas) = crate::features::resolve_variable_metric(
929        static_metadata,
930        values.iter().map(|item| (&item.0, &item.1)),
931    )
932    .map_err(|err| Error::AnchorDeltaError(glyph_name.to_owned(), err))?;
933
934    // don't bother encoding all zero deltas
935    if deltas.iter().all(|d| d.1 == 0) {
936        deltas.clear();
937    }
938
939    Ok(Some(CaretValueBuilder::Coordinate {
940        default,
941        deltas: deltas.into(),
942    }))
943}
944
945impl FeatureProvider for FeaRsMarks {
946    fn add_features(&self, builder: &mut fea_rs::compile::FeatureBuilder) {
947        // a little helper reused for abvm/blwm
948        fn add_all_lookups(
949            builder: &mut fea_rs::compile::FeatureBuilder,
950            lookups: &MarkLookups,
951        ) -> Vec<LookupId> {
952            let mut out = Vec::new();
953            out.extend(
954                lookups
955                    .mark_base
956                    .iter()
957                    .map(|lk| builder.add_lookup(lk.clone())),
958            );
959            out.extend(
960                lookups
961                    .mark_lig
962                    .iter()
963                    .map(|lk| builder.add_lookup(lk.clone())),
964            );
965            out.extend(
966                lookups
967                    .mark_mark
968                    .iter()
969                    .map(|lk| builder.add_lookup(lk.clone())),
970            );
971            out
972        }
973
974        // add these first, matching fontmake
975        let abvm_lookups = add_all_lookups(builder, &self.abvm);
976        let blwm_lookups = add_all_lookups(builder, &self.blwm);
977
978        let mut mark_lookups = Vec::new();
979        let mut mkmk_lookups = Vec::new();
980        let mut curs_lookups = Vec::new();
981
982        for mark_base in self.mark_mkmk.mark_base.iter() {
983            // each mark to base it's own lookup, whch differs from fontmake
984            mark_lookups.push(builder.add_lookup(mark_base.clone()));
985        }
986        for mark_lig in self.mark_mkmk.mark_lig.iter() {
987            mark_lookups.push(builder.add_lookup(mark_lig.clone()));
988        }
989
990        // If a mark has anchors that are themselves marks what we got here is a mark to mark
991        for mark_mark in self.mark_mkmk.mark_mark.iter() {
992            mkmk_lookups.push(builder.add_lookup(mark_mark.clone()));
993        }
994
995        for curs in self.curs.iter() {
996            curs_lookups.push(builder.add_lookup(curs.clone()));
997        }
998
999        for (lookups, tag) in [
1000            (mark_lookups, MARK),
1001            (mkmk_lookups, MKMK),
1002            (curs_lookups, CURS),
1003            (abvm_lookups, ABVM),
1004            (blwm_lookups, BLWM),
1005        ] {
1006            if !lookups.is_empty() {
1007                builder.add_to_default_language_systems(tag, &lookups);
1008            }
1009        }
1010
1011        if !self.lig_carets.is_empty()
1012            && builder
1013                .gdef()
1014                .map(|gdef| gdef.ligature_pos.is_empty())
1015                .unwrap_or(true)
1016        {
1017            builder.add_lig_carets(self.lig_carets.clone());
1018        }
1019    }
1020}
1021
1022//https://github.com/googlefonts/ufo2ft/blob/16ed156bd/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L338
1023fn scripts_using_abvm() -> HashSet<UnicodeShortName> {
1024    INDIC_SCRIPTS
1025        .iter()
1026        .chain(USE_SCRIPTS)
1027        .chain(std::iter::once(&"Khmr"))
1028        .filter_map(|s| UnicodeShortName::try_from_str(s).ok())
1029        .collect()
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use fea_rs::compile::Compilation;
1035    use fontdrasil::{
1036        agl,
1037        coords::{Coord, CoordConverter, NormalizedLocation},
1038        types::Axis,
1039    };
1040    use fontir::ir::{GdefCategories, NamedInstance};
1041    use kurbo::Point;
1042
1043    use write_fonts::{
1044        dump_table,
1045        read::{
1046            tables::{gdef::Gdef as RGdef, gpos::Gpos as RGpos},
1047            FontRead,
1048        },
1049        tables::gdef::CaretValue as RawCaretValue,
1050    };
1051
1052    use crate::features::test_helpers::{LayoutOutput, LayoutOutputBuilder};
1053
1054    use super::*;
1055
1056    fn char_for_glyph(name: &GlyphName) -> Option<char> {
1057        static MANUAL: &[(&str, char)] = &[
1058            ("brevecomb", '\u{0306}'),
1059            ("breveinvertedcomb", '\u{0311}'),
1060            ("macroncomb", '\u{0304}'),
1061            ("f_f", '\u{FB00}'),
1062            ("f_i", '\u{FB01}'),
1063            ("f_l", '\u{FB02}'),
1064            ("f_i_i", '\u{FB03}'),
1065            ("hamza", '\u{0621}'),
1066            ("dottedCircle", '\u{25CC}'),
1067            ("nukta-kannada", '\u{0CBC}'),
1068            ("candrabindu-kannada", '\u{0C81}'),
1069            ("halant-kannada", '\u{0CCD}'),
1070            ("ka-kannada", '\u{0C95}'),
1071            ("taonethousand", '\u{0BF2}'),
1072            ("uni25CC", '\u{25CC}'),
1073        ];
1074
1075        static UNMAPPED: &[&str] = &["ka-kannada.base", "a.alt"];
1076
1077        let c = agl::char_for_agl_name(name.as_str()).or_else(|| {
1078            MANUAL
1079                .iter()
1080                .find_map(|(n, uv)| (name.as_str() == *n).then_some(*uv))
1081        });
1082        if c.is_none() && !UNMAPPED.iter().any(|except| *except == name.as_str()) {
1083            panic!("please add a manual charmap entry for '{name}'");
1084        }
1085        c
1086    }
1087
1088    /// A helper for testing our mark generation code
1089    #[derive(Clone, Debug)]
1090    struct MarksInput<const N: usize> {
1091        prefer_gdef_categories_in_fea: bool,
1092        locations: [NormalizedLocation; N],
1093        anchors: BTreeMap<GlyphName, Vec<Anchor>>,
1094        categories: BTreeMap<GlyphName, GlyphClassDef>,
1095        char_map: HashMap<u32, GlyphName>,
1096        user_fea: &'static str,
1097    }
1098
1099    struct AnchorBuilder<const N: usize> {
1100        locations: [NormalizedLocation; N],
1101        anchors: Vec<Anchor>,
1102    }
1103
1104    impl Default for MarksInput<1> {
1105        fn default() -> Self {
1106            Self::new([&[0.0]])
1107        }
1108    }
1109
1110    // when describing an anchor position we almost always use integers, but
1111    // for some tests we want to accept floats so we can verify rounding behaviour.
1112    // we define our own trait and impl it for only these two types so that the
1113    // compiler doesn't need additional type info when we use int/float literals.
1114    trait F32OrI16 {
1115        fn to_f64(self) -> f64;
1116    }
1117
1118    impl F32OrI16 for f32 {
1119        fn to_f64(self) -> f64 {
1120            self as _
1121        }
1122    }
1123
1124    impl F32OrI16 for i16 {
1125        fn to_f64(self) -> f64 {
1126            self as _
1127        }
1128    }
1129
1130    impl<const N: usize> AnchorBuilder<N> {
1131        /// Add a new anchor, with positions defined for each of our locations
1132        ///
1133        /// The 'name' should be a raw anchor name, and should be known-good.
1134        /// Anchor name tests are in `fontir`.
1135        fn add<T: F32OrI16>(&mut self, anchor_name: &str, pos: [(T, T); N]) -> &mut Self {
1136            let positions = pos
1137                .into_iter()
1138                .enumerate()
1139                .map(|(i, pt)| {
1140                    (
1141                        self.locations[i].clone(),
1142                        Point::new(pt.0.to_f64(), pt.1.to_f64()),
1143                    )
1144                })
1145                .collect();
1146            let kind = AnchorKind::new(anchor_name).unwrap();
1147            self.anchors.push(Anchor { kind, positions });
1148            self
1149        }
1150    }
1151
1152    impl<const N: usize> MarksInput<N> {
1153        /// Create test input with `N` locations
1154        fn new(locations: [&[f64]; N]) -> Self {
1155            const TAG_NAMES: [Tag; 3] = [Tag::new(b"axs1"), Tag::new(b"axs2"), Tag::new(b"axs3")];
1156            let locations = locations
1157                .iter()
1158                .map(|loc| {
1159                    loc.iter()
1160                        .enumerate()
1161                        .map(|(i, pos)| {
1162                            (
1163                                *TAG_NAMES.get(i).expect("too many axes in test"),
1164                                Coord::new(*pos),
1165                            )
1166                        })
1167                        .collect()
1168                })
1169                .collect::<Vec<_>>()
1170                .try_into()
1171                .unwrap();
1172            Self {
1173                user_fea: "languagesystem DFLT dflt;",
1174                locations,
1175                anchors: Default::default(),
1176                categories: Default::default(),
1177                char_map: Default::default(),
1178                prefer_gdef_categories_in_fea: false,
1179            }
1180        }
1181
1182        /// Provide custom user FEA.
1183        ///
1184        /// By default we use a single 'languagesytem DFLT dflt' statement.
1185        fn set_user_fea(&mut self, fea: &'static str) -> &mut Self {
1186            self.user_fea = fea;
1187            self
1188        }
1189
1190        /// Set whether or not to prefer GDEF categories defined in FEA vs in metadata
1191        ///
1192        /// this is 'true' for UFO sources and 'false' for glyphs sources; default
1193        /// here is false.
1194        fn set_prefer_fea_gdef_categories(&mut self, flag: bool) -> &mut Self {
1195            self.prefer_gdef_categories_in_fea = flag;
1196            self
1197        }
1198
1199        /// Add a glyph with an optional GDEF category.
1200        ///
1201        /// the `anchors_fn` argument is a closure where anchors can be added
1202        /// to the newly added glyph.
1203        fn add_glyph(
1204            &mut self,
1205            name: &str,
1206            category: impl Into<Option<GlyphClassDef>>,
1207            mut anchors_fn: impl FnMut(&mut AnchorBuilder<N>),
1208        ) -> &mut Self {
1209            let name: GlyphName = name.into();
1210            if let Some(unic) = char_for_glyph(&name) {
1211                self.char_map.insert(unic as u32, name.clone());
1212            }
1213
1214            if let Some(category) = category.into() {
1215                self.categories.insert(name.clone(), category);
1216            }
1217            let mut anchors = AnchorBuilder {
1218                locations: self.locations.clone(),
1219                anchors: Default::default(),
1220            };
1221
1222            anchors_fn(&mut anchors);
1223            self.anchors.insert(name, anchors.anchors);
1224            self
1225        }
1226
1227        fn make_layout_output(&self) -> LayoutOutput {
1228            let (min, default, max) = (Coord::new(-1.), Coord::new(0.0), Coord::new(1.0));
1229            let axes = self.locations[0]
1230                .axis_tags()
1231                .map(|tag| Axis {
1232                    name: tag.to_string(),
1233                    tag: *tag,
1234                    min,
1235                    default,
1236                    max,
1237                    hidden: false,
1238                    converter: CoordConverter::unmapped(min, default, max),
1239                    localized_names: Default::default(),
1240                })
1241                .collect();
1242            let named_instances = self
1243                .locations
1244                .iter()
1245                .enumerate()
1246                .map(|(i, loc)| NamedInstance {
1247                    name: format!("instance{i}"),
1248                    postscript_name: None,
1249                    location: loc.to_user(&axes),
1250                })
1251                .collect();
1252            let glyph_locations = self.locations.iter().cloned().collect();
1253            let categories = GdefCategories {
1254                categories: self.categories.clone(),
1255                prefer_gdef_categories_in_fea: self.prefer_gdef_categories_in_fea,
1256            };
1257            LayoutOutputBuilder::new()
1258                .with_axes(axes)
1259                .with_instances(named_instances)
1260                .with_locations(glyph_locations)
1261                .with_categories(categories)
1262                .with_user_fea(self.user_fea)
1263                .with_glyph_order(self.anchors.keys().cloned().collect())
1264                .build()
1265        }
1266
1267        // you can pass in a closure and look at the builder; this is useful
1268        // for at least one test
1269        fn compile_and_inspect(&self, f: impl FnOnce(&MarkLookupBuilder)) -> Compilation {
1270            let layout_output = self.make_layout_output();
1271            let anchors = self
1272                .anchors
1273                .iter()
1274                .map(|(name, anchors)| GlyphAnchors::new(name.clone(), anchors.clone()))
1275                .collect::<Vec<_>>();
1276            let anchorsref = anchors.iter().collect();
1277            let char_map = self
1278                .char_map
1279                .iter()
1280                .map(|(uv, name)| (*uv, layout_output.glyph_order.glyph_id(name).unwrap()))
1281                .collect();
1282
1283            let ctx = MarkLookupBuilder::new(
1284                anchorsref,
1285                &layout_output.glyph_order,
1286                &layout_output.static_metadata,
1287                &layout_output.first_pass_fea,
1288                char_map,
1289            )
1290            .unwrap();
1291
1292            f(&ctx);
1293
1294            let marks = ctx.build().unwrap();
1295            layout_output.compile(&marks)
1296        }
1297
1298        // a thin wrapper, this is what most tests want to use
1299        fn compile(&self) -> Compilation {
1300            self.compile_and_inspect(|_| {})
1301        }
1302
1303        /// Build the GPOS & GDEF tables and get a textual representation
1304        fn get_normalized_output(&mut self) -> String {
1305            let result = self.compile();
1306            // okay, so now we have some write-fonts tables; convert to read-fonts
1307            let Some(gpos) = result.gpos else {
1308                return String::new();
1309            };
1310            let gpos_bytes = write_fonts::dump_table(&gpos).unwrap();
1311            let gdef_bytes = result.gdef.map(|gdef| dump_table(&gdef).unwrap());
1312            let gpos = RGpos::read(gpos_bytes.as_slice().into()).unwrap();
1313            let gdef = gdef_bytes
1314                .as_ref()
1315                .map(|b| RGdef::read(b.as_slice().into()).unwrap());
1316            let mut buf = Vec::new();
1317            let names = self.anchors.keys().cloned().collect();
1318
1319            // and pass these to layout normalizer
1320            otl_normalizer::print_gpos(&mut buf, &gpos, gdef.as_ref(), &names).unwrap();
1321            String::from_utf8(buf).unwrap()
1322        }
1323    }
1324
1325    // does some cleanup so that we don't need to worry about indentation when comparing strings
1326    fn normalize_layout_repr(s: &str) -> String {
1327        s.trim().chars().filter(|c| *c != ' ').collect()
1328    }
1329    macro_rules! assert_eq_ignoring_ws {
1330        ($left:expr, $right:expr) => {
1331            let left = normalize_layout_repr(&$left);
1332            let right = normalize_layout_repr(&$right);
1333            pretty_assertions::assert_str_eq!(left, right)
1334        };
1335    }
1336
1337    // a test font used in a bunch of python tests:
1338    // <https://github.com/googlefonts/ufo2ft/blob/779bbad84/tests/featureWriters/markFeatureWriter_test.py#L21>
1339    fn pytest_ufo() -> MarksInput<1> {
1340        let mut builder = MarksInput::default();
1341        builder
1342            .add_glyph("a", None, |anchors| {
1343                anchors.add("top", [(100, 200)]);
1344            })
1345            .add_glyph("f_i", None, |anchors| {
1346                anchors.add("top_1", [(100, 500)]);
1347                anchors.add("top_2", [(600, 500)]);
1348            })
1349            .add_glyph("acutecomb", None, |anchors| {
1350                anchors.add("_top", [(100, 200)]);
1351            })
1352            .add_glyph("tildecomb", None, |anchors| {
1353                anchors.add("_top", [(100, 200)]);
1354                anchors.add("top", [(100, 300)]);
1355            });
1356        builder
1357    }
1358
1359    fn simple_test_input() -> MarksInput<1> {
1360        let mut out = MarksInput::default();
1361        out.add_glyph("A", GlyphClassDef::Base, |anchors| {
1362            anchors.add("top", [(100, 400)]);
1363        })
1364        .add_glyph("acutecomb", GlyphClassDef::Mark, |anchors| {
1365            anchors.add("_top", [(50, 50)]);
1366        });
1367        out
1368    }
1369
1370    // sanity check that if we don't make empty lookups
1371    #[test]
1372    fn no_anchors_no_feature() {
1373        let out = MarksInput::default()
1374            .add_glyph("a", None, |_| {})
1375            .add_glyph("acutecomb", None, |_| {})
1376            .get_normalized_output();
1377
1378        assert!(out.is_empty());
1379    }
1380
1381    #[test]
1382    fn attach_a_mark_to_a_base() {
1383        let out = simple_test_input().get_normalized_output();
1384        assert_eq_ignoring_ws!(
1385            out,
1386            r#"
1387            # mark: DFLT/dflt
1388            # 1 MarkToBase rules
1389            # lookupflag LookupFlag(0)
1390            A @(x: 100, y: 400)
1391              @(x: 50, y: 50) acutecomb
1392            "#
1393        );
1394    }
1395
1396    // https://github.com/googlefonts/ufo2ft/blob/779bbad84a/tests/featureWriters/markFeatureWriter_test.py#L611
1397    #[test]
1398    fn mark_mkmk_features() {
1399        let out = pytest_ufo().get_normalized_output();
1400        assert_eq_ignoring_ws!(
1401            out,
1402            r#"
1403            # mark: DFLT/dflt
1404            # 1 MarkToBase rules
1405            # lookupflag LookupFlag(0)
1406            a @(x: 100, y: 200)
1407              @(x: 100, y: 200) [acutecomb, tildecomb]
1408            # 1 MarkToLig rules
1409            # lookupflag LookupFlag(0)
1410            f_i (lig) [@(x: 100, y: 500), @(x: 600, y: 500)]
1411              @(x: 100, y: 200) [acutecomb, tildecomb]
1412
1413            # mkmk: DFLT/dflt
1414            # 1 MarkToMark rules
1415            # lookupflag LookupFlag(16)
1416            # filter glyphs: [acutecomb, tildecomb]
1417            tildecomb @(x: 100, y: 300)
1418              @(x: 100, y: 200) [acutecomb, tildecomb]
1419            "#
1420        );
1421    }
1422
1423    // reduced from testdata/glyphs3/Oswald-AE-comb
1424    //
1425    // this ends up mostly being a test about how we generate mark filtering sets..
1426    #[test]
1427    fn oswald_ae_test_case() {
1428        let mut input = MarksInput::default();
1429        let out = input
1430            .add_glyph("A", None, |anchors| {
1431                anchors
1432                    .add("bottom", [(234, 0)])
1433                    .add("ogonek", [(411, 0)])
1434                    .add("top", [(234, 810)]);
1435            })
1436            .add_glyph("E", None, |anchors| {
1437                anchors
1438                    .add("topleft", [(20, 810)])
1439                    .add("bottom", [(216, 0)])
1440                    .add("ogonek", [(314, 0)])
1441                    .add("top", [(217, 810)]);
1442            })
1443            .add_glyph("acutecomb", None, |anchors| {
1444                anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1445            })
1446            .add_glyph("brevecomb", None, |anchors| {
1447                anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1448            })
1449            .add_glyph("tildecomb", None, |anchors| {
1450                anchors.add("top", [(0, 742)]).add("_top", [(0, 578)]);
1451            })
1452            .add_glyph("macroncomb", None, |anchors| {
1453                anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1454            })
1455            .add_glyph("breveinvertedcomb", None, |anchors| {
1456                anchors
1457                    .add("top", [(0, 810)])
1458                    .add("top.a", [(0, 810)])
1459                    .add("_top.a", [(0, 578)]);
1460            })
1461            .get_normalized_output();
1462
1463        assert_eq_ignoring_ws!(
1464            out,
1465            // this gets an abvm feature because we don't specify any languagesystems
1466            r#"
1467            # abvm: DFLT/dflt
1468            # 2 MarkToMark rules
1469            # lookupflag LookupFlag(16)
1470            # filter glyphs: [acutecomb,macroncomb]
1471            acutecomb @(x: 0, y: 810)
1472              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1473            macroncomb @(x: 0, y: 810)
1474              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1475
1476            # mark: DFLT/dflt
1477            # 2 MarkToBase rules
1478            # lookupflag LookupFlag(0)
1479            A @(x: 234, y: 810)
1480              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1481            E @(x: 217, y: 810)
1482              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1483
1484            # mkmk: DFLT/dflt
1485            # 6 MarkToMark rules
1486            # lookupflag LookupFlag(16)
1487            # filter glyphs: [acutecomb,brevecomb,breveinvertedcomb,macroncomb,tildecomb]
1488            acutecomb @(x: 0, y: 810)
1489              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1490            brevecomb @(x: 0, y: 810)
1491              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1492            breveinvertedcomb @(x: 0, y: 810)
1493              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1494            # filter glyphs: [breveinvertedcomb]
1495            breveinvertedcomb @(x: 0, y: 810)
1496              @(x: 0, y: 578) breveinvertedcomb
1497            # filter glyphs: [acutecomb,brevecomb,breveinvertedcomb,macroncomb,tildecomb]
1498            macroncomb @(x: 0, y: 810)
1499              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1500            tildecomb @(x: 0, y: 742)
1501              @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1502
1503            "#
1504        );
1505    }
1506
1507    #[test]
1508    fn abvm_blwm_features() {
1509        let mut input = MarksInput::default();
1510        let out = input
1511            .add_glyph("dottedCircle", None, |anchors| {
1512                anchors
1513                    .add("top", [(297, 552)])
1514                    .add("topright", [(491, 458)])
1515                    .add("bottom", [(297, 0)]);
1516            })
1517            .add_glyph("nukta-kannada", None, |anchors| {
1518                anchors.add("_bottom", [(0, 0)]);
1519            })
1520            .add_glyph("candrabindu-kannada", None, |anchors| {
1521                anchors.add("_top", [(0, 547)]);
1522            })
1523            .add_glyph("halant-kannada", None, |anchors| {
1524                anchors.add("_topright", [(-456, 460)]);
1525            })
1526            .add_glyph("ka-kannada", None, |anchors| {
1527                anchors.add("bottom", [(290, 0)]);
1528            })
1529            .add_glyph("ka-kannada.base", None, |anchors| {
1530                anchors
1531                    .add("top", [(291, 547)])
1532                    .add("topright", [(391, 460)])
1533                    .add("bottom", [(290, 0)]);
1534            })
1535            .set_user_fea(
1536                "
1537            languagesystem DFLT dflt;
1538            languagesystem knda dflt;
1539            languagesystem knd2 dflt;
1540
1541            feature psts {
1542                sub ka-kannada' halant-kannada by ka-kannada.base;
1543            } psts;",
1544            )
1545            .get_normalized_output();
1546
1547        assert_eq_ignoring_ws!(
1548            out,
1549            r#"
1550                # abvm: DFLT/dflt, knd2/dflt, knda/dflt
1551                # 2 MarkToBase rules
1552                # lookupflag LookupFlag(0)
1553                ka-kannada.base @(x: 291, y: 547)
1554                  @(x: 0, y: 547) candrabindu-kannada
1555                ka-kannada.base @(x: 391, y: 460)
1556                  @(x: -456, y: 460) halant-kannada
1557
1558                # blwm: DFLT/dflt, knd2/dflt, knda/dflt
1559                # 2 MarkToBase rules
1560                # lookupflag LookupFlag(0)
1561                ka-kannada @(x: 290, y: 0)
1562                  @(x: 0, y: 0) nukta-kannada
1563                ka-kannada.base @(x: 290, y: 0)
1564                  @(x: 0, y: 0) nukta-kannada
1565
1566                # mark: DFLT/dflt, knd2/dflt,knda/dflt
1567                # 3 MarkToBase rules
1568                # lookupflag LookupFlag(0)
1569                dottedCircle @(x: 297, y: 0)
1570                  @(x: 0, y: 0) nukta-kannada
1571                dottedCircle @(x: 297, y: 552)
1572                  @(x: 0, y: 547) candrabindu-kannada
1573                dottedCircle @(x: 491, y: 458)
1574                  @(x: -456, y: 460) halant-kannada
1575
1576                "#
1577        );
1578    }
1579
1580    #[test]
1581    fn include_unmapped_glyph_with_no_abvm() {
1582        // make sure that we are including all glyphs (even those only reachable
1583        // via GSUB closure) in the case where we do not have any abvm glyphs
1584        let mut out = MarksInput::default();
1585        let out = out
1586            // a.alt is only reachable via substitution
1587            .set_user_fea(
1588                "languagesystem latn dflt;
1589        feature test {
1590            sub a by a.alt;
1591        } test;",
1592            )
1593            .add_glyph("a", None, |anchors| {
1594                anchors.add("top", [(100, 0)]);
1595            })
1596            .add_glyph("a.alt", None, |anchors| {
1597                anchors.add("top", [(110, 0)]);
1598            })
1599            .add_glyph("acutecomb", None, |anchors| {
1600                anchors.add("_top", [(202, 0)]);
1601            })
1602            .get_normalized_output();
1603
1604        assert_eq_ignoring_ws!(
1605            out,
1606            r#"
1607            # mark: latn/dflt
1608            # 2 MarkToBase rules
1609            # lookupflag LookupFlag(0)
1610            a @(x: 100, y: 0)
1611              @(x: 202, y: 0) acutecomb
1612            a.alt @(x: 110, y: 0)
1613              @(x: 202, y: 0) acutecomb
1614          "#
1615        );
1616    }
1617
1618    #[test]
1619    fn include_unmapped_non_abvm_glyph_with_abvm() {
1620        // make sure that we are including all glyphs non-abvm glyphs (even
1621        // those only reachable via GSUB closure) in the case where we have
1622        // both abvm & non-abvm glyphs
1623        let mut out = MarksInput::default();
1624        out
1625            // a.alt is only reachable via substitution
1626            .set_user_fea(
1627                "languagesystem latn dflt;
1628                languagesystem knda dflt;
1629        ",
1630            )
1631            // this glyph is unreachable
1632            .add_glyph("ka-kannada.base", None, |_| {})
1633            .add_glyph("nukta-kannada", None, |_| {})
1634            .compile_and_inspect(|ctx| {
1635                let (abvm, non_abvm) = ctx.split_mark_and_abvm_blwm_glyphs().unwrap();
1636                let nukta = ctx.glyph_order.glyph_id("nukta-kannada").unwrap();
1637                let ka = ctx.glyph_order.glyph_id("ka-kannada.base").unwrap();
1638                assert!(abvm.contains(nukta));
1639                // all unreachable glyphs get stuffed into non-abvm
1640                // (although maybe this can change in the future, and we
1641                // can just drop them?)
1642                assert!(non_abvm.contains(ka));
1643            });
1644    }
1645    #[test]
1646    fn custom_fea() {
1647        let out = simple_test_input()
1648            .set_user_fea("languagesystem latn dflt;")
1649            .get_normalized_output();
1650        assert_eq_ignoring_ws!(
1651            out,
1652            r#"
1653            # mark: latn/dflt
1654            # 1 MarkToBase rules
1655            # lookupflag LookupFlag(0)
1656            A @(x: 100, y: 400)
1657              @(x: 50, y: 50) acutecomb
1658            "#
1659        );
1660    }
1661
1662    // shared between two tests below
1663    fn gdef_test_input() -> MarksInput<1> {
1664        let mut out = simple_test_input();
1665        out.set_user_fea(
1666            r#"
1667            @Bases = [A];
1668            @Marks = [acutecomb];
1669            table GDEF {
1670                GlyphClassDef @Bases, [], @Marks,;
1671            } GDEF;
1672            "#,
1673        )
1674        // is not in the FEA classes defined above
1675        .add_glyph("gravecomb", GlyphClassDef::Mark, |anchors| {
1676            anchors.add("_top", [(5, 15)]);
1677        });
1678        out
1679    }
1680
1681    #[test]
1682    fn prefer_fea_gdef_categories_true() {
1683        let out = gdef_test_input()
1684            .set_prefer_fea_gdef_categories(true)
1685            .get_normalized_output();
1686        assert_eq_ignoring_ws!(
1687            out,
1688            r#"
1689            # mark: DFLT/dflt
1690            # 1 MarkToBase rules
1691            # lookupflag LookupFlag(0)
1692            A @(x: 100, y: 400)
1693              @(x: 50, y: 50) acutecomb
1694            "#
1695        );
1696    }
1697
1698    #[test]
1699    fn prefer_fea_gdef_categories_false() {
1700        let out = gdef_test_input()
1701            .set_prefer_fea_gdef_categories(false)
1702            .get_normalized_output();
1703        assert_eq_ignoring_ws!(
1704            out,
1705            r#"
1706            # mark: DFLT/dflt
1707            # 1 MarkToBase rules
1708            # lookupflag LookupFlag(0)
1709            A @(x: 100, y: 400)
1710              @(x: 5, y: 15) gravecomb
1711              @(x: 50, y: 50) acutecomb
1712            "#
1713        );
1714    }
1715
1716    // reproduces a real failure, where we would incorrectly classify this glyph
1717    // as both abvm & not-abvm.
1718    #[test]
1719    fn non_abvm_glyphs_use_unfiltered_scripts() {
1720        let _ = MarksInput::default()
1721            .set_user_fea(
1722                r#"
1723                languagesystem DFLT dflt;
1724                languagesystem sinh dflt;
1725                languagesystem taml dflt;
1726                "#,
1727            )
1728            .add_glyph("taonethousand", None, |_| {})
1729            .compile_and_inspect(|builder| {
1730                let taonethousand = builder.glyph_order.glyph_id("taonethousand").unwrap();
1731                let (abvm, non_abvm) = builder.split_mark_and_abvm_blwm_glyphs().unwrap();
1732                assert!(abvm.contains(taonethousand));
1733                assert!(!non_abvm.contains(taonethousand));
1734            });
1735    }
1736
1737    #[test]
1738    fn glyph_in_abvm_but_not_if_no_abvm_lang() {
1739        let dotbelowcomb_char = char_for_glyph(&GlyphName::new("dotbelowcomb")).unwrap();
1740        let dotbelow_scripts =
1741            super::super::properties::scripts_for_codepoint(dotbelowcomb_char as _)
1742                .collect::<Vec<_>>();
1743        let abvm_scripts = super::scripts_using_abvm();
1744
1745        // this codepoint is used in both abvm and non-abvm scripts
1746        assert!(dotbelow_scripts.iter().any(|sc| abvm_scripts.contains(sc)));
1747        assert!(dotbelow_scripts.iter().any(|sc| !abvm_scripts.contains(sc)));
1748
1749        let _ = MarksInput::default()
1750            // but if the file contains only non-abvm scripts,
1751            .set_user_fea(
1752                r#"
1753                languagesystem DFLT dflt;
1754                languagesystem latn dflt;
1755                "#,
1756            )
1757            .add_glyph("dotbelowcomb", None, |_| {})
1758            .compile_and_inspect(|builder| {
1759                let dotbelowcomb = builder.glyph_order.glyph_id("dotbelowcomb").unwrap();
1760                let (abvm, non_abvm) = builder.split_mark_and_abvm_blwm_glyphs().unwrap();
1761                // it should only be in the non-abvm set.
1762                assert!(!abvm.contains(dotbelowcomb));
1763                assert!(non_abvm.contains(dotbelowcomb));
1764            });
1765    }
1766
1767    #[test]
1768    fn abvm_closure_excludes_glyphs_with_common_script() {
1769        let uni25cc = char_for_glyph(&GlyphName::new("uni25CC")).unwrap();
1770        assert_eq!(
1771            super::super::properties::scripts_for_codepoint(uni25cc as _).collect::<Vec<_>>(),
1772            [super::super::properties::COMMON_SCRIPT]
1773        );
1774        let _ = MarksInput::default()
1775            .set_user_fea(
1776                "languagesystem latn dflt;
1777                languagesystem knda dflt;
1778
1779                feature derp {
1780                    sub ka-kannada by uni25CC;
1781                } derp;
1782        ",
1783            )
1784            .add_glyph("ka-kannada", None, |_| {})
1785            .add_glyph("uni25CC", None, |_| {})
1786            .compile_and_inspect(|ctx| {
1787                let (abvm, non_abvm) = ctx.split_mark_and_abvm_blwm_glyphs().unwrap();
1788                // even though this is reachable by substitution from an abvm glyph,
1789                // we don't want it to go in abvm
1790                let uni25cc = ctx.glyph_order.glyph_id("uni25CC").unwrap();
1791                let ka = ctx.glyph_order.glyph_id("ka-kannada").unwrap();
1792                assert!(!abvm.contains(uni25cc));
1793                assert!(abvm.contains(ka));
1794                assert!(non_abvm.contains(uni25cc));
1795            });
1796    }
1797
1798    //https://github.com/googlefonts/ufo2ft/blob/779bbad84a/tests/featureWriters/markFeatureWriter_test.py#L1438
1799    #[test]
1800    fn multiple_anchor_classes_base() {
1801        let out = MarksInput::default()
1802            .add_glyph("a", None, |anchors| {
1803                anchors.add("topA", [(515, 581)]);
1804            })
1805            .add_glyph("e", None, |anchors| {
1806                anchors.add("topE", [(-21, 396)]);
1807            })
1808            .add_glyph("acutecomb", None, |anchors| {
1809                anchors
1810                    .add("_topA", [(-175, 589)])
1811                    .add("_topE", [(-175, 572)]);
1812            })
1813            .get_normalized_output();
1814
1815        assert_eq_ignoring_ws!(
1816            out,
1817            r#"
1818                # mark: DFLT/dflt
1819                # 2 MarkToBase rules
1820                # lookupflag LookupFlag(0)
1821                a @(x: 515, y: 581)
1822                  @(x: -175, y: 589) acutecomb
1823                e @(x: -21, y: 396)
1824                  @(x: -175, y: 572) acutecomb
1825                "#
1826        );
1827    }
1828
1829    // https://github.com/googlefonts/ufo2ft/blob/779bbad84a/tests/featureWriters/markFeatureWriter_test.py#L175
1830    #[test]
1831    fn ligature_null_anchor() {
1832        let out = MarksInput::default()
1833            .add_glyph("f_i_i", None, |anchors| {
1834                anchors
1835                    .add("top_1", [(250, 600)])
1836                    .add("top_2", [(500, 600)])
1837                    .add("_3", [(0, 0)]);
1838            })
1839            .add_glyph("acutecomb", None, |anchors| {
1840                anchors.add("_top", [(100, 200)]);
1841            })
1842            .get_normalized_output();
1843
1844        assert_eq_ignoring_ws!(
1845            out,
1846            r#"
1847    # mark: DFLT/dflt
1848    # 1 MarkToLig rules
1849    # lookupflag LookupFlag(0)
1850    f_i_i (lig) [@(x: 250, y: 600), @(x: 500, y: 600), <NULL>]
1851    @(x: 100, y: 200) acutecomb
1852    "#
1853        );
1854    }
1855
1856    // https://github.com/googlefonts/ufo2ft/blob/779bbad84a/tests/featureWriters/markFeatureWriter_test.py#L1543
1857    #[test]
1858    fn multiple_anchor_classes_liga() {
1859        let out = MarksInput::default()
1860            .add_glyph("f_i", None, |anchors| {
1861                anchors
1862                    .add("top_1", [(100, 500)])
1863                    .add("top_2", [(600, 500)]);
1864            })
1865            .add_glyph("f_f", None, |anchors| {
1866                anchors
1867                    .add("topOther_1", [(101, 501)])
1868                    .add("topOther_2", [(601, 501)]);
1869            })
1870            .add_glyph("f_l", None, |anchors| {
1871                anchors
1872                    .add("top_1", [(102, 502)])
1873                    .add("topOther_2", [(602, 502)]);
1874            })
1875            .add_glyph("acutecomb", None, |anchors| {
1876                anchors.add("_top", [(100, 200)]);
1877                anchors.add("_topOther", [(150, 250)]);
1878            })
1879            .get_normalized_output();
1880
1881        // we generate conflicting lookups for f_l, but topOther should win
1882        // because we order lookups lexicographically over the mark group names
1883        assert_eq_ignoring_ws!(
1884            out,
1885            r#"
1886                # mark: DFLT/dflt
1887                # 3 MarkToLig rules
1888                # lookupflag LookupFlag(0)
1889                f_f (lig) [@(x: 101, y: 501), @(x: 601, y: 501)]
1890                  @(x: 150, y: 250) acutecomb
1891                f_i (lig) [@(x: 100, y: 500), @(x: 600, y: 500)]
1892                  @(x: 100, y: 200) acutecomb
1893                f_l (lig) [<NULL>, @(x: 602, y: 502)]
1894                  @(x: 150, y: 250) acutecomb
1895                "#
1896        );
1897    }
1898
1899    #[test]
1900    fn basic_lig_carets() {
1901        let out = MarksInput::default()
1902            .add_glyph("f_i", None, |anchors| {
1903                anchors.add("caret_1", [(100, 0)]);
1904            })
1905            .add_glyph("f_l", None, |anchors| {
1906                anchors.add("vcaret_1", [(0, -222)]);
1907            })
1908            .compile();
1909
1910        let gdef = out.gdef.as_ref().unwrap();
1911        let lig_carets = gdef.lig_caret_list.as_ref().unwrap();
1912        let ff = &lig_carets.lig_glyphs[0].caret_values;
1913        assert_eq!(ff.len(), 1);
1914        let caret = ff[0].as_ref();
1915        assert!(
1916            matches!(caret, RawCaretValue::Format1(t) if t.coordinate == 100),
1917            "{caret:?}"
1918        );
1919
1920        let f_l = &lig_carets.lig_glyphs[1].caret_values;
1921        assert_eq!(f_l.len(), 1);
1922        let caret = f_l[0].as_ref();
1923        assert!(
1924            matches!(caret, RawCaretValue::Format1(t) if t.coordinate == -222),
1925            "{caret:?}"
1926        );
1927    }
1928
1929    #[test]
1930    fn lig_caret_nop_deltas() {
1931        use write_fonts::read::tables::gdef as rgdef;
1932        // these values are taken from Oswald.glyphs
1933        let out = MarksInput::new([&[-1.0], &[0.0], &[1.0]])
1934            .add_glyph("f_f", None, |anchors| {
1935                anchors.add("caret_1", [(10.0, 0.0), (10., 0.0), (10., 0.)]);
1936            })
1937            .compile();
1938
1939        let gdef_bytes = write_fonts::dump_table(&out.gdef.unwrap()).unwrap();
1940        let gdef = rgdef::Gdef::read(gdef_bytes.as_slice().into()).unwrap();
1941        let lig_carets = gdef.lig_caret_list().unwrap().unwrap();
1942        let ff = lig_carets.lig_glyphs().get(0).unwrap();
1943        let rgdef::CaretValue::Format1(caret) = ff.caret_values().get(0).unwrap() else {
1944            panic!("wrong caret format");
1945        };
1946        assert_eq!(caret.coordinate(), 10)
1947    }
1948
1949    #[test]
1950    fn lig_caret_rounding() {
1951        use write_fonts::read::tables::gdef as rgdef;
1952        // these values are taken from Oswald.glyphs
1953        let out = MarksInput::new([&[-1.0], &[0.0], &[1.0]])
1954            .add_glyph("f_f", None, |anchors| {
1955                anchors.add("caret_1", [(239.0, 0.0), (270.5, 0.0), (304.5, 0.)]);
1956            })
1957            .compile();
1958
1959        // we don't have good API on the write-fonts varstore for fetching the
1960        // delta values, so we convert to read fonts first
1961        let gdef_bytes = write_fonts::dump_table(&out.gdef.unwrap()).unwrap();
1962        let gdef = rgdef::Gdef::read(gdef_bytes.as_slice().into()).unwrap();
1963        let lig_carets = gdef.lig_caret_list().unwrap().unwrap();
1964        let varstore = gdef.item_var_store().unwrap().unwrap();
1965        let ivs_data = varstore.item_variation_data().get(0).unwrap().unwrap();
1966        let ff = lig_carets.lig_glyphs().get(0).unwrap();
1967        let caret = ff.caret_values().get(0).unwrap();
1968        let rgdef::CaretValue::Format3(caret) = caret else {
1969            panic!("expected variable caret!");
1970        };
1971        let default = caret.coordinate();
1972        let mut values = ivs_data
1973            .delta_set(0)
1974            .map(|delta| default + (delta as i16))
1975            .collect::<Vec<_>>();
1976        values.insert(1, default);
1977
1978        assert_eq!(values, [239, 271, 305]);
1979    }
1980
1981    #[test]
1982    fn mark_anchors_rounding() {
1983        // These values are taken from Teachers-Italic.glyphs. The composite glyph
1984        // 'ordfeminine' inherits a 'top' base anchor from a scaled 'a' component, so the
1985        // propagated anchor ends up with float coordinates; these are expected to be
1986        // rounded before deltas get computed in order to match the output of fontmake:
1987        // https://github.com/googlefonts/fontc/issues/1043#issuecomment-2444388789
1988        let out = MarksInput::new([&[0.0], &[0.75], &[1.0]])
1989            .add_glyph("a", None, |anchors| {
1990                anchors.add("top", [(377.0, 451.0), (386.0, 450.0), (389.0, 450.0)]);
1991            })
1992            .add_glyph("ordfeminine", None, |anchors| {
1993                anchors.add("top", [(282.75, 691.25), (289.5, 689.5), (291.75, 689.5)]);
1994            })
1995            .add_glyph("acutecomb", None, |anchors| {
1996                anchors.add("_top", [(218.0, 704.0), (265.0, 707.0), (282.0, 708.0)]);
1997            })
1998            .get_normalized_output();
1999
2000        assert_eq_ignoring_ws!(
2001            out,
2002            r#"
2003                # mark: DFLT/dflt
2004                # 2 MarkToBase rules
2005                # lookupflag LookupFlag(0)
2006                a @(x: 377 {386,389}, y: 451 {450,450})
2007                @(x: 218 {265,282}, y: 704 {707,708}) acutecomb
2008                ordfeminine @(x: 283 {290,292}, y: 691 {690,690})
2009                @(x: 218 {265,282}, y: 704 {707,708}) acutecomb
2010            "#
2011        );
2012    }
2013
2014    #[test]
2015    fn cursive_rtl_and_ltr() {
2016        let out = MarksInput::default()
2017            .add_glyph("a", None, |anchors| {
2018                anchors.add("entry", [(10., 10.)]).add("exit", [(10., 60.)]);
2019            })
2020            .add_glyph("hamza", None, |anchors| {
2021                anchors
2022                    .add("entry", [(-11., 33.)])
2023                    .add("exit", [(-11., 44.)]);
2024            })
2025            .get_normalized_output();
2026
2027        assert_eq_ignoring_ws!(
2028            out,
2029            r#"
2030            # curs: DFLT/dflt
2031            # 2 CursivePos rules
2032            # lookupflag LookupFlag(8)
2033            a
2034              entry: @(x: 10, y: 10)
2035              exit: @(x: 10, y: 60)
2036            # lookupflag LookupFlag(9)
2037            hamza
2038              entry: @(x: -11, y: 33)
2039              exit: @(x: -11, y: 44)
2040            "#
2041        );
2042    }
2043}