fea_rs/compile/
lookups.rs

1//! gsub/gpos lookup table stuff
2
3mod contextual;
4
5use std::{
6    collections::{BTreeMap, HashMap, HashSet},
7    convert::TryInto,
8    fmt::Debug,
9};
10
11use smol_str::SmolStr;
12
13use write_fonts::{
14    tables::{
15        gdef::GlyphClassDef,
16        gpos::{
17            self as write_gpos,
18            builders::{
19                AnchorBuilder as Anchor, CursivePosBuilder, MarkToBaseBuilder, MarkToLigBuilder,
20                MarkToMarkBuilder, PairPosBuilder, SinglePosBuilder,
21                ValueRecordBuilder as ValueRecord,
22            },
23        },
24        gsub::{
25            self as write_gsub,
26            builders::{
27                AlternateSubBuilder, LigatureSubBuilder, MultipleSubBuilder, SingleSubBuilder,
28            },
29        },
30        layout::{
31            ConditionSet as RawConditionSet, Feature, FeatureList, FeatureRecord,
32            FeatureTableSubstitution, FeatureTableSubstitutionRecord, FeatureVariationRecord,
33            FeatureVariations, LangSys, LangSysRecord, LookupFlag, LookupList, Script, ScriptList,
34            ScriptRecord,
35            builders::{Builder, LookupBuilder},
36        },
37        variations::ivs_builder::VariationStoreBuilder,
38    },
39    types::Tag,
40};
41
42use crate::{
43    Kind, Opts,
44    common::{GlyphId16, GlyphOrClass, GlyphSet},
45    compile::lookups::contextual::ChainOrNot,
46};
47
48use super::{features::AllFeatures, tags};
49
50use contextual::{
51    ContextualLookupBuilder, PosChainContextBuilder, PosContextBuilder, ReverseChainBuilder,
52    SubChainContextBuilder, SubContextBuilder,
53};
54
55pub(crate) type FilterSetId = u16;
56
57#[derive(Clone, Debug, Default)]
58pub(crate) struct AllLookups {
59    current: Option<SomeLookup>,
60    current_name: Option<SmolStr>,
61    gpos: Vec<PositionLookup>,
62    gsub: Vec<SubstitutionLookup>,
63    named: HashMap<SmolStr, LookupId>,
64}
65
66#[derive(Clone, Debug)]
67pub(crate) enum PositionLookup {
68    Single(LookupBuilder<SinglePosBuilder>),
69    Pair(LookupBuilder<PairPosBuilder>),
70    Cursive(LookupBuilder<CursivePosBuilder>),
71    MarkToBase(LookupBuilder<MarkToBaseBuilder>),
72    MarkToLig(LookupBuilder<MarkToLigBuilder>),
73    MarkToMark(LookupBuilder<MarkToMarkBuilder>),
74    // currently unused, matching feaLib: <https://github.com/fonttools/fonttools/issues/2539>
75    #[allow(dead_code)]
76    Contextual(LookupBuilder<PosContextBuilder>),
77    ChainedContextual(LookupBuilder<PosChainContextBuilder>),
78}
79
80// a litle helper to implement this conversion trait.
81//
82// Note: this is only used in the API for adding external features ( aka feature
83// writers) and so we only implement the conversion for the specific lookup types
84// that we want to allow the client to add externally.
85macro_rules! impl_into_lookup {
86    ($builder:ty, $typ:ident, $variant:ident) => {
87        impl From<LookupBuilder<$builder>> for $typ {
88            fn from(src: LookupBuilder<$builder>) -> $typ {
89                $typ::$variant(src)
90            }
91        }
92    };
93}
94
95impl_into_lookup!(PairPosBuilder, PositionLookup, Pair);
96impl_into_lookup!(MarkToBaseBuilder, PositionLookup, MarkToBase);
97impl_into_lookup!(MarkToMarkBuilder, PositionLookup, MarkToMark);
98impl_into_lookup!(MarkToLigBuilder, PositionLookup, MarkToLig);
99impl_into_lookup!(CursivePosBuilder, PositionLookup, Cursive);
100impl_into_lookup!(SingleSubBuilder, SubstitutionLookup, Single);
101
102#[derive(Clone, Debug)]
103pub(crate) enum SubstitutionLookup {
104    Single(LookupBuilder<SingleSubBuilder>),
105    Multiple(LookupBuilder<MultipleSubBuilder>),
106    Alternate(LookupBuilder<AlternateSubBuilder>),
107    Ligature(LookupBuilder<LigatureSubBuilder>),
108    Contextual(LookupBuilder<SubContextBuilder>),
109    ChainedContextual(LookupBuilder<SubChainContextBuilder>),
110    Reverse(LookupBuilder<ReverseChainBuilder>),
111}
112
113#[derive(Clone, Debug)]
114pub(crate) enum SomeLookup {
115    GsubLookup(SubstitutionLookup),
116    GposLookup(PositionLookup),
117    GposContextual(ContextualLookupBuilder<PositionLookup>),
118    GsubContextual(ContextualLookupBuilder<SubstitutionLookup>),
119}
120
121/// IDs assigned to lookups during compilation
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
123pub enum LookupId {
124    /// An id for a GPOS lookup
125    Gpos(usize),
126    /// An id for a GSUB lookup
127    Gsub(usize),
128    /// A temporary ID assigned to a GPOS lookup constructed by the client.
129    ///
130    /// This id will be remapped when the external features are merged into
131    /// the features generated from the FEA.
132    ExternalGpos(usize),
133    /// Like above, but for GSUB.
134    ExternalGsub(usize),
135    /// A temporary ID assigned to a lookup constructed by the client that should
136    /// be at the front of the lookup list.
137    ///
138    /// This is required for rvrn feature variations.
139    ExternalFrontOfList(usize),
140    /// Used when a named lookup block has no rules.
141    ///
142    /// We parse this, but then discard it immediately whenever it is referenced.
143    Empty,
144}
145
146/// A struct that remaps initial lookup ids to their final values.
147///
148/// LookupIds can need adjusting in a number of cases:
149/// - if the 'aalt' feature is present it causes additional lookups to be
150///   inserted at the start of the GSUB lookup list
151/// - FEA code can indicate with inline comments where additional lookups
152///   should be inserted
153#[derive(Clone, Debug, Default)]
154pub(crate) struct LookupIdMap {
155    // we could consider having this store the final values as u16?
156    mapping: HashMap<LookupId, LookupId>,
157}
158
159/// Tracks the current lookupflags state
160#[derive(Clone, Copy, Debug, Default, PartialEq)]
161pub(crate) struct LookupFlagInfo {
162    pub(crate) flags: LookupFlag,
163    pub(crate) mark_filter_set: Option<FilterSetId>,
164}
165
166/// A feature associated with a particular script and language.
167#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169pub struct FeatureKey {
170    pub(crate) feature: Tag,
171    pub(crate) language: Tag,
172    pub(crate) script: Tag,
173}
174
175type FeatureIdx = u16;
176type LookupIdx = u16;
177
178/// A helper for building GSUB/GPOS tables
179pub(crate) struct PosSubBuilder<T> {
180    lookups: Vec<T>,
181    scripts: BTreeMap<Tag, BTreeMap<Tag, LangSys>>,
182    // map a feature tag + set of lookups to an index
183    features: BTreeMap<(Tag, Vec<LookupIdx>), FeatureIdx>,
184    // map a conditionset to a map of target features and the lookups to substitute
185    variations: HashMap<RawConditionSet, HashMap<FeatureIdx, Vec<LookupIdx>>>,
186}
187
188trait RemapIds {
189    fn remap_ids(&mut self, id_map: &LookupIdMap);
190}
191
192impl<T: RemapIds> RemapIds for LookupBuilder<T> {
193    fn remap_ids(&mut self, id_map: &LookupIdMap) {
194        for lookup in &mut self.subtables {
195            lookup.remap_ids(id_map);
196        }
197    }
198}
199
200pub(crate) trait IterAaltPairs {
201    fn iter_aalt_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)>;
202}
203
204impl IterAaltPairs for LigatureSubBuilder {
205    fn iter_aalt_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> {
206        self.iter()
207            .flat_map(|(targ, ligs)| ligs.iter().map(|x| (*targ, x)))
208            .filter_map(|(targ, (components, replacement))| {
209                if components.is_empty() {
210                    Some((targ, *replacement))
211                } else {
212                    None
213                }
214            })
215    }
216}
217
218impl IterAaltPairs for MultipleSubBuilder {
219    fn iter_aalt_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> {
220        self.iter()
221            .filter_map(|(targ, replacement)| match replacement.as_slice() {
222                &[just_one] => Some((*targ, just_one)),
223                _ => None,
224            })
225    }
226}
227
228impl IterAaltPairs for AlternateSubBuilder {
229    fn iter_aalt_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> {
230        self.iter_pairs()
231    }
232}
233
234impl IterAaltPairs for SingleSubBuilder {
235    fn iter_aalt_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> {
236        self.iter()
237    }
238}
239
240impl PositionLookup {
241    fn remap_ids(&mut self, id_map: &LookupIdMap) {
242        match self {
243            PositionLookup::Contextual(lookup) => lookup.remap_ids(id_map),
244            PositionLookup::ChainedContextual(lookup) => lookup.remap_ids(id_map),
245            _ => (),
246        }
247    }
248
249    fn kind(&self) -> Kind {
250        match self {
251            PositionLookup::Single(_) => Kind::GposType1,
252            PositionLookup::Pair(_) => Kind::GposType2,
253            PositionLookup::Cursive(_) => Kind::GposType3,
254            PositionLookup::MarkToBase(_) => Kind::GposType4,
255            PositionLookup::MarkToLig(_) => Kind::GposType5,
256            PositionLookup::MarkToMark(_) => Kind::GposType6,
257            PositionLookup::Contextual(_) => Kind::GposType7,
258            PositionLookup::ChainedContextual(_) => Kind::GposType8,
259        }
260    }
261
262    fn force_subtable_break(&mut self) {
263        match self {
264            PositionLookup::Single(lookup) => lookup.force_subtable_break(),
265            PositionLookup::Pair(lookup) => lookup.force_subtable_break(),
266            PositionLookup::Cursive(lookup) => lookup.force_subtable_break(),
267            PositionLookup::MarkToBase(lookup) => lookup.force_subtable_break(),
268            PositionLookup::MarkToLig(lookup) => lookup.force_subtable_break(),
269            PositionLookup::MarkToMark(lookup) => lookup.force_subtable_break(),
270            PositionLookup::Contextual(lookup) => lookup.force_subtable_break(),
271            PositionLookup::ChainedContextual(lookup) => lookup.force_subtable_break(),
272        }
273    }
274}
275
276impl SubstitutionLookup {
277    fn remap_ids(&mut self, id_map: &LookupIdMap) {
278        match self {
279            SubstitutionLookup::Contextual(lookup) => lookup.remap_ids(id_map),
280            SubstitutionLookup::ChainedContextual(lookup) => lookup.remap_ids(id_map),
281            _ => (),
282        }
283    }
284
285    fn kind(&self) -> Kind {
286        match self {
287            SubstitutionLookup::Single(_) => Kind::GsubType1,
288            SubstitutionLookup::Multiple(_) => Kind::GsubType2,
289            SubstitutionLookup::Alternate(_) => Kind::GsubType3,
290            SubstitutionLookup::Ligature(_) => Kind::GsubType4,
291            SubstitutionLookup::Contextual(_) => Kind::GsubType5,
292            SubstitutionLookup::ChainedContextual(_) => Kind::GsubType6,
293            SubstitutionLookup::Reverse(_) => Kind::GsubType8,
294        }
295    }
296
297    fn force_subtable_break(&mut self) {
298        match self {
299            SubstitutionLookup::Single(lookup) => lookup.force_subtable_break(),
300            SubstitutionLookup::Multiple(lookup) => lookup.force_subtable_break(),
301            SubstitutionLookup::Alternate(lookup) => lookup.force_subtable_break(),
302            SubstitutionLookup::Ligature(lookup) => lookup.force_subtable_break(),
303            SubstitutionLookup::Contextual(lookup) => lookup.force_subtable_break(),
304            SubstitutionLookup::Reverse(lookup) => lookup.force_subtable_break(),
305            SubstitutionLookup::ChainedContextual(lookup) => lookup.force_subtable_break(),
306        }
307    }
308}
309
310impl Builder for PositionLookup {
311    type Output = write_gpos::PositionLookup;
312
313    fn build(self, var_store: &mut VariationStoreBuilder) -> Self::Output {
314        match self {
315            PositionLookup::Single(lookup) => {
316                write_gpos::PositionLookup::Single(lookup.build(var_store))
317            }
318            PositionLookup::Pair(lookup) => {
319                write_gpos::PositionLookup::Pair(lookup.build(var_store))
320            }
321            PositionLookup::Cursive(lookup) => {
322                write_gpos::PositionLookup::Cursive(lookup.build(var_store))
323            }
324            PositionLookup::MarkToBase(lookup) => {
325                write_gpos::PositionLookup::MarkToBase(lookup.build(var_store))
326            }
327            PositionLookup::MarkToLig(lookup) => {
328                write_gpos::PositionLookup::MarkToLig(lookup.build(var_store))
329            }
330            PositionLookup::MarkToMark(lookup) => {
331                write_gpos::PositionLookup::MarkToMark(lookup.build(var_store))
332            }
333            PositionLookup::Contextual(lookup) => {
334                write_gpos::PositionLookup::Contextual(lookup.build(var_store).into_concrete())
335            }
336            PositionLookup::ChainedContextual(lookup) => {
337                write_gpos::PositionLookup::ChainContextual(lookup.build(var_store).into_concrete())
338            }
339        }
340    }
341}
342
343impl Builder for SubstitutionLookup {
344    type Output = write_gsub::SubstitutionLookup;
345
346    fn build(self, _var_store: &mut VariationStoreBuilder) -> Self::Output {
347        match self {
348            SubstitutionLookup::Single(lookup) => {
349                write_gsub::SubstitutionLookup::Single(lookup.build(_var_store))
350            }
351            SubstitutionLookup::Multiple(lookup) => {
352                write_gsub::SubstitutionLookup::Multiple(lookup.build(_var_store))
353            }
354            SubstitutionLookup::Alternate(lookup) => {
355                write_gsub::SubstitutionLookup::Alternate(lookup.build(_var_store))
356            }
357            SubstitutionLookup::Ligature(lookup) => {
358                write_gsub::SubstitutionLookup::Ligature(lookup.build(_var_store))
359            }
360            SubstitutionLookup::Contextual(lookup) => {
361                write_gsub::SubstitutionLookup::Contextual(lookup.build(_var_store).into_concrete())
362            }
363            SubstitutionLookup::ChainedContextual(lookup) => {
364                write_gsub::SubstitutionLookup::ChainContextual(
365                    lookup.build(_var_store).into_concrete(),
366                )
367            }
368            SubstitutionLookup::Reverse(lookup) => {
369                write_gsub::SubstitutionLookup::Reverse(lookup.build(_var_store))
370            }
371        }
372    }
373}
374
375impl AllLookups {
376    fn push(&mut self, lookup: SomeLookup) -> LookupId {
377        match lookup {
378            SomeLookup::GsubLookup(sub) => {
379                self.gsub.push(sub);
380                LookupId::Gsub(self.gsub.len() - 1)
381            }
382            SomeLookup::GposLookup(pos) => {
383                self.gpos.push(pos);
384                LookupId::Gpos(self.gpos.len() - 1)
385            }
386            SomeLookup::GposContextual(lookup) => {
387                let id = LookupId::Gpos(self.gpos.len());
388                assert_eq!(id, lookup.root_id); // sanity check
389                let (lookup, anon_lookups) = lookup.into_lookups();
390                match lookup {
391                    ChainOrNot::Context(lookup) => self
392                        .gpos
393                        //NOTE: we currently force all GPOS7 into GPOS8, to match
394                        //the behaviour of fonttools.
395                        .push(PositionLookup::ChainedContextual(lookup.convert())),
396                    ChainOrNot::Chain(lookup) => self
397                        .gpos
398                        .push(PositionLookup::ChainedContextual(lookup.convert())),
399                }
400                self.gpos.extend(anon_lookups);
401                id
402            }
403            SomeLookup::GsubContextual(lookup) => {
404                let id = LookupId::Gsub(self.gsub.len());
405                assert_eq!(id, lookup.root_id); // sanity check
406                let (lookup, anon_lookups) = lookup.into_lookups();
407                match lookup {
408                    ChainOrNot::Context(lookup) => self
409                        .gsub
410                        .push(SubstitutionLookup::Contextual(lookup.convert())),
411                    ChainOrNot::Chain(lookup) => self
412                        .gsub
413                        .push(SubstitutionLookup::ChainedContextual(lookup.convert())),
414                }
415                self.gsub.extend(anon_lookups);
416                id
417            }
418        }
419    }
420
421    pub(crate) fn get_named(&self, name: &str) -> Option<LookupId> {
422        self.named.get(name).copied()
423    }
424
425    pub(crate) fn current_mut(&mut self) -> Option<&mut SomeLookup> {
426        self.current.as_mut()
427    }
428
429    pub(crate) fn has_current(&self) -> bool {
430        self.current.is_some()
431    }
432
433    pub(crate) fn next_gpos_id(&self) -> LookupId {
434        LookupId::Gpos(self.gpos.len())
435    }
436
437    pub(crate) fn next_gsub_id(&self) -> LookupId {
438        LookupId::Gsub(self.gsub.len())
439    }
440
441    /// insert a sequence of lookups into the GPOS list at a specific pos.
442    ///
443    /// After calling this, any existing items after `pos` will have invalid
444    /// `LookupId`s! the caller is expected to be doing bookkeeping, and to
445    /// subsequently remap ids.
446    pub(crate) fn splice_gpos(
447        &mut self,
448        pos: usize,
449        lookups: impl IntoIterator<Item = PositionLookup>,
450    ) {
451        self.gpos.splice(pos..pos, lookups);
452    }
453
454    /// insert a sequence of lookups into the GPOS list at a specific pos.
455    ///
456    /// After calling this, any existing items after `pos` will have invalid
457    /// `LookupId`s! the caller is expected to be doing bookkeeping, and to
458    /// subsequently remap ids.
459    pub(crate) fn splice_gsub(
460        &mut self,
461        pos: usize,
462        lookups: impl IntoIterator<Item = SubstitutionLookup>,
463    ) {
464        self.gsub.splice(pos..pos, lookups);
465    }
466
467    /// Returns `true` if there is an active lookup of this kind
468    pub(crate) fn has_current_kind(&self, kind: Kind) -> bool {
469        self.current.as_ref().map(SomeLookup::kind) == Some(kind)
470    }
471
472    pub(crate) fn has_same_flags(&self, flags: LookupFlagInfo) -> bool {
473        self.current.as_ref().map(SomeLookup::flags) == Some(flags)
474    }
475
476    // `false` if we didn't have an active lookup
477    pub(crate) fn add_subtable_break(&mut self) -> bool {
478        if let Some(current) = self.current.as_mut() {
479            match current {
480                SomeLookup::GsubLookup(lookup) => lookup.force_subtable_break(),
481                SomeLookup::GposLookup(lookup) => lookup.force_subtable_break(),
482                SomeLookup::GposContextual(lookup) => lookup.force_subtable_break(),
483                SomeLookup::GsubContextual(lookup) => lookup.force_subtable_break(),
484            }
485            true
486        } else {
487            false
488        }
489    }
490
491    // doesn't start it, just stashes the name
492    pub(crate) fn start_named(&mut self, name: SmolStr) {
493        self.current_name = Some(name);
494    }
495
496    pub(crate) fn start_lookup(&mut self, kind: Kind, flags: LookupFlagInfo) -> Option<LookupId> {
497        let finished_id = self.current.take().map(|lookup| self.push(lookup));
498        let mut new_one = SomeLookup::new(kind, flags.flags, flags.mark_filter_set);
499
500        let new_id = if is_gpos_rule(kind) {
501            LookupId::Gpos(self.gpos.len())
502        } else {
503            LookupId::Gsub(self.gsub.len())
504        };
505
506        match &mut new_one {
507            SomeLookup::GsubContextual(lookup) => lookup.root_id = new_id,
508            SomeLookup::GposContextual(lookup) => lookup.root_id = new_id,
509            //SomeLookup::GsubReverse(_) => (),
510            SomeLookup::GsubLookup(_) | SomeLookup::GposLookup(_) => (),
511        }
512        self.current = Some(new_one);
513        finished_id
514    }
515
516    pub(crate) fn finish_current(&mut self) -> Option<(LookupId, Option<SmolStr>)> {
517        if let Some(lookup) = self.current.take() {
518            let id = self.push(lookup);
519            if let Some(name) = self.current_name.take() {
520                self.named.insert(name.clone(), id);
521                Some((id, Some(name)))
522            } else {
523                Some((id, None))
524            }
525        } else if let Some(name) = self.current_name.take() {
526            self.named.insert(name.clone(), LookupId::Empty);
527            // there was a named block with no rules, return the empty lookup
528            Some((LookupId::Empty, Some(name)))
529        } else {
530            None
531        }
532    }
533
534    pub(crate) fn promote_single_sub_to_multi_if_necessary(&mut self) {
535        if !self.has_current_kind(Kind::GsubType1) {
536            return;
537        }
538        let Some(SomeLookup::GsubLookup(SubstitutionLookup::Single(lookup))) = self.current.take()
539        else {
540            unreachable!()
541        };
542        let promoted = LookupBuilder {
543            flags: lookup.flags,
544            mark_set: lookup.mark_set,
545            subtables: lookup
546                .subtables
547                .into_iter()
548                .map(SingleSubBuilder::promote_to_multi_sub)
549                .collect(),
550        };
551        self.current = Some(SomeLookup::GsubLookup(SubstitutionLookup::Multiple(
552            promoted,
553        )));
554    }
555
556    pub(crate) fn promote_single_sub_to_liga_if_necessary(&mut self) {
557        if !self.has_current_kind(Kind::GsubType1) {
558            return;
559        }
560
561        let Some(SomeLookup::GsubLookup(SubstitutionLookup::Single(lookup))) = self.current.take()
562        else {
563            return;
564        };
565        let promoted = LookupBuilder {
566            flags: lookup.flags,
567            mark_set: lookup.mark_set,
568            subtables: lookup
569                .subtables
570                .into_iter()
571                .map(SingleSubBuilder::promote_to_ligature_sub)
572                .collect(),
573        };
574        self.current = Some(SomeLookup::GsubLookup(SubstitutionLookup::Ligature(
575            promoted,
576        )));
577    }
578
579    pub(crate) fn infer_glyph_classes(&self, mut f: impl FnMut(GlyphId16, GlyphClassDef)) {
580        for lookup in &self.gpos {
581            match lookup {
582                PositionLookup::MarkToBase(lookup) => {
583                    for subtable in &lookup.subtables {
584                        subtable
585                            .base_glyphs()
586                            .for_each(|k| f(k, GlyphClassDef::Base));
587                        subtable
588                            .mark_glyphs()
589                            .for_each(|k| f(k, GlyphClassDef::Mark));
590                    }
591                }
592                PositionLookup::MarkToLig(lookup) => {
593                    for subtable in &lookup.subtables {
594                        subtable
595                            .lig_glyphs()
596                            .for_each(|k| f(k, GlyphClassDef::Ligature));
597                        subtable
598                            .mark_glyphs()
599                            .for_each(|k| f(k, GlyphClassDef::Mark));
600                    }
601                }
602                PositionLookup::MarkToMark(lookup) => {
603                    for subtable in &lookup.subtables {
604                        subtable
605                            .mark1_glyphs()
606                            .chain(subtable.mark2_glyphs())
607                            .for_each(|k| f(k, GlyphClassDef::Mark));
608                    }
609                }
610                _ => (),
611            }
612        }
613        //TODO: the spec says to do gsub too, but fonttools doesn't?
614    }
615
616    /// Return the aalt-relevant lookups for this lookup Id.
617    ///
618    /// If lookup is GSUB type 1 or 3, return a single lookup.
619    /// If contextual, returns any referenced single-sub lookups.
620    pub(crate) fn aalt_lookups(&self, id: LookupId) -> Vec<&SubstitutionLookup> {
621        let mut collect = Vec::new();
622        let mut seen = HashSet::new();
623        self.aalt_lookups_impl(id, &mut collect, &mut seen);
624        collect
625    }
626
627    fn aalt_lookups_impl<'a>(
628        &'a self,
629        id: LookupId,
630        collect: &mut Vec<&'a SubstitutionLookup>,
631        seen: &mut HashSet<LookupId>,
632    ) {
633        let Some(lookup) = self.get_gsub_lookup(&id) else {
634            return;
635        };
636        if !seen.insert(id) {
637            return;
638        }
639
640        match lookup {
641            SubstitutionLookup::Single(_)
642            | SubstitutionLookup::Alternate(_)
643            | SubstitutionLookup::Multiple(_)
644            | SubstitutionLookup::Ligature(_) => collect.push(lookup),
645
646            SubstitutionLookup::Contextual(lookup) => lookup
647                .subtables
648                .iter()
649                .flat_map(|sub| sub.iter_lookups())
650                .for_each(|id| self.aalt_lookups_impl(id, collect, seen)),
651            SubstitutionLookup::ChainedContextual(lookup) => lookup
652                .subtables
653                .iter()
654                .flat_map(|sub| sub.iter_lookups())
655                .for_each(|id| self.aalt_lookups_impl(id, collect, seen)),
656            _ => (),
657        }
658    }
659
660    fn get_gsub_lookup(&self, id: &LookupId) -> Option<&SubstitutionLookup> {
661        match id {
662            LookupId::Gsub(idx) => self.gsub.get(*idx),
663            _ => None,
664        }
665    }
666
667    pub(crate) fn remap_ids(&mut self, ids: &LookupIdMap) {
668        self.gpos
669            .iter_mut()
670            .for_each(|lookup| lookup.remap_ids(ids));
671        self.gsub
672            .iter_mut()
673            .for_each(|lookup| lookup.remap_ids(ids));
674    }
675
676    pub(crate) fn insert_aalt_lookups(
677        &mut self,
678        insert_point: usize,
679        all_alts: HashMap<GlyphId16, Vec<GlyphId16>>,
680    ) -> Vec<LookupId> {
681        let mut single = SingleSubBuilder::default();
682        let mut alt = AlternateSubBuilder::default();
683
684        for (target, alts) in all_alts {
685            if alts.len() == 1 {
686                single.insert(target, alts[0]);
687            } else {
688                alt.insert(target, alts);
689            }
690        }
691        let one = (!single.is_empty()).then(|| {
692            SubstitutionLookup::Single(LookupBuilder::new_with_lookups(
693                LookupFlag::empty(),
694                None,
695                vec![single],
696            ))
697        });
698        let two = (!alt.is_empty()).then(|| {
699            SubstitutionLookup::Alternate(LookupBuilder::new_with_lookups(
700                LookupFlag::empty(),
701                None,
702                vec![alt],
703            ))
704        });
705
706        let lookups = one.into_iter().chain(two).collect::<Vec<_>>();
707        let lookup_ids = (insert_point..insert_point + lookups.len())
708            .map(LookupId::Gsub)
709            .collect();
710
711        // now we need to insert these lookups at the front of our gsub lookups,
712        // and bump all of their ids:
713
714        self.gsub.iter_mut().for_each(|lookup| match lookup {
715            SubstitutionLookup::Contextual(lookup) => lookup
716                .subtables
717                .iter_mut()
718                .for_each(|sub| sub.bump_all_lookup_ids(insert_point, lookups.len())),
719            SubstitutionLookup::ChainedContextual(lookup) => lookup
720                .subtables
721                .iter_mut()
722                .for_each(|sub| sub.bump_all_lookup_ids(insert_point, lookups.len())),
723            _ => (),
724        });
725
726        self.gsub.splice(insert_point..insert_point, lookups);
727
728        lookup_ids
729    }
730
731    pub(crate) fn build(
732        &self,
733        features: &AllFeatures,
734        var_store: &mut VariationStoreBuilder,
735        opts: &Opts,
736    ) -> (Option<write_gsub::Gsub>, Option<write_gpos::Gpos>) {
737        let mut gpos_builder = PosSubBuilder::new(self.gpos.clone());
738        let mut gsub_builder = PosSubBuilder::new(self.gsub.clone());
739
740        for (key, feature_lookups) in features.iter() {
741            let required = features.is_required(key);
742
743            if key.feature == tags::SIZE {
744                gpos_builder.add(*key, Vec::new(), required);
745                continue;
746            }
747
748            let (gpos_idxes, gsub_idxes) = feature_lookups.split_base_lookups();
749            let mut gpos_feat_id = None;
750            let mut gsub_feat_id = None;
751            if opts.compile_gpos && !gpos_idxes.is_empty() {
752                gpos_feat_id = Some(gpos_builder.add(*key, gpos_idxes.clone(), required));
753            }
754
755            if opts.compile_gsub && !gsub_idxes.is_empty() {
756                gsub_feat_id = Some(gsub_builder.add(*key, gsub_idxes.clone(), required));
757            }
758
759            let variations = feature_lookups.split_variations();
760            for (cond, gpos_var_idxes, gsub_var_idxes) in variations {
761                if opts.compile_gpos && !gpos_var_idxes.is_empty() {
762                    // add the lookups for the base feature
763                    let mut all_ids = gpos_idxes.clone();
764                    all_ids.extend(gpos_var_idxes);
765
766                    // if this feature only has variations, we insert an empty
767                    // base feature
768                    let feat_id = gpos_feat_id
769                        .get_or_insert_with(|| gpos_builder.add(*key, Vec::new(), false));
770                    gpos_builder.add_variation(*feat_id, cond, all_ids);
771                }
772                if opts.compile_gsub && !gsub_var_idxes.is_empty() {
773                    // add the lookups for the base feature
774                    let mut all_ids = gsub_idxes.clone();
775                    all_ids.extend(gsub_var_idxes);
776
777                    let feat_id = gsub_feat_id
778                        .get_or_insert_with(|| gsub_builder.add(*key, Vec::new(), false));
779
780                    gsub_builder.add_variation(*feat_id, cond, all_ids);
781                }
782            }
783        }
784
785        (gsub_builder.build(var_store), gpos_builder.build(var_store))
786    }
787}
788
789impl LookupId {
790    pub(crate) fn to_raw(self) -> usize {
791        match self {
792            LookupId::Gpos(idx) => idx,
793            LookupId::Gsub(idx) => idx,
794            LookupId::Empty => usize::MAX,
795            LookupId::ExternalGpos(idx)
796            | LookupId::ExternalGsub(idx)
797            | LookupId::ExternalFrontOfList(idx) => idx,
798        }
799    }
800
801    pub(crate) fn adjust_if_gsub(&mut self, value: usize) {
802        if let LookupId::Gsub(idx) = self {
803            *idx += value;
804        }
805        if matches!(
806            self,
807            LookupId::ExternalGsub(_)
808                | LookupId::ExternalGpos(_)
809                | LookupId::ExternalFrontOfList(_)
810        ) {
811            panic!("external ids should be resolved before adjustment")
812        }
813    }
814
815    pub(crate) fn to_gpos_id_or_die(self) -> u16 {
816        let LookupId::Gpos(x) = self else {
817            panic!("this *really* shouldn't happen")
818        };
819        x.try_into().unwrap()
820    }
821
822    pub(crate) fn to_gsub_id_or_die(self) -> u16 {
823        let LookupId::Gsub(x) = self else {
824            panic!("this *really* shouldn't happen")
825        };
826        x.try_into().unwrap()
827    }
828}
829
830impl LookupIdMap {
831    pub(crate) fn insert(&mut self, from: LookupId, to: LookupId) {
832        self.mapping.insert(from, to);
833    }
834
835    pub(crate) fn get(&self, id: LookupId) -> LookupId {
836        self.mapping.get(&id).copied().unwrap_or(id)
837    }
838}
839
840impl LookupFlagInfo {
841    pub(crate) fn new(flags: LookupFlag, mark_filter_set: Option<FilterSetId>) -> Self {
842        LookupFlagInfo {
843            flags,
844            mark_filter_set,
845        }
846    }
847
848    pub(crate) fn clear(&mut self) {
849        self.flags = LookupFlag::empty();
850        self.mark_filter_set = None;
851    }
852}
853
854impl SomeLookup {
855    fn new(kind: Kind, flags: LookupFlag, filter: Option<FilterSetId>) -> Self {
856        // special kinds:
857        match kind {
858            Kind::GposType7 | Kind::GposType8 => {
859                return SomeLookup::GposContextual(ContextualLookupBuilder::new(flags, filter));
860            }
861            Kind::GsubType5 | Kind::GsubType6 => {
862                return SomeLookup::GsubContextual(ContextualLookupBuilder::new(flags, filter));
863            }
864            _ => (),
865        }
866
867        if is_gpos_rule(kind) {
868            let lookup = match kind {
869                Kind::GposType1 => PositionLookup::Single(LookupBuilder::new(flags, filter)),
870                Kind::GposType2 => PositionLookup::Pair(LookupBuilder::new(flags, filter)),
871                Kind::GposType3 => PositionLookup::Cursive(LookupBuilder::new(flags, filter)),
872                Kind::GposType4 => PositionLookup::MarkToBase(LookupBuilder::new(flags, filter)),
873                Kind::GposType5 => PositionLookup::MarkToLig(LookupBuilder::new(flags, filter)),
874                Kind::GposType6 => PositionLookup::MarkToMark(LookupBuilder::new(flags, filter)),
875                Kind::GposNode => unimplemented!("other gpos type?"),
876                other => panic!("illegal kind for lookup: '{other}'"),
877            };
878            SomeLookup::GposLookup(lookup)
879        } else {
880            let lookup = match kind {
881                Kind::GsubType1 => SubstitutionLookup::Single(LookupBuilder::new(flags, filter)),
882                Kind::GsubType2 => SubstitutionLookup::Multiple(LookupBuilder::new(flags, filter)),
883                Kind::GsubType3 => SubstitutionLookup::Alternate(LookupBuilder::new(flags, filter)),
884                Kind::GsubType4 => SubstitutionLookup::Ligature(LookupBuilder::new(flags, filter)),
885                Kind::GsubType5 => {
886                    SubstitutionLookup::Contextual(LookupBuilder::new(flags, filter))
887                }
888                Kind::GsubType7 => unimplemented!("extension"),
889                Kind::GsubType8 => SubstitutionLookup::Reverse(LookupBuilder::new(flags, filter)),
890                other => panic!("illegal kind for lookup: '{other}'"),
891            };
892            SomeLookup::GsubLookup(lookup)
893        }
894    }
895
896    fn kind(&self) -> Kind {
897        match self {
898            SomeLookup::GsubContextual(_) => Kind::GsubType6,
899            SomeLookup::GposContextual(_) => Kind::GposType8,
900            SomeLookup::GsubLookup(gsub) => gsub.kind(),
901            SomeLookup::GposLookup(gpos) => gpos.kind(),
902        }
903    }
904
905    fn flags(&self) -> LookupFlagInfo {
906        match self {
907            SomeLookup::GsubLookup(l) => match l {
908                SubstitutionLookup::Single(l) => LookupFlagInfo::new(l.flags, l.mark_set),
909                SubstitutionLookup::Multiple(l) => LookupFlagInfo::new(l.flags, l.mark_set),
910                SubstitutionLookup::Alternate(l) => LookupFlagInfo::new(l.flags, l.mark_set),
911                SubstitutionLookup::Ligature(l) => LookupFlagInfo::new(l.flags, l.mark_set),
912                SubstitutionLookup::Contextual(l) => LookupFlagInfo::new(l.flags, l.mark_set),
913                SubstitutionLookup::ChainedContextual(l) => {
914                    LookupFlagInfo::new(l.flags, l.mark_set)
915                }
916                SubstitutionLookup::Reverse(l) => LookupFlagInfo::new(l.flags, l.mark_set),
917            },
918            SomeLookup::GposLookup(l) => match l {
919                PositionLookup::Single(l) => LookupFlagInfo::new(l.flags, l.mark_set),
920                PositionLookup::Pair(l) => LookupFlagInfo::new(l.flags, l.mark_set),
921                PositionLookup::Cursive(l) => LookupFlagInfo::new(l.flags, l.mark_set),
922                PositionLookup::MarkToBase(l) => LookupFlagInfo::new(l.flags, l.mark_set),
923                PositionLookup::MarkToLig(l) => LookupFlagInfo::new(l.flags, l.mark_set),
924                PositionLookup::MarkToMark(l) => LookupFlagInfo::new(l.flags, l.mark_set),
925                PositionLookup::Contextual(l) => LookupFlagInfo::new(l.flags, l.mark_set),
926                PositionLookup::ChainedContextual(l) => LookupFlagInfo::new(l.flags, l.mark_set),
927            },
928            SomeLookup::GposContextual(l) => LookupFlagInfo::new(l.flags, l.mark_set),
929            SomeLookup::GsubContextual(l) => LookupFlagInfo::new(l.flags, l.mark_set),
930        }
931    }
932
933    pub(crate) fn add_gpos_type_1(&mut self, id: GlyphId16, record: ValueRecord) {
934        if let SomeLookup::GposLookup(PositionLookup::Single(table)) = self {
935            let subtable = table.last_mut().unwrap();
936            subtable.insert(id, record);
937        } else {
938            panic!("lookup mismatch");
939        }
940    }
941
942    pub(crate) fn add_gpos_type_2_pair(
943        &mut self,
944        one: GlyphId16,
945        two: GlyphId16,
946        val_one: ValueRecord,
947        val_two: ValueRecord,
948    ) {
949        if let SomeLookup::GposLookup(PositionLookup::Pair(table)) = self {
950            let subtable = table.last_mut().unwrap();
951            subtable.insert_pair(one, val_one, two, val_two)
952        } else {
953            panic!("lookup mismatch");
954        }
955    }
956
957    pub(crate) fn add_gpos_type_2_class(
958        &mut self,
959        one: GlyphSet,
960        two: GlyphSet,
961        val_one: ValueRecord,
962        val_two: ValueRecord,
963    ) {
964        if let SomeLookup::GposLookup(PositionLookup::Pair(table)) = self {
965            let subtable = table.last_mut().unwrap();
966            subtable.insert_classes(one, val_one, two, val_two)
967        } else {
968            panic!("lookup mismatch");
969        }
970    }
971    pub(crate) fn add_gpos_type_3(
972        &mut self,
973        id: GlyphId16,
974        entry: Option<Anchor>,
975        exit: Option<Anchor>,
976    ) {
977        if let SomeLookup::GposLookup(PositionLookup::Cursive(table)) = self {
978            let subtable = table.last_mut().unwrap();
979            subtable.insert(id, entry, exit);
980        } else {
981            panic!("lookup mismatch");
982        }
983    }
984
985    pub(crate) fn with_gpos_type_4<R>(&mut self, f: impl FnOnce(&mut MarkToBaseBuilder) -> R) -> R {
986        if let SomeLookup::GposLookup(PositionLookup::MarkToBase(table)) = self {
987            let subtable = table.last_mut().unwrap();
988            f(subtable)
989        } else {
990            panic!("lookup mismatch");
991        }
992    }
993
994    pub(crate) fn with_gpos_type_5<R>(&mut self, f: impl FnOnce(&mut MarkToLigBuilder) -> R) -> R {
995        if let SomeLookup::GposLookup(PositionLookup::MarkToLig(table)) = self {
996            let subtable = table.last_mut().unwrap();
997            f(subtable)
998        } else {
999            panic!("lookup mismatch");
1000        }
1001    }
1002
1003    pub(crate) fn with_gpos_type_6<R>(&mut self, f: impl FnOnce(&mut MarkToMarkBuilder) -> R) -> R {
1004        if let SomeLookup::GposLookup(PositionLookup::MarkToMark(table)) = self {
1005            let subtable = table.last_mut().unwrap();
1006            f(subtable)
1007        } else {
1008            panic!("lookup mismatch");
1009        }
1010    }
1011
1012    // shared between GSUB/GPOS contextual and chain contextual rules
1013    pub(crate) fn add_contextual_rule(
1014        &mut self,
1015        backtrack: Vec<GlyphOrClass>,
1016        input: Vec<(GlyphOrClass, Vec<LookupId>)>,
1017        lookahead: Vec<GlyphOrClass>,
1018    ) {
1019        match self {
1020            SomeLookup::GposContextual(lookup) => {
1021                lookup.last_mut().add(backtrack, input, lookahead)
1022            }
1023            SomeLookup::GsubContextual(lookup) => {
1024                lookup.last_mut().add(backtrack, input, lookahead)
1025            }
1026            _ => panic!("lookup mismatch : '{}'", self.kind()),
1027        }
1028    }
1029
1030    pub(crate) fn add_gsub_type_1(&mut self, id: GlyphId16, replacement: GlyphId16) {
1031        if let SomeLookup::GsubLookup(SubstitutionLookup::Single(table)) = self {
1032            let subtable = table.last_mut().unwrap();
1033            subtable.insert(id, replacement);
1034        } else {
1035            panic!("lookup mismatch");
1036        }
1037    }
1038
1039    pub(crate) fn add_gsub_type_2(&mut self, id: GlyphId16, replacement: Vec<GlyphId16>) {
1040        if let SomeLookup::GsubLookup(SubstitutionLookup::Multiple(table)) = self {
1041            let subtable = table.last_mut().unwrap();
1042            subtable.insert(id, replacement);
1043        } else {
1044            panic!("lookup mismatch");
1045        }
1046    }
1047
1048    pub(crate) fn add_gsub_type_3(&mut self, id: GlyphId16, alternates: Vec<GlyphId16>) {
1049        if let SomeLookup::GsubLookup(SubstitutionLookup::Alternate(table)) = self {
1050            let subtable = table.last_mut().unwrap();
1051            subtable.insert(id, alternates);
1052        } else {
1053            panic!("lookup mismatch");
1054        }
1055    }
1056
1057    /// Returns `true` if this replacement shadows an existing rule.
1058    ///
1059    /// In this case the rule is not added, and the client should report an error.
1060    pub(crate) fn add_gsub_type_4(
1061        &mut self,
1062        target: Vec<GlyphId16>,
1063        replacement: GlyphId16,
1064    ) -> bool {
1065        if let SomeLookup::GsubLookup(SubstitutionLookup::Ligature(table)) = self {
1066            let subtable = table.last_mut().unwrap();
1067            if !subtable.can_add(&target, replacement) {
1068                return true;
1069            }
1070            subtable.insert(target, replacement);
1071        } else {
1072            panic!("lookup mismatch");
1073        }
1074        false
1075    }
1076
1077    pub(crate) fn add_gsub_type_8(
1078        &mut self,
1079        backtrack: Vec<GlyphOrClass>,
1080        input: BTreeMap<GlyphId16, GlyphId16>,
1081        lookahead: Vec<GlyphOrClass>,
1082    ) {
1083        if let SomeLookup::GsubLookup(SubstitutionLookup::Reverse(table)) = self {
1084            let subtable = table.last_mut().unwrap();
1085            subtable.add(backtrack, input, lookahead);
1086        }
1087    }
1088
1089    pub(crate) fn as_gsub_contextual(
1090        &mut self,
1091    ) -> &mut ContextualLookupBuilder<SubstitutionLookup> {
1092        let SomeLookup::GsubContextual(table) = self else {
1093            panic!("lookup mismatch")
1094        };
1095        table
1096    }
1097
1098    pub(crate) fn as_gpos_contextual(&mut self) -> &mut ContextualLookupBuilder<PositionLookup> {
1099        if let SomeLookup::GposContextual(table) = self {
1100            table
1101        } else {
1102            panic!("lookup mismatch")
1103        }
1104    }
1105}
1106
1107impl<T> PosSubBuilder<T> {
1108    fn new(lookups: Vec<T>) -> Self {
1109        PosSubBuilder {
1110            lookups,
1111            scripts: Default::default(),
1112            features: Default::default(),
1113            variations: Default::default(),
1114        }
1115    }
1116
1117    fn add(&mut self, key: FeatureKey, lookups: Vec<LookupIdx>, required: bool) -> FeatureIdx {
1118        let feat_key = (key.feature, lookups);
1119        let next_feature = self.features.len();
1120        let idx = *self
1121            .features
1122            .entry(feat_key)
1123            .or_insert_with(|| next_feature.try_into().expect("ran out of u16s"));
1124
1125        let lang_sys = self
1126            .scripts
1127            .entry(key.script)
1128            .or_default()
1129            .entry(key.language)
1130            .or_default();
1131
1132        if required {
1133            lang_sys.required_feature_index = idx;
1134        } else {
1135            lang_sys.feature_indices.push(idx);
1136        }
1137        idx
1138    }
1139
1140    fn add_variation(
1141        &mut self,
1142        idx: FeatureIdx,
1143        conditions: &RawConditionSet,
1144        lookups: Vec<LookupIdx>,
1145    ) {
1146        // not using entry to avoid cloning conditions all the time?
1147        if !self.variations.contains_key(conditions) {
1148            self.variations
1149                .insert(conditions.clone(), Default::default());
1150        }
1151        self.variations
1152            .get_mut(conditions)
1153            .unwrap()
1154            .insert(idx, lookups);
1155    }
1156}
1157
1158impl<T> PosSubBuilder<T>
1159where
1160    T: Builder,
1161    T::Output: Default,
1162{
1163    #[allow(clippy::type_complexity)] // i love my big dumb tuple
1164    fn build_raw(
1165        self,
1166        var_store: &mut VariationStoreBuilder,
1167    ) -> Option<(
1168        LookupList<T::Output>,
1169        ScriptList,
1170        FeatureList,
1171        Option<FeatureVariations>,
1172    )> {
1173        if self.lookups.is_empty() && self.features.is_empty() {
1174            return None;
1175        }
1176
1177        // push empty items so we can insert by index
1178        let mut features = vec![Default::default(); self.features.len()];
1179        for ((tag, lookups), idx) in self.features {
1180            features[idx as usize] = FeatureRecord::new(tag, Feature::new(None, lookups));
1181        }
1182
1183        let scripts = self
1184            .scripts
1185            .into_iter()
1186            .map(|(script_tag, entry)| {
1187                let mut script = Script::default();
1188                for (lang_tag, lang_sys) in entry {
1189                    if lang_tag == tags::LANG_DFLT {
1190                        script.default_lang_sys = lang_sys.into();
1191                    } else {
1192                        script
1193                            .lang_sys_records
1194                            .push(LangSysRecord::new(lang_tag, lang_sys));
1195                    }
1196                }
1197                ScriptRecord::new(script_tag, script)
1198            })
1199            .collect::<Vec<_>>();
1200
1201        let lookups = self
1202            .lookups
1203            .into_iter()
1204            .map(|x| x.build(var_store))
1205            .collect();
1206
1207        let variations = if self.variations.is_empty() {
1208            None
1209        } else {
1210            let records = self
1211                .variations
1212                .into_iter()
1213                .map(|(condset, features)| {
1214                    // if this is an empty conditionset, leave the offset null
1215                    let condset = (!condset.conditions.is_empty()).then_some(condset);
1216                    FeatureVariationRecord::new(
1217                        condset,
1218                        FeatureTableSubstitution::new(
1219                            features
1220                                .into_iter()
1221                                .map(|(feat_id, lookup_ids)| {
1222                                    FeatureTableSubstitutionRecord::new(
1223                                        feat_id,
1224                                        Feature::new(None, lookup_ids),
1225                                    )
1226                                })
1227                                .collect(),
1228                        )
1229                        .into(),
1230                    )
1231                })
1232                .collect();
1233            Some(FeatureVariations::new(records))
1234        };
1235        Some((
1236            LookupList::new(lookups),
1237            ScriptList::new(scripts),
1238            FeatureList::new(features),
1239            variations,
1240        ))
1241    }
1242}
1243
1244impl Builder for PosSubBuilder<PositionLookup> {
1245    type Output = Option<write_gpos::Gpos>;
1246
1247    fn build(self, var_store: &mut VariationStoreBuilder) -> Self::Output {
1248        self.build_raw(var_store)
1249            .map(|(lookups, scripts, features, variations)| {
1250                let mut gpos = write_gpos::Gpos::new(scripts, features, lookups);
1251                gpos.feature_variations = variations.into();
1252                gpos
1253            })
1254    }
1255}
1256
1257impl Builder for PosSubBuilder<SubstitutionLookup> {
1258    type Output = Option<write_gsub::Gsub>;
1259
1260    fn build(self, var_store: &mut VariationStoreBuilder) -> Self::Output {
1261        self.build_raw(var_store)
1262            .map(|(lookups, scripts, features, variations)| {
1263                let mut gsub = write_gsub::Gsub::new(scripts, features, lookups);
1264                gsub.feature_variations = variations.into();
1265                gsub
1266            })
1267    }
1268}
1269
1270impl FeatureKey {
1271    /// Create a new feature key for the provided feature, language, and script.
1272    ///
1273    /// If you already have a [`super::LanguageSystem`], you can create a [`FeatureKey`]
1274    /// with the [`super::LanguageSystem::to_feature_key`] method.
1275    pub const fn new(feature: Tag, language: Tag, script: Tag) -> Self {
1276        FeatureKey {
1277            feature,
1278            language,
1279            script,
1280        }
1281    }
1282}
1283
1284impl Debug for FeatureKey {
1285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1286        write!(f, "{}: {}/{}", self.feature, self.script, self.language)
1287    }
1288}
1289
1290fn is_gpos_rule(kind: Kind) -> bool {
1291    matches!(
1292        kind,
1293        Kind::GposType1
1294            | Kind::GposType2
1295            | Kind::GposType3
1296            | Kind::GposType4
1297            | Kind::GposType5
1298            | Kind::GposType6
1299            | Kind::GposType7
1300            | Kind::GposType8
1301    )
1302}