1use std::{fmt::Display, ops::Range};
2
3use fea_rs::{
4 Kind,
5 typed::{AstNode as _, GlyphOrClass, GposIgnore, GsubIgnore},
6};
7use smol_str::SmolStr;
8
9use crate::{
10 AsFea, GlyphContainer, LigatureSubstStatement, MultipleSubstStatement, SingleSubstStatement,
11 Statement,
12};
13
14pub(crate) fn backtrack(val: &fea_rs::Node) -> Vec<GlyphContainer> {
15 fea_rs::Node::iter_children(val)
16 .find(|c| c.kind() == Kind::BacktrackSequence)
17 .unwrap()
18 .as_node()
19 .unwrap()
20 .iter_children()
21 .filter_map(GlyphOrClass::cast)
22 .map(|goc| goc.into())
23 .collect()
24}
25pub(crate) fn lookahead(val: &fea_rs::Node) -> Vec<GlyphContainer> {
26 fea_rs::Node::iter_children(val)
27 .find(|c| c.kind() == Kind::LookaheadSequence)
28 .unwrap()
29 .as_node()
30 .unwrap()
31 .iter_children()
32 .take_while(|c| c.kind() != Kind::InlineSubNode)
33 .filter_map(GlyphOrClass::cast)
34 .map(|goc| goc.into())
35 .collect()
36}
37
38pub(crate) fn context_glyphs(val: &fea_rs::Node) -> Vec<GlyphContainer> {
39 let glyphnodes = fea_rs::Node::iter_children(val)
40 .find(|c| c.kind() == Kind::ContextSequence)
41 .unwrap()
42 .as_node()
43 .unwrap()
44 .iter_children()
45 .filter(|c| c.kind() == Kind::ContextGlyphNode)
46 .collect::<Vec<_>>();
47 glyphnodes
48 .iter()
49 .flat_map(|gn| {
50 gn.as_node()
51 .unwrap()
52 .iter_children()
53 .filter_map(GlyphOrClass::cast)
54 })
55 .map(|goc| goc.into())
56 .collect()
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ChainedContextStatement<T: SubOrPos> {
62 pub location: Range<usize>,
64 pub prefix: Vec<GlyphContainer>,
66 pub suffix: Vec<GlyphContainer>,
68 pub glyphs: Vec<GlyphContainer>,
70 pub lookups: Vec<Vec<SmolStr>>,
72 sub_or_pos: T,
73}
74
75impl<T: SubOrPos> ChainedContextStatement<T> {
76 pub fn new(
89 glyphs: Vec<GlyphContainer>,
90 prefix: Vec<GlyphContainer>,
91 suffix: Vec<GlyphContainer>,
92 lookups: Vec<Vec<SmolStr>>,
93 location: Range<usize>,
94 sub_or_pos: T,
95 ) -> Self {
96 Self {
97 prefix,
98 suffix,
99 glyphs,
100 lookups,
101 location,
102 sub_or_pos,
103 }
104 }
105}
106
107impl<T: SubOrPos> PotentiallyContextualStatement for ChainedContextStatement<T> {
108 fn is_contextual(&self) -> bool {
109 true
110 }
111 fn prefix(&self) -> &[GlyphContainer] {
112 &self.prefix
113 }
114 fn suffix(&self) -> &[GlyphContainer] {
115 &self.suffix
116 }
117 fn force_chain(&self) -> bool {
118 true
119 }
120 fn format_begin(&self, _indent: &str) -> String {
121 format!("{} ", self.sub_or_pos)
122 }
123 fn format_contextual_parts(&self, indent: &str) -> Vec<String> {
124 let mut parts = Vec::new();
125 for (i, g) in self.glyphs.iter().enumerate() {
126 let mut s = format!("{}'", g.as_fea(indent));
127 if !self.lookups[i].is_empty() {
128 for lu in &self.lookups[i] {
129 s.push_str(&format!(" lookup {}", lu));
130 }
131 }
132 parts.push(s);
133 }
134 parts
135 }
136 fn format_noncontextual_parts(&self, _indent: &str) -> Vec<String> {
137 unreachable!()
138 }
139}
140
141impl From<fea_rs::typed::Gsub6> for Statement {
142 fn from(val: fea_rs::typed::Gsub6) -> Self {
143 let prefix = backtrack(val.node());
149 let suffix = lookahead(val.node());
150 let context_glyph_nodes = fea_rs::Node::iter_children(val.node())
151 .find(|c| c.kind() == Kind::ContextSequence)
152 .unwrap()
153 .as_node()
154 .unwrap()
155 .iter_children()
156 .filter(|c| c.kind() == Kind::ContextGlyphNode)
157 .collect::<Vec<_>>(); if let Some((context_glyphs, lookups)) = check_for_simple_contextual(&context_glyph_nodes) {
160 return Statement::ChainedContextSubst(ChainedContextStatement::new(
161 context_glyphs,
162 prefix,
163 suffix,
164 lookups,
165 val.node().range(),
166 Subst,
167 ));
168 }
169 let Some(inline_sub) = val
171 .node()
172 .iter_children()
173 .find_map(fea_rs::typed::InlineSubRule::cast)
174 else {
175 panic!(
176 "No LookRefNode or InlineSubNode found in Gsub6, can't get here, fea-rs has failed me: {}",
177 val.node()
178 .iter_tokens()
179 .map(|t| t.text.clone())
180 .collect::<Vec<_>>()
181 .join("")
182 );
183 };
184 let target_glyphs = inline_sub
185 .node()
186 .iter_children()
187 .filter_map(GlyphOrClass::cast)
188 .map(|goc| goc.into())
189 .collect::<Vec<_>>();
190 let mut context_glyphs = context_glyphs(val.node());
191 if target_glyphs.len() > 1 {
192 return Statement::MultipleSubst(MultipleSubstStatement {
193 prefix,
194 suffix,
195 glyph: context_glyphs.remove(0),
196 replacement: target_glyphs,
197 location: val.node().range(),
198 force_chain: true,
199 });
200 }
201 if context_glyphs.len() == 1 && target_glyphs.len() == 1 {
202 return Statement::SingleSubst(SingleSubstStatement {
203 prefix,
204 suffix,
205 glyphs: context_glyphs,
206 replacement: target_glyphs,
207 location: val.node().range(),
208 force_chain: true,
209 });
210 }
211 if context_glyphs.len() > 1 && target_glyphs.len() == 1 {
212 return Statement::LigatureSubst(LigatureSubstStatement {
214 prefix,
215 suffix,
216 glyphs: context_glyphs,
217 replacement: target_glyphs[0].clone(),
218 location: val.node().range(),
219 force_chain: true,
220 });
221 }
222 panic!("Don't know what this GSUB6 is supposed to be!")
223 }
224}
225
226fn check_for_simple_contextual(
227 context_glyph_nodes: &[&fea_rs::NodeOrToken],
228) -> Option<(Vec<GlyphContainer>, Vec<Vec<SmolStr>>)> {
229 if context_glyph_nodes.iter().any(|cgn| {
231 cgn.as_node()
232 .unwrap()
233 .iter_children()
234 .any(|child| child.kind() == Kind::LookupRefNode)
235 }) {
236 let mut context_glyphs = Vec::new();
239 let mut lookups = Vec::new();
240 for context_glyph_node in context_glyph_nodes.iter() {
241 let glyph_node = context_glyph_node.as_node().unwrap();
242 for node in glyph_node.iter_children() {
243 if let Some(goc) = GlyphOrClass::cast(node) {
244 context_glyphs.push(goc.into());
245 lookups.push(vec![]);
246 } else if let Some(lookup_ref) = fea_rs::typed::LookupRef::cast(node)
247 && let Some(last) = lookups.last_mut()
248 {
249 last.push(SmolStr::new(
250 &lookup_ref
251 .node()
252 .iter_tokens()
253 .find(|t| t.kind == Kind::Ident)
254 .unwrap()
255 .text,
256 ));
257 }
258 }
259 }
260 return Some((context_glyphs, lookups));
261 }
262 None
263}
264
265impl From<fea_rs::typed::Gpos8> for Statement {
266 fn from(val: fea_rs::typed::Gpos8) -> Self {
267 let prefix = backtrack(val.node());
268 let suffix = lookahead(val.node());
269 let context_glyph_nodes = fea_rs::Node::iter_children(val.node())
270 .find(|c| c.kind() == Kind::ContextSequence)
271 .unwrap()
272 .as_node()
273 .unwrap()
274 .iter_children()
275 .filter(|c| c.kind() == Kind::ContextGlyphNode)
276 .collect::<Vec<_>>(); if let Some((context_glyphs, lookups)) = check_for_simple_contextual(&context_glyph_nodes) {
278 return Statement::ChainedContextPos(ChainedContextStatement::new(
279 context_glyphs,
280 prefix,
281 suffix,
282 lookups,
283 val.node().range(),
284 Pos,
285 ));
286 }
287 let mut context_glyphs = Vec::new();
289 for context_glyph_node in context_glyph_nodes.iter() {
290 let glyph_node = context_glyph_node.as_node().unwrap();
291 let glyph = glyph_node.iter_children().find_map(GlyphOrClass::cast);
292 let value_record_node = glyph_node
293 .iter_children()
294 .find_map(fea_rs::typed::ValueRecord::cast);
295 if let Some(goc) = glyph {
296 context_glyphs.push((goc.into(), value_record_node.map(|vr| vr.into())));
297 }
298 }
299 Statement::SinglePos(crate::gpos::SinglePosStatement::new(
300 prefix,
301 suffix,
302 context_glyphs,
303 true,
304 val.node().range(),
305 ))
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Copy)]
311pub struct Pos;
312
313#[derive(Debug, Clone, PartialEq, Eq, Copy)]
315pub struct Subst;
316pub trait SubOrPos: Display + Copy {}
318impl SubOrPos for Pos {}
319impl SubOrPos for Subst {}
320impl Display for Pos {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 write!(f, "pos")
323 }
324}
325impl Display for Subst {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 write!(f, "sub")
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct IgnoreStatement<T: SubOrPos> {
334 pub location: Range<usize>,
336 pub chain_contexts: Vec<(
338 Vec<GlyphContainer>,
339 Vec<GlyphContainer>,
340 Vec<GlyphContainer>,
341 )>,
342 sub_or_pos: T,
343}
344
345impl<T: SubOrPos> IgnoreStatement<T> {
346 pub fn new(
348 chain_contexts: Vec<(
349 Vec<GlyphContainer>,
350 Vec<GlyphContainer>,
351 Vec<GlyphContainer>,
352 )>,
353 location: Range<usize>,
354 sub_or_pos: T,
355 ) -> Self {
356 Self {
357 chain_contexts,
358 location,
359 sub_or_pos,
360 }
361 }
362}
363
364impl<T: SubOrPos> AsFea for IgnoreStatement<T> {
365 fn as_fea(&self, indent: &str) -> String {
366 let mut res = String::new();
367 res.push_str(&format!("ignore {} ", self.sub_or_pos));
368 let contexts_str: Vec<String> = self
369 .chain_contexts
370 .iter()
371 .map(|(prefix, glyphs, suffix)| {
372 let mut s = String::new();
373 if !prefix.is_empty() {
374 let prefix_str: Vec<String> = prefix.iter().map(|g| g.as_fea(indent)).collect();
375 s.push_str(&prefix_str.join(" ").to_string());
376 s.push(' ');
377 }
378 let glyphs_str: Vec<String> =
379 glyphs.iter().map(|g| g.as_fea(indent) + "'").collect();
380 s.push_str(&glyphs_str.join(" "));
381 if !suffix.is_empty() {
382 s.push(' ');
383 let suffix_str: Vec<String> = suffix.iter().map(|g| g.as_fea(indent)).collect();
384 s.push_str(&suffix_str.join(" "));
385 }
386 s
387 })
388 .collect();
389 res.push_str(&contexts_str.join(", "));
390 res.push(';');
391 res
392 }
393}
394
395impl From<GsubIgnore> for IgnoreStatement<Subst> {
396 fn from(val: GsubIgnore) -> Self {
397 let mut chain_contexts = Vec::new();
398 for context in val.node().iter_children() {
399 if let Some(chain_context) = fea_rs::typed::IgnoreRule::cast(context) {
400 let prefix = backtrack(chain_context.node());
401 let suffix = lookahead(chain_context.node());
402 let glyphs = context_glyphs(chain_context.node());
403 chain_contexts.push((prefix, glyphs, suffix));
404 }
405 }
406 IgnoreStatement {
407 chain_contexts,
408 location: val.node().range(),
409 sub_or_pos: Subst,
410 }
411 }
412}
413
414impl From<GposIgnore> for IgnoreStatement<Pos> {
415 fn from(val: GposIgnore) -> Self {
416 let mut chain_contexts = Vec::new();
417 for context in val.node().iter_children() {
418 if let Some(chain_context) = fea_rs::typed::IgnoreRule::cast(context) {
419 let prefix = backtrack(chain_context.node());
420 let suffix = lookahead(chain_context.node());
421 let glyphs = context_glyphs(chain_context.node());
422 chain_contexts.push((prefix, glyphs, suffix));
423 }
424 }
425 IgnoreStatement {
426 chain_contexts,
427 location: val.node().range(),
428 sub_or_pos: Pos,
429 }
430 }
431}
432
433pub(crate) trait PotentiallyContextualStatement {
438 fn prefix(&self) -> &[GlyphContainer];
439 fn suffix(&self) -> &[GlyphContainer];
440 fn force_chain(&self) -> bool;
441 fn format_begin(&self, indent: &str) -> String;
442 fn format_contextual_parts(&self, indent: &str) -> Vec<String>;
443 fn format_noncontextual_parts(&self, indent: &str) -> Vec<String>;
444 fn format_end(&self, _indent: &str) -> String {
445 "".to_string()
446 }
447
448 fn is_contextual(&self) -> bool {
449 !self.prefix().is_empty() || !self.suffix().is_empty() || self.force_chain()
450 }
451}
452
453impl<T: PotentiallyContextualStatement> AsFea for T {
454 fn as_fea(&self, indent: &str) -> String {
455 let mut res = String::new();
456 res.push_str(&self.format_begin(indent));
457 if self.is_contextual() {
458 if !self.prefix().is_empty() {
459 let prefix_str: Vec<String> = self.prefix().iter().map(|g| g.as_fea("")).collect();
460 res.push_str(&prefix_str.join(" ").to_string());
461 res.push(' ');
462 }
463 res.push_str(&self.format_contextual_parts(indent).join(" "));
464 if !self.suffix().is_empty() {
465 let suffix_str: Vec<String> = self.suffix().iter().map(|g| g.as_fea("")).collect();
466 res.push_str(&format!(" {}", suffix_str.join(" ")));
467 }
468 } else {
469 res.push_str(self.format_noncontextual_parts(indent).join(" ").as_str());
470 }
471 res.push_str(&self.format_end(indent));
472 res.push(';');
473 res
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use crate::{GlyphClass, GlyphName};
480
481 use super::*;
482
483 #[test]
484 fn test_generate_chain_subst() {
485 let chain_subst = ChainedContextStatement::new(
486 vec![
487 GlyphContainer::GlyphName(GlyphName::new("x")),
488 GlyphContainer::GlyphName(GlyphName::new("y")),
489 GlyphContainer::GlyphName(GlyphName::new("z")),
490 ],
491 vec![GlyphContainer::GlyphClass(GlyphClass::new(
492 vec![
493 GlyphContainer::GlyphName(GlyphName::new("a.smcp")),
494 GlyphContainer::GlyphName(GlyphName::new("b.smcp")),
495 ],
496 0..0,
497 ))],
498 vec![],
499 vec![
500 vec![SmolStr::new("lookup1")],
501 vec![],
502 vec![SmolStr::new("lookup2"), SmolStr::new("lookup3")],
503 ],
504 0..0,
505 Subst,
506 );
507 assert_eq!(
508 chain_subst.as_fea(""),
509 "sub [a.smcp b.smcp] x' lookup lookup1 y' z' lookup lookup2 lookup lookup3;"
510 );
511 }
512
513 #[test]
514 fn chain_context_subst_from_gsub6() {
515 let fea =
516 r#"feature foo { sub x [a b] c' lookup test d' e' lookup bar lookup quux f; } foo;"#;
517 let (parsed, _) = fea_rs::parse::parse_string(fea);
518 let gsub6 = parsed
519 .root()
520 .iter_children()
521 .find_map(fea_rs::typed::Feature::cast)
522 .and_then(|feature| {
523 feature
524 .node()
525 .iter_children()
526 .find_map(fea_rs::typed::Gsub6::cast)
527 })
528 .unwrap();
529 let statement = Statement::from(gsub6);
530 let Statement::ChainedContextSubst(chain_subst) = statement else {
531 panic!("Expected ChainedContextSubstStatement, got {:?}", statement);
532 };
533 assert_eq!(chain_subst.prefix.len(), 2);
534 assert_eq!(chain_subst.suffix.len(), 1);
535 assert_eq!(chain_subst.glyphs.len(), 3);
536 assert_eq!(
537 chain_subst.lookups,
538 vec![
539 vec![SmolStr::new("test")],
540 vec![],
541 vec![SmolStr::new("bar"), SmolStr::new("quux")]
542 ]
543 );
544 }
545
546 #[test]
547 fn chain_context_subst_round_trip() {
548 let fea =
549 r#"feature foo { sub [a b] x' lookup test y' z' lookup bar lookup quux f; } foo;"#;
550 let (parsed, _) = fea_rs::parse::parse_string(fea);
551 let gsub6 = parsed
552 .root()
553 .iter_children()
554 .find_map(fea_rs::typed::Feature::cast)
555 .and_then(|feature| {
556 feature
557 .node()
558 .iter_children()
559 .find_map(fea_rs::typed::Gsub6::cast)
560 })
561 .unwrap();
562 let Statement::ChainedContextSubst(chain_subst) = Statement::from(gsub6) else {
563 panic!("Expected ChainedContextSubstStatement");
564 };
565 let fea_generated = chain_subst.as_fea("");
566 assert_eq!(
567 fea_generated,
568 "sub [a b] x' lookup test y' z' lookup bar lookup quux f;"
569 );
570 }
571
572 #[test]
573 fn generate_ignore_subst() {
574 let ignore_subst = IgnoreStatement::new(
575 vec![
576 (
577 vec![GlyphContainer::GlyphName(GlyphName::new("a"))],
578 vec![GlyphContainer::GlyphName(GlyphName::new("x"))],
579 vec![GlyphContainer::GlyphName(GlyphName::new("b"))],
580 ),
581 (
582 vec![],
583 vec![GlyphContainer::GlyphName(GlyphName::new("y"))],
584 vec![],
585 ),
586 ],
587 0..0,
588 Subst,
589 );
590 assert_eq!(ignore_subst.as_fea(""), "ignore sub a x' b, y';");
591 }
592
593 #[test]
594 fn test_roundtrip_ignore_subst() {
595 let fea = "feature foo { ignore sub a x' b, y'; } foo;";
596 let (parsed, _) = fea_rs::parse::parse_string(fea);
597 let gsub_ignore = parsed
598 .root()
599 .iter_children()
600 .find_map(fea_rs::typed::Feature::cast)
601 .and_then(|feature| {
602 feature
603 .node()
604 .iter_children()
605 .find_map(fea_rs::typed::GsubIgnore::cast)
606 })
607 .unwrap();
608 let ignore_subst = IgnoreStatement::<Subst>::from(gsub_ignore);
609 assert_eq!(ignore_subst.as_fea(""), "ignore sub a x' b, y';");
610 }
611
612 #[test]
613 fn test_roundtrip_gsub1_contextual() {
614 let fea = "feature smcp { sub x a' by a.smcp; } smcp;";
615 let (parsed, _) = fea_rs::parse::parse_string(fea);
616 let gsub6 = parsed
617 .root()
618 .iter_children()
619 .find_map(fea_rs::typed::Feature::cast)
620 .and_then(|feature| {
621 feature
622 .node()
623 .iter_children()
624 .find_map(fea_rs::typed::Gsub6::cast)
625 })
626 .unwrap();
627 let single_subst = Statement::from(gsub6);
628 assert!(
629 matches!(
630 single_subst,
631 Statement::SingleSubst(SingleSubstStatement { .. })
632 ),
633 "Expected SingleSubstStatement, got {:?}",
634 single_subst
635 );
636 assert_eq!(single_subst.as_fea(""), "sub x a' by a.smcp;");
637 }
638
639 #[test]
640 fn test_roundtrip_gsub2_contextual() {
641 let fea = "feature smcp { sub x a' by a b; } smcp;";
642 let (parsed, _) = fea_rs::parse::parse_string(fea);
643 let gsub6 = parsed
644 .root()
645 .iter_children()
646 .find_map(fea_rs::typed::Feature::cast)
647 .and_then(|feature| {
648 feature
649 .node()
650 .iter_children()
651 .find_map(fea_rs::typed::Gsub6::cast)
652 })
653 .unwrap();
654 let mult_subst = Statement::from(gsub6);
655 assert!(
656 matches!(
657 mult_subst,
658 Statement::MultipleSubst(MultipleSubstStatement { .. })
659 ),
660 "Expected MultipleSubstStatement, got {:?}",
661 mult_subst
662 );
663 assert_eq!(mult_subst.as_fea(""), "sub x a' by a b;");
664 }
665
666 #[test]
667 fn test_roundtrip_gsub4_contextual() {
668 let fea = "feature smcp { sub x a' b' by a; } smcp;";
669 let (parsed, _) = fea_rs::parse::parse_string(fea);
670 let gsub6 = parsed
671 .root()
672 .iter_children()
673 .find_map(fea_rs::typed::Feature::cast)
674 .and_then(|feature| {
675 feature
676 .node()
677 .iter_children()
678 .find_map(fea_rs::typed::Gsub6::cast)
679 })
680 .unwrap();
681 let liga_subst = Statement::from(gsub6);
682 assert!(
683 matches!(
684 liga_subst,
685 Statement::LigatureSubst(LigatureSubstStatement { .. })
686 ),
687 "Expected LigatureSubstStatement, got {:?}",
688 liga_subst
689 );
690 assert_eq!(
691 liga_subst.as_fea(""),
692 "sub x a' b' by a;",
693 "{:#?}",
694 liga_subst
695 );
696 }
697}