fea_rs/compile/
lookups.rs

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