Skip to main content

fea_rs_ast/
glyphcontainers.rs

1use std::ops::Range;
2
3use fea_rs::typed::{AstNode as _, GlyphOrClass};
4use smol_str::SmolStr;
5
6use crate::AsFea;
7
8const FEA_KEYWORDS: [&str; 52] = [
9    "anchor",
10    "anchordef",
11    "anon",
12    "anonymous",
13    "by",
14    "contour",
15    "cursive",
16    "device",
17    "enum",
18    "enumerate",
19    "excludedflt",
20    "exclude_dflt",
21    "feature",
22    "from",
23    "ignore",
24    "ignorebaseglyphs",
25    "ignoreligatures",
26    "ignoremarks",
27    "include",
28    "includedflt",
29    "include_dflt",
30    "language",
31    "languagesystem",
32    "lookup",
33    "lookupflag",
34    "mark",
35    "markattachmenttype",
36    "markclass",
37    "nameid",
38    "null",
39    "parameters",
40    "pos",
41    "position",
42    "required",
43    "righttoleft",
44    "reversesub",
45    "rsub",
46    "script",
47    "sub",
48    "substitute",
49    "subtable",
50    "table",
51    "usemarkfilteringset",
52    "useextension",
53    "valuerecorddef",
54    "base",
55    "gdef",
56    "head",
57    "hhea",
58    "name",
59    "vhea",
60    "vmtx",
61];
62
63/// A single glyph name, such as `cedilla`.
64#[derive(Clone, PartialEq, Eq, Hash)]
65pub struct GlyphName {
66    /// The name itself as a string
67    pub name: SmolStr,
68}
69
70impl std::fmt::Debug for GlyphName {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "GlyphName({})", self.name)
73    }
74}
75
76impl GlyphName {
77    /// Creates a new `GlyphName`, representing a single named glyph.
78    pub fn new(name: &str) -> Self {
79        Self {
80            name: SmolStr::new(name),
81        }
82    }
83
84    /// Returns an iterator over the glyph names in this `GlyphName`.
85    pub fn glyphset(&self) -> impl Iterator<Item = &SmolStr> {
86        std::iter::once(&self.name)
87    }
88}
89impl AsFea for GlyphName {
90    fn as_fea(&self, _indent: &str) -> String {
91        if FEA_KEYWORDS.contains(&self.name.as_str()) {
92            format!("\\{}", self.name)
93        } else {
94            self.name.to_string()
95        }
96    }
97}
98
99/// A glyph class literal, such as `[a b c]` or `[a-z A-Z]`.
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
101pub struct GlyphClass {
102    /// The glyphs in the class literal
103    pub glyphs: Vec<GlyphContainer>,
104    /// The location of the glyph class in the source feature file
105    pub location: Range<usize>,
106}
107impl GlyphClass {
108    /// Creates a new `GlyphClass` with the given glyph containers and location.
109    pub fn new(glyphs: Vec<GlyphContainer>, location: Range<usize>) -> Self {
110        Self { glyphs, location }
111    }
112}
113impl AsFea for GlyphClass {
114    fn as_fea(&self, _indent: &str) -> String {
115        let inner: Vec<String> = self.glyphs.iter().map(|g| g.as_fea("")).collect();
116        format!("[{}]", inner.join(" "))
117    }
118}
119impl From<fea_rs::typed::GlyphClass> for GlyphClass {
120    fn from(val: fea_rs::typed::GlyphClass) -> Self {
121        match val {
122            fea_rs::typed::GlyphClass::Named(glyph_class_name) => {
123                let members = vec![GlyphContainer::GlyphClassName(SmolStr::new(
124                    glyph_class_name.text(),
125                ))];
126                GlyphClass::new(members, glyph_class_name.range())
127            }
128            fea_rs::typed::GlyphClass::Literal(glyph_class_literal) => glyph_class_literal.into(),
129        }
130    }
131}
132impl From<fea_rs::typed::GlyphClassLiteral> for GlyphClass {
133    fn from(val: fea_rs::typed::GlyphClassLiteral) -> Self {
134        let members: Vec<GlyphContainer> = val
135            .node()
136            .iter_children()
137            .flat_map(|child| {
138                if let Some(gc) = fea_rs::typed::GlyphOrClass::cast(child) {
139                    Some(gc.into())
140                } else if let Some(gr) = fea_rs::typed::GlyphRange::cast(child) {
141                    let start = gr
142                        .iter()
143                        .find_map(fea_rs::typed::GlyphName::cast)
144                        .map(|gn| SmolStr::new(gn.text()))
145                        .unwrap();
146                    let end = gr
147                        .iter()
148                        .skip_while(|t| t.kind() != fea_rs::Kind::Hyphen)
149                        .find_map(fea_rs::typed::GlyphName::cast)
150                        .map(|gn| SmolStr::new(gn.text()))
151                        .unwrap();
152
153                    Some(GlyphContainer::GlyphRange(GlyphRange::new(start, end)))
154                // "GlyphNameOrRange" doesn't go into the typed AST, we have to handle it ourselves
155                } else if child.kind() == fea_rs::Kind::GlyphNameOrRange {
156                    Some(GlyphContainer::GlyphNameOrRange(
157                        child.token_text().unwrap().into(),
158                    ))
159                } else {
160                    None
161                }
162            })
163            .collect();
164        GlyphClass::new(members, val.node().range())
165    }
166}
167
168/// A glyph range, such as `a-z` or `A01-A05`.
169#[derive(Debug, Clone, PartialEq, Eq, Hash)]
170pub struct GlyphRange {
171    /// Start glyph name of the range
172    pub start: SmolStr,
173    /// End glyph name of the range
174    pub end: SmolStr,
175}
176impl GlyphRange {
177    /// Creates a new `GlyphRange` with the given start and end glyph names.
178    pub fn new(start: SmolStr, end: SmolStr) -> Self {
179        Self { start, end }
180    }
181
182    /// Returns an iterator over the glyph names in this `GlyphRange`.
183    pub fn glyphset(&self) -> impl Iterator<Item = SmolStr> {
184        // OK, the rules are:
185        // <firstGlyph> and <lastGlyph> must be the same length and can differ only in one of the following ways:
186        // By a single letter from A-Z, either uppercase or lowercase.
187        // By up to 3 decimal digits in a contiguous run
188        // So first we find a common prefix and suffix
189        let start_bytes = self.start.as_bytes();
190        let end_bytes = self.end.as_bytes();
191        let mut prefix_len = 0;
192        let mut suffix_len = 0;
193        for (start_byte, end_byte) in start_bytes.iter().zip(end_bytes.iter()) {
194            if start_byte == end_byte {
195                prefix_len += 1;
196            } else {
197                break;
198            }
199        }
200        for (start_byte, end_byte) in start_bytes.iter().rev().zip(end_bytes.iter().rev()) {
201            if start_byte == end_byte {
202                suffix_len += 1;
203            } else {
204                break;
205            }
206        }
207        let start_core = &self.start[prefix_len..self.start.len() - suffix_len];
208        let end_core = &self.end[prefix_len..self.end.len() - suffix_len];
209        if start_core.len() != end_core.len() {
210            // invalid range
211            return vec![self.start.clone(), self.end.clone()].into_iter();
212        }
213        if start_core.len() == 1 {
214            // Check if both cores are a single lower case or upper case letter
215            let start_char = start_core.chars().next().unwrap();
216            let end_char = end_core.chars().next().unwrap();
217            if (start_char.is_ascii_lowercase() && end_char.is_ascii_lowercase())
218                || (start_char.is_ascii_uppercase() && end_char.is_ascii_uppercase())
219            {
220                let range = start_char as u8..=end_char as u8;
221                let glyphs: Vec<SmolStr> = range
222                    .map(|b| {
223                        SmolStr::new(format!(
224                            "{}{}{}",
225                            &self.start[..prefix_len],
226                            b as char,
227                            &self.start[self.start.len() - suffix_len..]
228                        ))
229                    })
230                    .collect();
231                return glyphs.into_iter();
232            } else {
233                // invalid range
234                return vec![self.start.clone(), self.end.clone()].into_iter();
235            }
236        }
237        // So it should be a 1 to 3 digit number
238        let start_num: Option<usize> = start_core.parse().ok();
239        let end_num: Option<usize> = end_core.parse().ok();
240        if let (Some(start_num), Some(end_num)) = (start_num, end_num) {
241            let range = start_num..=end_num;
242            // Format each number to the correct width with leading zeros
243            let glyphs: Vec<SmolStr> = range
244                .map(|n| {
245                    let mut s = String::new();
246                    s.push_str(&self.start[..prefix_len]);
247                    s.push_str(&format!("{:0width$}", n, width = start_core.len()));
248                    s.push_str(&self.start[self.start.len() - suffix_len..]);
249                    SmolStr::new(s)
250                })
251                .collect();
252            return glyphs.into_iter();
253        }
254        // invalid range
255        vec![self.start.clone(), self.end.clone()].into_iter()
256    }
257}
258impl From<Range<SmolStr>> for GlyphRange {
259    fn from(val: Range<SmolStr>) -> Self {
260        GlyphRange::new(val.start, val.end)
261    }
262}
263impl AsFea for GlyphRange {
264    fn as_fea(&self, _indent: &str) -> String {
265        format!("{} - {}", self.start.as_str(), self.end.as_str())
266    }
267}
268
269/// A container for glyphs in various forms: single glyph names, glyph classes,
270/// glyph ranges, or glyph name/range literals.
271#[derive(Debug, Clone, PartialEq, Eq, Hash)]
272pub enum GlyphContainer {
273    /// A single glyph name
274    GlyphName(GlyphName),
275    /// A glyph class literal
276    GlyphClass(GlyphClass),
277    /// A named glyph class
278    GlyphClassName(SmolStr),
279    /// A glyph range
280    GlyphRange(GlyphRange),
281    /// An ambiguity: either a glyph name or range literal, up to the user to resolve
282    GlyphNameOrRange(SmolStr),
283}
284
285impl AsFea for GlyphContainer {
286    fn as_fea(&self, _indent: &str) -> String {
287        match self {
288            GlyphContainer::GlyphName(gn) => gn.as_fea(_indent),
289            GlyphContainer::GlyphClass(gcs) => {
290                let inner: Vec<String> = gcs.glyphs.iter().map(|g| g.as_fea("")).collect();
291                format!("[{}]", inner.join(" "))
292            }
293            GlyphContainer::GlyphClassName(name) => {
294                if FEA_KEYWORDS.contains(&name.as_str()) {
295                    format!("\\{}", name)
296                } else {
297                    name.to_string()
298                }
299            }
300            GlyphContainer::GlyphRange(range) => range.as_fea(""),
301            GlyphContainer::GlyphNameOrRange(name_or_range) => name_or_range.to_string(),
302        }
303    }
304}
305
306impl From<fea_rs::typed::GlyphOrClass> for GlyphContainer {
307    fn from(val: fea_rs::typed::GlyphOrClass) -> Self {
308        match val {
309            GlyphOrClass::Glyph(glyph) => GlyphContainer::GlyphName(GlyphName::new(glyph.text())),
310            GlyphOrClass::Class(glyph_class_literal) => {
311                GlyphContainer::GlyphClass(glyph_class_literal.into())
312            }
313            GlyphOrClass::Cid(_cid) => todo!(),
314            GlyphOrClass::NamedClass(glyph_class_name) => {
315                GlyphContainer::GlyphClassName(SmolStr::new(glyph_class_name.text()))
316            }
317            GlyphOrClass::Null(_) => GlyphContainer::GlyphName(GlyphName::new("NULL")),
318        }
319    }
320}
321
322impl From<fea_rs::typed::GlyphClass> for GlyphContainer {
323    fn from(val: fea_rs::typed::GlyphClass) -> Self {
324        match val {
325            fea_rs::typed::GlyphClass::Named(glyph_class_name) => {
326                GlyphContainer::GlyphClassName(SmolStr::new(glyph_class_name.text()))
327            }
328            fea_rs::typed::GlyphClass::Literal(glyph_class_literal) => {
329                GlyphContainer::GlyphClass(glyph_class_literal.into())
330            }
331        }
332    }
333}
334
335impl GlyphContainer {
336    /// Creates a new `GlyphContainer` representing a glyph class literal
337    /// containing the given glyph names.
338    pub fn new_class(glyph_names: &[&str]) -> Self {
339        let members: Vec<GlyphContainer> = glyph_names
340            .iter()
341            .map(|name| GlyphContainer::GlyphName(GlyphName::new(name)))
342            .collect();
343        GlyphContainer::GlyphClass(GlyphClass::new(
344            members,
345            0..0, // location is not relevant here
346        ))
347    }
348
349    /// Returns true if this `GlyphContainer` is an empty glyph class.
350    pub fn is_empty(&self) -> bool {
351        match self {
352            GlyphContainer::GlyphClass(gcs) => gcs.glyphs.is_empty(),
353            _ => false,
354        }
355    }
356}
357
358/// The name of a mark class
359///
360/// Note that this differs from the Python `fontTools` representation. In
361/// Python, a `MarkClass` object contains `MarkClassDefinition` objects
362/// for the glyphs in the class, and the `MarkClassDefinition` objects
363/// recursively refer to the `MarkClass` they belong to. In Rust, the
364/// `MarkClass` is just a name, and the relationship between the class name
365/// and the glyphs and their anchor points is stored at the feature file level.
366///
367/// The name should not begin with `@`.
368#[derive(Debug, Clone, PartialEq, Eq, Hash)]
369pub struct MarkClass {
370    /// The name of the mark class, without the leading `@`
371    pub name: SmolStr,
372}
373impl MarkClass {
374    /// Creates a new `MarkClass` with the given name.
375    pub fn new(name: &str) -> Self {
376        Self {
377            name: SmolStr::new(name),
378        }
379    }
380}
381impl From<fea_rs::typed::GlyphClassDef> for MarkClass {
382    fn from(val: fea_rs::typed::GlyphClassDef) -> Self {
383        let label = val
384            .iter()
385            .find_map(fea_rs::typed::GlyphClassName::cast)
386            .unwrap();
387        MarkClass::new(label.text().trim_start_matches('@'))
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn test_glyphclass() {
397        const FEA: &str = "@foo = [a-b c d-e @bar];";
398        let (parsed, _) = fea_rs::parse::parse_string(FEA);
399        let definition = parsed
400            .root()
401            .iter_children()
402            .find_map(fea_rs::typed::GlyphClassDef::cast)
403            .unwrap();
404        let literal = definition
405            .node()
406            .iter_children()
407            .find_map(fea_rs::typed::GlyphClassLiteral::cast)
408            .unwrap();
409        println!("{:#?}", literal);
410        let glyphs: GlyphClass = literal.into();
411        assert_eq!(glyphs.glyphs.len(), 4);
412    }
413
414    #[test]
415    fn test_glyphrange() {
416        let range = GlyphRange::new(SmolStr::new("a01"), SmolStr::new("a05"));
417        let glyphs: Vec<SmolStr> = range.glyphset().collect();
418        assert_eq!(
419            glyphs,
420            vec![
421                SmolStr::new("a01"),
422                SmolStr::new("a02"),
423                SmolStr::new("a03"),
424                SmolStr::new("a04"),
425                SmolStr::new("a05"),
426            ]
427        );
428
429        let range = GlyphRange::new(SmolStr::new("B"), SmolStr::new("F"));
430        let glyphs: Vec<SmolStr> = range.glyphset().collect();
431        assert_eq!(
432            glyphs,
433            vec![
434                SmolStr::new("B"),
435                SmolStr::new("C"),
436                SmolStr::new("D"),
437                SmolStr::new("E"),
438                SmolStr::new("F"),
439            ]
440        );
441
442        let range = GlyphRange::new(SmolStr::new("cat"), SmolStr::new("cet"));
443        let glyphs: Vec<SmolStr> = range.glyphset().collect();
444        assert_eq!(
445            glyphs,
446            vec![
447                SmolStr::new("cat"),
448                SmolStr::new("cbt"),
449                SmolStr::new("cct"),
450                SmolStr::new("cdt"),
451                SmolStr::new("cet"),
452            ]
453        );
454    }
455}