fea_rs/compile/
feature_writer.rs

1//! API for the client to manually add additional features
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4
5use write_fonts::{
6    tables::layout::{builders::LookupBuilder, LookupFlag},
7    types::{GlyphId16, Tag},
8};
9
10use crate::GlyphSet;
11
12use super::{
13    features::{AllFeatures, FeatureLookups},
14    language_system::{DefaultLanguageSystems, LanguageSystem},
15    lookups::{AllLookups, FeatureKey, FilterSetId, LookupId, LookupIdMap, PositionLookup},
16    tables::{GdefBuilder, Tables},
17    CaretValue,
18};
19
20/// A trait that can be implemented by the client to do custom feature writing.
21pub trait FeatureProvider {
22    /// The client can write additional features into the provided builder
23    fn add_features(&self, builder: &mut FeatureBuilder);
24}
25
26/// A nop implementation of [FeatureProvider]
27pub struct NopFeatureProvider;
28
29impl FeatureProvider for NopFeatureProvider {
30    fn add_features(&self, _: &mut FeatureBuilder) {}
31}
32
33/// A structure that allows client code to add additional features to the compilation.
34pub struct FeatureBuilder<'a> {
35    pub(crate) language_systems: &'a DefaultLanguageSystems,
36    pub(crate) tables: &'a mut Tables,
37    pub(crate) lookups: Vec<(LookupId, PositionLookup)>,
38    pub(crate) features: BTreeMap<FeatureKey, FeatureLookups>,
39    pub(crate) lig_carets: BTreeMap<GlyphId16, Vec<CaretValue>>,
40    mark_filter_sets: &'a mut HashMap<GlyphSet, FilterSetId>,
41}
42
43pub trait GposSubtableBuilder: Sized {
44    #[doc(hidden)]
45    fn to_pos_lookup(
46        flags: LookupFlag,
47        filter_set: Option<FilterSetId>,
48        subtables: Vec<Self>,
49    ) -> ExternalGposLookup;
50}
51
52/// A lookup generated outside of user FEA
53///
54/// This will be merged into any user-provided features during compilation.
55#[derive(Debug, Default, Clone, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct PendingLookup<T> {
58    subtables: Vec<T>,
59    flags: LookupFlag,
60    mark_filter_set: Option<GlyphSet>,
61}
62
63impl<T> PendingLookup<T> {
64    /// Create a new lookup.
65    ///
66    /// This can later be added to the feature builder via [`FeatureBuilder::add_lookup`]
67    pub fn new(subtables: Vec<T>, flags: LookupFlag, mark_filter_set: Option<GlyphSet>) -> Self {
68        Self {
69            subtables,
70            flags,
71            mark_filter_set,
72        }
73    }
74
75    /// Return a reference to the subtables in this lookup.
76    pub fn subtables(&self) -> &[T] {
77        &self.subtables
78    }
79
80    /// Return the `LookupFlag` for this lookup.
81    pub fn flags(&self) -> LookupFlag {
82        self.flags
83    }
84}
85
86/// An externally created GPOS lookup.
87///
88/// This only exists so that we can avoid making our internal types `pub`.
89pub struct ExternalGposLookup(PositionLookup);
90
91impl<'a> FeatureBuilder<'a> {
92    pub(crate) fn new(
93        language_systems: &'a DefaultLanguageSystems,
94        tables: &'a mut Tables,
95        mark_filter_sets: &'a mut HashMap<GlyphSet, u16>,
96    ) -> Self {
97        Self {
98            language_systems,
99            tables,
100            lookups: Default::default(),
101            features: Default::default(),
102            mark_filter_sets,
103            lig_carets: Default::default(),
104        }
105    }
106
107    /// An iterator over the default language systems registered in the FEA
108    pub fn language_systems(&self) -> impl Iterator<Item = LanguageSystem> + 'a {
109        self.language_systems.iter()
110    }
111
112    /// If the FEA text contained an explicit GDEF table block, return its contents
113    pub fn gdef(&self) -> Option<&GdefBuilder> {
114        self.tables.gdef.as_ref()
115    }
116
117    /// Add caret positions for the GDEF `LigCaretList` table
118    pub fn add_lig_carets(&mut self, lig_carets: BTreeMap<GlyphId16, Vec<CaretValue>>) {
119        self.lig_carets = lig_carets;
120    }
121
122    /// Add a lookup to the lookup list.
123    ///
124    /// The `LookupId` that is returned can then be included in features (i.e,
125    /// passed to [`add_feature`](Self::add_feature).)
126    pub fn add_lookup<T: GposSubtableBuilder>(&mut self, lookup: PendingLookup<T>) -> LookupId {
127        let PendingLookup {
128            subtables,
129            flags,
130            mark_filter_set,
131        } = lookup;
132        let filter_set_id = mark_filter_set.map(|cls| self.get_filter_set_id(cls));
133        let lookup = T::to_pos_lookup(flags, filter_set_id, subtables);
134        let next_id = LookupId::External(self.lookups.len());
135        self.lookups.push((next_id, lookup.0));
136        next_id
137    }
138
139    /// Add lookups to every default language system.
140    ///
141    /// Convenience method for recurring pattern.
142    pub fn add_to_default_language_systems(&mut self, feature_tag: Tag, lookups: &[LookupId]) {
143        for langsys in self.language_systems() {
144            let feature_key = langsys.to_feature_key(feature_tag);
145            self.add_feature(feature_key, lookups.to_vec());
146        }
147    }
148
149    /// Create a new feature, registered for a particular language system.
150    ///
151    /// The caller must call this method once for each language system under
152    /// which a feature is to be registered.
153    pub fn add_feature(&mut self, key: FeatureKey, lookups: Vec<LookupId>) {
154        self.features.entry(key).or_default().base = lookups;
155    }
156
157    fn get_filter_set_id(&mut self, cls: GlyphSet) -> FilterSetId {
158        let next_id = self.mark_filter_sets.len();
159        *self.mark_filter_sets.entry(cls).or_insert_with(|| {
160            next_id
161                .try_into()
162                // is this in any way an expected error condition?
163                .expect("too many filter sets?")
164        })
165    }
166
167    pub(crate) fn finish(self) -> ExternalFeatures {
168        let FeatureBuilder {
169            lookups,
170            features,
171            lig_carets,
172            ..
173        } = self;
174        ExternalFeatures {
175            features,
176            lookups,
177            lig_carets,
178        }
179    }
180}
181
182impl<T> GposSubtableBuilder for T
183where
184    T: Default,
185    LookupBuilder<T>: Into<PositionLookup>,
186{
187    fn to_pos_lookup(
188        flags: LookupFlag,
189        filter_set: Option<FilterSetId>,
190        subtables: Vec<Self>,
191    ) -> ExternalGposLookup {
192        ExternalGposLookup(LookupBuilder::new_with_lookups(flags, filter_set, subtables).into())
193    }
194}
195
196// features that can be added by a feature writer
197const CURS: Tag = Tag::new(b"curs");
198const MARK: Tag = Tag::new(b"mark");
199const MKMK: Tag = Tag::new(b"mkmk");
200const ABVM: Tag = Tag::new(b"abvm");
201const BLWM: Tag = Tag::new(b"blwm");
202const KERN: Tag = Tag::new(b"kern");
203const DIST: Tag = Tag::new(b"dist");
204
205/// All of the state that is generated by the external provider
206pub(crate) struct ExternalFeatures {
207    pub(crate) lookups: Vec<(LookupId, PositionLookup)>,
208    pub(crate) features: BTreeMap<FeatureKey, FeatureLookups>,
209    pub(crate) lig_carets: BTreeMap<GlyphId16, Vec<CaretValue>>,
210}
211
212#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
213pub(crate) struct InsertionPoint {
214    /// The position in the lookuplist to insert a set of new lookups
215    pub(crate) lookup_id: LookupId,
216    /// if multiple sets are inserted at the same lookup index, this breaks ties
217    ///
218    /// This is common! for instance if you have FEA with two feature blocks
219    /// that contain `# Automatic Code` comments and nothing else, this is used
220    /// to order them.
221    pub(crate) priority: usize,
222}
223
224struct MergeCtx<'a> {
225    all_lookups: &'a mut AllLookups,
226    all_feats: &'a mut AllFeatures,
227    // that were explicit
228    insert_markers: &'a HashMap<Tag, InsertionPoint>,
229    ext_lookups: BTreeMap<LookupId, PositionLookup>,
230    ext_features: BTreeMap<FeatureKey, FeatureLookups>,
231    // ready for insertion
232    processed_lookups: Vec<(InsertionPoint, Vec<(LookupId, PositionLookup)>)>,
233    // track how many groups of lookups have been appended on the end,
234    // so we can give them the right priority
235    append_priority: usize,
236}
237
238impl MergeCtx<'_> {
239    fn merge(mut self) {
240        // This is complicated.
241        //
242        // We are trying to match the behaviour provided by 'feature writers'
243        // in ufo2ft.
244        //
245        // These work by actually generating FEA, and inserting it into the AST
246        // before compilation.
247        //
248        // We are not modifying the AST; the AST has already been compiled, and
249        // we are now inserting generated lookups into those that were compiled
250        // from the AST.
251        //
252        // This means we need to figure out where in the AST these external
253        // lookups _would have been inserted_, and what IDs would have been
254        // assigned to them if they had been there.
255
256        // To make it easier to follow our logic, we will process the external
257        // lookups and features in groups, replicating how they would be
258        // handled by the various feature writers.
259
260        self.do_curs();
261        self.do_kern_and_dist();
262        self.do_marks();
263
264        // okay so now 'processed_lookups' should contain insertion points for
265        // all of our lookups
266
267        if !self.ext_lookups.is_empty() {
268            log::warn!("feature merging left unhandled features!");
269        }
270        self.finalize();
271    }
272
273    fn finalize(mut self) {
274        self.processed_lookups.sort_by_key(|(key, _)| *key);
275
276        // this is the actual logic for inserting the lookups into the main
277        // lookup list, keeping track of how the ids change.
278
279        let mut map = LookupIdMap::default();
280        let mut inserted_so_far = 0;
281
282        // 'adjustments' stores the state we need to remap existing ids, if needed.
283        let mut adjustments = Vec::new();
284
285        for (insert_point, mut lookups) in self.processed_lookups {
286            let first_id = insert_point.lookup_id.to_raw();
287            // within a feature, the lookups should honor the ordering they were
288            // assigned by the user
289            lookups.sort_by_key(|(key, _)| *key);
290            // first update the ids
291            for (i, (temp_id, _)) in lookups.iter().enumerate() {
292                let final_id = LookupId::Gpos(first_id + inserted_so_far + i);
293                map.insert(*temp_id, final_id);
294            }
295            // then insert the lookups into the correct position
296            let insert_at = first_id + inserted_so_far;
297            inserted_so_far += lookups.len();
298            self.all_lookups
299                .splice_gpos(insert_at, lookups.into_iter().map(|v| v.1.clone()));
300            adjustments.push((first_id, inserted_so_far));
301        }
302
303        // now based on our recorded adjustments, figure out the remapping
304        // for the existing ids. each entry in adjustment is an (index, delta)
305        // pair, where the delta applies from adjustment[n] to adjustment[n +1]
306        if !adjustments.is_empty() {
307            // add the end of the last range
308            adjustments.push((self.all_lookups.next_gpos_id().to_raw(), inserted_so_far));
309        }
310        let (mut range_start, mut adjust) = (0, 0);
311        let mut adjustments = adjustments.as_slice();
312        while let Some(((next_start, next_adjust), remaining)) = adjustments.split_first() {
313            if adjust > 0 {
314                for old_id in range_start..*next_start {
315                    map.insert(LookupId::Gpos(old_id), LookupId::Gpos(old_id + adjust));
316                }
317            }
318            (range_start, adjust, adjustments) = (*next_start, *next_adjust, remaining);
319        }
320
321        self.all_feats.merge_external_features(self.ext_features);
322        self.all_feats.remap_ids(&map);
323        self.all_lookups.remap_ids(&map);
324    }
325
326    fn do_curs(&mut self) {
327        let curs_pos = self
328            .insert_markers
329            .get(&CURS)
330            .copied()
331            .unwrap_or_else(|| self.insertion_point_for_append());
332        self.finalize_lookups_for_feature(CURS, curs_pos);
333    }
334
335    fn do_kern_and_dist(&mut self) {
336        // the lookups for these two features are grouped together,
337        // and are inserted before whichever of them occurs first.
338
339        let marker = self
340            .insert_markers
341            .get(&DIST)
342            .or_else(|| self.insert_markers.get(&KERN))
343            .copied()
344            .unwrap_or_else(|| self.insertion_point_for_append());
345
346        let lookups = self.take_lookups_for_features(&[KERN, DIST]);
347        if !lookups.is_empty() {
348            self.processed_lookups.push((marker, lookups));
349        }
350    }
351
352    fn do_marks(&mut self) {
353        // This one is complicated!
354        // there is logic in the base feature writer that says, when multiple
355        // features are added, features that are earlier in the input list will
356        // be inserted before features that come later.
357        //
358        // the markFeatureWriter passes these features in in alphabetical order:
359        // https://github.com/googlefonts/ufo2ft/blob/16ed156bd6a/Lib/ufo2ft/featureWriters/markFeatureWriter.py#L1174
360        //
361        // BUT! the lookups for abvm & blwm are included _in_ the feature blocks
362        // for those features, but the lookups for mark and mkmk are not
363        // inlined, and so they are grouped together, but inserted before any
364        // other lookups in this group. My head hurts :) :)
365
366        const ORDER: [Tag; 4] = [ABVM, BLWM, MARK, MKMK];
367        let mut inserts = [None; 4];
368        for (i, tag) in ORDER.iter().enumerate() {
369            inserts[i] = self.insert_markers.get(tag).copied();
370        }
371
372        for i in 0..ORDER.len() {
373            if let Some(insert) = inserts[i] {
374                // if a later feature has an explicit position, put the earlier
375                // features in front of it
376                for j in 0..i {
377                    let j = i - j - 1; // we want to go in reverse order,
378                                       // prepending each time
379                    if inserts[j].is_none() {
380                        inserts[j] = Some(InsertionPoint {
381                            lookup_id: insert.lookup_id,
382                            priority: insert.priority - 1,
383                        })
384                    }
385                }
386            }
387        }
388
389        // so now `inserts` has explicit positions for features that need them.
390        // lets fill in any features that were not assigned positions this way:
391        for insert in inserts.iter_mut() {
392            if insert.is_none() {
393                *insert = Some(self.insertion_point_for_append());
394            }
395        }
396
397        // and finally lets put the lookups in our processed list:
398        self.finalize_lookups_for_feature(ABVM, inserts[0].unwrap());
399        self.finalize_lookups_for_feature(BLWM, inserts[1].unwrap());
400        self.finalize_lookups_for_feature(MARK, inserts[2].unwrap());
401        self.finalize_lookups_for_feature(MKMK, inserts[3].unwrap());
402    }
403
404    fn finalize_lookups_for_feature(&mut self, feature: Tag, pos: InsertionPoint) {
405        let lookups = self.take_lookups_for_features(&[feature]);
406        if !lookups.is_empty() {
407            self.processed_lookups.push((pos, lookups));
408        }
409    }
410
411    fn lookup_ids_for_features(&self, features: &[Tag]) -> BTreeSet<LookupId> {
412        self.ext_features
413            .iter()
414            .filter(|(feat, _)| features.contains(&feat.feature))
415            .flat_map(|(_, lookups)| lookups.iter_ids())
416            .collect()
417    }
418
419    fn take_lookups_for_features(&mut self, features: &[Tag]) -> Vec<(LookupId, PositionLookup)> {
420        self.lookup_ids_for_features(features)
421            .into_iter()
422            .map(|id| (id, self.ext_lookups.remove(&id).unwrap()))
423            .collect()
424    }
425
426    fn insertion_point_for_append(&mut self) -> InsertionPoint {
427        let lookup_id = self.all_lookups.next_gpos_id();
428        self.append_priority += 1;
429        InsertionPoint {
430            lookup_id,
431            priority: self.append_priority,
432        }
433    }
434}
435
436impl ExternalFeatures {
437    /// Merge the external features into the already compiled features.
438    pub(crate) fn merge_into(
439        &mut self,
440        all_lookups: &mut AllLookups,
441        all_feats: &mut AllFeatures,
442        markers: &HashMap<Tag, InsertionPoint>,
443    ) {
444        let ctx = MergeCtx {
445            all_lookups,
446            all_feats,
447            ext_lookups: self.lookups.iter().cloned().collect(),
448            ext_features: self.features.clone(),
449            insert_markers: markers,
450            processed_lookups: Default::default(),
451            append_priority: 1_000_000_000,
452        };
453        ctx.merge();
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    use crate::compile::tags::{LANG_DFLT, SCRIPT_DFLT};
462
463    impl AllFeatures {
464        // after remapping, return the order of features, based on the final
465        // ordering of lookups.
466        //
467        // this works because for these tests we add one unique lookup to each
468        // feature, so lookup order is a proxy for feature order.
469        // - one lookup per feature
470        // - no duplicates
471        fn feature_order_for_test(&self) -> Vec<Tag> {
472            let mut id_and_tag = self
473                .features
474                .iter()
475                .map(|(key, val)| (val.iter_ids().next().unwrap(), key.feature))
476                .collect::<Vec<_>>();
477            id_and_tag.sort();
478            id_and_tag.into_iter().map(|(_, tag)| tag).collect()
479        }
480    }
481
482    #[test]
483    fn merge_external_lookups_before() {
484        let mut all = AllLookups::default();
485        all.splice_gpos(
486            0,
487            (0..8).map(|_| PositionLookup::Single(Default::default())),
488        );
489
490        let lookups = (0..6)
491            .map(|id| {
492                (
493                    LookupId::External(id),
494                    PositionLookup::Pair(Default::default()),
495                )
496            })
497            .collect();
498        let features: BTreeMap<_, _> =
499            [(MARK, [0].as_slice()), (MKMK, &[1, 2]), (KERN, &[3, 4, 5])]
500                .iter()
501                .map(|(tag, ids)| {
502                    let mut features = FeatureLookups::default();
503                    features.base = ids.iter().copied().map(LookupId::External).collect();
504                    (FeatureKey::new(*tag, LANG_DFLT, SCRIPT_DFLT), features)
505                })
506                .collect();
507
508        // mark is 'after', kern is 'before', and mkmk has no marker (goes at the end)
509        let markers = HashMap::from([
510            (
511                MARK,
512                InsertionPoint {
513                    lookup_id: LookupId::Gpos(3),
514                    priority: 100,
515                },
516            ),
517            (
518                KERN,
519                InsertionPoint {
520                    lookup_id: LookupId::Gpos(5),
521                    priority: 200,
522                },
523            ),
524        ]);
525
526        let mut external_features = ExternalFeatures {
527            lookups,
528            features,
529            lig_carets: Default::default(),
530        };
531
532        let mut all_features = AllFeatures::default();
533        external_features.merge_into(&mut all, &mut all_features, &markers);
534        let expected_ids: [(Tag, &[usize]); 3] =
535            [(MARK, &[3]), (MKMK, &[12, 13]), (KERN, &[6, 7, 8])];
536
537        for (tag, ids) in expected_ids {
538            let key = FeatureKey::new(tag, LANG_DFLT, SCRIPT_DFLT);
539            let result = all_features
540                .get_or_insert(key)
541                .iter_ids()
542                .map(|id| id.to_raw())
543                .collect::<Vec<_>>();
544            assert_eq!(ids, result)
545        }
546    }
547
548    fn mock_external_features(tags: &[Tag]) -> ExternalFeatures {
549        let mut lookups = Vec::new();
550        let mut features = BTreeMap::new();
551
552        for (i, feature) in tags.iter().enumerate() {
553            let id = LookupId::External(i);
554            let lookup = PositionLookup::Single(Default::default());
555            let key = FeatureKey::new(*feature, LANG_DFLT, SCRIPT_DFLT);
556
557            let mut feature_lookups = FeatureLookups::default();
558            feature_lookups.base = vec![id];
559            lookups.push((id, lookup));
560            features.insert(key, feature_lookups);
561        }
562        ExternalFeatures {
563            lookups,
564            features,
565            lig_carets: Default::default(),
566        }
567    }
568
569    fn make_markers_with_order<const N: usize>(order: [Tag; N]) -> HashMap<Tag, InsertionPoint> {
570        order
571            .into_iter()
572            .enumerate()
573            .map(|(i, tag)| {
574                (
575                    tag,
576                    InsertionPoint {
577                        lookup_id: LookupId::Gpos(0),
578                        priority: i + 10,
579                    },
580                )
581            })
582            .collect()
583    }
584
585    // respect dependencies: kern before dist, abvm/blwm/mark before mkmk.
586    #[test]
587    fn feature_ordering_without_markers() {
588        let mut external = mock_external_features(&[KERN, DIST, MKMK, ABVM, BLWM, MARK, CURS]);
589        let markers = make_markers_with_order([]);
590        let mut all = AllLookups::default();
591        let mut all_feats = AllFeatures::default();
592        external.merge_into(&mut all, &mut all_feats, &markers);
593
594        assert_eq!(
595            all_feats.feature_order_for_test(),
596            [CURS, KERN, DIST, ABVM, BLWM, MARK, MKMK]
597        );
598    }
599
600    #[test]
601    fn kern_and_dist_respect_input_order() {
602        // for kern/dist lookups are inserted together, and respect the lookup
603        // order assigned when they were passed in.
604
605        let mut external = mock_external_features(&[DIST, KERN, CURS]);
606
607        let markers = make_markers_with_order([]);
608        let mut all = AllLookups::default();
609        let mut all_feats = AllFeatures::default();
610        external.merge_into(&mut all, &mut all_feats, &markers);
611        assert_eq!(all_feats.feature_order_for_test(), [CURS, DIST, KERN]);
612    }
613
614    #[test]
615    fn kern_and_dist_respect_input_order_with_marker() {
616        // - dist is earlier in input order
617        // - kern has an explicit marker
618        // = the marker is used for both kern/dist, and they keep input order
619
620        let mut external = mock_external_features(&[CURS, DIST, KERN]);
621
622        let markers = make_markers_with_order([KERN]);
623        let mut all = AllLookups::default();
624        let mut all_feats = AllFeatures::default();
625        external.merge_into(&mut all, &mut all_feats, &markers);
626        assert_eq!(all_feats.feature_order_for_test(), [DIST, KERN, CURS]);
627    }
628
629    #[test]
630    fn blwm_with_marker_takes_abvm_with_it() {
631        let mut external = mock_external_features(&[BLWM, ABVM, DIST]);
632        let markers = make_markers_with_order([BLWM]);
633        let mut all = AllLookups::default();
634        let mut all_feats = AllFeatures::default();
635        external.merge_into(&mut all, &mut all_feats, &markers);
636        // because BLWM has a marker and ABVM depends on it, insert ABVM first
637        assert_eq!(all_feats.feature_order_for_test(), [ABVM, BLWM, DIST]);
638    }
639
640    #[test]
641    fn marks_with_marker_goes_before_kern() {
642        let mut external = mock_external_features(&[MARK, KERN]);
643        // 'mark' gets an explicit location
644        let markers = make_markers_with_order([MARK]);
645        let mut all = AllLookups::default();
646        let mut all_feats = AllFeatures::default();
647        external.merge_into(&mut all, &mut all_feats, &markers);
648        assert_eq!(all_feats.feature_order_for_test(), [MARK, KERN]);
649    }
650
651    #[test]
652    fn mkmk_brings_along_the_whole_family() {
653        let mut external = mock_external_features(&[BLWM, KERN, MKMK, DIST, MARK, ABVM]);
654        let markers = make_markers_with_order([MKMK]);
655        let mut all = AllLookups::default();
656        let mut all_feats = AllFeatures::default();
657        external.merge_into(&mut all, &mut all_feats, &markers);
658        assert_eq!(
659            all_feats.feature_order_for_test(),
660            // abvm/blwm/mark all have to go before mkmk
661            [ABVM, BLWM, MARK, MKMK, KERN, DIST]
662        );
663    }
664}