1use std::ops::Range;
2
3use fea_rs::typed::{AstNode as _, Tag};
4use smol_str::SmolStr;
5
6use crate::{
7 Anchor, AsFea, GlyphClass, GlyphContainer, MarkClass, Metric, SHIFT, Statement, ValueRecord,
8 from_anchor,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct AnchorDefinition {
14 pub x: Metric,
16 pub y: Metric,
18 pub contourpoint: Option<u16>,
20 pub name: String,
22 pub location: Range<usize>,
24}
25impl AnchorDefinition {
26 pub fn new(
28 x: Metric,
29 y: Metric,
30 contourpoint: Option<u16>,
31 name: String,
32 location: Range<usize>,
33 ) -> Self {
34 Self {
35 x,
36 y,
37 contourpoint,
38 name,
39 location,
40 }
41 }
42}
43impl AsFea for AnchorDefinition {
44 fn as_fea(&self, _indent: &str) -> String {
45 let mut res = format!("anchorDef {} {}", self.x.as_fea(""), self.y.as_fea(""));
46 if let Some(cp) = self.contourpoint {
47 res.push_str(&format!(" contourpoint {}", cp));
48 }
49 res.push_str(&format!(" {};", self.name));
50 res
51 }
52}
53impl From<fea_rs::typed::AnchorDef> for AnchorDefinition {
54 fn from(val: fea_rs::typed::AnchorDef) -> Self {
55 let anchor_node = val
56 .iter()
57 .filter_map(fea_rs::typed::Anchor::cast)
58 .next()
59 .unwrap();
60 let our_anchor: Anchor = from_anchor(anchor_node).unwrap();
61 let name = val
62 .iter()
63 .find(|t| t.kind() == fea_rs::Kind::Ident)
64 .unwrap();
65 AnchorDefinition::new(
66 our_anchor.x,
67 our_anchor.y,
68 our_anchor.contourpoint,
69 name.token_text().unwrap().to_string(),
70 val.node().range(),
71 )
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Comment {
78 pub text: String,
80}
81impl Comment {
82 pub fn new(text: String) -> Self {
84 Self { text }
85 }
86}
87impl AsFea for Comment {
88 fn as_fea(&self, _indent: &str) -> String {
89 self.text.clone()
90 }
91}
92impl From<&str> for Comment {
93 fn from(text: &str) -> Self {
94 Self::new(text.to_string())
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct FeatureReferenceStatement {
101 pub feature_name: String,
103}
104impl FeatureReferenceStatement {
105 pub fn new(feature_name: String) -> Self {
107 Self { feature_name }
108 }
109}
110impl AsFea for FeatureReferenceStatement {
111 fn as_fea(&self, _indent: &str) -> String {
112 format!("feature {};", self.feature_name)
113 }
114}
115impl From<fea_rs::typed::FeatureRef> for FeatureReferenceStatement {
116 fn from(feature: fea_rs::typed::FeatureRef) -> Self {
117 Self::new(
118 feature
119 .iter()
120 .find_map(Tag::cast)
121 .unwrap()
122 .text()
123 .to_string(),
124 )
125 }
126}
127
128#[derive(Debug, Clone)]
133pub struct FontRevisionStatement {
134 pub revision: f32,
136}
137impl FontRevisionStatement {
138 pub fn new(revision: f32) -> Self {
140 Self { revision }
141 }
142}
143impl AsFea for FontRevisionStatement {
144 fn as_fea(&self, _indent: &str) -> String {
145 format!("FontRevision {:.3};", self.revision)
146 }
147}
148impl From<fea_rs::typed::HeadFontRevision> for FontRevisionStatement {
149 fn from(val: fea_rs::typed::HeadFontRevision) -> Self {
150 let revision_token = val
151 .iter()
152 .find(|t| t.kind() == fea_rs::Kind::Float)
153 .unwrap();
154 FontRevisionStatement {
155 revision: revision_token.as_token().unwrap().text.parse().unwrap(),
156 }
157 }
158}
159impl PartialEq for FontRevisionStatement {
160 fn eq(&self, other: &Self) -> bool {
161 (self.revision * 1000.0).round() == (other.revision * 1000.0).round()
162 }
163}
164impl Eq for FontRevisionStatement {}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct GlyphClassDefinition {
171 pub name: String,
173 pub glyphs: GlyphClass,
175 pub location: Range<usize>,
177}
178impl GlyphClassDefinition {
179 pub fn new(name: String, glyphs: GlyphClass, location: Range<usize>) -> Self {
181 Self {
182 name,
183 glyphs,
184 location,
185 }
186 }
187}
188impl AsFea for GlyphClassDefinition {
189 fn as_fea(&self, _indent: &str) -> String {
190 format!("@{} = {};", self.name, self.glyphs.as_fea(""))
191 }
192}
193impl From<fea_rs::typed::GlyphClassDef> for GlyphClassDefinition {
194 fn from(val: fea_rs::typed::GlyphClassDef) -> Self {
195 let label = val
196 .iter()
197 .find_map(fea_rs::typed::GlyphClassName::cast)
198 .unwrap();
199 let members: fea_rs::typed::GlyphClassLiteral = val
200 .iter()
201 .find_map(fea_rs::typed::GlyphClassLiteral::cast)
202 .unwrap();
203 GlyphClassDefinition {
204 name: label.text().trim_start_matches('@').to_string(),
205 glyphs: members.into(),
206 location: val.node().range(),
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct LanguageStatement {
214 pub tag: String,
216 pub include_dflt: bool,
218 pub required: bool,
220}
221impl LanguageStatement {
222 pub fn new(tag: String, include_dflt: bool, required: bool) -> Self {
224 Self {
225 tag,
226 include_dflt,
227 required,
228 }
229 }
230}
231impl AsFea for LanguageStatement {
232 fn as_fea(&self, _indent: &str) -> String {
233 format!(
234 "language {}{}{};",
235 self.tag,
236 if !self.include_dflt {
237 " exclude_dflt"
238 } else {
239 ""
240 },
241 if self.required { " required" } else { "" },
242 )
243 }
244}
245impl From<fea_rs::typed::Language> for LanguageStatement {
246 fn from(language: fea_rs::typed::Language) -> Self {
247 let exclude_dflt = language
248 .iter()
249 .any(|t| t.kind() == fea_rs::Kind::ExcludeDfltKw);
250 let required = language
251 .iter()
252 .any(|t| t.kind() == fea_rs::Kind::RequiredKw);
253 Self::new(
254 language
255 .iter()
256 .find_map(Tag::cast)
257 .unwrap()
258 .text()
259 .to_string(),
260 !exclude_dflt,
261 required,
262 )
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct LanguageSystemStatement {
269 pub script: String,
271 pub language: String,
273}
274impl LanguageSystemStatement {
275 pub fn new(script: String, language: String) -> Self {
277 Self { script, language }
278 }
279}
280impl AsFea for LanguageSystemStatement {
281 fn as_fea(&self, _indent: &str) -> String {
282 format!(
283 "languagesystem {} {};",
284 self.script,
285 self.language.trim_ascii_end()
286 )
287 }
288}
289impl From<fea_rs::typed::LanguageSystem> for LanguageSystemStatement {
290 fn from(langsys: fea_rs::typed::LanguageSystem) -> Self {
291 let mut tags = langsys.iter().filter_map(Tag::cast);
292 let script = tags.next().unwrap().text().to_string();
293 let language = tags.next().unwrap().text().to_string();
294 Self::new(script, language)
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct LookupReferenceStatement {
301 pub lookup_name: String,
306 pub location: Range<usize>,
308}
309impl LookupReferenceStatement {
310 pub fn new(lookup_name: String, location: Range<usize>) -> Self {
312 Self {
313 lookup_name,
314 location,
315 }
316 }
317}
318impl AsFea for LookupReferenceStatement {
319 fn as_fea(&self, _indent: &str) -> String {
320 format!("lookup {};", self.lookup_name)
321 }
322}
323impl From<fea_rs::typed::LookupRef> for LookupReferenceStatement {
324 fn from(lookup_ref: fea_rs::typed::LookupRef) -> Self {
325 Self::new(
326 lookup_ref
327 .iter()
328 .find(|t| t.kind() == fea_rs::Kind::Ident)
329 .unwrap()
330 .token_text()
331 .unwrap()
332 .to_string(),
333 lookup_ref.node().range(),
334 )
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct ScriptStatement {
341 pub tag: String,
343}
344impl ScriptStatement {
345 pub fn new(tag: String) -> Self {
347 Self { tag }
348 }
349}
350impl AsFea for ScriptStatement {
351 fn as_fea(&self, _indent: &str) -> String {
352 format!("script {};", self.tag)
353 }
354}
355impl From<fea_rs::typed::Script> for ScriptStatement {
356 fn from(script: fea_rs::typed::Script) -> Self {
357 Self::new(
358 script
359 .iter()
360 .find_map(Tag::cast)
361 .unwrap()
362 .text()
363 .to_string(),
364 )
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Default)]
370pub struct SubtableStatement;
371impl SubtableStatement {
372 pub fn new() -> Self {
374 Self {}
375 }
376}
377impl AsFea for SubtableStatement {
378 fn as_fea(&self, _indent: &str) -> String {
379 "subtable;".to_string()
380 }
381}
382
383#[derive(Debug, Clone, PartialEq)]
391pub struct SizeParameters {
392 pub design_size: f64,
394 pub subfamily_id: u16,
396 pub range_start: f64,
398 pub range_end: f64,
400 pub location: Range<usize>,
402}
403impl Eq for SizeParameters {}
404
405impl SizeParameters {
406 pub fn new(
408 design_size: f64,
409 subfamily_id: u16,
410 range_start: f64,
411 range_end: f64,
412 location: Range<usize>,
413 ) -> Self {
414 Self {
415 design_size,
416 subfamily_id,
417 range_start,
418 range_end,
419 location,
420 }
421 }
422}
423
424impl AsFea for SizeParameters {
425 fn as_fea(&self, _indent: &str) -> String {
426 let mut res = format!("parameters {:.1} {}", self.design_size, self.subfamily_id);
427 if self.range_start != 0.0 || self.range_end != 0.0 {
428 res.push_str(&format!(
429 " {} {}",
430 (self.range_start * 10.0) as i32,
431 (self.range_end * 10.0) as i32
432 ));
433 }
434 res.push(';');
435 res
436 }
437}
438
439impl From<fea_rs::typed::Parameters> for SizeParameters {
440 fn from(val: fea_rs::typed::Parameters) -> Self {
441 let parse_float = |fl: fea_rs::typed::FloatLike| -> f64 {
443 match fl {
444 fea_rs::typed::FloatLike::Float(f) => f.text().parse().unwrap(),
445 fea_rs::typed::FloatLike::Number(n) => n.text().parse::<i16>().unwrap() as f64,
446 }
447 };
448
449 let design_size = val
451 .iter()
452 .find_map(fea_rs::typed::FloatLike::cast)
453 .map(parse_float)
454 .unwrap();
455
456 let subfamily_id = val
458 .iter()
459 .filter(|t| t.kind() == fea_rs::Kind::Number || t.kind() == fea_rs::Kind::Float)
460 .nth(1)
461 .and_then(fea_rs::typed::Number::cast)
462 .map(|n| n.text().parse().unwrap())
463 .unwrap();
464
465 let range_start = val
467 .iter()
468 .filter_map(fea_rs::typed::FloatLike::cast)
469 .nth(2)
470 .map(|fl| parse_float(fl) / 10.0)
471 .unwrap_or(0.0);
472
473 let range_end = val
475 .iter()
476 .filter_map(fea_rs::typed::FloatLike::cast)
477 .nth(3)
478 .map(|fl| parse_float(fl) / 10.0)
479 .unwrap_or(0.0);
480
481 Self::new(
482 design_size,
483 subfamily_id,
484 range_start,
485 range_end,
486 val.range(),
487 )
488 }
489}
490
491#[derive(Debug, Clone, PartialEq)]
500pub struct ConditionSet {
501 pub name: String,
503 pub conditions: Vec<(String, f32, f32)>,
505 pub location: Range<usize>,
507}
508impl Eq for ConditionSet {}
509
510impl ConditionSet {
511 pub fn new(name: String, conditions: Vec<(String, f32, f32)>, location: Range<usize>) -> Self {
513 Self {
514 name,
515 conditions,
516 location,
517 }
518 }
519}
520
521impl From<fea_rs::typed::ConditionSet> for ConditionSet {
522 fn from(val: fea_rs::typed::ConditionSet) -> Self {
523 let name = val
525 .iter()
526 .find_map(|t| {
527 if t.kind() == fea_rs::Kind::Label {
528 t.as_token().map(|tok| tok.text.to_string())
529 } else {
530 None
531 }
532 })
533 .unwrap();
534
535 let parse_number =
537 |n: fea_rs::typed::Number| -> f32 { n.text().parse::<i16>().unwrap() as f32 };
538
539 let conditions: Vec<(String, f32, f32)> = val
541 .iter()
542 .filter_map(fea_rs::typed::Condition::cast)
543 .map(|cond| {
544 let tag = cond
546 .iter()
547 .find_map(fea_rs::typed::Tag::cast)
548 .unwrap()
549 .text()
550 .to_string();
551
552 let mut numbers = cond.iter().filter_map(fea_rs::typed::Number::cast);
554 let min = parse_number(numbers.next().unwrap());
555 let max = parse_number(numbers.next().unwrap());
556
557 (tag, min, max)
558 })
559 .collect();
560
561 Self::new(name, conditions, val.node().range())
562 }
563}
564
565impl AsFea for ConditionSet {
566 fn as_fea(&self, indent: &str) -> String {
567 let mut res = format!("{}conditionset {} {{\n", indent, self.name);
568 for (tag, min, max) in &self.conditions {
569 let format_num = |n: &f32| {
571 let s = format!("{}", n);
572 if s.contains('.') {
573 s.trim_end_matches('0').trim_end_matches('.').to_string()
574 } else {
575 s
576 }
577 };
578 res.push_str(&format!(
579 "{}\t{} {} {};\n",
580 indent,
581 tag,
582 format_num(min),
583 format_num(max)
584 ));
585 }
586 res.push_str(&format!("{}}}", indent));
587 res.push_str(&format!(" {};\n", self.name));
588 res
589 }
590}
591
592#[derive(Debug, Clone, PartialEq, Eq)]
601pub struct VariationBlock {
602 pub name: SmolStr,
604 pub conditionset: String,
606 pub statements: Vec<Statement>,
608 pub use_extension: bool,
610 pub location: Range<usize>,
612}
613
614impl VariationBlock {
615 pub fn new(
617 name: SmolStr,
618 conditionset: String,
619 statements: Vec<Statement>,
620 use_extension: bool,
621 location: Range<usize>,
622 ) -> Self {
623 Self {
624 name,
625 conditionset,
626 statements,
627 use_extension,
628 location,
629 }
630 }
631}
632
633impl From<fea_rs::typed::FeatureVariation> for VariationBlock {
634 fn from(val: fea_rs::typed::FeatureVariation) -> Self {
635 let name = val
637 .iter()
638 .find_map(fea_rs::typed::Tag::cast)
639 .map(|tag| SmolStr::new(tag.text()))
640 .unwrap();
641
642 let conditionset = val
644 .iter()
645 .skip_while(|t| t.kind() != fea_rs::Kind::Tag) .skip(1) .find_map(|t| {
648 if t.kind() == fea_rs::Kind::Label || t.kind() == fea_rs::Kind::Ident {
649 t.as_token().map(|tok| tok.text.to_string())
650 } else {
651 None
652 }
653 })
654 .unwrap_or_default();
655
656 let use_extension = val.iter().any(|t| t.kind() == fea_rs::Kind::UseExtensionKw);
658
659 let statements: Vec<Statement> = val
661 .node()
662 .iter_children()
663 .filter_map(crate::to_statement)
664 .collect();
665
666 Self::new(
667 name,
668 conditionset,
669 statements,
670 use_extension,
671 val.node().range(),
672 )
673 }
674}
675
676impl AsFea for VariationBlock {
677 fn as_fea(&self, indent: &str) -> String {
678 let mut res = format!("{}variation {} {}", indent, self.name, self.conditionset);
679 if self.use_extension {
680 res.push_str(" useExtension");
681 }
682 res.push_str(" {\n");
683
684 let mid_indent = indent.to_string() + SHIFT;
685 for stmt in &self.statements {
686 res.push_str(&stmt.as_fea(&mid_indent));
687 res.push('\n');
688 }
689
690 res.push_str(&format!("{}}} {};\n", indent, self.name));
691 res
692 }
693}
694
695#[derive(Debug, Clone, PartialEq, Eq)]
697pub struct LookupFlagStatement {
698 pub value: u16,
700 pub mark_attachment: Option<GlyphContainer>,
702 pub mark_filtering_set: Option<GlyphContainer>,
704 pub location: Range<usize>,
706}
707
708impl LookupFlagStatement {
709 pub fn new(
711 value: u16,
712 mark_attachment: Option<GlyphContainer>,
713 mark_filtering_set: Option<GlyphContainer>,
714 location: Range<usize>,
715 ) -> Self {
716 Self {
717 value,
718 mark_attachment,
719 mark_filtering_set,
720 location,
721 }
722 }
723}
724
725impl AsFea for LookupFlagStatement {
726 fn as_fea(&self, _indent: &str) -> String {
727 let mut res = Vec::new();
728 let flags = [
729 "RightToLeft",
730 "IgnoreBaseGlyphs",
731 "IgnoreLigatures",
732 "IgnoreMarks",
733 ];
734 let mut curr = 1u16;
735 for flag in &flags {
736 if self.value & curr != 0 {
737 res.push(flag.to_string());
738 }
739 curr <<= 1;
740 }
741 if let Some(mark_attachment) = &self.mark_attachment {
742 res.push(format!("MarkAttachmentType {}", mark_attachment.as_fea("")));
743 }
744 if let Some(mark_filtering_set) = &self.mark_filtering_set {
745 res.push(format!(
746 "UseMarkFilteringSet {}",
747 mark_filtering_set.as_fea("")
748 ));
749 }
750 if res.is_empty() {
751 res.push("0".to_string());
752 }
753 format!("lookupflag {};", res.join(" "))
754 }
755}
756
757impl From<fea_rs::typed::LookupFlag> for LookupFlagStatement {
758 fn from(val: fea_rs::typed::LookupFlag) -> Self {
759 let mut value = 0u16;
760 if let Some(number) = val.iter().find_map(fea_rs::typed::Number::cast) {
762 value = number.text().parse().unwrap();
763 } else {
764 for item in val.iter() {
765 match item.kind() {
766 fea_rs::Kind::RightToLeftKw => value |= 1,
767 fea_rs::Kind::IgnoreBaseGlyphsKw => value |= 2,
768 fea_rs::Kind::IgnoreLigaturesKw => value |= 4,
769 fea_rs::Kind::IgnoreMarksKw => value |= 8,
770 _ => {}
771 }
772 }
773 }
774
775 let mark_attachment = val
777 .iter()
778 .skip_while(|k| k.kind() != fea_rs::Kind::MarkAttachmentTypeKw)
779 .find_map(|gc| fea_rs::typed::GlyphClass::cast(gc).map(|g| g.into()));
780 let mark_filtering_set = val
781 .iter()
782 .skip_while(|k| k.kind() != fea_rs::Kind::UseMarkFilteringSetKw)
783 .find_map(|gc| fea_rs::typed::GlyphClass::cast(gc).map(|g| g.into()));
784
785 LookupFlagStatement::new(value, mark_attachment, mark_filtering_set, val.range())
786 }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq)]
794pub struct MarkClassDefinition {
795 pub mark_class: MarkClass,
797 pub anchor: crate::Anchor,
799 pub glyphs: GlyphContainer,
801}
802impl MarkClassDefinition {
803 pub fn new(mark_class: MarkClass, anchor: crate::Anchor, glyphs: GlyphContainer) -> Self {
805 Self {
806 mark_class,
807 anchor,
808 glyphs,
809 }
810 }
811}
812impl AsFea for MarkClassDefinition {
813 fn as_fea(&self, _indent: &str) -> String {
814 format!(
815 "markClass {} {} @{};",
816 self.glyphs.as_fea(""),
817 self.anchor.as_fea(""),
818 self.mark_class.name,
819 )
820 }
821}
822impl From<fea_rs::typed::MarkClassDef> for MarkClassDefinition {
823 fn from(val: fea_rs::typed::MarkClassDef) -> Self {
824 let glyphs_node = val
826 .iter()
827 .find_map(fea_rs::typed::GlyphOrClass::cast)
828 .unwrap();
829 let anchor_node = val.iter().find_map(fea_rs::typed::Anchor::cast).unwrap();
831 let anchor = from_anchor(anchor_node).unwrap();
832 let mark_class_node = val
834 .iter()
835 .skip_while(|t| t.kind() != fea_rs::Kind::AnchorNode)
836 .find_map(fea_rs::typed::GlyphClassName::cast)
837 .unwrap();
838 let mark_class = MarkClass::new(mark_class_node.text().trim_start_matches('@'));
839 MarkClassDefinition::new(mark_class, anchor, GlyphContainer::from(glyphs_node))
840 }
841}
842
843#[derive(Debug, Clone, PartialEq, Eq)]
845pub struct ValueRecordDefinition {
846 pub name: SmolStr,
848 pub value: ValueRecord,
850 pub location: Range<usize>,
852}
853impl ValueRecordDefinition {
854 pub fn new(name: SmolStr, value: ValueRecord, location: Range<usize>) -> Self {
856 Self {
857 name,
858 value,
859 location,
860 }
861 }
862}
863
864impl AsFea for ValueRecordDefinition {
865 fn as_fea(&self, _indent: &str) -> String {
866 format!("valueRecordDef {} {};", self.value.as_fea(""), self.name)
867 }
868}
869
870impl From<fea_rs::typed::ValueRecordDef> for ValueRecordDefinition {
871 fn from(val: fea_rs::typed::ValueRecordDef) -> Self {
872 let name = val
873 .iter()
874 .find(|t| t.kind() == fea_rs::Kind::Ident)
875 .unwrap();
876 let value_record_node = val
877 .iter()
878 .find_map(fea_rs::typed::ValueRecord::cast)
879 .unwrap();
880 ValueRecordDefinition::new(
881 name.as_token().unwrap().text.clone(),
882 ValueRecord::from(value_record_node),
883 val.node().range(),
884 )
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891 use crate::{GlyphContainer, GlyphName};
892
893 #[test]
894 fn test_roundtrip_lookupflag_simple() {
895 const FEA: &str = "lookup test { lookupflag RightToLeft; } test;";
896 let (parsed, _) = fea_rs::parse::parse_string(FEA);
897 let lookup = parsed
898 .root()
899 .iter_children()
900 .find_map(fea_rs::typed::LookupBlock::cast)
901 .unwrap();
902 let lookupflag = lookup
903 .node()
904 .iter_children()
905 .find_map(fea_rs::typed::LookupFlag::cast)
906 .unwrap();
907 let stmt = LookupFlagStatement::from(lookupflag);
908 assert_eq!(stmt.value, 1);
909 assert_eq!(stmt.as_fea(""), "lookupflag RightToLeft;");
910 }
911
912 #[test]
913 fn test_roundtrip_lookupflag_multiple() {
914 const FEA: &str = "lookup test { lookupflag RightToLeft IgnoreMarks; } test;";
915 let (parsed, _) = fea_rs::parse::parse_string(FEA);
916 let lookup = parsed
917 .root()
918 .iter_children()
919 .find_map(fea_rs::typed::LookupBlock::cast)
920 .unwrap();
921 let lookupflag = lookup
922 .node()
923 .iter_children()
924 .find_map(fea_rs::typed::LookupFlag::cast)
925 .unwrap();
926 let stmt = LookupFlagStatement::from(lookupflag);
927 assert_eq!(stmt.value, 9); assert_eq!(stmt.as_fea(""), "lookupflag RightToLeft IgnoreMarks;");
929 }
930
931 #[test]
932 fn test_roundtrip_lookupflag_zero() {
933 const FEA: &str = "lookup test { lookupflag 0; } test;";
934 let (parsed, _) = fea_rs::parse::parse_string(FEA);
935 let lookup = parsed
936 .root()
937 .iter_children()
938 .find_map(fea_rs::typed::LookupBlock::cast)
939 .unwrap();
940 let lookupflag = lookup
941 .node()
942 .iter_children()
943 .find_map(fea_rs::typed::LookupFlag::cast)
944 .unwrap();
945 let stmt = LookupFlagStatement::from(lookupflag);
946 assert_eq!(stmt.value, 0);
947 assert_eq!(stmt.as_fea(""), "lookupflag 0;");
948 }
949
950 #[test]
951 fn test_generate_lookupflag() {
952 let stmt = LookupFlagStatement::new(
953 10, None,
955 None,
956 0..0,
957 );
958 assert_eq!(stmt.as_fea(""), "lookupflag IgnoreBaseGlyphs IgnoreMarks;");
959 }
960
961 #[test]
962 fn test_generate_lookupflag_with_mark_attachment() {
963 let stmt = LookupFlagStatement::new(
964 0,
965 Some(GlyphContainer::GlyphClass(GlyphClass::new(
966 vec![
967 GlyphContainer::GlyphName(GlyphName::new("acute")),
968 GlyphContainer::GlyphName(GlyphName::new("grave")),
969 ],
970 0..0,
971 ))),
972 None,
973 0..0,
974 );
975 assert_eq!(
976 stmt.as_fea(""),
977 "lookupflag MarkAttachmentType [acute grave];"
978 );
979 }
980
981 #[test]
983 fn test_roundtrip_anchordef_simple() {
984 const FEA: &str = "anchorDef 300 100 ANCHOR_1;";
985 let (parsed, _) = fea_rs::parse::parse_string(FEA);
986 let anchor_def = parsed
987 .root()
988 .iter_children()
989 .find_map(fea_rs::typed::AnchorDef::cast)
990 .unwrap();
991 let stmt = AnchorDefinition::from(anchor_def);
992 assert_eq!(stmt.x, 300.into());
993 assert_eq!(stmt.y, 100.into());
994 assert_eq!(stmt.name, "ANCHOR_1");
995 assert_eq!(stmt.contourpoint, None);
996 assert_eq!(stmt.as_fea(""), "anchorDef 300 100 ANCHOR_1;");
997 }
998
999 #[test]
1000 fn test_roundtrip_anchordef_contourpoint() {
1001 const FEA: &str = "anchorDef 300 100 contourpoint 5 ANCHOR_1;";
1002 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1003 let anchor_def = parsed
1004 .root()
1005 .iter_children()
1006 .find_map(fea_rs::typed::AnchorDef::cast)
1007 .unwrap();
1008 let stmt = AnchorDefinition::from(anchor_def);
1009 assert_eq!(stmt.x, 300.into());
1010 assert_eq!(stmt.y, 100.into());
1011 assert_eq!(stmt.contourpoint, Some(5));
1012 assert_eq!(stmt.name, "ANCHOR_1");
1013 assert_eq!(
1014 stmt.as_fea(""),
1015 "anchorDef 300 100 contourpoint 5 ANCHOR_1;"
1016 );
1017 }
1018
1019 #[test]
1020 fn test_generation_anchordef() {
1021 let stmt = AnchorDefinition::new(150.into(), (-50).into(), None, "BASE".to_string(), 0..0);
1022 assert_eq!(stmt.as_fea(""), "anchorDef 150 -50 BASE;");
1023 }
1024
1025 #[test]
1027 fn test_roundtrip_featurereference() {
1028 const FEA: &str = "feature test { feature salt; } test;";
1029 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1030 let feature = parsed
1031 .root()
1032 .iter_children()
1033 .find_map(fea_rs::typed::Feature::cast)
1034 .unwrap();
1035 let feature_ref = feature
1036 .node()
1037 .iter_children()
1038 .find_map(fea_rs::typed::FeatureRef::cast)
1039 .unwrap();
1040 let stmt = FeatureReferenceStatement::from(feature_ref);
1041 assert_eq!(stmt.feature_name, "salt");
1042 assert_eq!(stmt.as_fea(""), "feature salt;");
1043 }
1044
1045 #[test]
1046 fn test_generation_featurereference() {
1047 let stmt = FeatureReferenceStatement::new("liga".to_string());
1048 assert_eq!(stmt.as_fea(""), "feature liga;");
1049 }
1050
1051 #[test]
1053 fn test_roundtrip_fontrevision() {
1054 const FEA: &str = "table head { FontRevision 2.500; } head;";
1055 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1056 let table = parsed
1057 .root()
1058 .iter_children()
1059 .find_map(fea_rs::typed::HeadTable::cast)
1060 .unwrap();
1061 let font_rev = table
1062 .node()
1063 .iter_children()
1064 .find_map(fea_rs::typed::HeadFontRevision::cast)
1065 .unwrap();
1066 let stmt = FontRevisionStatement::from(font_rev);
1067 assert_eq!(stmt.revision, 2.5);
1068 assert_eq!(stmt.as_fea(""), "FontRevision 2.500;");
1069 }
1070
1071 #[test]
1072 fn test_generation_fontrevision() {
1073 let stmt = FontRevisionStatement::new(1.125);
1074 assert_eq!(stmt.as_fea(""), "FontRevision 1.125;");
1075 }
1076
1077 #[test]
1079 fn test_roundtrip_glyphclassdef() {
1080 const FEA: &str = "@UPPERCASE = [A B C D E F];";
1081 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1082 let glyph_class_def = parsed
1083 .root()
1084 .iter_children()
1085 .find_map(fea_rs::typed::GlyphClassDef::cast)
1086 .unwrap();
1087 let stmt = GlyphClassDefinition::from(glyph_class_def);
1088 assert_eq!(stmt.name, "UPPERCASE");
1089 assert_eq!(stmt.glyphs.glyphs.len(), 6);
1090 assert_eq!(stmt.as_fea(""), "@UPPERCASE = [A B C D E F];");
1091 }
1092
1093 #[test]
1094 fn test_generation_glyphclassdef() {
1095 let glyphs = GlyphClass::new(
1096 vec![
1097 GlyphContainer::GlyphName(GlyphName::new("a")),
1098 GlyphContainer::GlyphName(GlyphName::new("b")),
1099 GlyphContainer::GlyphName(GlyphName::new("c")),
1100 ],
1101 0..0,
1102 );
1103 let stmt = GlyphClassDefinition::new("lowercase".to_string(), glyphs, 0..0);
1104 assert_eq!(stmt.as_fea(""), "@lowercase = [a b c];");
1105 }
1106
1107 #[test]
1109 fn test_roundtrip_language() {
1110 const FEA: &str = "feature test { language TRK; } test;";
1111 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1112 let feature = parsed
1113 .root()
1114 .iter_children()
1115 .find_map(fea_rs::typed::Feature::cast)
1116 .unwrap();
1117 let lang = feature
1118 .node()
1119 .iter_children()
1120 .find_map(fea_rs::typed::Language::cast)
1121 .unwrap();
1122 let stmt = LanguageStatement::from(lang);
1123 assert_eq!(stmt.as_fea(""), "language TRK;");
1125 }
1126
1127 #[test]
1128 fn test_generation_language() {
1129 let stmt = LanguageStatement::new("DEU ".to_string(), true, false);
1130 assert_eq!(stmt.as_fea(""), "language DEU ;");
1131 }
1132
1133 #[test]
1135 fn test_roundtrip_languagesystem() {
1136 const FEA: &str = "languagesystem latn dflt;";
1137 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1138 let langsys = parsed
1139 .root()
1140 .iter_children()
1141 .find_map(fea_rs::typed::LanguageSystem::cast)
1142 .unwrap();
1143 let stmt = LanguageSystemStatement::from(langsys);
1144 assert_eq!(stmt.script, "latn");
1145 assert_eq!(stmt.language, "dflt");
1146 assert_eq!(stmt.as_fea(""), "languagesystem latn dflt;");
1147 }
1148
1149 #[test]
1150 fn test_generation_languagesystem() {
1151 let stmt = LanguageSystemStatement::new("cyrl".to_string(), "SRB ".to_string());
1152 assert_eq!(stmt.as_fea(""), "languagesystem cyrl SRB;");
1153 }
1154
1155 #[test]
1157 fn test_roundtrip_script() {
1158 const FEA: &str = "feature test { script latn; } test;";
1159 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1160 let feature = parsed
1161 .root()
1162 .iter_children()
1163 .find_map(fea_rs::typed::Feature::cast)
1164 .unwrap();
1165 let script = feature
1166 .node()
1167 .iter_children()
1168 .find_map(fea_rs::typed::Script::cast)
1169 .unwrap();
1170 let stmt = ScriptStatement::from(script);
1171 assert_eq!(stmt.tag, "latn");
1172 assert_eq!(stmt.as_fea(""), "script latn;");
1173 }
1174
1175 #[test]
1176 fn test_generation_script() {
1177 let stmt = ScriptStatement::new("arab".to_string());
1178 assert_eq!(stmt.as_fea(""), "script arab;");
1179 }
1180
1181 #[test]
1183 fn test_generation_subtable() {
1184 let stmt = SubtableStatement::new();
1185 assert_eq!(stmt.as_fea(""), "subtable;");
1186 }
1187
1188 #[test]
1190 fn test_roundtrip_lookupreference() {
1191 const FEA: &str = "feature test { lookup myLookup; } test;";
1192 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1193 let feature = parsed
1194 .root()
1195 .iter_children()
1196 .find_map(fea_rs::typed::Feature::cast)
1197 .unwrap();
1198 let lookup_ref = feature
1199 .node()
1200 .iter_children()
1201 .find_map(fea_rs::typed::LookupRef::cast)
1202 .unwrap();
1203 let stmt = LookupReferenceStatement::from(lookup_ref);
1204 assert_eq!(stmt.lookup_name, "myLookup");
1205 assert_eq!(stmt.as_fea(""), "lookup myLookup;");
1206 }
1207
1208 #[test]
1209 fn test_generation_lookupreference() {
1210 let stmt = LookupReferenceStatement::new("anotherLookup".to_string(), 0..0);
1211 assert_eq!(stmt.as_fea(""), "lookup anotherLookup;");
1212 }
1213
1214 #[test]
1216 fn test_roundtrip_sizeparameters_simple() {
1217 const FEA: &str = "feature size { parameters 10.0 0; } size;";
1218 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1219 let feature = parsed
1220 .root()
1221 .iter_children()
1222 .find_map(fea_rs::typed::Feature::cast)
1223 .unwrap();
1224 let params = feature
1225 .node()
1226 .iter_children()
1227 .find_map(fea_rs::typed::Parameters::cast)
1228 .unwrap();
1229 let stmt = SizeParameters::from(params);
1230 assert_eq!(stmt.design_size, 10.0);
1231 assert_eq!(stmt.subfamily_id, 0);
1232 assert_eq!(stmt.range_start, 0.0);
1233 assert_eq!(stmt.range_end, 0.0);
1234 assert_eq!(stmt.as_fea(""), "parameters 10.0 0;");
1235 }
1236
1237 #[test]
1238 fn test_roundtrip_sizeparameters_with_range() {
1239 const FEA: &str = "feature size { parameters 10.0 0 80 120; } size;";
1240 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1241 let feature = parsed
1242 .root()
1243 .iter_children()
1244 .find_map(fea_rs::typed::Feature::cast)
1245 .unwrap();
1246 let params = feature
1247 .node()
1248 .iter_children()
1249 .find_map(fea_rs::typed::Parameters::cast)
1250 .unwrap();
1251 let stmt = SizeParameters::from(params);
1252 assert_eq!(stmt.design_size, 10.0);
1253 assert_eq!(stmt.subfamily_id, 0);
1254 assert_eq!(stmt.range_start, 8.0); assert_eq!(stmt.range_end, 12.0); assert_eq!(stmt.as_fea(""), "parameters 10.0 0 80 120;");
1257 }
1258
1259 #[test]
1260 fn test_generate_sizeparameters() {
1261 let stmt = SizeParameters::new(12.5, 1, 100.0, 150.0, 0..0);
1262 assert_eq!(stmt.as_fea(""), "parameters 12.5 1 1000 1500;");
1263 }
1264
1265 #[test]
1266 fn test_generation_lookupflag() {
1267 let stmt = LookupFlagStatement::new(
1268 0,
1269 Some(GlyphContainer::GlyphClass(GlyphClass::new(
1270 vec![
1271 GlyphContainer::GlyphName(GlyphName::new("acute")),
1272 GlyphContainer::GlyphName(GlyphName::new("grave")),
1273 ],
1274 0..0,
1275 ))),
1276 None,
1277 0..0,
1278 );
1279 assert_eq!(
1280 stmt.as_fea(""),
1281 "lookupflag MarkAttachmentType [acute grave];"
1282 );
1283 let stmt = LookupFlagStatement::new(
1284 9,
1285 None,
1286 Some(GlyphContainer::GlyphClass(GlyphClass::new(
1287 vec![
1288 GlyphContainer::GlyphName(GlyphName::new("acute")),
1289 GlyphContainer::GlyphName(GlyphName::new("grave")),
1290 ],
1291 0..0,
1292 ))),
1293 0..0,
1294 );
1295 assert_eq!(
1296 stmt.as_fea(""),
1297 "lookupflag RightToLeft IgnoreMarks UseMarkFilteringSet [acute grave];"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_roundtrip_lookupflag() {
1303 const FEA: &str = "lookup test { lookupflag RightToLeft IgnoreMarks UseMarkFilteringSet [acute grave]; } test;";
1304 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1305 let lookup = parsed
1306 .root()
1307 .iter_children()
1308 .find_map(fea_rs::typed::LookupBlock::cast)
1309 .unwrap();
1310 let lookupflag = lookup
1311 .node()
1312 .iter_children()
1313 .find_map(fea_rs::typed::LookupFlag::cast)
1314 .unwrap();
1315 let stmt = LookupFlagStatement::from(lookupflag);
1316 assert_eq!(stmt.value, 9); assert_eq!(
1318 stmt.clone().mark_filtering_set.unwrap().as_fea(""),
1319 "[acute grave]"
1320 );
1321 assert_eq!(
1322 stmt.as_fea(""),
1323 "lookupflag RightToLeft IgnoreMarks UseMarkFilteringSet [acute grave];"
1324 );
1325
1326 const FEA2: &str =
1327 "lookup test { lookupflag RightToLeft IgnoreMarks MarkAttachmentType @foo; } test;";
1328 let (parsed, _) = fea_rs::parse::parse_string(FEA2);
1329 let lookup = parsed
1330 .root()
1331 .iter_children()
1332 .find_map(fea_rs::typed::LookupBlock::cast)
1333 .unwrap();
1334 let lookupflag = lookup
1335 .node()
1336 .iter_children()
1337 .find_map(fea_rs::typed::LookupFlag::cast)
1338 .unwrap();
1339 let stmt = LookupFlagStatement::from(lookupflag);
1340 assert_eq!(stmt.value, 9);
1341 assert_eq!(
1342 stmt.as_fea(""),
1343 "lookupflag RightToLeft IgnoreMarks MarkAttachmentType @foo;"
1344 );
1345 }
1346
1347 #[test]
1349 fn test_roundtrip_conditionset_simple() {
1350 const FEA: &str = r#"conditionset heavy {
1351 wght 700 900;
1352} heavy;"#;
1353 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1354 let condset = parsed
1355 .root()
1356 .iter_children()
1357 .find_map(fea_rs::typed::ConditionSet::cast)
1358 .unwrap();
1359 let stmt = ConditionSet::from(condset);
1360 assert_eq!(stmt.name, "heavy");
1361 assert_eq!(stmt.conditions.len(), 1);
1362 assert_eq!(stmt.conditions[0].0, "wght");
1363 assert_eq!(stmt.conditions[0].1, 700.0);
1364 assert_eq!(stmt.conditions[0].2, 900.0);
1365
1366 let output = stmt.as_fea("");
1367 assert!(output.contains("conditionset heavy"));
1368 assert!(output.contains("wght 700 900"));
1369 }
1370
1371 #[test]
1372 fn test_roundtrip_conditionset_multiple_conditions() {
1373 const FEA: &str = r#"conditionset complex {
1374 wght 400 700;
1375 wdth 75 100;
1376} complex;"#;
1377 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1378 let condset = parsed
1379 .root()
1380 .iter_children()
1381 .find_map(fea_rs::typed::ConditionSet::cast)
1382 .unwrap();
1383 let stmt = ConditionSet::from(condset);
1384 assert_eq!(stmt.name, "complex");
1385 assert_eq!(stmt.conditions.len(), 2);
1386 assert_eq!(stmt.conditions[0], ("wght".to_string(), 400.0, 700.0));
1387 assert_eq!(stmt.conditions[1], ("wdth".to_string(), 75.0, 100.0));
1388
1389 let output = stmt.as_fea("");
1390 assert!(output.contains("conditionset complex"));
1391 assert!(output.contains("wght 400 700"));
1392 assert!(output.contains("wdth 75 100"));
1393 }
1394
1395 #[test]
1396 fn test_roundtrip_conditionset_from_file() {
1397 const FEA: &str = include_str!("../resources/test/variable_conditionset.fea");
1398 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1399 let condset = parsed
1400 .root()
1401 .iter_children()
1402 .find_map(fea_rs::typed::ConditionSet::cast)
1403 .unwrap();
1404 let stmt = ConditionSet::from(condset);
1405 assert_eq!(stmt.name, "heavy");
1406 assert_eq!(stmt.conditions.len(), 1);
1407 assert_eq!(stmt.conditions[0], ("wght".to_string(), 700.0, 900.0));
1408 }
1409
1410 #[test]
1411 fn test_generate_conditionset() {
1412 let stmt = ConditionSet::new(
1413 "myCondition".to_string(),
1414 vec![
1415 ("wght".to_string(), 300.0, 500.0),
1416 ("opsz".to_string(), 8.0, 12.0),
1417 ],
1418 0..0,
1419 );
1420
1421 let output = stmt.as_fea("");
1422 assert!(output.contains("conditionset myCondition"));
1423 assert!(output.contains("wght 300 500"));
1424 assert!(output.contains("opsz 8 12"));
1425 assert!(output.contains("} myCondition;"));
1426 }
1427
1428 #[test]
1429 fn test_conditionset_integration() {
1430 const FEA: &str = r#"languagesystem DFLT dflt;
1432
1433conditionset heavy {
1434 wght 700 900;
1435} heavy;"#;
1436
1437 let ff = crate::FeatureFile::new_from_fea(FEA, None::<&[&str]>, None::<&str>).unwrap();
1438 assert_eq!(ff.statements.len(), 2);
1439
1440 let cs = ff
1442 .statements
1443 .iter()
1444 .find_map(|item| {
1445 if let crate::ToplevelItem::ConditionSet(cs) = item {
1446 Some(cs)
1447 } else {
1448 None
1449 }
1450 })
1451 .expect("Should have found ConditionSet");
1452
1453 assert_eq!(cs.name, "heavy");
1454 assert_eq!(cs.conditions.len(), 1);
1455 assert_eq!(cs.conditions[0], ("wght".to_string(), 700.0, 900.0));
1456
1457 let output = cs.as_fea("");
1459 assert!(output.contains("conditionset heavy"));
1460 assert!(output.contains("wght 700 900"));
1461 }
1462
1463 #[test]
1465 fn test_roundtrip_variationblock() {
1466 const FEA: &str = include_str!("../resources/test/variable_conditionset.fea");
1467 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1468 let variation = parsed
1469 .root()
1470 .iter_children()
1471 .find_map(fea_rs::typed::FeatureVariation::cast)
1472 .unwrap();
1473 let stmt = VariationBlock::from(variation);
1474
1475 assert_eq!(stmt.name, "rvrn");
1476 assert_eq!(stmt.conditionset, "heavy");
1477 assert_eq!(stmt.statements.len(), 1);
1478
1479 let output = stmt.as_fea("");
1480 assert!(output.contains("variation rvrn heavy"));
1481 assert!(output.contains("lookup symbols_heavy"));
1482 }
1483
1484 #[test]
1485 fn test_generate_variationblock() {
1486 let stmt = VariationBlock::new(
1487 "rvrn".into(),
1488 "myCondition".to_string(),
1489 vec![crate::Statement::Comment(crate::Comment::from("# Test"))],
1490 false,
1491 0..0,
1492 );
1493
1494 let output = stmt.as_fea("");
1495 assert!(output.contains("variation rvrn myCondition"));
1496 assert!(output.contains("# Test"));
1497 assert!(output.contains("} rvrn;"));
1498 }
1499
1500 #[test]
1501 fn test_variationblock_integration() {
1502 const FEA: &str = include_str!("../resources/test/variable_conditionset.fea");
1503
1504 let ff = crate::FeatureFile::new_from_fea(FEA, None::<&[&str]>, None::<&str>).unwrap();
1505
1506 assert!(ff.statements.len() >= 4);
1508
1509 let vb = ff
1511 .statements
1512 .iter()
1513 .find_map(|item| {
1514 if let crate::ToplevelItem::VariationBlock(vb) = item {
1515 Some(vb)
1516 } else {
1517 None
1518 }
1519 })
1520 .expect("Should have found VariationBlock");
1521
1522 assert_eq!(vb.name.as_str(), "rvrn");
1523 assert_eq!(vb.conditionset, "heavy");
1524 }
1525}