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
9pub trait FeaTable {
11 type Statement: AsFea;
13 type FeaRsTable: AstNode;
15 const TAG: &'static str;
17 fn to_statement(child: &NodeOrToken) -> Option<Self::Statement>;
19 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#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Table<T: FeaTable> {
30 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)]
48pub 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 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)]
91pub 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)]
112pub enum HeadStatement {
114 Comment(Comment),
116 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)]
137pub 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#[derive(Debug, Clone, PartialEq, Eq)]
159pub enum NameStatement {
160 Comment(Comment),
162 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)]
183pub 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#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum HheaField {
220 Comment(Comment),
222 CaretOffset(i16),
224 Ascender(i16),
226 Descender(i16),
228 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#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct HheaStatement {
246 pub field: HheaField,
248 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)]
295pub 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#[derive(Debug, Clone, PartialEq, Eq)]
320pub enum VheaField {
321 Comment(Comment),
323 VertTypoAscender(i16),
325 VertTypoDescender(i16),
327 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)]
342pub 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)]
390pub 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}