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