1#![deny(missing_docs)]
2use std::{
244 ops::Range,
245 path::{Path, PathBuf},
246 sync::Arc,
247};
248mod base;
249mod contextual;
250mod dummyresolver;
251mod error;
252mod gdef;
253mod glyphcontainers;
254mod gpos;
255mod gsub;
256mod miscellenea;
257mod name;
258mod os2;
259mod stat;
260mod tables;
261mod values;
262mod visitor;
263pub use contextual::*;
264pub use error::Error;
265pub use fea_rs;
266use fea_rs::{GlyphMap, NodeOrToken, ParseTree, parse::FileSystemResolver, typed::AstNode as _};
267pub use gdef::*;
268pub use glyphcontainers::*;
269pub use gpos::*;
270pub use gsub::*;
271pub use miscellenea::*;
272pub use name::*;
273use smol_str::SmolStr;
274pub use tables::*;
275pub use values::*;
276pub use visitor::LayoutVisitor;
277
278use crate::{base::Base, os2::Os2};
279
280pub(crate) const SHIFT: &str = " ";
281
282pub trait AsFea {
284 fn as_fea(&self, indent: &str) -> String;
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum Statement {
295 SingleSubst(SingleSubstStatement),
298 MultipleSubst(MultipleSubstStatement),
300 AlternateSubst(AlternateSubstStatement),
302 LigatureSubst(LigatureSubstStatement),
304 ReverseChainSubst(ReverseChainSingleSubstStatement),
306 ChainedContextSubst(ChainedContextStatement<Subst>),
308 IgnoreSubst(IgnoreStatement<Subst>),
310 SinglePos(SinglePosStatement),
313 PairPos(PairPosStatement),
315 CursivePos(CursivePosStatement),
317 MarkBasePos(MarkBasePosStatement),
319 MarkLigPos(MarkLigPosStatement),
321 MarkMarkPos(MarkMarkPosStatement),
323 ChainedContextPos(ChainedContextStatement<Pos>),
325 IgnorePos(IgnoreStatement<Pos>),
327 AnchorDefinition(AnchorDefinition),
330 MarkClassDefinition(MarkClassDefinition),
332 Comment(Comment),
334 FeatureNameStatement(NameRecord),
336 FontRevision(FontRevisionStatement),
338 FeatureReference(FeatureReferenceStatement),
340 GlyphClassDefinition(GlyphClassDefinition),
342 Language(LanguageStatement),
344 LanguageSystem(LanguageSystemStatement),
346 LookupFlag(LookupFlagStatement),
348 LookupReference(LookupReferenceStatement),
350 SizeParameters(SizeParameters),
352 SizeMenuName(NameRecord),
354 Subtable(SubtableStatement),
356 Script(ScriptStatement),
358 ValueRecordDefinition(ValueRecordDefinition),
360 ConditionSet(ConditionSet),
362 VariationBlock(VariationBlock),
364 Base(Table<Base>),
367 Gdef(Table<Gdef>),
369 Head(Table<Head>),
371 Hhea(Table<Hhea>),
373 Name(Table<Name>),
375 Os2(Table<Os2>),
377 Stat(Table<Stat>),
379 Vhea(Table<Vhea>),
381 FeatureBlock(FeatureBlock),
383 LookupBlock(LookupBlock),
385 NestedBlock(NestedBlock),
387 GdefAttach(AttachStatement),
390 GdefClassDef(GlyphClassDefStatement),
392 GdefLigatureCaretByIndex(LigatureCaretByIndexStatement),
394 GdefLigatureCaretByPos(LigatureCaretByPosStatement),
396}
397impl AsFea for Statement {
398 fn as_fea(&self, indent: &str) -> String {
399 match self {
400 Statement::SingleSubst(ss) => ss.as_fea(indent),
402 Statement::MultipleSubst(ms) => ms.as_fea(indent),
403 Statement::AlternateSubst(alt) => alt.as_fea(indent),
404 Statement::LigatureSubst(ls) => ls.as_fea(indent),
405 Statement::ChainedContextSubst(ccs) => ccs.as_fea(indent),
406 Statement::IgnoreSubst(is) => is.as_fea(indent),
407 Statement::ReverseChainSubst(rss) => rss.as_fea(indent),
408 Statement::SinglePos(sp) => sp.as_fea(indent),
410 Statement::PairPos(pp) => pp.as_fea(indent),
411 Statement::CursivePos(cp) => cp.as_fea(indent),
412 Statement::MarkBasePos(mbp) => mbp.as_fea(indent),
413 Statement::MarkLigPos(mlp) => mlp.as_fea(indent),
414 Statement::MarkMarkPos(mmp) => mmp.as_fea(indent),
415 Statement::ChainedContextPos(ccs) => ccs.as_fea(indent),
416 Statement::IgnorePos(ip) => ip.as_fea(indent),
417 Statement::AnchorDefinition(ad) => ad.as_fea(indent),
419 Statement::Comment(c) => c.as_fea(indent),
420 Statement::FeatureReference(fr) => fr.as_fea(indent),
421 Statement::FeatureNameStatement(fr) => fr.as_fea(indent),
422 Statement::FontRevision(fr) => fr.as_fea(indent),
423 Statement::GlyphClassDefinition(gcd) => gcd.as_fea(indent),
424 Statement::Language(ls) => ls.as_fea(indent),
425 Statement::LanguageSystem(ls) => ls.as_fea(indent),
426 Statement::LookupFlag(lf) => lf.as_fea(indent),
427 Statement::LookupReference(lr) => lr.as_fea(indent),
428 Statement::MarkClassDefinition(mc) => mc.as_fea(indent),
429 Statement::Script(sc) => sc.as_fea(indent),
430 Statement::SizeMenuName(sm) => sm.as_fea(indent),
431 Statement::SizeParameters(sp) => sp.as_fea(indent),
432 Statement::Subtable(st) => st.as_fea(indent),
433 Statement::ValueRecordDefinition(vrd) => vrd.as_fea(indent),
434 Statement::ConditionSet(cs) => cs.as_fea(indent),
435 Statement::VariationBlock(vb) => vb.as_fea(indent),
436 Statement::GdefAttach(at) => at.as_fea(indent),
438 Statement::GdefClassDef(gcd) => gcd.as_fea(indent),
439 Statement::GdefLigatureCaretByIndex(lc) => lc.as_fea(indent),
440 Statement::GdefLigatureCaretByPos(lc) => lc.as_fea(indent),
441 Statement::Base(base) => base.as_fea(indent),
443 Statement::Gdef(gdef) => gdef.as_fea(indent),
444 Statement::Head(head) => head.as_fea(indent),
445 Statement::Hhea(hhea) => hhea.as_fea(indent),
446 Statement::Name(name) => name.as_fea(indent),
447 Statement::Os2(os2) => os2.as_fea(indent),
448 Statement::Stat(stat) => stat.as_fea(indent),
449 Statement::Vhea(vhea) => vhea.as_fea(indent),
450 Statement::FeatureBlock(fb) => fb.as_fea(indent),
451 Statement::LookupBlock(lb) => lb.as_fea(indent),
452 Statement::NestedBlock(nb) => nb.as_fea(indent),
453 }
454 }
455}
456
457fn to_statement(child: &NodeOrToken) -> Option<Statement> {
458 if child.kind() == fea_rs::Kind::Comment {
459 return Some(Statement::Comment(Comment::from(
460 child.token_text().unwrap(),
461 )));
462 } else if child.kind() == fea_rs::Kind::SubtableNode {
463 return Some(Statement::Subtable(SubtableStatement::new()));
464 }
465 #[allow(clippy::manual_map)]
466 if let Some(gsub1) = fea_rs::typed::Gsub1::cast(child) {
468 Some(Statement::SingleSubst(gsub1.into()))
469 } else if let Some(gsub2) = fea_rs::typed::Gsub2::cast(child) {
470 Some(Statement::MultipleSubst(gsub2.into()))
471 } else if let Some(gsub3) = fea_rs::typed::Gsub3::cast(child) {
472 Some(Statement::AlternateSubst(gsub3.into()))
473 } else if let Some(gsub4) = fea_rs::typed::Gsub4::cast(child) {
474 Some(Statement::LigatureSubst(gsub4.into()))
475 } else if let Some(gsub6) = fea_rs::typed::Gsub6::cast(child) {
476 Some(gsub6.into())
477 } else if let Some(rss) = fea_rs::typed::Gsub8::cast(child) {
478 Some(Statement::ReverseChainSubst(rss.into()))
479 } else if let Some(gsig) = fea_rs::typed::GsubIgnore::cast(child) {
480 Some(Statement::IgnoreSubst(gsig.into()))
481 } else if let Some(gpos1) = fea_rs::typed::Gpos1::cast(child) {
483 Some(Statement::SinglePos(gpos1.into()))
484 } else if let Some(gpos2) = fea_rs::typed::Gpos2::cast(child) {
485 Some(Statement::PairPos(gpos2.into()))
486 } else if let Some(gpos3) = fea_rs::typed::Gpos3::cast(child) {
487 Some(Statement::CursivePos(gpos3.into()))
488 } else if let Some(gpos4) = fea_rs::typed::Gpos4::cast(child) {
489 Some(Statement::MarkBasePos(gpos4.into()))
490 } else if let Some(gpos5) = fea_rs::typed::Gpos5::cast(child) {
491 Some(Statement::MarkLigPos(gpos5.into()))
492 } else if let Some(gpos6) = fea_rs::typed::Gpos6::cast(child) {
493 Some(Statement::MarkMarkPos(gpos6.into()))
494 } else if let Some(gpos8) = fea_rs::typed::Gpos8::cast(child) {
495 Some(gpos8.into())
496 } else if let Some(gpig) = fea_rs::typed::GposIgnore::cast(child) {
497 Some(Statement::IgnorePos(gpig.into()))
498 } else if let Some(ad) = fea_rs::typed::AnchorDef::cast(child) {
500 Some(Statement::AnchorDefinition(ad.into()))
501 } else if let Some(at) = fea_rs::typed::GdefAttach::cast(child) {
502 Some(Statement::GdefAttach(at.into()))
503 } else if let Some(gcd) = fea_rs::typed::GdefClassDef::cast(child) {
504 Some(Statement::GdefClassDef(gcd.into()))
505 } else if let Some(lc) = fea_rs::typed::GdefLigatureCaret::cast(child) {
506 let is_by_pos = lc
508 .iter()
509 .next()
510 .map(|t| t.kind() == fea_rs::Kind::LigatureCaretByPosKw)
511 .unwrap_or(false);
512 if is_by_pos {
513 Some(Statement::GdefLigatureCaretByPos(lc.into()))
514 } else {
515 Some(Statement::GdefLigatureCaretByIndex(lc.into()))
516 }
517 } else if let Some(fr) = fea_rs::typed::FeatureRef::cast(child) {
518 Some(Statement::FeatureReference(fr.into()))
519 } else if let Some(fr) = fea_rs::typed::HeadFontRevision::cast(child) {
520 Some(Statement::FontRevision(fr.into()))
521 } else if let Some(gcd) = fea_rs::typed::GlyphClassDef::cast(child) {
522 Some(Statement::GlyphClassDefinition(gcd.into()))
523 } else if let Some(lang) = fea_rs::typed::Language::cast(child) {
524 Some(Statement::Language(lang.into()))
525 } else if let Some(langsys) = fea_rs::typed::LanguageSystem::cast(child) {
526 Some(Statement::LanguageSystem(langsys.into()))
527 } else if let Some(lookupflag) = fea_rs::typed::LookupFlag::cast(child) {
528 Some(Statement::LookupFlag(lookupflag.into()))
529 } else if let Some(lookupref) = fea_rs::typed::LookupRef::cast(child) {
530 Some(Statement::LookupReference(lookupref.into()))
531 } else if let Some(mcd) = fea_rs::typed::MarkClassDef::cast(child) {
532 Some(Statement::MarkClassDefinition(mcd.into()))
533 } else if let Some(script) = fea_rs::typed::Script::cast(child) {
534 Some(Statement::Script(script.into()))
535 } else if let Some(menuname) = fea_rs::typed::SizeMenuName::cast(child) {
536 Some(Statement::SizeMenuName(menuname.into()))
537 } else if let Some(sizeparams) = fea_rs::typed::Parameters::cast(child) {
538 Some(Statement::SizeParameters(sizeparams.into()))
539 } else if let Some(featurenames) = fea_rs::typed::FeatureNames::cast(child) {
540 Some(Statement::NestedBlock(featurenames.into()))
541 } else if let Some(vrd) = fea_rs::typed::ValueRecordDef::cast(child) {
545 Some(Statement::ValueRecordDefinition(vrd.into()))
546 } else if let Some(cs) = fea_rs::typed::ConditionSet::cast(child) {
547 Some(Statement::ConditionSet(cs.into()))
548 } else if let Some(fv) = fea_rs::typed::FeatureVariation::cast(child) {
549 Some(Statement::VariationBlock(fv.into()))
550 } else if let Some(lookup) = fea_rs::typed::LookupBlock::cast(child) {
552 Some(Statement::LookupBlock(lookup.into()))
553 } else {
554 None
555 }
556}
557
558#[derive(Debug, Clone, PartialEq, Eq)]
560pub struct FeatureBlock {
561 pub name: SmolStr,
563 pub statements: Vec<Statement>,
565 pub use_extension: bool,
567 pub pos: Range<usize>,
569}
570
571impl FeatureBlock {
572 pub fn new(
574 name: SmolStr,
575 statements: Vec<Statement>,
576 use_extension: bool,
577 pos: Range<usize>,
578 ) -> Self {
579 Self {
580 name,
581 statements,
582 use_extension,
583 pos,
584 }
585 }
586}
587
588impl AsFea for FeatureBlock {
589 fn as_fea(&self, indent: &str) -> String {
590 let mut res = String::new();
591 res.push_str(&format!("{}feature {} {{\n", indent, self.name));
592 let mid_indent = indent.to_string() + SHIFT;
593 res.push_str(&format!(
594 "{}\n",
595 self.statements
596 .iter()
597 .map(|s| s.as_fea(&mid_indent))
598 .collect::<Vec<_>>()
599 .join(&format!("\n{mid_indent}"))
600 ));
601 res.push_str(&format!("{}}} {};", indent, self.name));
602 res
603 }
604}
605
606impl From<fea_rs::typed::Feature> for FeatureBlock {
607 fn from(val: fea_rs::typed::Feature) -> Self {
608 let statements: Vec<Statement> = val
609 .node()
610 .iter_children()
611 .filter_map(to_statement)
612 .collect();
613 FeatureBlock {
614 name: SmolStr::new(&val.tag().token().text),
615 use_extension: val.iter().any(|t| t.kind() == fea_rs::Kind::UseExtensionKw),
616 statements,
617 pos: val.node().range(),
618 }
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct LookupBlock {
625 pub name: SmolStr,
627 pub statements: Vec<Statement>,
629 pub use_extension: bool,
631 pub pos: Range<usize>,
633}
634
635impl LookupBlock {
636 pub fn new(
638 name: SmolStr,
639 statements: Vec<Statement>,
640 use_extension: bool,
641 pos: Range<usize>,
642 ) -> Self {
643 Self {
644 name,
645 statements,
646 use_extension,
647 pos,
648 }
649 }
650}
651
652impl AsFea for LookupBlock {
653 fn as_fea(&self, indent: &str) -> String {
654 let mut res = String::new();
655 res.push_str(&format!("{}lookup {} {{\n", indent, self.name));
656 let mid_indent = indent.to_string() + SHIFT;
657 res.push_str(&format!(
658 "{mid_indent}{}\n",
659 self.statements
660 .iter()
661 .map(|s| s.as_fea(&mid_indent))
662 .collect::<Vec<_>>()
663 .join(&format!("\n{mid_indent}"))
664 ));
665 res.push_str(&format!("{}}} {};", indent, self.name));
666 res
667 }
668}
669
670impl From<fea_rs::typed::LookupBlock> for LookupBlock {
671 fn from(val: fea_rs::typed::LookupBlock) -> Self {
672 let statements: Vec<Statement> = val
673 .node()
674 .iter_children()
675 .filter_map(to_statement)
676 .collect();
677 let label = val
678 .iter()
679 .find(|t| t.kind() == fea_rs::Kind::Label)
680 .unwrap();
681 LookupBlock {
682 name: SmolStr::from(label.as_token().unwrap().text.as_str()),
683 use_extension: val.iter().any(|t| t.kind() == fea_rs::Kind::UseExtensionKw),
684 statements,
685 pos: val.node().range(),
686 }
687 }
688}
689
690#[derive(Debug, Clone, PartialEq, Eq)]
692pub struct NestedBlock {
693 pub tag: SmolStr,
695 pub statements: Vec<Statement>,
697 pub pos: Range<usize>,
699}
700
701impl AsFea for NestedBlock {
702 fn as_fea(&self, indent: &str) -> String {
703 let mut res = String::new();
704 res.push_str(&format!("{}{} {{\n", indent, self.tag));
705 let mid_indent = indent.to_string() + SHIFT;
706 res.push_str(&format!(
707 "{mid_indent}{}\n",
708 self.statements
709 .iter()
710 .map(|s| s.as_fea(&mid_indent))
711 .collect::<Vec<_>>()
712 .join(&format!("\n{mid_indent}"))
713 ));
714 res.push_str(&format!("{}}};\n", indent));
715 res
716 }
717}
718
719impl From<fea_rs::typed::FeatureNames> for NestedBlock {
720 fn from(val: fea_rs::typed::FeatureNames) -> Self {
721 #[allow(clippy::manual_map)]
722 let statements: Vec<Statement> = val
723 .node()
724 .iter_children()
725 .filter_map(|child| {
726 if child.kind() == fea_rs::Kind::Comment {
728 return Some(Statement::Comment(Comment::from(
729 child.token_text().unwrap(),
730 )));
731 }
732 if let Some(name_spec) = fea_rs::typed::NameSpec::cast(child) {
733 let (platform_id, plat_enc_id, lang_id, string) = parse_namespec(name_spec);
734 Some(Statement::FeatureNameStatement(NameRecord {
735 platform_id,
736 plat_enc_id,
737 lang_id,
738 string,
739 kind: NameRecordKind::FeatureName,
740 location: child.range(),
741 }))
742 } else {
743 None
744 }
745 })
746 .collect();
747 NestedBlock {
748 tag: SmolStr::new("featureNames"),
749 statements,
750 pos: val.node().range(),
751 }
752 }
753}
754
755#[derive(Debug, Clone, PartialEq, Eq)]
761pub enum ToplevelItem {
762 GlyphClassDefinition(GlyphClassDefinition),
764 MarkClassDefinition(MarkClassDefinition),
766 LanguageSystem(LanguageSystemStatement),
768 Feature(FeatureBlock),
771 Lookup(LookupBlock),
773 Comment(Comment),
775 AnchorDefinition(AnchorDefinition),
777 ValueRecordDefinition(ValueRecordDefinition),
779 ConditionSet(ConditionSet),
781 VariationBlock(VariationBlock),
783 Base(Table<Base>),
786 Gdef(Table<Gdef>),
788 Head(Table<Head>),
790 Hhea(Table<Hhea>),
792 Name(Table<Name>),
794 Os2(Table<Os2>),
796 Stat(Table<Stat>),
798 Vhea(Table<Vhea>),
800}
801impl From<ToplevelItem> for Statement {
802 fn from(val: ToplevelItem) -> Self {
803 match val {
804 ToplevelItem::GlyphClassDefinition(gcd) => Statement::GlyphClassDefinition(gcd),
805 ToplevelItem::MarkClassDefinition(gcd) => Statement::MarkClassDefinition(gcd),
806
807 ToplevelItem::LanguageSystem(ls) => Statement::LanguageSystem(ls),
808 ToplevelItem::Feature(fb) => Statement::FeatureBlock(fb),
809 ToplevelItem::Lookup(lb) => Statement::LookupBlock(lb),
810 ToplevelItem::Comment(cmt) => Statement::Comment(cmt),
811 ToplevelItem::AnchorDefinition(ad) => Statement::AnchorDefinition(ad),
812 ToplevelItem::ValueRecordDefinition(vrd) => Statement::ValueRecordDefinition(vrd),
813 ToplevelItem::ConditionSet(cs) => Statement::ConditionSet(cs),
814 ToplevelItem::VariationBlock(vb) => Statement::VariationBlock(vb),
815 ToplevelItem::Base(base) => Statement::Base(base),
816 ToplevelItem::Gdef(gdef) => Statement::Gdef(gdef),
817 ToplevelItem::Head(head) => Statement::Head(head),
818 ToplevelItem::Hhea(hhea) => Statement::Hhea(hhea),
819 ToplevelItem::Name(name) => Statement::Name(name),
820 ToplevelItem::Os2(os2) => Statement::Os2(os2),
821 ToplevelItem::Stat(stat) => Statement::Stat(stat),
822 ToplevelItem::Vhea(vhea) => Statement::Vhea(vhea),
823 }
824 }
825}
826impl TryFrom<Statement> for ToplevelItem {
827 type Error = crate::Error;
828
829 fn try_from(value: Statement) -> Result<Self, Self::Error> {
830 match value {
831 Statement::GlyphClassDefinition(gcd) => Ok(ToplevelItem::GlyphClassDefinition(gcd)),
832 Statement::MarkClassDefinition(mcd) => Ok(ToplevelItem::MarkClassDefinition(mcd)),
833 Statement::LanguageSystem(ls) => Ok(ToplevelItem::LanguageSystem(ls)),
834 Statement::FeatureBlock(fb) => Ok(ToplevelItem::Feature(fb)),
835 Statement::LookupBlock(lb) => Ok(ToplevelItem::Lookup(lb)),
836 Statement::Comment(cmt) => Ok(ToplevelItem::Comment(cmt)),
837 Statement::AnchorDefinition(ad) => Ok(ToplevelItem::AnchorDefinition(ad)),
838 Statement::ValueRecordDefinition(vrd) => Ok(ToplevelItem::ValueRecordDefinition(vrd)),
839 Statement::ConditionSet(cs) => Ok(ToplevelItem::ConditionSet(cs)),
840 Statement::VariationBlock(vb) => Ok(ToplevelItem::VariationBlock(vb)),
841 Statement::Base(base) => Ok(ToplevelItem::Base(base)),
842 Statement::Gdef(gdef) => Ok(ToplevelItem::Gdef(gdef)),
843 Statement::Head(head) => Ok(ToplevelItem::Head(head)),
844 Statement::Hhea(hhea) => Ok(ToplevelItem::Hhea(hhea)),
845 Statement::Name(name) => Ok(ToplevelItem::Name(name)),
846 Statement::Os2(os2) => Ok(ToplevelItem::Os2(os2)),
847 Statement::Stat(stat) => Ok(ToplevelItem::Stat(stat)),
848 Statement::Vhea(vhea) => Ok(ToplevelItem::Vhea(vhea)),
849 _ => Err(crate::Error::CannotConvert),
850 }
851 }
852}
853
854impl AsFea for ToplevelItem {
855 fn as_fea(&self, indent: &str) -> String {
856 match self {
857 ToplevelItem::GlyphClassDefinition(gcd) => gcd.as_fea(indent),
858 ToplevelItem::MarkClassDefinition(mcd) => mcd.as_fea(indent),
859 ToplevelItem::LanguageSystem(ls) => ls.as_fea(indent),
860 ToplevelItem::Feature(fb) => fb.as_fea(indent),
861 ToplevelItem::Lookup(lb) => lb.as_fea(indent),
862 ToplevelItem::Comment(cmt) => cmt.as_fea(indent),
863 ToplevelItem::AnchorDefinition(ad) => ad.as_fea(indent),
864 ToplevelItem::ValueRecordDefinition(vrd) => vrd.as_fea(indent),
865 ToplevelItem::ConditionSet(cs) => cs.as_fea(indent),
866 ToplevelItem::VariationBlock(vb) => vb.as_fea(indent),
867 ToplevelItem::Base(base) => base.as_fea(indent),
868 ToplevelItem::Gdef(gdef) => gdef.as_fea(indent),
869 ToplevelItem::Head(head) => head.as_fea(indent),
870 ToplevelItem::Hhea(hhea) => hhea.as_fea(indent),
871 ToplevelItem::Name(name) => name.as_fea(indent),
872 ToplevelItem::Os2(os2) => os2.as_fea(indent),
873 ToplevelItem::Stat(stat) => stat.as_fea(indent),
874 ToplevelItem::Vhea(vhea) => vhea.as_fea(indent),
875 }
876 }
877}
878#[allow(clippy::manual_map)]
879fn to_toplevel_item(child: &NodeOrToken) -> Option<ToplevelItem> {
880 if child.kind() == fea_rs::Kind::Comment {
881 Some(ToplevelItem::Comment(Comment::from(
882 child.token_text().unwrap(),
883 )))
884 } else if let Some(gcd) = fea_rs::typed::GlyphClassDef::cast(child) {
885 Some(ToplevelItem::GlyphClassDefinition(gcd.into()))
886 } else if let Some(mcd) = fea_rs::typed::MarkClassDef::cast(child) {
887 Some(ToplevelItem::MarkClassDefinition(mcd.into()))
888 } else if let Some(langsys) = fea_rs::typed::LanguageSystem::cast(child) {
889 Some(ToplevelItem::LanguageSystem(langsys.into()))
890 } else if let Some(feature) = fea_rs::typed::Feature::cast(child) {
891 Some(ToplevelItem::Feature(feature.into()))
892 } else if let Some(lookup) = fea_rs::typed::LookupBlock::cast(child) {
893 Some(ToplevelItem::Lookup(lookup.into()))
894 } else if let Some(ad) = fea_rs::typed::AnchorDef::cast(child) {
895 Some(ToplevelItem::AnchorDefinition(ad.into()))
896 } else if let Some(vrd) = fea_rs::typed::ValueRecordDef::cast(child) {
897 Some(ToplevelItem::ValueRecordDefinition(vrd.into()))
898 } else if let Some(cs) = fea_rs::typed::ConditionSet::cast(child) {
899 Some(ToplevelItem::ConditionSet(cs.into()))
900 } else if let Some(fv) = fea_rs::typed::FeatureVariation::cast(child) {
901 Some(ToplevelItem::VariationBlock(fv.into()))
902 } else if let Some(base) = fea_rs::typed::BaseTable::cast(child) {
903 Some(ToplevelItem::Base(base.into()))
904 } else if let Some(gdef) = fea_rs::typed::GdefTable::cast(child) {
905 Some(ToplevelItem::Gdef(gdef.into()))
906 } else if let Some(head) = fea_rs::typed::HeadTable::cast(child) {
907 Some(ToplevelItem::Head(head.into()))
908 } else if let Some(hhea) = fea_rs::typed::HheaTable::cast(child) {
909 Some(ToplevelItem::Hhea(hhea.into()))
910 } else if let Some(vhea) = fea_rs::typed::VheaTable::cast(child) {
911 Some(ToplevelItem::Vhea(vhea.into()))
912 } else if let Some(name) = fea_rs::typed::NameTable::cast(child) {
913 Some(ToplevelItem::Name(name.into()))
914 } else if let Some(os2) = fea_rs::typed::Os2Table::cast(child) {
915 Some(ToplevelItem::Os2(os2.into()))
916 } else if let Some(stat) = fea_rs::typed::StatTable::cast(child) {
917 Some(ToplevelItem::Stat(stat.into()))
918 } else {
919 None
920 }
921}
922
923pub struct FeatureFile {
938 pub statements: Vec<ToplevelItem>,
940}
941impl FeatureFile {
942 pub fn new(statements: Vec<ToplevelItem>) -> Self {
944 Self { statements }
945 }
946
947 pub fn iter(&self) -> impl Iterator<Item = &ToplevelItem> {
949 self.statements.iter()
950 }
951
952 pub fn new_from_fea(
973 features: &str,
974 glyph_names: Option<&[&str]>,
975 project_root: Option<impl Into<PathBuf>>,
976 ) -> Result<Self, crate::Error> {
977 let glyph_map = glyph_names.map(|gn| GlyphMap::from_iter(gn.iter().cloned()));
978 let resolver: Box<dyn fea_rs::parse::SourceResolver> =
979 if let Some(project_root) = project_root {
980 let path = project_root.into();
981 Box::new(FileSystemResolver::new(path))
982 } else {
983 Box::new(dummyresolver::DummyResolver)
984 };
985 let features_text: Arc<str> = Arc::from(features);
986 let (parse_tree, mut diagnostics) = fea_rs::parse::parse_root(
987 "get_parse_tree".into(),
988 glyph_map.as_ref(),
989 Box::new(move |s: &Path| {
990 if s == Path::new("get_parse_tree") {
991 Ok(features_text.clone())
992 } else {
993 let path = resolver.resolve_raw_path(s.as_ref(), None);
994 let canonical = resolver.canonicalize(&path)?;
995 resolver.get_contents(&canonical)
996 }
997 }),
998 )?;
999 diagnostics.split_off_warnings();
1000 if diagnostics.has_errors() {
1001 return Err(crate::Error::FeatureParsing(diagnostics));
1002 }
1003 Ok(parse_tree.into())
1004 }
1005}
1006impl AsFea for FeatureFile {
1007 fn as_fea(&self, indent: &str) -> String {
1008 let mut res = String::new();
1009 for stmt in &self.statements {
1010 res.push_str(&stmt.as_fea(indent));
1011 res.push('\n');
1012 }
1013 res
1014 }
1015}
1016impl From<ParseTree> for FeatureFile {
1017 fn from(val: ParseTree) -> Self {
1018 let statements: Vec<ToplevelItem> = val
1019 .root()
1020 .iter_children()
1021 .filter_map(to_toplevel_item)
1022 .collect();
1023 FeatureFile { statements }
1024 }
1025}
1026
1027impl TryFrom<&str> for FeatureFile {
1032 type Error = fea_rs::DiagnosticSet;
1033
1034 fn try_from(value: &str) -> Result<Self, Self::Error> {
1035 let (parsed, diag) = fea_rs::parse::parse_string(value);
1036 if diag.has_errors() {
1037 Err(diag)
1038 } else {
1039 Ok(parsed.into())
1040 }
1041 }
1042}
1043#[cfg(test)]
1044mod tests {
1045 use rstest::rstest;
1046
1047 use super::*;
1048
1049 #[test]
1050 fn test_parse() {
1051 const FEA: &str = r#"feature smcp {
1052 sub a by a.smcp;
1053 } smcp;
1054 "#;
1055 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1056 let feature_block = parsed.root().iter_children().next().unwrap();
1057
1058 let Some(feature) = fea_rs::typed::Feature::cast(feature_block) else {
1059 panic!("Expected Feature, got {:?}", feature_block.kind());
1060 };
1061 let feature_block: FeatureBlock = feature.into();
1062 assert_eq!(feature_block.name.as_str(), "smcp");
1063 assert_eq!(feature_block.statements.len(), 1);
1064 assert_eq!(
1065 normalize_whitespace(&feature_block.as_fea("")),
1066 normalize_whitespace("feature smcp {\n sub a by a.smcp;\n} smcp;\n")
1067 );
1068 }
1069
1070 fn normalize_whitespace(s: &str) -> String {
1071 s.replace("#", "\n#")
1072 .replace("\n\n", "\n")
1073 .lines()
1074 .filter(|l| !l.trim().is_empty())
1075 .map(|l| l.trim())
1076 .collect::<Vec<_>>()
1077 .join("\n")
1078 .replace("\t", " ")
1079 .replace("position ", "pos ")
1080 .replace("substitute ", "sub ")
1081 .replace("reversesub ", "rsub ")
1082 }
1083
1084 #[rstest]
1085 fn for_each_file(
1086 #[files("resources/test/*.fea")]
1087 #[exclude("ChainPosSubtable_fea")] #[exclude("AlternateChained.fea")] #[exclude("baseClass.fea")] #[exclude("STAT_bad.fea")] #[exclude("include0.fea")] #[exclude("GSUB_error.fea")] #[exclude("spec10.fea")] path: std::path::PathBuf,
1095 ) {
1096 let fea_str = std::fs::read_to_string(&path).unwrap();
1097 let (parsed, diag) = fea_rs::parse::parse_string(fea_str.clone());
1098 if diag.has_errors() {
1099 panic!("fea-rs didn't like file {:?}:\n{:#?}", path, diag);
1100 }
1101 let feature_file: FeatureFile = parsed.into();
1102 let fea_output = feature_file.as_fea("");
1103 let orig = normalize_whitespace(&fea_str);
1104 let output = normalize_whitespace(&fea_output);
1105 let mut orig_lines = orig.lines().collect::<Vec<_>>();
1106 for i in 0..orig_lines.len() {
1107 if let Some(replacement) = orig_lines[i].strip_prefix("#test-fea2fea: ") {
1108 orig_lines[i + 1] = replacement;
1109 }
1110 }
1111 let orig = orig_lines.join("\n");
1112 pretty_assertions::assert_eq!(orig, output, "Mismatch in file {:?}", path);
1113 }
1114}