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::{parse::FileSystemResolver, typed::AstNode as _, GlyphMap, NodeOrToken, ParseTree};
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
282#[cfg(feature = "serde")]
284pub(crate) fn default_range() -> Range<usize> {
285 0..0
286}
287
288#[cfg(feature = "serde")]
290pub(crate) fn is_default_range(r: &Range<usize>) -> bool {
291 r.start == 0 && r.end == 0
292}
293
294pub trait AsFea {
296 fn as_fea(&self, indent: &str) -> String;
298}
299
300#[allow(clippy::large_enum_variant)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub enum Statement {
309 SingleSubst(SingleSubstStatement),
312 MultipleSubst(MultipleSubstStatement),
314 AlternateSubst(AlternateSubstStatement),
316 LigatureSubst(LigatureSubstStatement),
318 ReverseChainSubst(ReverseChainSingleSubstStatement),
320 ChainedContextSubst(ChainedContextStatement<Subst>),
322 IgnoreSubst(IgnoreStatement<Subst>),
324 SinglePos(SinglePosStatement),
327 PairPos(PairPosStatement),
329 CursivePos(CursivePosStatement),
331 MarkBasePos(MarkBasePosStatement),
333 MarkLigPos(MarkLigPosStatement),
335 MarkMarkPos(MarkMarkPosStatement),
337 ChainedContextPos(ChainedContextStatement<Pos>),
339 IgnorePos(IgnoreStatement<Pos>),
341 AnchorDefinition(AnchorDefinition),
344 MarkClassDefinition(MarkClassDefinition),
346 Comment(Comment),
348 FeatureNameStatement(NameRecord),
350 FontRevision(FontRevisionStatement),
352 FeatureReference(FeatureReferenceStatement),
354 GlyphClassDefinition(GlyphClassDefinition),
356 Language(LanguageStatement),
358 LanguageSystem(LanguageSystemStatement),
360 LookupFlag(LookupFlagStatement),
362 LookupReference(LookupReferenceStatement),
364 SizeParameters(SizeParameters),
366 SizeMenuName(NameRecord),
368 Subtable(SubtableStatement),
370 Script(ScriptStatement),
372 ValueRecordDefinition(ValueRecordDefinition),
374 ConditionSet(ConditionSet),
376 VariationBlock(VariationBlock),
378 Base(Table<Base>),
381 Gdef(Table<Gdef>),
383 Head(Table<Head>),
385 Hhea(Table<Hhea>),
387 Name(Table<Name>),
389 Os2(Table<Os2>),
391 Stat(Table<Stat>),
393 Vhea(Table<Vhea>),
395 FeatureBlock(FeatureBlock),
397 LookupBlock(LookupBlock),
399 NestedBlock(NestedBlock),
401 GdefAttach(AttachStatement),
404 GdefClassDef(GlyphClassDefStatement),
406 GdefLigatureCaretByIndex(LigatureCaretByIndexStatement),
408 GdefLigatureCaretByPos(LigatureCaretByPosStatement),
410}
411impl AsFea for Statement {
412 fn as_fea(&self, indent: &str) -> String {
413 match self {
414 Statement::SingleSubst(ss) => ss.as_fea(indent),
416 Statement::MultipleSubst(ms) => ms.as_fea(indent),
417 Statement::AlternateSubst(alt) => alt.as_fea(indent),
418 Statement::LigatureSubst(ls) => ls.as_fea(indent),
419 Statement::ChainedContextSubst(ccs) => ccs.as_fea(indent),
420 Statement::IgnoreSubst(is) => is.as_fea(indent),
421 Statement::ReverseChainSubst(rss) => rss.as_fea(indent),
422 Statement::SinglePos(sp) => sp.as_fea(indent),
424 Statement::PairPos(pp) => pp.as_fea(indent),
425 Statement::CursivePos(cp) => cp.as_fea(indent),
426 Statement::MarkBasePos(mbp) => mbp.as_fea(indent),
427 Statement::MarkLigPos(mlp) => mlp.as_fea(indent),
428 Statement::MarkMarkPos(mmp) => mmp.as_fea(indent),
429 Statement::ChainedContextPos(ccs) => ccs.as_fea(indent),
430 Statement::IgnorePos(ip) => ip.as_fea(indent),
431 Statement::AnchorDefinition(ad) => ad.as_fea(indent),
433 Statement::Comment(c) => c.as_fea(indent),
434 Statement::FeatureReference(fr) => fr.as_fea(indent),
435 Statement::FeatureNameStatement(fr) => fr.as_fea(indent),
436 Statement::FontRevision(fr) => fr.as_fea(indent),
437 Statement::GlyphClassDefinition(gcd) => gcd.as_fea(indent),
438 Statement::Language(ls) => ls.as_fea(indent),
439 Statement::LanguageSystem(ls) => ls.as_fea(indent),
440 Statement::LookupFlag(lf) => lf.as_fea(indent),
441 Statement::LookupReference(lr) => lr.as_fea(indent),
442 Statement::MarkClassDefinition(mc) => mc.as_fea(indent),
443 Statement::Script(sc) => sc.as_fea(indent),
444 Statement::SizeMenuName(sm) => sm.as_fea(indent),
445 Statement::SizeParameters(sp) => sp.as_fea(indent),
446 Statement::Subtable(st) => st.as_fea(indent),
447 Statement::ValueRecordDefinition(vrd) => vrd.as_fea(indent),
448 Statement::ConditionSet(cs) => cs.as_fea(indent),
449 Statement::VariationBlock(vb) => vb.as_fea(indent),
450 Statement::GdefAttach(at) => at.as_fea(indent),
452 Statement::GdefClassDef(gcd) => gcd.as_fea(indent),
453 Statement::GdefLigatureCaretByIndex(lc) => lc.as_fea(indent),
454 Statement::GdefLigatureCaretByPos(lc) => lc.as_fea(indent),
455 Statement::Base(base) => base.as_fea(indent),
457 Statement::Gdef(gdef) => gdef.as_fea(indent),
458 Statement::Head(head) => head.as_fea(indent),
459 Statement::Hhea(hhea) => hhea.as_fea(indent),
460 Statement::Name(name) => name.as_fea(indent),
461 Statement::Os2(os2) => os2.as_fea(indent),
462 Statement::Stat(stat) => stat.as_fea(indent),
463 Statement::Vhea(vhea) => vhea.as_fea(indent),
464 Statement::FeatureBlock(fb) => fb.as_fea(indent),
465 Statement::LookupBlock(lb) => lb.as_fea(indent),
466 Statement::NestedBlock(nb) => nb.as_fea(indent),
467 }
468 }
469}
470
471fn to_statement(child: &NodeOrToken) -> Option<Statement> {
472 if child.kind() == fea_rs::Kind::Comment {
473 return Some(Statement::Comment(Comment::from(
474 child.token_text().unwrap(),
475 )));
476 } else if child.kind() == fea_rs::Kind::SubtableNode {
477 return Some(Statement::Subtable(SubtableStatement::new()));
478 }
479 #[allow(clippy::manual_map)]
480 if let Some(gsub1) = fea_rs::typed::Gsub1::cast(child) {
482 Some(Statement::SingleSubst(gsub1.into()))
483 } else if let Some(gsub2) = fea_rs::typed::Gsub2::cast(child) {
484 Some(Statement::MultipleSubst(gsub2.into()))
485 } else if let Some(gsub3) = fea_rs::typed::Gsub3::cast(child) {
486 Some(Statement::AlternateSubst(gsub3.into()))
487 } else if let Some(gsub4) = fea_rs::typed::Gsub4::cast(child) {
488 Some(Statement::LigatureSubst(gsub4.into()))
489 } else if let Some(gsub6) = fea_rs::typed::Gsub6::cast(child) {
490 Some(gsub6.into())
491 } else if let Some(rss) = fea_rs::typed::Gsub8::cast(child) {
492 Some(Statement::ReverseChainSubst(rss.into()))
493 } else if let Some(gsig) = fea_rs::typed::GsubIgnore::cast(child) {
494 Some(Statement::IgnoreSubst(gsig.into()))
495 } else if let Some(gpos1) = fea_rs::typed::Gpos1::cast(child) {
497 Some(Statement::SinglePos(gpos1.into()))
498 } else if let Some(gpos2) = fea_rs::typed::Gpos2::cast(child) {
499 Some(Statement::PairPos(gpos2.into()))
500 } else if let Some(gpos3) = fea_rs::typed::Gpos3::cast(child) {
501 Some(Statement::CursivePos(gpos3.into()))
502 } else if let Some(gpos4) = fea_rs::typed::Gpos4::cast(child) {
503 Some(Statement::MarkBasePos(gpos4.into()))
504 } else if let Some(gpos5) = fea_rs::typed::Gpos5::cast(child) {
505 Some(Statement::MarkLigPos(gpos5.into()))
506 } else if let Some(gpos6) = fea_rs::typed::Gpos6::cast(child) {
507 Some(Statement::MarkMarkPos(gpos6.into()))
508 } else if let Some(gpos8) = fea_rs::typed::Gpos8::cast(child) {
509 Some(gpos8.into())
510 } else if let Some(gpig) = fea_rs::typed::GposIgnore::cast(child) {
511 Some(Statement::IgnorePos(gpig.into()))
512 } else if let Some(ad) = fea_rs::typed::AnchorDef::cast(child) {
514 Some(Statement::AnchorDefinition(ad.into()))
515 } else if let Some(at) = fea_rs::typed::GdefAttach::cast(child) {
516 Some(Statement::GdefAttach(at.into()))
517 } else if let Some(gcd) = fea_rs::typed::GdefClassDef::cast(child) {
518 Some(Statement::GdefClassDef(gcd.into()))
519 } else if let Some(lc) = fea_rs::typed::GdefLigatureCaret::cast(child) {
520 let is_by_pos = lc
522 .iter()
523 .next()
524 .map(|t| t.kind() == fea_rs::Kind::LigatureCaretByPosKw)
525 .unwrap_or(false);
526 if is_by_pos {
527 Some(Statement::GdefLigatureCaretByPos(lc.into()))
528 } else {
529 Some(Statement::GdefLigatureCaretByIndex(lc.into()))
530 }
531 } else if let Some(fr) = fea_rs::typed::FeatureRef::cast(child) {
532 Some(Statement::FeatureReference(fr.into()))
533 } else if let Some(fr) = fea_rs::typed::HeadFontRevision::cast(child) {
534 Some(Statement::FontRevision(fr.into()))
535 } else if let Some(gcd) = fea_rs::typed::GlyphClassDef::cast(child) {
536 Some(Statement::GlyphClassDefinition(gcd.into()))
537 } else if let Some(lang) = fea_rs::typed::Language::cast(child) {
538 Some(Statement::Language(lang.into()))
539 } else if let Some(langsys) = fea_rs::typed::LanguageSystem::cast(child) {
540 Some(Statement::LanguageSystem(langsys.into()))
541 } else if let Some(lookupflag) = fea_rs::typed::LookupFlag::cast(child) {
542 Some(Statement::LookupFlag(lookupflag.into()))
543 } else if let Some(lookupref) = fea_rs::typed::LookupRef::cast(child) {
544 Some(Statement::LookupReference(lookupref.into()))
545 } else if let Some(mcd) = fea_rs::typed::MarkClassDef::cast(child) {
546 Some(Statement::MarkClassDefinition(mcd.into()))
547 } else if let Some(script) = fea_rs::typed::Script::cast(child) {
548 Some(Statement::Script(script.into()))
549 } else if let Some(menuname) = fea_rs::typed::SizeMenuName::cast(child) {
550 Some(Statement::SizeMenuName(menuname.into()))
551 } else if let Some(sizeparams) = fea_rs::typed::Parameters::cast(child) {
552 Some(Statement::SizeParameters(sizeparams.into()))
553 } else if let Some(featurenames) = fea_rs::typed::FeatureNames::cast(child) {
554 Some(Statement::NestedBlock(featurenames.into()))
555 } else if let Some(vrd) = fea_rs::typed::ValueRecordDef::cast(child) {
559 Some(Statement::ValueRecordDefinition(vrd.into()))
560 } else if let Some(cs) = fea_rs::typed::ConditionSet::cast(child) {
561 Some(Statement::ConditionSet(cs.into()))
562 } else if let Some(fv) = fea_rs::typed::FeatureVariation::cast(child) {
563 Some(Statement::VariationBlock(fv.into()))
564 } else if let Some(lookup) = fea_rs::typed::LookupBlock::cast(child) {
566 Some(Statement::LookupBlock(lookup.into()))
567 } else {
568 None
569 }
570}
571
572#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
574#[derive(Debug, Clone, PartialEq, Eq)]
575pub struct FeatureBlock {
576 pub name: SmolStr,
578 pub statements: Vec<Statement>,
580 pub use_extension: bool,
582 #[cfg_attr(
584 feature = "serde",
585 serde(
586 default = "crate::default_range",
587 skip_serializing_if = "crate::is_default_range"
588 )
589 )]
590 pub pos: Range<usize>,
591}
592
593impl FeatureBlock {
594 pub fn new(
596 name: SmolStr,
597 statements: Vec<Statement>,
598 use_extension: bool,
599 pos: Range<usize>,
600 ) -> Self {
601 Self {
602 name,
603 statements,
604 use_extension,
605 pos,
606 }
607 }
608}
609
610impl AsFea for FeatureBlock {
611 fn as_fea(&self, indent: &str) -> String {
612 let mut res = String::new();
613 res.push_str(&format!("{}feature {} {{\n", indent, self.name));
614 let mid_indent = indent.to_string() + SHIFT;
615 res.push_str(&format!(
616 "{}\n",
617 self.statements
618 .iter()
619 .map(|s| s.as_fea(&mid_indent))
620 .collect::<Vec<_>>()
621 .join(&format!("\n{mid_indent}"))
622 ));
623 res.push_str(&format!("{}}} {};", indent, self.name));
624 res
625 }
626}
627
628impl From<fea_rs::typed::Feature> for FeatureBlock {
629 fn from(val: fea_rs::typed::Feature) -> Self {
630 let statements: Vec<Statement> = val
631 .node()
632 .iter_children()
633 .filter_map(to_statement)
634 .collect();
635 FeatureBlock {
636 name: SmolStr::new(&val.tag().token().text),
637 use_extension: val.iter().any(|t| t.kind() == fea_rs::Kind::UseExtensionKw),
638 statements,
639 pos: val.node().range(),
640 }
641 }
642}
643
644#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
646#[derive(Debug, Clone, PartialEq, Eq)]
647pub struct LookupBlock {
648 pub name: SmolStr,
650 pub statements: Vec<Statement>,
652 pub use_extension: bool,
654 #[cfg_attr(
656 feature = "serde",
657 serde(
658 default = "crate::default_range",
659 skip_serializing_if = "crate::is_default_range"
660 )
661 )]
662 pub pos: Range<usize>,
663}
664
665impl LookupBlock {
666 pub fn new(
668 name: SmolStr,
669 statements: Vec<Statement>,
670 use_extension: bool,
671 pos: Range<usize>,
672 ) -> Self {
673 Self {
674 name,
675 statements,
676 use_extension,
677 pos,
678 }
679 }
680}
681
682impl AsFea for LookupBlock {
683 fn as_fea(&self, indent: &str) -> String {
684 let mut res = String::new();
685 res.push_str(&format!("{}lookup {} {{\n", indent, self.name));
686 let mid_indent = indent.to_string() + SHIFT;
687 res.push_str(&format!(
688 "{mid_indent}{}\n",
689 self.statements
690 .iter()
691 .map(|s| s.as_fea(&mid_indent))
692 .collect::<Vec<_>>()
693 .join(&format!("\n{mid_indent}"))
694 ));
695 res.push_str(&format!("{}}} {};", indent, self.name));
696 res
697 }
698}
699
700impl From<fea_rs::typed::LookupBlock> for LookupBlock {
701 fn from(val: fea_rs::typed::LookupBlock) -> Self {
702 let statements: Vec<Statement> = val
703 .node()
704 .iter_children()
705 .filter_map(to_statement)
706 .collect();
707 let label = val
708 .iter()
709 .find(|t| t.kind() == fea_rs::Kind::Label)
710 .unwrap();
711 LookupBlock {
712 name: SmolStr::from(label.as_token().unwrap().text.as_str()),
713 use_extension: val.iter().any(|t| t.kind() == fea_rs::Kind::UseExtensionKw),
714 statements,
715 pos: val.node().range(),
716 }
717 }
718}
719
720#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
722#[derive(Debug, Clone, PartialEq, Eq)]
723pub struct NestedBlock {
724 pub tag: SmolStr,
726 pub statements: Vec<Statement>,
728 #[cfg_attr(
730 feature = "serde",
731 serde(
732 default = "crate::default_range",
733 skip_serializing_if = "crate::is_default_range"
734 )
735 )]
736 pub pos: Range<usize>,
737}
738
739impl AsFea for NestedBlock {
740 fn as_fea(&self, indent: &str) -> String {
741 let mut res = String::new();
742 res.push_str(&format!("{}{} {{\n", indent, self.tag));
743 let mid_indent = indent.to_string() + SHIFT;
744 res.push_str(&format!(
745 "{mid_indent}{}\n",
746 self.statements
747 .iter()
748 .map(|s| s.as_fea(&mid_indent))
749 .collect::<Vec<_>>()
750 .join(&format!("\n{mid_indent}"))
751 ));
752 res.push_str(&format!("{}}};\n", indent));
753 res
754 }
755}
756
757impl From<fea_rs::typed::FeatureNames> for NestedBlock {
758 fn from(val: fea_rs::typed::FeatureNames) -> Self {
759 #[allow(clippy::manual_map)]
760 let statements: Vec<Statement> = val
761 .node()
762 .iter_children()
763 .filter_map(|child| {
764 if child.kind() == fea_rs::Kind::Comment {
766 return Some(Statement::Comment(Comment::from(
767 child.token_text().unwrap(),
768 )));
769 }
770 if let Some(name_spec) = fea_rs::typed::NameSpec::cast(child) {
771 let (platform_id, plat_enc_id, lang_id, string) = parse_namespec(name_spec);
772 Some(Statement::FeatureNameStatement(NameRecord {
773 platform_id,
774 plat_enc_id,
775 lang_id,
776 string,
777 kind: NameRecordKind::FeatureName,
778 location: child.range(),
779 }))
780 } else {
781 None
782 }
783 })
784 .collect();
785 NestedBlock {
786 tag: SmolStr::new("featureNames"),
787 statements,
788 pos: val.node().range(),
789 }
790 }
791}
792
793#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
799#[derive(Debug, Clone, PartialEq, Eq)]
800pub enum ToplevelItem {
801 GlyphClassDefinition(GlyphClassDefinition),
803 MarkClassDefinition(MarkClassDefinition),
805 LanguageSystem(LanguageSystemStatement),
807 Feature(FeatureBlock),
810 Lookup(LookupBlock),
812 Comment(Comment),
814 AnchorDefinition(AnchorDefinition),
816 ValueRecordDefinition(ValueRecordDefinition),
818 ConditionSet(ConditionSet),
820 VariationBlock(VariationBlock),
822 GdefClassDef(GlyphClassDefStatement),
824 Base(Table<Base>),
827 Gdef(Table<Gdef>),
829 Head(Table<Head>),
831 Hhea(Table<Hhea>),
833 Name(Table<Name>),
835 Os2(Table<Os2>),
837 Stat(Table<Stat>),
839 Vhea(Table<Vhea>),
841}
842impl From<ToplevelItem> for Statement {
843 fn from(val: ToplevelItem) -> Self {
844 match val {
845 ToplevelItem::GlyphClassDefinition(gcd) => Statement::GlyphClassDefinition(gcd),
846 ToplevelItem::MarkClassDefinition(gcd) => Statement::MarkClassDefinition(gcd),
847 ToplevelItem::GdefClassDef(gcds) => Statement::GdefClassDef(gcds),
848
849 ToplevelItem::LanguageSystem(ls) => Statement::LanguageSystem(ls),
850 ToplevelItem::Feature(fb) => Statement::FeatureBlock(fb),
851 ToplevelItem::Lookup(lb) => Statement::LookupBlock(lb),
852 ToplevelItem::Comment(cmt) => Statement::Comment(cmt),
853 ToplevelItem::AnchorDefinition(ad) => Statement::AnchorDefinition(ad),
854 ToplevelItem::ValueRecordDefinition(vrd) => Statement::ValueRecordDefinition(vrd),
855 ToplevelItem::ConditionSet(cs) => Statement::ConditionSet(cs),
856 ToplevelItem::VariationBlock(vb) => Statement::VariationBlock(vb),
857 ToplevelItem::Base(base) => Statement::Base(base),
858 ToplevelItem::Gdef(gdef) => Statement::Gdef(gdef),
859 ToplevelItem::Head(head) => Statement::Head(head),
860 ToplevelItem::Hhea(hhea) => Statement::Hhea(hhea),
861 ToplevelItem::Name(name) => Statement::Name(name),
862 ToplevelItem::Os2(os2) => Statement::Os2(os2),
863 ToplevelItem::Stat(stat) => Statement::Stat(stat),
864 ToplevelItem::Vhea(vhea) => Statement::Vhea(vhea),
865 }
866 }
867}
868impl TryFrom<Statement> for ToplevelItem {
869 type Error = crate::Error;
870
871 fn try_from(value: Statement) -> Result<Self, Self::Error> {
872 match value {
873 Statement::GlyphClassDefinition(gcd) => Ok(ToplevelItem::GlyphClassDefinition(gcd)),
874 Statement::MarkClassDefinition(mcd) => Ok(ToplevelItem::MarkClassDefinition(mcd)),
875 Statement::GdefClassDef(gcds) => Ok(ToplevelItem::GdefClassDef(gcds)),
876 Statement::LanguageSystem(ls) => Ok(ToplevelItem::LanguageSystem(ls)),
877 Statement::FeatureBlock(fb) => Ok(ToplevelItem::Feature(fb)),
878 Statement::LookupBlock(lb) => Ok(ToplevelItem::Lookup(lb)),
879 Statement::Comment(cmt) => Ok(ToplevelItem::Comment(cmt)),
880 Statement::AnchorDefinition(ad) => Ok(ToplevelItem::AnchorDefinition(ad)),
881 Statement::ValueRecordDefinition(vrd) => Ok(ToplevelItem::ValueRecordDefinition(vrd)),
882 Statement::ConditionSet(cs) => Ok(ToplevelItem::ConditionSet(cs)),
883 Statement::VariationBlock(vb) => Ok(ToplevelItem::VariationBlock(vb)),
884 Statement::Base(base) => Ok(ToplevelItem::Base(base)),
885 Statement::Gdef(gdef) => Ok(ToplevelItem::Gdef(gdef)),
886 Statement::Head(head) => Ok(ToplevelItem::Head(head)),
887 Statement::Hhea(hhea) => Ok(ToplevelItem::Hhea(hhea)),
888 Statement::Name(name) => Ok(ToplevelItem::Name(name)),
889 Statement::Os2(os2) => Ok(ToplevelItem::Os2(os2)),
890 Statement::Stat(stat) => Ok(ToplevelItem::Stat(stat)),
891 Statement::Vhea(vhea) => Ok(ToplevelItem::Vhea(vhea)),
892 _ => Err(crate::Error::CannotConvert),
893 }
894 }
895}
896
897impl AsFea for ToplevelItem {
898 fn as_fea(&self, indent: &str) -> String {
899 match self {
900 ToplevelItem::GlyphClassDefinition(gcd) => gcd.as_fea(indent),
901 ToplevelItem::MarkClassDefinition(mcd) => mcd.as_fea(indent),
902 ToplevelItem::GdefClassDef(gcds) => gcds.as_fea(indent),
903 ToplevelItem::LanguageSystem(ls) => ls.as_fea(indent),
904 ToplevelItem::Feature(fb) => fb.as_fea(indent),
905 ToplevelItem::Lookup(lb) => lb.as_fea(indent),
906 ToplevelItem::Comment(cmt) => cmt.as_fea(indent),
907 ToplevelItem::AnchorDefinition(ad) => ad.as_fea(indent),
908 ToplevelItem::ValueRecordDefinition(vrd) => vrd.as_fea(indent),
909 ToplevelItem::ConditionSet(cs) => cs.as_fea(indent),
910 ToplevelItem::VariationBlock(vb) => vb.as_fea(indent),
911 ToplevelItem::Base(base) => base.as_fea(indent),
912 ToplevelItem::Gdef(gdef) => gdef.as_fea(indent),
913 ToplevelItem::Head(head) => head.as_fea(indent),
914 ToplevelItem::Hhea(hhea) => hhea.as_fea(indent),
915 ToplevelItem::Name(name) => name.as_fea(indent),
916 ToplevelItem::Os2(os2) => os2.as_fea(indent),
917 ToplevelItem::Stat(stat) => stat.as_fea(indent),
918 ToplevelItem::Vhea(vhea) => vhea.as_fea(indent),
919 }
920 }
921}
922#[allow(clippy::manual_map)]
923fn to_toplevel_item(child: &NodeOrToken) -> Option<ToplevelItem> {
924 if child.kind() == fea_rs::Kind::Comment {
925 Some(ToplevelItem::Comment(Comment::from(
926 child.token_text().unwrap(),
927 )))
928 } else if let Some(gcd) = fea_rs::typed::GlyphClassDef::cast(child) {
929 Some(ToplevelItem::GlyphClassDefinition(gcd.into()))
930 } else if let Some(mcd) = fea_rs::typed::MarkClassDef::cast(child) {
931 Some(ToplevelItem::MarkClassDefinition(mcd.into()))
932 } else if let Some(langsys) = fea_rs::typed::LanguageSystem::cast(child) {
933 Some(ToplevelItem::LanguageSystem(langsys.into()))
934 } else if let Some(feature) = fea_rs::typed::Feature::cast(child) {
935 Some(ToplevelItem::Feature(feature.into()))
936 } else if let Some(lookup) = fea_rs::typed::LookupBlock::cast(child) {
937 Some(ToplevelItem::Lookup(lookup.into()))
938 } else if let Some(ad) = fea_rs::typed::AnchorDef::cast(child) {
939 Some(ToplevelItem::AnchorDefinition(ad.into()))
940 } else if let Some(vrd) = fea_rs::typed::ValueRecordDef::cast(child) {
941 Some(ToplevelItem::ValueRecordDefinition(vrd.into()))
942 } else if let Some(cs) = fea_rs::typed::ConditionSet::cast(child) {
943 Some(ToplevelItem::ConditionSet(cs.into()))
944 } else if let Some(fv) = fea_rs::typed::FeatureVariation::cast(child) {
945 Some(ToplevelItem::VariationBlock(fv.into()))
946 } else if let Some(base) = fea_rs::typed::BaseTable::cast(child) {
947 Some(ToplevelItem::Base(base.into()))
948 } else if let Some(gdef) = fea_rs::typed::GdefTable::cast(child) {
949 Some(ToplevelItem::Gdef(gdef.into()))
950 } else if let Some(head) = fea_rs::typed::HeadTable::cast(child) {
951 Some(ToplevelItem::Head(head.into()))
952 } else if let Some(hhea) = fea_rs::typed::HheaTable::cast(child) {
953 Some(ToplevelItem::Hhea(hhea.into()))
954 } else if let Some(vhea) = fea_rs::typed::VheaTable::cast(child) {
955 Some(ToplevelItem::Vhea(vhea.into()))
956 } else if let Some(name) = fea_rs::typed::NameTable::cast(child) {
957 Some(ToplevelItem::Name(name.into()))
958 } else if let Some(os2) = fea_rs::typed::Os2Table::cast(child) {
959 Some(ToplevelItem::Os2(os2.into()))
960 } else if let Some(stat) = fea_rs::typed::StatTable::cast(child) {
961 Some(ToplevelItem::Stat(stat.into()))
962 } else {
963 None
964 }
965}
966
967#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
982#[derive(Debug, Clone, PartialEq, Eq)]
983pub struct FeatureFile {
984 pub statements: Vec<ToplevelItem>,
986}
987impl FeatureFile {
988 pub fn new(statements: Vec<ToplevelItem>) -> Self {
990 Self { statements }
991 }
992
993 pub fn iter(&self) -> impl Iterator<Item = &ToplevelItem> {
995 self.statements.iter()
996 }
997
998 pub fn new_from_fea(
1019 features: &str,
1020 glyph_names: Option<&[&str]>,
1021 project_root: Option<impl Into<PathBuf>>,
1022 ) -> Result<Self, crate::Error> {
1023 let glyph_map = glyph_names
1024 .map(|gn| GlyphMap::new(gn.iter().cloned()))
1025 .transpose()?;
1026 let resolver: Box<dyn fea_rs::parse::SourceResolver> =
1027 if let Some(project_root) = project_root {
1028 let path = project_root.into();
1029 Box::new(FileSystemResolver::new(path))
1030 } else {
1031 Box::new(dummyresolver::DummyResolver)
1032 };
1033 let features_text: Arc<str> = Arc::from(features);
1034 let (parse_tree, mut diagnostics) = fea_rs::parse::parse_root(
1035 "get_parse_tree".into(),
1036 glyph_map.as_ref(),
1037 Box::new(move |s: &Path| {
1038 if s == Path::new("get_parse_tree") {
1039 Ok(features_text.clone())
1040 } else {
1041 let path = resolver.resolve_raw_path(s.as_ref(), None);
1042 let canonical = resolver.canonicalize(&path)?;
1043 resolver.get_contents(&canonical)
1044 }
1045 }),
1046 )?;
1047 diagnostics.split_off_warnings();
1048 if diagnostics.has_errors() {
1049 return Err(crate::Error::FeatureParsing(diagnostics));
1050 }
1051 Ok(parse_tree.into())
1052 }
1053}
1054impl AsFea for FeatureFile {
1055 fn as_fea(&self, indent: &str) -> String {
1056 let mut res = String::new();
1057 for stmt in &self.statements {
1058 res.push_str(&stmt.as_fea(indent));
1059 res.push('\n');
1060 }
1061 res
1062 }
1063}
1064impl From<ParseTree> for FeatureFile {
1065 fn from(val: ParseTree) -> Self {
1066 let statements: Vec<ToplevelItem> = val
1067 .root()
1068 .iter_children()
1069 .filter_map(to_toplevel_item)
1070 .collect();
1071 FeatureFile { statements }
1072 }
1073}
1074
1075impl TryFrom<&str> for FeatureFile {
1080 type Error = fea_rs::DiagnosticSet;
1081
1082 fn try_from(value: &str) -> Result<Self, Self::Error> {
1083 let (parsed, diag) = fea_rs::parse::parse_string(value);
1084 if diag.has_errors() {
1085 Err(diag)
1086 } else {
1087 Ok(parsed.into())
1088 }
1089 }
1090}
1091#[cfg(test)]
1092mod tests {
1093 use rstest::rstest;
1094
1095 use super::*;
1096
1097 #[test]
1098 fn test_parse() {
1099 const FEA: &str = r#"feature smcp {
1100 sub a by a.smcp;
1101 } smcp;
1102 "#;
1103 let (parsed, _) = fea_rs::parse::parse_string(FEA);
1104 let feature_block = parsed.root().iter_children().next().unwrap();
1105
1106 let Some(feature) = fea_rs::typed::Feature::cast(feature_block) else {
1107 panic!("Expected Feature, got {:?}", feature_block.kind());
1108 };
1109 let feature_block: FeatureBlock = feature.into();
1110 assert_eq!(feature_block.name.as_str(), "smcp");
1111 assert_eq!(feature_block.statements.len(), 1);
1112 assert_eq!(
1113 normalize_whitespace(&feature_block.as_fea("")),
1114 normalize_whitespace("feature smcp {\n sub a by a.smcp;\n} smcp;\n")
1115 );
1116 }
1117
1118 fn normalize_whitespace(s: &str) -> String {
1119 s.replace("#", "\n#")
1120 .replace("\n\n", "\n")
1121 .lines()
1122 .filter(|l| !l.trim().is_empty())
1123 .map(|l| l.trim())
1124 .collect::<Vec<_>>()
1125 .join("\n")
1126 .replace("\t", " ")
1127 .replace("position ", "pos ")
1128 .replace("substitute ", "sub ")
1129 .replace("reversesub ", "rsub ")
1130 }
1131
1132 #[rstest]
1133 fn for_each_file(
1134 #[files("resources/test/*.fea")]
1135 #[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,
1143 ) {
1144 let fea_str = std::fs::read_to_string(&path).unwrap();
1145 let (parsed, diag) = fea_rs::parse::parse_string(fea_str.clone());
1146 if diag.has_errors() {
1147 panic!("fea-rs didn't like file {:?}:\n{:#?}", path, diag);
1148 }
1149 let feature_file: FeatureFile = parsed.into();
1150 let fea_output = feature_file.as_fea("");
1151 let orig = normalize_whitespace(&fea_str);
1152 let output = normalize_whitespace(&fea_output);
1153 let mut orig_lines = orig.lines().collect::<Vec<_>>();
1154 for i in 0..orig_lines.len() {
1155 if let Some(replacement) = orig_lines[i].strip_prefix("#test-fea2fea: ") {
1156 orig_lines[i + 1] = replacement;
1157 }
1158 }
1159 let orig = orig_lines.join("\n");
1160 pretty_assertions::assert_eq!(orig, output, "Mismatch in file {:?}", path);
1161 }
1162}