Skip to main content

fea_rs_ast/
tables.rs

1use std::ops::Range;
2
3use fea_rs::{NodeOrToken, typed::AstNode};
4
5use crate::{
6    AsFea, Comment, FontRevisionStatement, GdefStatement, NameRecord, SHIFT, stat::StatStatement,
7};
8
9/// A helper for constructing tables which hold statements of a particular type.
10pub trait FeaTable {
11    /// The type of statement contained in this table.
12    type Statement: AsFea;
13    /// The corresponding fea-rs typed table.
14    type FeaRsTable: AstNode;
15    /// The tag of this table.
16    const TAG: &'static str;
17    /// Convert a child node or token into a statement of this table's type, if possible.
18    fn to_statement(child: &NodeOrToken) -> Option<Self::Statement>;
19    /// Extract all statements of this table's type from a fea-rs node.
20    fn statements_from_node(node: &fea_rs::Node) -> Vec<Self::Statement> {
21        node.iter_children()
22            .filter_map(Self::to_statement)
23            .collect()
24    }
25}
26
27/// A table in a feature file, parameterized by the type of statements it contains.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Table<T: FeaTable> {
30    /// The statements in this table.
31    pub statements: Vec<T::Statement>,
32}
33
34impl<T: FeaTable> AsFea for Table<T> {
35    fn as_fea(&self, indent: &str) -> String {
36        let mut res = String::new();
37        res.push_str(&format!("{}table {} {{\n", indent, T::TAG));
38        for stmt in &self.statements {
39            res.push_str(&stmt.as_fea(&(indent.to_string() + SHIFT)));
40            res.push('\n');
41        }
42        res.push_str(&format!("{}}} {};\n", indent, T::TAG));
43        res
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48/// The `GDEF` table
49pub struct Gdef;
50impl FeaTable for Gdef {
51    type Statement = GdefStatement;
52    type FeaRsTable = fea_rs::typed::GdefTable;
53    const TAG: &'static str = "GDEF";
54    #[allow(clippy::manual_map)]
55    fn to_statement(child: &NodeOrToken) -> Option<Self::Statement> {
56        if child.kind() == fea_rs::Kind::Comment {
57            Some(GdefStatement::Comment(Comment::from(
58                child.token_text().unwrap(),
59            )))
60        } else if let Some(at) = fea_rs::typed::GdefAttach::cast(child) {
61            Some(GdefStatement::Attach(at.into()))
62        } else if let Some(gcd) = fea_rs::typed::GdefClassDef::cast(child) {
63            Some(GdefStatement::GlyphClassDef(gcd.into()))
64        } else if let Some(lc) = fea_rs::typed::GdefLigatureCaret::cast(child) {
65            // Check if it's by position or by index based on the first keyword
66            let is_by_pos = lc
67                .iter()
68                .next()
69                .map(|t| t.kind() == fea_rs::Kind::LigatureCaretByPosKw)
70                .unwrap_or(false);
71            if is_by_pos {
72                Some(GdefStatement::LigatureCaretByPos(lc.into()))
73            } else {
74                Some(GdefStatement::LigatureCaretByIndex(lc.into()))
75            }
76        } else {
77            None
78        }
79    }
80}
81
82impl From<fea_rs::typed::GdefTable> for Table<Gdef> {
83    fn from(val: fea_rs::typed::GdefTable) -> Self {
84        Self {
85            statements: Gdef::statements_from_node(val.node()),
86        }
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91/// The `head` table
92pub struct Head;
93impl FeaTable for Head {
94    type Statement = HeadStatement;
95    const TAG: &'static str = "head";
96    type FeaRsTable = fea_rs::typed::HeadTable;
97    #[allow(clippy::manual_map)]
98    fn to_statement(child: &NodeOrToken) -> Option<HeadStatement> {
99        if child.kind() == fea_rs::Kind::Comment {
100            Some(HeadStatement::Comment(Comment::from(
101                child.token_text().unwrap(),
102            )))
103        } else if let Some(fr) = fea_rs::typed::HeadFontRevision::cast(child) {
104            Some(HeadStatement::FontRevision(fr.into()))
105        } else {
106            None
107        }
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112/// A statement in the `head` table
113pub enum HeadStatement {
114    /// A comment
115    Comment(Comment),
116    /// a `FontRevision` statement
117    FontRevision(FontRevisionStatement),
118}
119impl AsFea for HeadStatement {
120    fn as_fea(&self, indent: &str) -> String {
121        match self {
122            HeadStatement::Comment(cmt) => cmt.as_fea(indent),
123            HeadStatement::FontRevision(stmt) => stmt.as_fea(indent),
124        }
125    }
126}
127
128impl From<fea_rs::typed::HeadTable> for Table<Head> {
129    fn from(val: fea_rs::typed::HeadTable) -> Self {
130        Self {
131            statements: Head::statements_from_node(val.node()),
132        }
133    }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137/// The `name` table
138pub struct Name;
139impl FeaTable for Name {
140    type Statement = NameStatement;
141    const TAG: &'static str = "name";
142    type FeaRsTable = fea_rs::typed::NameTable;
143    #[allow(clippy::manual_map)]
144    fn to_statement(child: &NodeOrToken) -> Option<NameStatement> {
145        if child.kind() == fea_rs::Kind::Comment {
146            Some(NameStatement::Comment(Comment::from(
147                child.token_text().unwrap(),
148            )))
149        } else if let Some(fr) = fea_rs::typed::NameRecord::cast(child) {
150            Some(NameStatement::NameRecord(fr.into()))
151        } else {
152            None
153        }
154    }
155}
156
157/// A statement in the `name` table
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub enum NameStatement {
160    /// A comment
161    Comment(Comment),
162    /// a `NameRecord` statement
163    NameRecord(NameRecord),
164}
165impl AsFea for NameStatement {
166    fn as_fea(&self, indent: &str) -> String {
167        match self {
168            NameStatement::Comment(cmt) => cmt.as_fea(indent),
169            NameStatement::NameRecord(stmt) => stmt.as_fea(indent),
170        }
171    }
172}
173
174impl From<fea_rs::typed::NameTable> for Table<Name> {
175    fn from(val: fea_rs::typed::NameTable) -> Self {
176        Self {
177            statements: Name::statements_from_node(val.node()),
178        }
179    }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183/// The `STAT` table
184pub struct Stat;
185impl FeaTable for Stat {
186    type Statement = StatStatement;
187    const TAG: &'static str = "STAT";
188    type FeaRsTable = fea_rs::typed::StatTable;
189    #[allow(clippy::manual_map)]
190    fn to_statement(child: &NodeOrToken) -> Option<StatStatement> {
191        if child.kind() == fea_rs::Kind::Comment {
192            Some(StatStatement::Comment(Comment::from(
193                child.token_text().unwrap(),
194            )))
195        } else if let Some(da) = fea_rs::typed::StatDesignAxis::cast(child) {
196            Some(StatStatement::DesignAxis(da.into()))
197        } else if let Some(efn) = fea_rs::typed::StatElidedFallbackName::cast(child) {
198            Some(StatStatement::from(efn))
199        } else if let Some(efn) = fea_rs::typed::StatAxisValue::cast(child) {
200            Some(StatStatement::AxisValue(efn.into()))
201        } else {
202            None
203        }
204    }
205}
206
207impl From<fea_rs::typed::StatTable> for Table<Stat> {
208    fn from(val: fea_rs::typed::StatTable) -> Self {
209        Self {
210            statements: Stat::statements_from_node(val.node()),
211        }
212    }
213}
214
215// hhea
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218/// Fields in the `hhea` table
219pub enum HheaField {
220    /// A comment
221    Comment(Comment),
222    /// A `CaretOffset` statement
223    CaretOffset(i16),
224    /// An `Ascender` statement
225    Ascender(i16),
226    /// A `Descender` statement
227    Descender(i16),
228    /// A `LineGap` statement
229    LineGap(i16),
230}
231impl AsFea for HheaField {
232    fn as_fea(&self, indent: &str) -> String {
233        match self {
234            HheaField::Comment(cmt) => cmt.as_fea(indent),
235            HheaField::CaretOffset(x) => format!("{}CaretOffset {};", indent, x),
236            HheaField::Ascender(x) => format!("{}Ascender {};", indent, x),
237            HheaField::Descender(x) => format!("{}Descender {};", indent, x),
238            HheaField::LineGap(x) => format!("{}LineGap {};", indent, x),
239        }
240    }
241}
242
243/// A statement in the `hhea` table
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct HheaStatement {
246    /// The field of this statement
247    pub field: HheaField,
248    /// The location of this statement in the source
249    pub location: Range<usize>,
250}
251impl AsFea for HheaStatement {
252    fn as_fea(&self, indent: &str) -> String {
253        self.field.as_fea(indent)
254    }
255}
256impl From<fea_rs::typed::MetricRecord> for HheaStatement {
257    fn from(val: fea_rs::typed::MetricRecord) -> Self {
258        let keyword = val
259            .node()
260            .iter_children()
261            .next()
262            .and_then(|t| t.as_token())
263            .unwrap();
264        let metric = val
265            .node()
266            .iter_children()
267            .find_map(fea_rs::typed::Metric::cast)
268            .unwrap();
269        let value = match metric {
270            fea_rs::typed::Metric::Scalar(number) => number.text().parse::<i16>().unwrap(),
271            _ => unimplemented!(),
272        };
273        HheaStatement {
274            field: match keyword.kind {
275                fea_rs::Kind::CaretOffsetKw => HheaField::CaretOffset(value),
276                fea_rs::Kind::AscenderKw => HheaField::Ascender(value),
277                fea_rs::Kind::DescenderKw => HheaField::Descender(value),
278                fea_rs::Kind::LineGapKw => HheaField::LineGap(value),
279                _ => panic!("Unexpected keyword in HHEA metric record"),
280            },
281            location: val.range(),
282        }
283    }
284}
285
286impl From<fea_rs::typed::HheaTable> for Table<Hhea> {
287    fn from(val: fea_rs::typed::HheaTable) -> Self {
288        Self {
289            statements: Hhea::statements_from_node(val.node()),
290        }
291    }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295/// The `hhea` table
296pub struct Hhea;
297impl FeaTable for Hhea {
298    type Statement = HheaStatement;
299    const TAG: &'static str = "hhea";
300    type FeaRsTable = fea_rs::typed::HheaTable;
301    #[allow(clippy::manual_map)]
302    fn to_statement(child: &NodeOrToken) -> Option<HheaStatement> {
303        if child.kind() == fea_rs::Kind::Comment {
304            Some(HheaStatement {
305                field: HheaField::Comment(Comment::from(child.token_text().unwrap())),
306                location: child.range(),
307            })
308        } else if let Some(fr) = fea_rs::typed::MetricRecord::cast(child) {
309            Some(fr.into())
310        } else {
311            None
312        }
313    }
314}
315
316// vhea
317
318/// A field in the `vhea` table
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub enum VheaField {
321    /// A comment
322    Comment(Comment),
323    /// A `VertTypoAscender` statement
324    VertTypoAscender(i16),
325    /// A `VertTypoDescender` statement
326    VertTypoDescender(i16),
327    /// A `VertTypoLineGap` statement
328    VertTypoLineGap(i16),
329}
330impl AsFea for VheaField {
331    fn as_fea(&self, indent: &str) -> String {
332        match self {
333            VheaField::Comment(cmt) => cmt.as_fea(indent),
334            VheaField::VertTypoAscender(x) => format!("{}VertTypoAscender {};", indent, x),
335            VheaField::VertTypoDescender(x) => format!("{}VertTypoDescender {};", indent, x),
336            VheaField::VertTypoLineGap(x) => format!("{}VertTypoLineGap {};", indent, x),
337        }
338    }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
342/// A statement in the `vhea` table
343pub struct VheaStatement {
344    field: VheaField,
345    location: Range<usize>,
346}
347impl AsFea for VheaStatement {
348    fn as_fea(&self, indent: &str) -> String {
349        self.field.as_fea(indent)
350    }
351}
352impl From<fea_rs::typed::MetricRecord> for VheaStatement {
353    fn from(val: fea_rs::typed::MetricRecord) -> Self {
354        let keyword = val
355            .node()
356            .iter_children()
357            .next()
358            .and_then(|t| t.as_token())
359            .unwrap();
360        let metric = val
361            .node()
362            .iter_children()
363            .find_map(fea_rs::typed::Metric::cast)
364            .unwrap();
365        let value = match metric {
366            fea_rs::typed::Metric::Scalar(number) => number.text().parse::<i16>().unwrap(),
367            _ => unimplemented!(),
368        };
369        VheaStatement {
370            field: match keyword.kind {
371                fea_rs::Kind::VertTypoAscenderKw => VheaField::VertTypoAscender(value),
372                fea_rs::Kind::VertTypoDescenderKw => VheaField::VertTypoDescender(value),
373                fea_rs::Kind::VertTypoLineGapKw => VheaField::VertTypoLineGap(value),
374                _ => panic!("Unexpected keyword in Vhea metric record"),
375            },
376            location: val.range(),
377        }
378    }
379}
380
381impl From<fea_rs::typed::VheaTable> for Table<Vhea> {
382    fn from(val: fea_rs::typed::VheaTable) -> Self {
383        Self {
384            statements: Vhea::statements_from_node(val.node()),
385        }
386    }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390/// The `vhea` table
391pub struct Vhea;
392impl FeaTable for Vhea {
393    type Statement = VheaStatement;
394    const TAG: &'static str = "vhea";
395    type FeaRsTable = fea_rs::typed::VheaTable;
396    #[allow(clippy::manual_map)]
397    fn to_statement(child: &NodeOrToken) -> Option<VheaStatement> {
398        if child.kind() == fea_rs::Kind::Comment {
399            Some(VheaStatement {
400                field: VheaField::Comment(Comment::from(child.token_text().unwrap())),
401                location: child.range(),
402            })
403        } else if let Some(fr) = fea_rs::typed::MetricRecord::cast(child) {
404            Some(fr.into())
405        } else {
406            None
407        }
408    }
409}