Skip to main content

fea_rs_ast/
gdef.rs

1use std::ops::Range;
2
3use fea_rs::typed::AstNode as _;
4
5use crate::{AsFea, Comment, GlyphContainer, Statement};
6
7/// A ``GDEF`` table ``Attach`` statement
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AttachStatement {
10    /// The glyphs to which the attachment points apply
11    pub glyphs: GlyphContainer,
12    /// The contour point indices
13    pub contour_points: Vec<usize>,
14    /// The location of the statement in the source
15    pub location: Range<usize>,
16}
17impl AttachStatement {
18    /// Creates a new `Attach` statement.
19    pub fn new(glyphs: GlyphContainer, contour_points: Vec<usize>, location: Range<usize>) -> Self {
20        Self {
21            glyphs,
22            contour_points,
23            location,
24        }
25    }
26}
27impl AsFea for AttachStatement {
28    fn as_fea(&self, _indent: &str) -> String {
29        let points = self
30            .contour_points
31            .iter()
32            .map(|p| p.to_string())
33            .collect::<Vec<_>>()
34            .join(" ");
35        format!("Attach {} {};", self.glyphs.as_fea(""), points)
36    }
37}
38impl From<fea_rs::typed::GdefAttach> for AttachStatement {
39    fn from(val: fea_rs::typed::GdefAttach) -> Self {
40        let glyphs = val
41            .iter()
42            .find_map(fea_rs::typed::GlyphOrClass::cast)
43            .unwrap();
44        let contour_points: Vec<usize> = val
45            .iter()
46            .filter(|t| t.kind() == fea_rs::Kind::Number)
47            .map(|t| t.as_token().unwrap().text.parse().unwrap())
48            .collect();
49        AttachStatement::new(glyphs.into(), contour_points, val.node().range())
50    }
51}
52
53/// A ``GDEF`` table ``GlyphClassDef`` statement
54///
55/// Example: ``GlyphClassDef [a b c], [f_f_i f_f_l], [acute grave], [n.sc t.sc];``
56/// Or with named classes: ``GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENT;``
57///
58/// The four parameters represent base glyphs, ligature glyphs, mark glyphs,
59/// and component glyphs respectively. Any parameter can be None.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct GlyphClassDefStatement {
62    /// The base glyphs class (or None)
63    pub base_glyphs: Option<GlyphContainer>,
64    /// The ligature glyphs class (or None)
65    pub ligature_glyphs: Option<GlyphContainer>,
66    /// The mark glyphs class (or None)
67    pub mark_glyphs: Option<GlyphContainer>,
68    /// The component glyphs class (or None)
69    pub component_glyphs: Option<GlyphContainer>,
70    /// The location of the statement in the source
71    pub location: Range<usize>,
72}
73
74impl GlyphClassDefStatement {
75    /// Creates a new `GlyphClassDef` statement.
76    pub fn new(
77        base_glyphs: Option<GlyphContainer>,
78        ligature_glyphs: Option<GlyphContainer>,
79        mark_glyphs: Option<GlyphContainer>,
80        component_glyphs: Option<GlyphContainer>,
81        location: Range<usize>,
82    ) -> Self {
83        Self {
84            base_glyphs,
85            ligature_glyphs,
86            mark_glyphs,
87            component_glyphs,
88            location,
89        }
90    }
91}
92
93impl AsFea for GlyphClassDefStatement {
94    fn as_fea(&self, _indent: &str) -> String {
95        let base = self
96            .base_glyphs
97            .as_ref()
98            .map(|g| g.as_fea(""))
99            .unwrap_or_default();
100        let liga = self
101            .ligature_glyphs
102            .as_ref()
103            .map(|g| g.as_fea(""))
104            .unwrap_or_default();
105        let mark = self
106            .mark_glyphs
107            .as_ref()
108            .map(|g| g.as_fea(""))
109            .unwrap_or_default();
110        let comp = self
111            .component_glyphs
112            .as_ref()
113            .map(|g| g.as_fea(""))
114            .unwrap_or_default();
115        format!("GlyphClassDef {}, {}, {}, {};", base, liga, mark, comp)
116    }
117}
118
119impl From<fea_rs::typed::GdefClassDef> for GlyphClassDefStatement {
120    fn from(val: fea_rs::typed::GdefClassDef) -> Self {
121        // Extract the 4 glyph class entries in order: base, ligature, mark, component
122        let mut entries = val
123            .iter()
124            .filter(|t| t.kind() == fea_rs::Kind::GdefClassDefEntryNode)
125            .filter_map(fea_rs::typed::GdefClassDefEntry::cast);
126
127        // Helper to extract GlyphContainer from an entry (handles both literal classes and named class references)
128        let extract_container =
129            |entry: fea_rs::typed::GdefClassDefEntry| -> Option<GlyphContainer> {
130                entry
131                    .iter()
132                    .find_map(fea_rs::typed::GlyphOrClass::cast)
133                    .map(Into::into)
134            };
135
136        let base_glyphs = entries.next().and_then(extract_container);
137        let ligature_glyphs = entries.next().and_then(extract_container);
138        let mark_glyphs = entries.next().and_then(extract_container);
139        let component_glyphs = entries.next().and_then(extract_container);
140
141        GlyphClassDefStatement::new(
142            base_glyphs,
143            ligature_glyphs,
144            mark_glyphs,
145            component_glyphs,
146            val.range(),
147        )
148    }
149}
150
151/// A ``GDEF`` table ``LigatureCaretByIndex`` statement
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct LigatureCaretByIndexStatement {
154    /// The glyphs to which the caret indices apply
155    pub glyphs: GlyphContainer,
156    /// The caret indices
157    pub carets: Vec<usize>,
158    /// The location of the statement in the source
159    pub location: Range<usize>,
160}
161
162impl LigatureCaretByIndexStatement {
163    /// Creates a new `LigatureCaretByIndex` statement.
164    pub fn new(glyphs: GlyphContainer, carets: Vec<usize>, location: Range<usize>) -> Self {
165        Self {
166            glyphs,
167            carets,
168            location,
169        }
170    }
171}
172
173impl AsFea for LigatureCaretByIndexStatement {
174    fn as_fea(&self, _indent: &str) -> String {
175        let carets = self
176            .carets
177            .iter()
178            .map(|c| c.to_string())
179            .collect::<Vec<_>>()
180            .join(" ");
181        format!(
182            "LigatureCaretByIndex {} {};",
183            self.glyphs.as_fea(""),
184            carets
185        )
186    }
187}
188
189impl From<fea_rs::typed::GdefLigatureCaret> for LigatureCaretByIndexStatement {
190    fn from(val: fea_rs::typed::GdefLigatureCaret) -> Self {
191        let glyphs = val
192            .iter()
193            .find_map(fea_rs::typed::GlyphOrClass::cast)
194            .unwrap();
195
196        // Extract the caret indices as unsigned integers
197        let carets: Vec<usize> = val
198            .iter()
199            .filter(|t| t.kind() == fea_rs::Kind::Number)
200            .map(|t| t.as_token().unwrap().text.parse().unwrap())
201            .collect();
202
203        LigatureCaretByIndexStatement::new(glyphs.into(), carets, val.node().range())
204    }
205}
206
207/// A ``GDEF`` table ``LigatureCaretByPos`` statement
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct LigatureCaretByPosStatement {
210    /// The glyphs to which the caret positions apply
211    pub glyphs: GlyphContainer,
212    /// The caret positions
213    pub carets: Vec<i16>,
214    /// The location of the statement in the source
215    pub location: Range<usize>,
216}
217
218impl LigatureCaretByPosStatement {
219    /// Creates a new `LigatureCaretByPos` statement.
220    pub fn new(glyphs: GlyphContainer, carets: Vec<i16>, location: Range<usize>) -> Self {
221        Self {
222            glyphs,
223            carets,
224            location,
225        }
226    }
227}
228
229impl AsFea for LigatureCaretByPosStatement {
230    fn as_fea(&self, _indent: &str) -> String {
231        let carets = self
232            .carets
233            .iter()
234            .map(|c| c.to_string())
235            .collect::<Vec<_>>()
236            .join(" ");
237        format!("LigatureCaretByPos {} {};", self.glyphs.as_fea(""), carets)
238    }
239}
240
241impl From<fea_rs::typed::GdefLigatureCaret> for LigatureCaretByPosStatement {
242    fn from(val: fea_rs::typed::GdefLigatureCaret) -> Self {
243        let glyphs = val
244            .iter()
245            .find_map(fea_rs::typed::GlyphOrClass::cast)
246            .unwrap();
247
248        // Extract the caret positions as signed integers
249        let carets: Vec<i16> = val
250            .iter()
251            .filter(|t| t.kind() == fea_rs::Kind::Number)
252            .map(|t| t.as_token().unwrap().text.parse().unwrap())
253            .collect();
254
255        LigatureCaretByPosStatement::new(glyphs.into(), carets, val.node().range())
256    }
257}
258
259/// A statement in a `table GDEF { ... } GDEF` block
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum GdefStatement {
262    /// A ``GDEF`` table ``Attach`` statement
263    Attach(AttachStatement),
264    /// A ``GDEF`` table ``GlyphClassDef`` statement
265    GlyphClassDef(GlyphClassDefStatement),
266    /// A ``GDEF`` table ``LigatureCaretByIndex`` statement
267    LigatureCaretByIndex(LigatureCaretByIndexStatement),
268    /// A ``GDEF`` table ``LigatureCaretByPos`` statement
269    LigatureCaretByPos(LigatureCaretByPosStatement),
270    /// A comment
271    Comment(Comment),
272    // Include(IncludeStatement),
273}
274impl AsFea for GdefStatement {
275    fn as_fea(&self, indent: &str) -> String {
276        match self {
277            GdefStatement::Attach(stmt) => stmt.as_fea(indent),
278            GdefStatement::GlyphClassDef(stmt) => stmt.as_fea(indent),
279            GdefStatement::LigatureCaretByIndex(stmt) => stmt.as_fea(indent),
280            GdefStatement::LigatureCaretByPos(stmt) => stmt.as_fea(indent),
281            GdefStatement::Comment(cmt) => cmt.as_fea(indent),
282            // GdefStatement::Include(stmt) => stmt.as_fea(indent),
283        }
284    }
285}
286impl From<GdefStatement> for Statement {
287    fn from(val: GdefStatement) -> Self {
288        match val {
289            GdefStatement::Attach(stmt) => Statement::GdefAttach(stmt),
290            GdefStatement::GlyphClassDef(stmt) => Statement::GdefClassDef(stmt),
291            GdefStatement::LigatureCaretByIndex(stmt) => Statement::GdefLigatureCaretByIndex(stmt),
292            GdefStatement::LigatureCaretByPos(stmt) => Statement::GdefLigatureCaretByPos(stmt),
293            GdefStatement::Comment(cmt) => Statement::Comment(cmt),
294            // GdefStatement::Include(stmt) => Statement::Include(stmt),
295        }
296    }
297}
298impl TryFrom<Statement> for GdefStatement {
299    type Error = crate::Error;
300    fn try_from(value: Statement) -> Result<Self, Self::Error> {
301        match value {
302            Statement::GdefAttach(stmt) => Ok(GdefStatement::Attach(stmt)),
303            Statement::GdefClassDef(stmt) => Ok(GdefStatement::GlyphClassDef(stmt)),
304            Statement::GdefLigatureCaretByIndex(stmt) => {
305                Ok(GdefStatement::LigatureCaretByIndex(stmt))
306            }
307            Statement::GdefLigatureCaretByPos(stmt) => Ok(GdefStatement::LigatureCaretByPos(stmt)),
308            Statement::Comment(cmt) => Ok(GdefStatement::Comment(cmt)),
309            // Statement::Include(stmt) => Ok(GdefStatement::Include(stmt)),
310            _ => Err(crate::Error::CannotConvert),
311        }
312    }
313}
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::{GlyphClass, GlyphName};
318
319    #[test]
320    fn test_roundtrip_ligature_caret_by_index() {
321        const FEA: &str = "table GDEF { LigatureCaretByIndex f_f_i 2 3; } GDEF;";
322        let (parsed, _) = fea_rs::parse::parse_string(FEA);
323        let gdef_table = parsed
324            .root()
325            .iter_children()
326            .find_map(fea_rs::typed::GdefTable::cast)
327            .unwrap();
328        let ligature_caret = gdef_table
329            .node()
330            .iter_children()
331            .find_map(fea_rs::typed::GdefLigatureCaret::cast)
332            .unwrap();
333        let stmt = LigatureCaretByIndexStatement::from(ligature_caret);
334        assert_eq!(stmt.glyphs.as_fea(""), "f_f_i");
335        assert_eq!(stmt.carets, vec![2, 3]);
336        assert_eq!(stmt.as_fea(""), "LigatureCaretByIndex f_f_i 2 3;");
337    }
338
339    #[test]
340    fn test_roundtrip_ligature_caret_by_pos() {
341        const FEA: &str = "table GDEF { LigatureCaretByPos f_f_i 200 400; } GDEF;";
342        let (parsed, _) = fea_rs::parse::parse_string(FEA);
343        let gdef_table = parsed
344            .root()
345            .iter_children()
346            .find_map(fea_rs::typed::GdefTable::cast)
347            .unwrap();
348        let ligature_caret = gdef_table
349            .node()
350            .iter_children()
351            .find_map(fea_rs::typed::GdefLigatureCaret::cast)
352            .unwrap();
353        let stmt = LigatureCaretByPosStatement::from(ligature_caret);
354        assert_eq!(stmt.glyphs.as_fea(""), "f_f_i");
355        assert_eq!(stmt.carets, vec![200, 400]);
356        assert_eq!(stmt.as_fea(""), "LigatureCaretByPos f_f_i 200 400;");
357    }
358
359    #[test]
360    fn test_generate_ligature_caret_by_index() {
361        let stmt = LigatureCaretByIndexStatement::new(
362            GlyphContainer::GlyphName(GlyphName::new("f_f_i")),
363            vec![2, 3],
364            0..0,
365        );
366        assert_eq!(stmt.as_fea(""), "LigatureCaretByIndex f_f_i 2 3;");
367    }
368
369    #[test]
370    fn test_generate_ligature_caret_by_pos() {
371        let stmt = LigatureCaretByPosStatement::new(
372            GlyphContainer::GlyphClass(GlyphClass::new(
373                vec![
374                    GlyphContainer::GlyphName(GlyphName::new("f_f_i")),
375                    GlyphContainer::GlyphName(GlyphName::new("f_f_l")),
376                ],
377                0..0,
378            )),
379            vec![200, 400],
380            0..0,
381        );
382        assert_eq!(stmt.as_fea(""), "LigatureCaretByPos [f_f_i f_f_l] 200 400;");
383    }
384
385    #[test]
386    fn test_roundtrip_attach() {
387        const FEA: &str = "table GDEF { Attach [a e o] 1 2; } GDEF;";
388        let (parsed, _) = fea_rs::parse::parse_string(FEA);
389        let gdef_table = parsed
390            .root()
391            .iter_children()
392            .find_map(fea_rs::typed::GdefTable::cast)
393            .unwrap();
394        let attach = gdef_table
395            .node()
396            .iter_children()
397            .find_map(fea_rs::typed::GdefAttach::cast)
398            .unwrap();
399        let stmt = AttachStatement::from(attach);
400        assert_eq!(stmt.as_fea(""), "Attach [a e o] 1 2;");
401    }
402
403    #[test]
404    fn test_generation_attach() {
405        let stmt = AttachStatement::new(
406            GlyphContainer::GlyphName(GlyphName::new("acutecomb")),
407            vec![3, 5, 7],
408            0..0,
409        );
410        assert_eq!(stmt.as_fea(""), "Attach acutecomb 3 5 7;");
411    }
412
413    #[test]
414    fn test_roundtrip_glyphclassdef() {
415        const FEA: &str = "table GDEF { GlyphClassDef [a b c], [f_f_i], [acute grave], ; } GDEF;";
416        let (parsed, _) = fea_rs::parse::parse_string(FEA);
417        let gdef_table = parsed
418            .root()
419            .iter_children()
420            .find_map(fea_rs::typed::GdefTable::cast)
421            .unwrap();
422        let class_def = gdef_table
423            .node()
424            .iter_children()
425            .find_map(fea_rs::typed::GdefClassDef::cast)
426            .unwrap();
427
428        let stmt = GlyphClassDefStatement::from(class_def);
429        assert!(stmt.base_glyphs.is_some());
430        assert!(stmt.ligature_glyphs.is_some());
431        assert!(stmt.mark_glyphs.is_some());
432        assert!(stmt.component_glyphs.is_none());
433        assert_eq!(
434            stmt.as_fea(""),
435            "GlyphClassDef [a b c], [f_f_i], [acute grave], ;"
436        );
437    }
438
439    #[test]
440    fn test_generation_glyphclassdef() {
441        let stmt = GlyphClassDefStatement::new(
442            Some(GlyphContainer::GlyphClass(GlyphClass::new(
443                vec![GlyphContainer::GlyphName(GlyphName::new("a"))],
444                0..0,
445            ))),
446            None,
447            Some(GlyphContainer::GlyphClass(GlyphClass::new(
448                vec![GlyphContainer::GlyphName(GlyphName::new("acutecomb"))],
449                0..0,
450            ))),
451            None,
452            0..0,
453        );
454        assert_eq!(stmt.as_fea(""), "GlyphClassDef [a], , [acutecomb], ;");
455    }
456
457    #[test]
458    fn test_roundtrip_glyphclassdef_named_classes() {
459        const FEA: &str =
460            "table GDEF { GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENT; } GDEF;";
461        let (parsed, _) = fea_rs::parse::parse_string(FEA);
462        let gdef_table = parsed
463            .root()
464            .iter_children()
465            .find_map(fea_rs::typed::GdefTable::cast)
466            .unwrap();
467        let class_def = gdef_table
468            .node()
469            .iter_children()
470            .find_map(fea_rs::typed::GdefClassDef::cast)
471            .unwrap();
472
473        let stmt = GlyphClassDefStatement::from(class_def);
474        assert!(stmt.base_glyphs.is_some());
475        assert!(stmt.ligature_glyphs.is_some());
476        assert!(stmt.mark_glyphs.is_some());
477        assert!(stmt.component_glyphs.is_some());
478        assert_eq!(
479            stmt.as_fea(""),
480            "GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENT;"
481        );
482    }
483}