1use std::ops::Range;
2
3use fea_rs::{
4 Kind,
5 typed::{AstNode as _, GlyphOrClass},
6};
7
8use crate::{
9 Anchor, AsFea, GlyphContainer, MarkClass, PotentiallyContextualStatement, ValueRecord,
10 from_anchor,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SinglePosStatement {
16 pub pos: Vec<(GlyphContainer, Option<ValueRecord>)>,
18 pub prefix: Vec<GlyphContainer>,
20 pub suffix: Vec<GlyphContainer>,
22 pub force_chain: bool,
24 pub location: Range<usize>,
26}
27
28impl SinglePosStatement {
29 pub fn new(
31 prefix: Vec<GlyphContainer>,
32 suffix: Vec<GlyphContainer>,
33 pos: Vec<(GlyphContainer, Option<ValueRecord>)>,
34 force_chain: bool,
35 location: Range<usize>,
36 ) -> Self {
37 Self {
38 prefix,
39 suffix,
40 pos,
41 force_chain,
42 location,
43 }
44 }
45}
46
47impl PotentiallyContextualStatement for SinglePosStatement {
48 fn prefix(&self) -> &[GlyphContainer] {
49 &self.prefix
50 }
51 fn suffix(&self) -> &[GlyphContainer] {
52 &self.suffix
53 }
54 fn force_chain(&self) -> bool {
55 self.force_chain
56 }
57
58 fn format_begin(&self, _indent: &str) -> String {
59 "pos ".to_string()
60 }
61
62 fn format_contextual_parts(&self, indent: &str) -> Vec<String> {
63 self.pos
64 .iter()
65 .map(|(p, vr)| {
66 format!(
67 "{}'{}",
68 p.as_fea(""),
69 vr.as_ref()
70 .map(|v| format!(" {}", v.as_fea(indent)))
71 .unwrap_or_default()
72 )
73 })
74 .collect()
75 }
76
77 fn format_noncontextual_parts(&self, indent: &str) -> Vec<String> {
78 self.pos
79 .iter()
80 .map(|(p, vr)| {
81 format!(
82 "{} {}",
83 p.as_fea(""),
84 vr.as_ref()
85 .map(|v| v.as_fea(indent).to_string())
86 .unwrap_or("<NULL>".to_string())
87 )
88 })
89 .collect()
90 }
91}
92
93impl From<fea_rs::typed::Gpos1> for SinglePosStatement {
94 fn from(val: fea_rs::typed::Gpos1) -> Self {
95 let target = val.iter().find_map(GlyphOrClass::cast).unwrap();
96 let value_record = val
97 .iter()
98 .find_map(fea_rs::typed::ValueRecord::cast)
99 .unwrap();
100 Self::new(
101 vec![],
102 vec![],
103 vec![(target.into(), Some(value_record.into()))],
104 false,
105 val.node().range(),
106 )
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct PairPosStatement {
113 pub glyphs_1: GlyphContainer,
115 pub glyphs_2: GlyphContainer,
117 pub value_record_1: ValueRecord,
119 pub value_record_2: Option<ValueRecord>,
121 pub enumerated: bool,
123 pub location: Range<usize>,
125}
126
127impl PairPosStatement {
128 pub fn new(
130 glyphs_1: GlyphContainer,
131 glyphs_2: GlyphContainer,
132 value_record_1: ValueRecord,
133 value_record_2: Option<ValueRecord>,
134 enumerated: bool,
135 location: Range<usize>,
136 ) -> Self {
137 Self {
138 glyphs_1,
139 glyphs_2,
140 value_record_1,
141 value_record_2,
142 enumerated,
143 location,
144 }
145 }
146}
147
148impl AsFea for PairPosStatement {
149 fn as_fea(&self, indent: &str) -> String {
150 let mut res = String::new();
151 if self.enumerated {
152 res.push_str("enum ");
153 }
154 res.push_str("pos ");
155 if let Some(vr2) = &self.value_record_2 {
156 res.push_str(&format!(
158 "{} {} {} {}",
159 self.glyphs_1.as_fea(""),
160 self.value_record_1.as_fea(indent),
161 self.glyphs_2.as_fea(""),
162 vr2.as_fea(indent)
163 ));
164 } else {
165 res.push_str(&format!(
167 "{} {} {}",
168 self.glyphs_1.as_fea(""),
169 self.glyphs_2.as_fea(""),
170 self.value_record_1.as_fea(indent),
171 ));
172 }
173 res.push(';');
174 res
175 }
176}
177
178impl From<fea_rs::typed::Gpos2> for PairPosStatement {
179 fn from(val: fea_rs::typed::Gpos2) -> Self {
180 let enumerated = val.iter().any(|t| t.kind() == Kind::EnumKw);
181 let glyphs_1 = val.iter().find_map(GlyphOrClass::cast).unwrap().into();
182 let glyphs_2 = val
183 .iter()
184 .filter_map(GlyphOrClass::cast)
185 .nth(1)
186 .unwrap()
187 .into();
188 let value_record_1 = val
189 .iter()
190 .find_map(fea_rs::typed::ValueRecord::cast)
191 .unwrap();
192 let value_record_2 = val
193 .iter()
194 .filter_map(fea_rs::typed::ValueRecord::cast)
195 .nth(1)
196 .map(|vr| vr.into());
197 Self::new(
198 glyphs_1,
199 glyphs_2,
200 value_record_1.into(),
201 value_record_2,
202 enumerated,
203 val.node().range(),
204 )
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct CursivePosStatement {
211 pub location: Range<usize>,
213 pub glyphclass: GlyphContainer,
215 pub entry: Option<Anchor>,
217 pub exit: Option<Anchor>,
219}
220
221impl CursivePosStatement {
222 pub fn new(
224 glyphclass: GlyphContainer,
225 entry: Option<Anchor>,
226 exit: Option<Anchor>,
227 location: Range<usize>,
228 ) -> Self {
229 Self {
230 glyphclass,
231 entry,
232 exit,
233 location,
234 }
235 }
236}
237
238impl AsFea for CursivePosStatement {
239 fn as_fea(&self, indent: &str) -> String {
240 format!(
241 "pos cursive {} {} {};",
242 self.glyphclass.as_fea(""),
243 self.entry
244 .as_ref()
245 .map(|e| e.as_fea(indent))
246 .unwrap_or_else(|| "<anchor NULL>".to_string()),
247 self.exit
248 .as_ref()
249 .map(|e| e.as_fea(indent))
250 .unwrap_or_else(|| "<anchor NULL>".to_string()),
251 )
252 }
253}
254impl From<fea_rs::typed::Gpos3> for CursivePosStatement {
255 fn from(val: fea_rs::typed::Gpos3) -> Self {
256 let glyphclass = val.iter().find_map(GlyphOrClass::cast).unwrap().into();
257 let entry = val.iter().find_map(fea_rs::typed::Anchor::cast).unwrap();
258 let exit = val
259 .iter()
260 .filter_map(fea_rs::typed::Anchor::cast)
261 .nth(1)
262 .unwrap();
263 Self::new(
264 glyphclass,
265 from_anchor(entry),
266 from_anchor(exit),
267 val.node().range(),
268 )
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct MarkBasePosStatement {
277 pub base: GlyphContainer,
279 pub marks: Vec<(Anchor, MarkClass)>,
281 pub location: Range<usize>,
283}
284
285impl MarkBasePosStatement {
286 pub fn new(
288 base: GlyphContainer,
289 marks: Vec<(Anchor, MarkClass)>,
290 location: Range<usize>,
291 ) -> Self {
292 Self {
293 base,
294 marks,
295 location,
296 }
297 }
298}
299
300impl AsFea for MarkBasePosStatement {
301 fn as_fea(&self, indent: &str) -> String {
302 let mut res = format!("pos base {}", self.base.as_fea(""));
303 for (anchor, mark_class) in &self.marks {
304 res.push_str(&format!(
305 "\n{} {} mark @{}",
306 indent,
307 anchor.as_fea(""),
308 mark_class.name
309 ));
310 }
311 res.push(';');
312 res
313 }
314}
315
316impl From<fea_rs::typed::Gpos4> for MarkBasePosStatement {
317 fn from(val: fea_rs::typed::Gpos4) -> Self {
318 let base: GlyphContainer = val
320 .iter()
321 .filter(|t| t.kind() != Kind::Whitespace)
322 .nth(2) .and_then(GlyphOrClass::cast)
324 .unwrap()
325 .into();
326
327 let marks: Vec<(Anchor, MarkClass)> = val
329 .iter()
330 .filter_map(fea_rs::typed::AnchorMark::cast)
331 .map(|anchor_mark| {
332 let anchor_node = anchor_mark
334 .iter()
335 .find_map(fea_rs::typed::Anchor::cast)
336 .unwrap();
337 let anchor = from_anchor(anchor_node).unwrap();
338
339 let mark_class_node = anchor_mark
341 .iter()
342 .find_map(fea_rs::typed::GlyphClassName::cast)
343 .unwrap();
344 let mark_class_name = mark_class_node.text().trim_start_matches('@');
345 let mark_class = MarkClass::new(mark_class_name);
346
347 (anchor, mark_class)
348 })
349 .collect();
350
351 MarkBasePosStatement::new(base, marks, val.range())
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct MarkLigPosStatement {
363 pub ligatures: GlyphContainer,
365 pub marks: Vec<Vec<(Anchor, MarkClass)>>,
367 pub location: Range<usize>,
369}
370
371impl MarkLigPosStatement {
372 pub fn new(
374 ligatures: GlyphContainer,
375 marks: Vec<Vec<(Anchor, MarkClass)>>,
376 location: Range<usize>,
377 ) -> Self {
378 Self {
379 ligatures,
380 marks,
381 location,
382 }
383 }
384}
385
386impl AsFea for MarkLigPosStatement {
387 fn as_fea(&self, indent: &str) -> String {
388 let mut res = format!("pos ligature {}", self.ligatures.as_fea(""));
389
390 let mut ligs = Vec::new();
392 for component in &self.marks {
393 if component.is_empty() {
394 ligs.push(format!("\n{} <anchor NULL>", indent));
396 } else {
397 let mut temp = String::new();
398 for (anchor, mark_class) in component {
399 temp.push_str(&format!(
400 "\n{} {} mark @{}",
401 indent,
402 anchor.as_fea(""),
403 mark_class.name
404 ));
405 }
406 ligs.push(temp);
407 }
408 }
409
410 res.push_str(&ligs.join(&format!("\n{} ligComponent", indent)));
412 res.push(';');
413 res
414 }
415}
416
417impl From<fea_rs::typed::Gpos5> for MarkLigPosStatement {
418 fn from(val: fea_rs::typed::Gpos5) -> Self {
419 let ligatures: GlyphContainer = val
421 .iter()
422 .filter(|t| t.kind() != Kind::Whitespace)
423 .nth(2) .and_then(GlyphOrClass::cast)
425 .unwrap()
426 .into();
427
428 let marks: Vec<Vec<(Anchor, MarkClass)>> = val
430 .iter()
431 .filter_map(fea_rs::typed::LigatureComponent::cast)
432 .map(|lig_component| {
433 lig_component
435 .iter()
436 .filter_map(fea_rs::typed::AnchorMark::cast)
437 .flat_map(|anchor_mark| {
438 let anchor_node = anchor_mark
440 .iter()
441 .find_map(fea_rs::typed::Anchor::cast)
442 .unwrap();
443 let anchor = from_anchor(anchor_node)?;
444
445 let mark_class_node = anchor_mark
447 .iter()
448 .find_map(fea_rs::typed::GlyphClassName::cast)?;
449 let mark_class_name = mark_class_node.text().trim_start_matches('@');
450 let mark_class = MarkClass::new(mark_class_name);
451
452 Some((anchor, mark_class))
453 })
454 .collect()
455 })
456 .collect();
457
458 MarkLigPosStatement::new(ligatures, marks, val.range())
459 }
460}
461
462#[derive(Debug, Clone, PartialEq, Eq)]
464pub struct MarkMarkPosStatement {
465 pub base_marks: GlyphContainer,
467 pub marks: Vec<(Anchor, MarkClass)>,
469 pub location: Range<usize>,
471}
472
473impl MarkMarkPosStatement {
474 pub fn new(
476 base_marks: GlyphContainer,
477 marks: Vec<(Anchor, MarkClass)>,
478 location: Range<usize>,
479 ) -> Self {
480 Self {
481 base_marks,
482 marks,
483 location,
484 }
485 }
486}
487
488impl AsFea for MarkMarkPosStatement {
489 fn as_fea(&self, indent: &str) -> String {
490 let mut res = format!("pos mark {}", self.base_marks.as_fea(""));
491 for (anchor, mark_class) in &self.marks {
492 res.push_str(&format!(
493 "\n{} {} mark @{}",
494 indent,
495 anchor.as_fea(""),
496 mark_class.name
497 ));
498 }
499 res.push(';');
500 res
501 }
502}
503
504impl From<fea_rs::typed::Gpos6> for MarkMarkPosStatement {
505 fn from(val: fea_rs::typed::Gpos6) -> Self {
506 let base_marks: GlyphContainer = val
508 .iter()
509 .filter(|t| t.kind() != Kind::Whitespace)
510 .nth(2) .and_then(GlyphOrClass::cast)
512 .unwrap()
513 .into();
514
515 let marks: Vec<(Anchor, MarkClass)> = val
517 .iter()
518 .filter_map(fea_rs::typed::AnchorMark::cast)
519 .map(|anchor_mark| {
520 let anchor_node = anchor_mark
522 .iter()
523 .find_map(fea_rs::typed::Anchor::cast)
524 .unwrap();
525 let anchor = from_anchor(anchor_node).unwrap();
526
527 let mark_class_node = anchor_mark
529 .iter()
530 .find_map(fea_rs::typed::GlyphClassName::cast)
531 .unwrap();
532 let mark_class_name = mark_class_node.text().trim_start_matches('@');
533 let mark_class = MarkClass::new(mark_class_name);
534
535 (anchor, mark_class)
536 })
537 .collect();
538
539 MarkMarkPosStatement::new(base_marks, marks, val.node().range())
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use crate::GlyphName;
546
547 use super::*;
548
549 #[test]
550 fn test_generate_gpos1() {
551 let gpos1 = SinglePosStatement::new(
552 vec![GlyphContainer::GlyphName(GlyphName::new("x"))],
553 vec![],
554 vec![(
555 GlyphContainer::GlyphName(GlyphName::new("A")),
556 Some(ValueRecord {
557 x_advance: Some(50.into()),
558 y_advance: None,
559 x_placement: None,
560 y_placement: None,
561 x_placement_device: None,
562 y_placement_device: None,
563 x_advance_device: None,
564 y_advance_device: None,
565 vertical: false,
566 location: 0..0,
567 name: None,
568 }),
569 )],
570 false,
571 0..10,
572 );
573 let fea_str = gpos1.as_fea("");
574 assert_eq!(fea_str, "pos x A' 50;");
575 }
576
577 #[test]
578 fn test_roundtrip_gpos1() {
579 const FEA: &str = "feature foo { pos A 50; } foo;";
580 let (parsed, _) = fea_rs::parse::parse_string(FEA);
581 let gpos1 = parsed
582 .root()
583 .iter_children()
584 .find_map(fea_rs::typed::Feature::cast)
585 .and_then(|feature| {
586 feature
587 .node()
588 .iter_children()
589 .find_map(fea_rs::typed::Gpos1::cast)
590 })
591 .unwrap();
592 let gpos1_stmt: SinglePosStatement = gpos1.into();
593 let fea_str_roundtrip = gpos1_stmt.as_fea("");
594 assert_eq!(fea_str_roundtrip, "pos A 50;");
595 }
596
597 #[test]
598 fn test_generate_gpos2() {
599 let gpos2 = PairPosStatement::new(
600 GlyphContainer::GlyphName(GlyphName::new("A")),
601 GlyphContainer::GlyphName(GlyphName::new("B")),
602 ValueRecord {
603 x_advance: Some(50.into()),
604 y_advance: None,
605 x_placement: None,
606 y_placement: None,
607 x_placement_device: None,
608 y_placement_device: None,
609 x_advance_device: None,
610 y_advance_device: None,
611 vertical: false,
612 location: 0..0,
613 name: None,
614 },
615 Some(ValueRecord {
616 x_advance: Some(30.into()),
617 y_advance: None,
618 x_placement: None,
619 y_placement: None,
620 x_placement_device: None,
621 y_placement_device: None,
622 x_advance_device: None,
623 y_advance_device: None,
624 vertical: false,
625 location: 0..0,
626 name: None,
627 }),
628 false,
629 0..10,
630 );
631 let fea_str = gpos2.as_fea("");
632 assert_eq!(fea_str, "pos A 50 B 30;");
633 }
634
635 #[test]
636 fn test_generate_gpos3() {
637 let gpos3 = CursivePosStatement::new(
638 GlyphContainer::GlyphName(GlyphName::new("A")),
639 Some(Anchor::new_simple(100, 200, 0..0)),
640 Some(Anchor::new_simple(150, 250, 0..0)),
641 0..10,
642 );
643 let fea_str = gpos3.as_fea("");
644 assert_eq!(fea_str, "pos cursive A <anchor 100 200> <anchor 150 250>;");
645
646 let gpos3_null = CursivePosStatement::new(
648 GlyphContainer::GlyphName(GlyphName::new("A")),
649 None,
650 Some(Anchor::new_simple(150, 250, 0..10)),
651 0..10,
652 );
653 let fea_str_null = gpos3_null.as_fea("");
654 assert_eq!(
655 fea_str_null,
656 "pos cursive A <anchor NULL> <anchor 150 250>;"
657 );
658 }
659
660 #[test]
661 fn test_roundtrip_gpos3() {
662 const FEA: &str = "feature foo { pos cursive A <anchor 100 200> <anchor 150 250>; } foo;";
663 let (parsed, _) = fea_rs::parse::parse_string(FEA);
664 let gpos3 = parsed
665 .root()
666 .iter_children()
667 .find_map(fea_rs::typed::Feature::cast)
668 .and_then(|feature| {
669 feature
670 .node()
671 .iter_children()
672 .find_map(fea_rs::typed::Gpos3::cast)
673 })
674 .unwrap();
675 let gpos3_stmt: CursivePosStatement = gpos3.into();
676 let fea_str_roundtrip = gpos3_stmt.as_fea("");
677 assert_eq!(
678 fea_str_roundtrip,
679 "pos cursive A <anchor 100 200> <anchor 150 250>;"
680 );
681 }
682
683 #[test]
684 fn test_roundtrip_gpos4() {
685 const FEA: &str = "feature mark { pos base a <anchor 625 1800> mark @TOP_MARKS; } mark;";
686 let (parsed, _) = fea_rs::parse::parse_string(FEA);
687 let gpos4 = parsed
688 .root()
689 .iter_children()
690 .find_map(fea_rs::typed::Feature::cast)
691 .and_then(|feature| {
692 feature
693 .node()
694 .iter_children()
695 .find_map(fea_rs::typed::Gpos4::cast)
696 })
697 .unwrap();
698 let stmt = MarkBasePosStatement::from(gpos4);
699 assert_eq!(stmt.base.as_fea(""), "a");
700 assert_eq!(stmt.marks.len(), 1);
701 assert_eq!(stmt.marks[0].1.name, "TOP_MARKS");
702 assert_eq!(
703 stmt.as_fea(""),
704 "pos base a\n <anchor 625 1800> mark @TOP_MARKS;"
705 );
706 }
707
708 #[test]
709 fn test_generation_gpos4() {
710 let stmt = MarkBasePosStatement::new(
711 GlyphContainer::GlyphName(GlyphName::new("a")),
712 vec![
713 (
714 Anchor::new_simple(300, 450, 0..0),
715 MarkClass::new("TOP_MARKS"),
716 ),
717 (
718 Anchor::new_simple(300, -100, 0..0),
719 MarkClass::new("BOTTOM_MARKS"),
720 ),
721 ],
722 0..0,
723 );
724 assert_eq!(
725 stmt.as_fea(""),
726 "pos base a\n <anchor 300 450> mark @TOP_MARKS\n <anchor 300 -100> mark @BOTTOM_MARKS;"
727 );
728 }
729
730 #[test]
731 fn test_roundtrip_gpos5() {
732 const FEA: &str = "feature test { pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS ligComponent <anchor 376 -378> mark @BOTTOM_MARKS; } test;";
733 let (parsed, _) = fea_rs::parse::parse_string(FEA);
734 let gpos5 = parsed
735 .root()
736 .iter_children()
737 .find_map(fea_rs::typed::Feature::cast)
738 .and_then(|feature| {
739 feature
740 .node()
741 .iter_children()
742 .find_map(fea_rs::typed::Gpos5::cast)
743 })
744 .unwrap();
745 let gpos5_stmt: MarkLigPosStatement = gpos5.into();
746 let fea_str_roundtrip = gpos5_stmt.as_fea("");
747 assert_eq!(
748 fea_str_roundtrip,
749 "pos ligature lam_meem_jeem\n <anchor 625 1800> mark @TOP_MARKS\n ligComponent\n <anchor 376 -378> mark @BOTTOM_MARKS;"
750 );
751 }
752
753 #[test]
754 fn test_generate_gpos5() {
755 let stmt = MarkLigPosStatement::new(
756 GlyphContainer::GlyphName(GlyphName::new("lam_meem_jeem")),
757 vec![
758 vec![(
759 Anchor::new_simple(625, 1800, 0..0),
760 MarkClass::new("TOP_MARKS"),
761 )],
762 vec![(
763 Anchor::new_simple(376, -378, 0..0),
764 MarkClass::new("BOTTOM_MARKS"),
765 )],
766 vec![], ],
768 0..0,
769 );
770 assert_eq!(
771 stmt.as_fea(""),
772 "pos ligature lam_meem_jeem\n <anchor 625 1800> mark @TOP_MARKS\n ligComponent\n <anchor 376 -378> mark @BOTTOM_MARKS\n ligComponent\n <anchor NULL>;"
773 );
774 }
775}