Skip to main content

fea_rs_ast/
gsub.rs

1use fea_rs::{
2    Kind,
3    typed::{AstNode as _, GlyphOrClass},
4};
5
6use crate::{
7    AsFea, GlyphContainer, PotentiallyContextualStatement,
8    contextual::{backtrack, context_glyphs, lookahead},
9};
10use std::ops::Range;
11
12/// A single substitution (GSUB type 1) statement
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SingleSubstStatement {
15    /// The location of the statement in the source FEA.
16    pub location: Range<usize>,
17    /// The prefix (backtrack) glyphs
18    pub prefix: Vec<GlyphContainer>,
19    /// The suffix (lookahead) glyphs
20    pub suffix: Vec<GlyphContainer>,
21    /// The glyphs to be substituted
22    ///
23    /// Although this is a single substitution, there may be multiple
24    /// glyphs in e.g. `sub [a b] by [c d];` where `a` and `b` are
25    /// both substituted by `c` and `d` respectively.
26    pub glyphs: Vec<GlyphContainer>,
27    /// The replacement glyphs
28    pub replacement: Vec<GlyphContainer>,
29    /// Whether to force this substitution to be treated as contextual
30    pub force_chain: bool,
31}
32
33impl SingleSubstStatement {
34    /// Create a new single substitution statement.
35    ///
36    /// Note the unusual argument order: `prefix` and suffix come *after*
37    /// the replacement `glyphs`. `prefix`, `suffix`, `glyphs` and
38    /// `replacement` should be lists of `glyph-containing objects`_. `glyphs` and
39    /// `replacement` should be one-item lists.
40    pub fn new(
41        glyphs: Vec<GlyphContainer>,
42        replacement: Vec<GlyphContainer>,
43        prefix: Vec<GlyphContainer>,
44        suffix: Vec<GlyphContainer>,
45        location: Range<usize>,
46        force_chain: bool,
47    ) -> Self {
48        Self {
49            prefix,
50            suffix,
51            glyphs,
52            replacement,
53            location,
54            force_chain,
55        }
56    }
57}
58
59impl PotentiallyContextualStatement for SingleSubstStatement {
60    fn prefix(&self) -> &[GlyphContainer] {
61        &self.prefix
62    }
63    fn suffix(&self) -> &[GlyphContainer] {
64        &self.suffix
65    }
66    fn force_chain(&self) -> bool {
67        self.force_chain
68    }
69
70    fn format_begin(&self, _indent: &str) -> String {
71        "sub ".to_string()
72    }
73
74    fn format_contextual_parts(&self, _indent: &str) -> Vec<String> {
75        self.glyphs
76            .iter()
77            .map(|g| format!("{}'", g.as_fea("")))
78            .collect()
79    }
80
81    fn format_noncontextual_parts(&self, _indent: &str) -> Vec<String> {
82        self.glyphs.iter().map(|g| g.as_fea("")).collect()
83    }
84    fn format_end(&self, _indent: &str) -> String {
85        let replacement_str: Vec<String> = self.replacement.iter().map(|g| g.as_fea("")).collect();
86        format!(" by {}", replacement_str.join(" "))
87    }
88}
89
90impl From<fea_rs::typed::Gsub1> for SingleSubstStatement {
91    fn from(val: fea_rs::typed::Gsub1) -> Self {
92        let target = val
93            .node()
94            .iter_children()
95            .find_map(GlyphOrClass::cast)
96            .unwrap();
97        let replacement = val
98            .node()
99            .iter_children()
100            .skip_while(|t| t.kind() != Kind::ByKw)
101            .find_map(GlyphOrClass::cast)
102            .unwrap();
103        SingleSubstStatement {
104            prefix: vec![],
105            suffix: vec![],
106            glyphs: vec![target.into()],
107            replacement: vec![replacement.into()],
108            location: val.node().range(),
109            force_chain: false,
110        }
111    }
112}
113
114/// A multiple substitution (GSUB type 2) statement
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct MultipleSubstStatement {
117    /// The location of the statement in the source FEA.
118    pub location: Range<usize>,
119    /// The prefix (backtrack) glyphs
120    pub prefix: Vec<GlyphContainer>,
121    /// The suffix (lookahead) glyphs
122    pub suffix: Vec<GlyphContainer>,
123    /// The glyph to be substituted
124    pub glyph: GlyphContainer,
125    /// The replacement glyphs
126    pub replacement: Vec<GlyphContainer>,
127    /// Whether to force this substitution to be treated as contextual
128    pub force_chain: bool,
129}
130
131impl MultipleSubstStatement {
132    /// Create a new multiple substitution statement.
133    pub fn new(
134        glyph: GlyphContainer,
135        replacement: Vec<GlyphContainer>,
136        prefix: Vec<GlyphContainer>,
137        suffix: Vec<GlyphContainer>,
138        location: Range<usize>,
139        force_chain: bool,
140    ) -> Self {
141        Self {
142            prefix,
143            suffix,
144            glyph,
145            replacement,
146            location,
147            force_chain,
148        }
149    }
150}
151
152impl PotentiallyContextualStatement for MultipleSubstStatement {
153    fn prefix(&self) -> &[GlyphContainer] {
154        &self.prefix
155    }
156    fn suffix(&self) -> &[GlyphContainer] {
157        &self.suffix
158    }
159    fn force_chain(&self) -> bool {
160        self.force_chain
161    }
162
163    fn format_begin(&self, _indent: &str) -> String {
164        "sub ".to_string()
165    }
166
167    fn format_contextual_parts(&self, _indent: &str) -> Vec<String> {
168        vec![format!("{}'", self.glyph.as_fea(""))]
169    }
170
171    fn format_noncontextual_parts(&self, _indent: &str) -> Vec<String> {
172        vec![self.glyph.as_fea("")]
173    }
174
175    fn format_end(&self, _indent: &str) -> String {
176        let replacement_str: Vec<String> = self.replacement.iter().map(|g| g.as_fea("")).collect();
177        format!(" by {}", replacement_str.join(" "))
178    }
179}
180
181impl From<fea_rs::typed::Gsub2> for MultipleSubstStatement {
182    fn from(val: fea_rs::typed::Gsub2) -> Self {
183        let target = val
184            .node()
185            .iter_children()
186            .find_map(GlyphOrClass::cast)
187            .unwrap();
188        let replacement = val
189            .node()
190            .iter_children()
191            .skip_while(|t| t.kind() != Kind::ByKw)
192            .skip(1)
193            .filter_map(GlyphOrClass::cast);
194        MultipleSubstStatement {
195            prefix: vec![],
196            suffix: vec![],
197            glyph: target.into(),
198            replacement: replacement.map(|x| x.into()).collect(),
199            location: val.node().range(),
200            force_chain: false,
201        }
202    }
203}
204
205impl TryFrom<fea_rs::typed::Gsub6> for MultipleSubstStatement {
206    type Error = ();
207
208    fn try_from(val: fea_rs::typed::Gsub6) -> Result<Self, ()> {
209        // Two conditions: We need to see an InlineSubRule, and it must have
210        // more than one child.
211        if !val.node().iter_children().any(|c| {
212            if let Some(inline) = fea_rs::typed::InlineSubRule::cast(c) {
213                inline
214                    .node()
215                    .iter_children()
216                    .filter(|k| GlyphOrClass::cast(k).is_some())
217                    .count()
218                    > 1
219            } else {
220                false
221            }
222        }) {
223            return Err(());
224        }
225        let prefix = backtrack(val.node());
226        let context = context_glyphs(val.node());
227        let suffix = lookahead(val.node());
228        let targets = inline_sub_targets(val.node());
229
230        Ok(MultipleSubstStatement {
231            prefix,
232            suffix,
233            glyph: context.into_iter().next().unwrap(),
234            replacement: targets,
235            location: val.node().range(),
236            force_chain: true,
237        })
238    }
239}
240
241fn inline_sub_targets(val: &fea_rs::Node) -> Vec<GlyphContainer> {
242    let inline_sub = val
243        .iter_children()
244        .find_map(fea_rs::typed::InlineSubRule::cast)
245        .unwrap();
246    inline_sub
247        .node()
248        .iter_children()
249        .filter_map(GlyphOrClass::cast)
250        .map(|goc| goc.into())
251        .collect()
252}
253
254/// An alternate substitution (GSUB type 3) statement
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct AlternateSubstStatement {
257    /// The location of the statement in the source FEA.
258    pub location: Range<usize>,
259    /// The prefix (backtrack) glyphs
260    pub prefix: Vec<GlyphContainer>,
261    /// The suffix (lookahead) glyphs
262    pub suffix: Vec<GlyphContainer>,
263    /// The glyph to be substituted
264    pub glyph: GlyphContainer,
265    /// The replacement glyph
266    pub replacement: GlyphContainer,
267    /// Whether to force this substitution to be treated as contextual
268    pub force_chain: bool,
269}
270
271impl AlternateSubstStatement {
272    /// Create a new Alternate substitution statement.
273    pub fn new(
274        glyph: GlyphContainer,
275        replacement: GlyphContainer,
276        prefix: Vec<GlyphContainer>,
277        suffix: Vec<GlyphContainer>,
278        location: Range<usize>,
279        force_chain: bool,
280    ) -> Self {
281        Self {
282            prefix,
283            suffix,
284            glyph,
285            replacement,
286            location,
287            force_chain,
288        }
289    }
290}
291
292impl PotentiallyContextualStatement for AlternateSubstStatement {
293    fn prefix(&self) -> &[GlyphContainer] {
294        &self.prefix
295    }
296    fn suffix(&self) -> &[GlyphContainer] {
297        &self.suffix
298    }
299    fn force_chain(&self) -> bool {
300        self.force_chain
301    }
302
303    fn format_begin(&self, _indent: &str) -> String {
304        "sub ".to_string()
305    }
306
307    fn format_contextual_parts(&self, _indent: &str) -> Vec<String> {
308        vec![format!("{}'", self.glyph.as_fea(""))]
309    }
310
311    fn format_noncontextual_parts(&self, _indent: &str) -> Vec<String> {
312        vec![self.glyph.as_fea("")]
313    }
314    fn format_end(&self, _indent: &str) -> String {
315        let replacement_str: String = self.replacement.as_fea("");
316        format!(" from {}", replacement_str)
317    }
318}
319
320impl From<fea_rs::typed::Gsub3> for AlternateSubstStatement {
321    fn from(val: fea_rs::typed::Gsub3) -> Self {
322        let target = val
323            .node()
324            .iter_children()
325            .find_map(GlyphOrClass::cast)
326            .unwrap();
327        let replacement = val
328            .node()
329            .iter_children()
330            .skip_while(|t| t.kind() != Kind::FromKw)
331            .find_map(fea_rs::typed::GlyphClass::cast)
332            .unwrap();
333        AlternateSubstStatement {
334            prefix: vec![],
335            suffix: vec![],
336            glyph: target.into(),
337            replacement: replacement.into(),
338            location: val.node().range(),
339            force_chain: false,
340        }
341    }
342}
343
344/// A ligature substitution (GSUB type 4) statement
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct LigatureSubstStatement {
347    /// The location of the statement in the source FEA.
348    pub location: Range<usize>,
349    /// The prefix (backtrack) glyphs
350    pub prefix: Vec<GlyphContainer>,
351    /// The suffix (lookahead) glyphs
352    pub suffix: Vec<GlyphContainer>,
353    /// The glyphs to be substituted
354    pub glyphs: Vec<GlyphContainer>,
355    /// The replacement glyph
356    pub replacement: GlyphContainer,
357    /// Whether to force this substitution to be treated as contextual
358    pub force_chain: bool,
359}
360
361impl LigatureSubstStatement {
362    /// Create a new ligature substitution statement.
363    pub fn new(
364        glyphs: Vec<GlyphContainer>,
365        replacement: GlyphContainer,
366        prefix: Vec<GlyphContainer>,
367        suffix: Vec<GlyphContainer>,
368        location: Range<usize>,
369        force_chain: bool,
370    ) -> Self {
371        Self {
372            prefix,
373            suffix,
374            glyphs,
375            replacement,
376            location,
377            force_chain,
378        }
379    }
380}
381
382impl PotentiallyContextualStatement for LigatureSubstStatement {
383    fn prefix(&self) -> &[GlyphContainer] {
384        &self.prefix
385    }
386    fn suffix(&self) -> &[GlyphContainer] {
387        &self.suffix
388    }
389    fn force_chain(&self) -> bool {
390        self.force_chain
391    }
392
393    fn format_begin(&self, _indent: &str) -> String {
394        "sub ".to_string()
395    }
396
397    fn format_contextual_parts(&self, _indent: &str) -> Vec<String> {
398        self.glyphs
399            .iter()
400            .map(|g| format!("{}'", g.as_fea("")))
401            .collect()
402    }
403
404    fn format_noncontextual_parts(&self, _indent: &str) -> Vec<String> {
405        self.glyphs.iter().map(|g| g.as_fea("")).collect()
406    }
407
408    fn format_end(&self, _indent: &str) -> String {
409        let replacement_str: String = self.replacement.as_fea("");
410        format!(" by {}", replacement_str)
411    }
412}
413
414impl From<fea_rs::typed::Gsub4> for LigatureSubstStatement {
415    fn from(val: fea_rs::typed::Gsub4) -> Self {
416        let target = val
417            .node()
418            .iter_children()
419            .take_while(|t| t.kind() != Kind::ByKw)
420            .filter_map(GlyphOrClass::cast)
421            .collect::<Vec<_>>();
422        let replacement = val
423            .node()
424            .iter_children()
425            .skip_while(|t| t.kind() != Kind::ByKw)
426            .find_map(GlyphOrClass::cast)
427            .unwrap();
428        LigatureSubstStatement {
429            prefix: vec![],
430            suffix: vec![],
431            glyphs: target.into_iter().map(|g| g.into()).collect(),
432            replacement: replacement.into(),
433            location: val.node().range(),
434            force_chain: false,
435        }
436    }
437}
438
439/// A reverse chaining substitution statement
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct ReverseChainSingleSubstStatement {
442    /// The location of the statement in the source FEA.
443    pub location: Range<usize>,
444    /// The prefix (backtrack) glyphs
445    pub prefix: Vec<GlyphContainer>,
446    /// The suffix (lookahead) glyphs
447    pub suffix: Vec<GlyphContainer>,
448    /// The glyphs to be substituted
449    pub glyphs: Vec<GlyphContainer>,
450    /// The replacement glyphs
451    pub replacements: Vec<GlyphContainer>,
452}
453
454impl ReverseChainSingleSubstStatement {
455    /// Create a new reverse chaining single substitution statement.
456    pub fn new(
457        glyphs: Vec<GlyphContainer>,
458        replacements: Vec<GlyphContainer>,
459        prefix: Vec<GlyphContainer>,
460        suffix: Vec<GlyphContainer>,
461        location: Range<usize>,
462    ) -> Self {
463        Self {
464            prefix,
465            suffix,
466            glyphs,
467            replacements,
468            location,
469        }
470    }
471}
472
473impl AsFea for ReverseChainSingleSubstStatement {
474    fn as_fea(&self, _indent: &str) -> String {
475        let mut res = String::new();
476        res.push_str("rsub ");
477        if !self.prefix.is_empty() || !self.suffix.is_empty() {
478            if !self.prefix.is_empty() {
479                let prefix_str: Vec<String> = self.prefix.iter().map(|g| g.as_fea("")).collect();
480                res.push_str(prefix_str.join(" ").as_str());
481                res.push(' ');
482            }
483            let glyphs_str: Vec<String> = self
484                .glyphs
485                .iter()
486                .map(|g| format!("{}'", g.as_fea("")))
487                .collect();
488            res.push_str(&glyphs_str.join(" "));
489            if !self.suffix.is_empty() {
490                res.push(' ');
491                let suffix_str: Vec<String> = self.suffix.iter().map(|g| g.as_fea("")).collect();
492                res.push_str(suffix_str.join(" ").as_str());
493            }
494        } else {
495            let glyphs_str: Vec<String> = self.glyphs.iter().map(|g| g.as_fea("")).collect();
496            res.push_str(&glyphs_str.join(" "));
497        }
498        let replacement_str: Vec<String> = self.replacements.iter().map(|g| g.as_fea("")).collect();
499        res.push_str(&format!(" by {};", replacement_str.join(" ")));
500        res
501    }
502}
503
504impl From<fea_rs::typed::Gsub8> for ReverseChainSingleSubstStatement {
505    fn from(val: fea_rs::typed::Gsub8) -> Self {
506        let prefix = backtrack(val.node());
507        let context = context_glyphs(val.node());
508        let suffix = lookahead(val.node());
509        let targets = inline_sub_targets(val.node());
510        // println!("Prefix = {:?}", prefix);
511        // println!("Suffix = {:?}", suffix);
512        // println!("Context = {:?}", context);
513        // println!("Targets = {:?}", targets);
514
515        ReverseChainSingleSubstStatement {
516            prefix,
517            suffix,
518            glyphs: context, // Should only be one glyph here really but python uses an array
519            replacements: targets,
520            location: val.node().range(),
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use smol_str::SmolStr;
528
529    use crate::{GlyphClass, GlyphName};
530
531    use super::*;
532
533    #[test]
534    fn test_generate_gsub1() {
535        let gsub1 = SingleSubstStatement::new(
536            vec![GlyphContainer::GlyphName(GlyphName::new("x"))],
537            vec![GlyphContainer::GlyphName(GlyphName::new("a"))],
538            vec![GlyphContainer::GlyphClass(GlyphClass::new(
539                vec![
540                    GlyphContainer::GlyphName(GlyphName::new("a.smcp")),
541                    GlyphContainer::GlyphName(GlyphName::new("b.smcp")),
542                ],
543                0..0,
544            ))],
545            vec![],
546            0..0,
547            false,
548        );
549        assert_eq!(gsub1.as_fea(""), "sub [a.smcp b.smcp] x' by a;");
550    }
551
552    #[test]
553    fn test_roundtrip_gsub1() {
554        let fea = "feature smcp { sub a by a.smcp; } smcp;";
555        let (parsed, _) = fea_rs::parse::parse_string(fea);
556        let gsub1 = parsed
557            .root()
558            .iter_children()
559            .find_map(fea_rs::typed::Feature::cast)
560            .and_then(|feature| {
561                feature
562                    .node()
563                    .iter_children()
564                    .find_map(fea_rs::typed::Gsub1::cast)
565            })
566            .unwrap();
567        let single_subst = SingleSubstStatement::from(gsub1);
568        assert_eq!(single_subst.as_fea(""), "sub a by a.smcp;");
569    }
570
571    #[test]
572    fn test_generate_gsub2() {
573        let gsub2 = MultipleSubstStatement::new(
574            GlyphContainer::GlyphName(GlyphName::new("x")),
575            vec![
576                GlyphContainer::GlyphName(GlyphName::new("a")),
577                GlyphContainer::GlyphName(GlyphName::new("b")),
578            ],
579            vec![],
580            vec![GlyphContainer::GlyphClass(GlyphClass::new(
581                vec![
582                    GlyphContainer::GlyphName(GlyphName::new("c.smcp")),
583                    GlyphContainer::GlyphName(GlyphName::new("d.smcp")),
584                ],
585                0..0,
586            ))],
587            0..0,
588            false,
589        );
590        assert_eq!(gsub2.as_fea(""), "sub x' [c.smcp d.smcp] by a b;");
591    }
592
593    #[test]
594    fn test_roundtrip_gsub2() {
595        let fea = "feature liga { sub x by a b; } liga;";
596        let (parsed, _) = fea_rs::parse::parse_string(fea);
597        let gsub2 = parsed
598            .root()
599            .iter_children()
600            .find_map(fea_rs::typed::Feature::cast)
601            .and_then(|feature| {
602                feature
603                    .node()
604                    .iter_children()
605                    .find_map(fea_rs::typed::Gsub2::cast)
606            })
607            .unwrap();
608        let multiple_subst = MultipleSubstStatement::from(gsub2);
609        assert_eq!(multiple_subst.as_fea(""), "sub x by a b;");
610    }
611
612    #[test]
613    fn test_generate_gsub3() {
614        let gsub3 = AlternateSubstStatement::new(
615            GlyphContainer::GlyphName(GlyphName::new("x")),
616            GlyphContainer::GlyphClassName(SmolStr::new("@a_alternates")),
617            vec![],
618            vec![],
619            0..0,
620            false,
621        );
622        assert_eq!(gsub3.as_fea(""), "sub x from @a_alternates;");
623    }
624
625    #[test]
626    fn test_roundtrip_gsub3() {
627        let fea = "feature calt { sub x from @x_alternates; } calt;";
628        let (parsed, _) = fea_rs::parse::parse_string(fea);
629        let gsub3 = parsed
630            .root()
631            .iter_children()
632            .find_map(fea_rs::typed::Feature::cast)
633            .and_then(|feature| {
634                feature
635                    .node()
636                    .iter_children()
637                    .find_map(fea_rs::typed::Gsub3::cast)
638            })
639            .unwrap();
640        let alternate_subst = AlternateSubstStatement::from(gsub3);
641        assert_eq!(alternate_subst.as_fea(""), "sub x from @x_alternates;");
642    }
643
644    #[test]
645    fn test_generate_gsub4() {
646        let gsub4 = LigatureSubstStatement::new(
647            vec![
648                GlyphContainer::GlyphName(GlyphName::new("f")),
649                GlyphContainer::GlyphName(GlyphName::new("i")),
650            ],
651            GlyphContainer::GlyphName(GlyphName::new("fi")),
652            vec![],
653            vec![],
654            0..0,
655            false,
656        );
657        assert_eq!(gsub4.as_fea(""), "sub f i by fi;");
658    }
659
660    #[test]
661    fn test_roundtrip_gsub4() {
662        let fea = "feature lig { sub f i by fi; } lig;";
663        let (parsed, _) = fea_rs::parse::parse_string(fea);
664        let gsub4 = parsed
665            .root()
666            .iter_children()
667            .find_map(fea_rs::typed::Feature::cast)
668            .and_then(|feature| {
669                feature
670                    .node()
671                    .iter_children()
672                    .find_map(fea_rs::typed::Gsub4::cast)
673            })
674            .unwrap();
675        let ligature_subst = LigatureSubstStatement::from(gsub4);
676        assert_eq!(ligature_subst.as_fea(""), "sub f i by fi;");
677    }
678
679    #[test]
680    fn test_multiple_subst_from_gsub6() {
681        let fea = r#"feature foo { sub [a b c] d' e f g h i j by k l m n o p; } foo;"#;
682        let (parsed, _) = fea_rs::parse::parse_string(fea);
683        let gsub6 = parsed
684            .root()
685            .iter_children()
686            .find_map(fea_rs::typed::Feature::cast)
687            .and_then(|feature| {
688                feature
689                    .node()
690                    .iter_children()
691                    .find_map(fea_rs::typed::Gsub6::cast)
692            })
693            .unwrap();
694        let multiple_subst = MultipleSubstStatement::try_from(gsub6).unwrap();
695        assert_eq!(multiple_subst.prefix.len(), 1);
696        assert_eq!(multiple_subst.suffix.len(), 6);
697        assert_eq!(multiple_subst.glyph.as_fea(""), "d");
698        assert_eq!(
699            multiple_subst
700                .replacement
701                .iter()
702                .map(|g| g.as_fea(""))
703                .collect::<Vec<_>>(),
704            vec!["k", "l", "m", "n", "o", "p"]
705        );
706    }
707
708    #[test]
709    fn test_roundtrip_gsub8_contextual() {
710        let fea = r#"feature foo { rsub x y a' z w by b; } foo;"#;
711        let (parsed, _) = fea_rs::parse::parse_string(fea);
712        let gsub8 = parsed
713            .root()
714            .iter_children()
715            .find_map(fea_rs::typed::Feature::cast)
716            .and_then(|feature| {
717                feature
718                    .node()
719                    .iter_children()
720                    .find_map(fea_rs::typed::Gsub8::cast)
721            })
722            .unwrap();
723        let stmt = ReverseChainSingleSubstStatement::from(gsub8);
724        assert_eq!(stmt.glyphs.len(), 1);
725        assert_eq!(stmt.glyphs[0].as_fea(""), "a");
726        assert_eq!(stmt.replacements.len(), 1);
727        assert_eq!(stmt.replacements[0].as_fea(""), "b");
728        assert_eq!(stmt.prefix.len(), 2);
729        assert_eq!(stmt.suffix.len(), 2);
730        assert_eq!(stmt.as_fea(""), "rsub x y a' z w by b;");
731    }
732
733    #[test]
734    fn test_generation_gsub8() {
735        let stmt = ReverseChainSingleSubstStatement::new(
736            vec![GlyphContainer::GlyphName(GlyphName::new("x"))],
737            vec![GlyphContainer::GlyphName(GlyphName::new("y"))],
738            vec![],
739            vec![],
740            0..0,
741        );
742        assert_eq!(stmt.as_fea(""), "rsub x by y;");
743    }
744}