tfm/ligkern/mod.rs
1//! Lig/kern programming.
2//!
3//! TFM files can include information about ligatures and kerns.
4//! A [ligature](https://en.wikipedia.org/wiki/Ligature_(writing))
5//! is a special character that can replace two or more adjacent characters.
6//! For example, the pair of characters ae can be replaced by the æ ligature which is a single character.
7//! A [kern](https://en.wikipedia.org/wiki/Kerning) is special space inserted between
8//! two adjacent characters to align them better.
9//! For example, a kern can be inserted between A and V to compensate for the large
10//! amount of space created by the specific combination of these two characters.
11//!
12//! ## The lig/kern programming language
13//!
14//! TFM provides ligature and kern data in the form of
15//! "instructions in a simple programming language that explains what to do for special letter pairs"
16//! (quoting TFtoPL.2014.13).
17//! This lig/kern programming language can be used to specify instructions like
18//! "replace the pair (a,e) by æ" and
19//! "insert a kern of width -0.1pt between the pair (A,V)".
20//! But it can also specify more complex behaviors.
21//! For example, a lig/kern program can specify "replace the pair (x,y) by the pair (z,y)".
22//!
23//! In general for any pair of characters (x,y) the program specifies zero or one lig/kern instructions.
24//! After this instruction is executed, there may be a new
25//! pair of characters remaining, as in the (x,y) to (z,y) instruction.
26//! The lig/kern instruction for this pair is then executed, if it exists.
27//! This process continues until there are no more instructions left to run.
28//!
29//! Lig/kern instructions are represented in this module by the [`lang::Instruction`] type.
30//!
31//! ## Related code by Knuth
32//!
33//! The TFtoPL and PLtoTF programs don't contain any code for running lig/kern programs.
34//! They only contain logic for translating between the `.tfm` and `.pl`
35//! formats for lig/kern programs, and for doing some validation as described below.
36//! Lig/kern programs are actually executed in TeX; see KnuthTeX.2021.1032-1040.
37//!
38//! One of the challenges with lig/kern programs is that they can contain infinite loops.
39//! Here is a simple example of a lig/kern program with two instruction and an infinite loop:
40//!
41//! - Replace (x,y) with (z,y) (in property list format, `(LABEL C x)(LIG/ C y C z)`)
42//! - Replace (z,y) with (x,y) (in property list format, `(LABEL C z)(LIG/ C y C x)`)
43//!
44//! When this program runs (x,y) will be swapped with (z,y) ad infinitum.
45//! See TFtoPL.2014.88 for more examples.
46//!
47//! Both TFtoPL and PLtoTF contain code that checks that a lig/kern program
48//! does not contain infinite loops (TFtoPL.2014.88-95 and PLtoTF.2014.116-125).
49//! The algorithm for detecting infinite loops is a topological sorting algorithm
50//! over a graph where each node is a pair of characters.
51//! However it's a bit complicated because the full graph cannot be constructed without
52//! running the lig/kern program.
53//!
54//! TeX does not check for infinite loops, presumably under the assumption that any `.tfm` file will have
55//! been generated by PLtoTF and thus already validated.
56//! However TeX does check for interrupts when executing lig/kern programs so that
57//! at least a user can terminate TeX if an infinite loop is hit.
58//! (See the `check_interrupt` line in KnuthTeX.2021.1040.)
59//!
60//! ## Functionality in this module
61//!
62//! This module handles lig/kern programs in a different way,
63//! inspired by the ["parse don't validate"](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
64//! philosophy.
65//! This module is able to represent raw lig/kern programs as a vector of [`lang::Instruction`] values.
66//! But can also _compile_ lig/kern programs (into a [`CompiledProgram`]).
67//! This compilation process essentially executes the lig/kern program for every possible character pair.
68//! The result is a map from each character pair to the full list of
69//! replacement characters and kerns for that pair.
70//! If there is an infinite loop in the program this compilation will naturally fail.
71//! The compiled program is thus a "parsed" version of the lig/kern program
72//! and it is impossible for infinite loops to appear in it.
73//!
74//! An advantage of this model is that the lig/kern program does not need to be repeatedly
75//! executed in the main hot loop of TeX.
76//! This may make TeX faster.
77//! However the compiled lig/kern program does have a larger memory footprint than the raw program,
78//! and so it may be slower if TeX is memory bound.
79
80mod compiler;
81use crate::ligkern::compiler::Replacement;
82use crate::Char;
83use crate::FixWord;
84use std::collections::HashMap;
85use std::rc::Rc;
86pub mod lang;
87
88/// A compiled lig/kern program.
89///
90/// The default value is an empty program with no kerns or ligatures.
91#[derive(Clone, Debug, Default)]
92pub struct CompiledProgram {
93 right_boundary_char: Option<Char>,
94 replacements: HashMap<(Option<Char>, Char), compiler::Replacement>,
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98enum IntermediateOp {
99 // Emit the kern.
100 Kern(common::Scaled),
101 // Emit the char in the payload.
102 C(compiler::C),
103}
104
105impl CompiledProgram {
106 /// Compile a lig/kern program.
107 pub fn compile(
108 program: &lang::Program,
109 design_size: FixWord,
110 kerns: &[FixWord],
111 entrypoints: HashMap<Char, u16>,
112 ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
113 compiler::compile(program, design_size, kerns, &entrypoints)
114 }
115
116 /// Compile a lig/kern program from a PL file.
117 pub fn compile_from_pl_file(
118 pl_file: &super::pl::File,
119 ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
120 let entrypoints = pl_file.lig_kern_entrypoints(false);
121 // The kerns array, which is used for KernAtIndex operations, is empty
122 // because PL files do not have such operations.
123 CompiledProgram::compile(
124 &pl_file.lig_kern_program,
125 pl_file.header.design_size,
126 &[],
127 entrypoints,
128 )
129 }
130
131 /// Compile a lig/kern program from a TFM file.
132 pub fn compile_from_tfm_file(
133 tfm_file: &mut super::File,
134 ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
135 let entrypoints: HashMap<Char, u16> = tfm_file
136 .lig_kern_entrypoints()
137 .into_iter()
138 .filter_map(|(c, e)| {
139 tfm_file
140 .lig_kern_program
141 .unpack_entrypoint(e)
142 .ok()
143 .map(|e| (c, e))
144 })
145 .collect();
146 CompiledProgram::compile(
147 &tfm_file.lig_kern_program,
148 tfm_file.header.design_size,
149 &tfm_file.kerns,
150 entrypoints,
151 )
152 }
153
154 fn get_replacement(
155 &self,
156 left_char: Option<Char>,
157 right_char: Option<Char>,
158 ) -> Option<&Replacement> {
159 let right_char = match right_char {
160 None => self.right_boundary_char?,
161 Some(c) => c,
162 };
163 self.replacements.get(&(left_char, right_char))
164 }
165
166 fn get_replacement_utf8(
167 &self,
168 left_char: Option<char>,
169 right_char: Option<char>,
170 ) -> Option<&Replacement> {
171 let left_char = match left_char {
172 None => None,
173 Some(c) => {
174 let Ok(c) = c.try_into() else {
175 return None;
176 };
177 Some(c)
178 }
179 };
180 let right_char = match right_char {
181 None => None,
182 Some(c) => {
183 let Ok(c) = c.try_into() else {
184 return None;
185 };
186 Some(c)
187 }
188 };
189 self.get_replacement(left_char, right_char)
190 }
191
192 /// Returns an iterator over all pairs `(char,char)` that have a replacement
193 /// specified in the lig/kern program.
194 pub fn all_pairs_with_replacements(&self) -> Vec<(Option<Char>, Char)> {
195 let mut v: Vec<(Option<Char>, Char)> = self.replacements.keys().copied().collect();
196 v.sort();
197 v
198 }
199
200 /// Returns whether this program is seven-bit safe.
201 ///
202 /// A lig/kern program is seven-bit safe if the replacement for any
203 /// pair of seven-bit safe characters
204 /// consists only of seven-bit characters.
205 /// Conversely a program is seven-bit unsafe if there is a
206 /// pair of seven-bit characters whose replacement
207 /// contains a non-seven-bit character.
208 pub fn is_seven_bit_safe(&self) -> bool {
209 self.all_pairs_with_replacements()
210 .into_iter()
211 .filter(|(l, r)| l.map(|c| c.is_seven_bit()).unwrap_or(true) && r.is_seven_bit())
212 .flat_map(|(l, r)| self.get_replacement(l, Some(r)))
213 .all(|rep| {
214 rep.0.iter().all(|op| match op {
215 IntermediateOp::Kern(_) => true,
216 IntermediateOp::C(c) => c.c.is_seven_bit(),
217 }) && rep.1.c.is_seven_bit()
218 })
219 }
220
221 /// Run this lig/kern program.
222 pub fn run<T: Emitter>(&self, word: &str, emitter: &mut T) {
223 struct PendingLigature {
224 s: String,
225 includes_left_boundary: bool,
226 includes_right_boundary: bool,
227 }
228 impl PendingLigature {
229 fn into_ligature(self, c: char) -> Ligature {
230 Ligature {
231 c,
232 original: self.s.into(),
233 includes_left_boundary: self.includes_left_boundary,
234 includes_right_boundary: self.includes_right_boundary,
235 }
236 }
237 }
238 let mut ligature: Option<PendingLigature> = None;
239 let mut iter = word.chars();
240 let mut left: Option<char> = None;
241 let mut left_in_original = true;
242 loop {
243 let right = iter.next();
244 if left.is_none() && right.is_none() {
245 break;
246 }
247
248 match self.get_replacement_utf8(left, right) {
249 Some(replacement) => {
250 for op in &replacement.0 {
251 match op {
252 IntermediateOp::Kern(kern) => emitter.emit_kern(*kern),
253 IntermediateOp::C(compiler::C {
254 c,
255 is_lig: false,
256 consumes_left,
257 consumes_right,
258 }) => {
259 debug_assert!(consumes_left);
260 debug_assert!(!consumes_right);
261 match ligature.take() {
262 Some(l) => {
263 // This happens when left is a ligature.
264 emitter.emit_ligature(l.into_ligature((*c).into()));
265 }
266 None => {
267 emitter.emit_character((*c).into());
268 }
269 }
270 }
271 IntermediateOp::C(compiler::C {
272 c,
273 is_lig: true,
274 consumes_left,
275 consumes_right,
276 }) => {
277 let mut s = ligature.take().unwrap_or(PendingLigature {
278 s: Default::default(),
279 includes_left_boundary: false,
280 includes_right_boundary: false,
281 });
282 if *consumes_left && left_in_original {
283 // TODO: figure out where in TeX the '|' comes from!
284 s.s.push(left.unwrap_or('|'));
285 s.includes_left_boundary = left.is_none();
286 }
287 if *consumes_right {
288 s.s.push(right.unwrap_or('|'));
289 s.includes_right_boundary = right.is_none();
290 }
291 emitter.emit_ligature(s.into_ligature((*c).into()));
292 }
293 }
294 }
295 match replacement.1 {
296 compiler::C {
297 c,
298 is_lig: false,
299 consumes_left: _,
300 consumes_right,
301 } => {
302 debug_assert!(consumes_right);
303 if right.is_none() {
304 left = None;
305 } else {
306 left = Some(c.into());
307 left_in_original = true; // = consumes_right;
308 }
309 }
310 compiler::C {
311 c,
312 is_lig: true,
313 consumes_left,
314 consumes_right,
315 } => {
316 let s = ligature.get_or_insert(PendingLigature {
317 s: Default::default(),
318 includes_left_boundary: false,
319 includes_right_boundary: false,
320 });
321 if consumes_left && left_in_original {
322 s.s.push(left.unwrap_or('|'));
323 s.includes_left_boundary = left.is_none();
324 }
325 if consumes_right {
326 s.s.push(right.unwrap_or('|'));
327 s.includes_right_boundary = right.is_none();
328 }
329 left = Some(c.into());
330 left_in_original = false;
331 }
332 }
333 }
334 None => {
335 if let Some(left) = left {
336 match ligature.take() {
337 Some(l) => {
338 emitter.emit_ligature(l.into_ligature(left));
339 }
340 None => {
341 emitter.emit_character(left);
342 }
343 }
344 }
345 left = right;
346 left_in_original = true;
347 }
348 }
349 }
350 }
351}
352
353/// Implementations of this trait determine how characters, kerns and ligatures
354/// are handled when running a lig/kern program.
355pub trait Emitter {
356 fn emit_character(&mut self, c: char);
357 fn emit_kern(&mut self, kern: common::Scaled);
358 fn emit_ligature(&mut self, ligature: Ligature);
359}
360
361#[derive(PartialEq, Debug)]
362pub struct Ligature {
363 pub c: char,
364 pub original: Rc<str>,
365 includes_left_boundary: bool,
366 includes_right_boundary: bool,
367}
368
369/// An error returned from lig/kern compilation.
370///
371/// TODO: rename Cycle everywhere including the docs
372#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct InfiniteLoopError {
374 /// The pair of characters the starts the infinite loop.
375 pub starting_pair: (Option<Char>, Char),
376}
377
378impl InfiniteLoopError {
379 pub fn pltotf_message(&self) -> String {
380 let left = match self.starting_pair.0 {
381 Some(c) => format!["'{:03o}", c.0],
382 None => "boundary".to_string(),
383 };
384 format!(
385 "Infinite ligature loop starting with {} and '{:03o}!",
386 left, self.starting_pair.1 .0
387 )
388 }
389 pub fn pltotf_section(&self) -> u8 {
390 125
391 }
392}
393
394/// One step in a lig/kern infinite loop.
395///
396/// A vector of these steps is returned in a [`InfiniteLoopError`].
397#[derive(Clone, Debug, PartialEq, Eq)]
398pub struct InfiniteLoopStep {
399 /// The index of the instruction to apply in this step.
400 pub instruction_index: usize,
401 /// The replacement text after applying this step.
402 ///
403 /// The boolean specifies whether the replacement begins with the
404 /// left boundary char.
405 pub post_replacement: (bool, Vec<Char>),
406 /// The position of the cursor after applying this step.
407 pub post_cursor_position: usize,
408}
409
410#[cfg(test)]
411mod tests {
412 use common::Scaled;
413
414 use super::Ligature as L;
415 use super::*;
416
417 const LIGAROO: &'static str = include_str!["ligaroo.plst"];
418
419 #[derive(PartialEq, Debug)]
420 enum Element {
421 Char(char),
422 Kern(common::Scaled),
423 Ligature(L),
424 }
425
426 #[derive(Default)]
427 struct ElementEmitter(Vec<Element>);
428
429 impl Emitter for ElementEmitter {
430 fn emit_character(&mut self, c: char) {
431 self.0.push(Element::Char(c))
432 }
433
434 fn emit_kern(&mut self, kern: common::Scaled) {
435 self.0.push(Element::Kern(kern))
436 }
437
438 fn emit_ligature(&mut self, ligature: L) {
439 self.0.push(Element::Ligature(ligature))
440 }
441 }
442
443 fn run_test(program: &str, input: &str, want: Vec<Element>) {
444 let source = LIGAROO.replace("(LIGTABLE", &format!["(LIGTABLE\n{program}"]);
445 let pl_file = crate::pl::File::from_pl_source_code(&source).0;
446 let program = CompiledProgram::compile_from_pl_file(&pl_file).0;
447 let mut emitter: ElementEmitter = Default::default();
448 program.run(input, &mut emitter);
449 assert_eq!(emitter.0, want);
450 }
451
452 macro_rules! tests {
453 ( $(
454 ($name: ident, $program: expr, $input: expr, $want: expr, ),
455 )+ ) => { $(
456 #[test]
457 fn $name() {
458 use Element::*;
459 let program = $program;
460 let input = $input;
461 let want = $want;
462 run_test(program, input, want);
463 }
464 )+ };
465 }
466
467 tests!(
468 // AB -> ^1
469 (
470 single_lig_1,
471 "
472 (LABEL C A)
473 (LIG C B C 1)
474 (KRN C 1 R 0.1)
475 (STOP)
476
477 (LABEL C 1)
478 (KRN C B R 0.3)
479 (STOP)
480 ",
481 "AB",
482 vec![Ligature(L {
483 c: '1',
484 original: "AB".into(),
485 includes_left_boundary: false,
486 includes_right_boundary: false,
487 })],
488 ),
489 // AB -> ^A1
490 (
491 single_lig_2,
492 "
493 (LABEL C A)
494 (/LIG C B C 1)
495 (KRN C 1 R 0.1)
496 (STOP)
497
498 (LABEL C 1)
499 (KRN C B R 0.3)
500 (STOP)
501 ",
502 "AB",
503 vec![
504 Char('A'),
505 Kern(Scaled::ONE),
506 Ligature(L {
507 c: '1',
508 original: "B".into(),
509 includes_left_boundary: false,
510 includes_right_boundary: false,
511 })
512 ],
513 ),
514 // AB -> A^1
515 (
516 single_lig_3,
517 "
518 (LABEL C A)
519 (/LIG> C B C 1)
520 (KRN C 1 R 0.1)
521 (STOP)
522
523 (LABEL C 1)
524 (KRN C B R 0.3)
525 (STOP)
526 ",
527 "AB",
528 vec![
529 Char('A'),
530 Ligature(L {
531 c: '1',
532 original: "B".into(),
533 includes_left_boundary: false,
534 includes_right_boundary: false,
535 }),
536 ],
537 ),
538 // AB -> ^1B
539 (
540 single_lig_4,
541 "
542 (LABEL C A)
543 (LIG/ C B C 1)
544 (KRN C 1 R 0.1)
545 (STOP)
546
547 (LABEL C 1)
548 (KRN C B R 0.3)
549 (STOP)
550 ",
551 "AB",
552 vec![
553 Ligature(L {
554 c: '1',
555 original: "A".into(),
556 includes_left_boundary: false,
557 includes_right_boundary: false,
558 }),
559 Kern(Scaled::ONE * 3),
560 Char('B'),
561 ],
562 ),
563 // AB -> 1^B
564 (
565 single_lig_5,
566 "
567 (LABEL C A)
568 (LIG/> C B C 1)
569 (KRN C 1 R 0.1)
570 (STOP)
571
572 (LABEL C 1)
573 (KRN C B R 0.3)
574 (STOP)
575 ",
576 "AB",
577 vec![
578 Ligature(L {
579 c: '1',
580 original: "A".into(),
581 includes_left_boundary: false,
582 includes_right_boundary: false,
583 }),
584 Char('B'),
585 ],
586 ),
587 // AB -> ^A1B
588 (
589 single_lig_6,
590 "
591 (LABEL C A)
592 (/LIG/ C B C 1)
593 (KRN C 1 R 0.1)
594 (STOP)
595
596 (LABEL C 1)
597 (KRN C B R 0.3)
598 (STOP)
599 ",
600 "AB",
601 vec![
602 Char('A'),
603 Kern(Scaled::ONE),
604 Ligature(L {
605 c: '1',
606 original: "".into(),
607 includes_left_boundary: false,
608 includes_right_boundary: false,
609 }),
610 Kern(Scaled::ONE * 3),
611 Char('B'),
612 ],
613 ),
614 // AB -> A^1B
615 (
616 single_lig_7,
617 "
618 (LABEL C A)
619 (/LIG/> C B C 1)
620 (KRN C 1 R 0.1)
621 (STOP)
622
623 (LABEL C 1)
624 (KRN C B R 0.3)
625 (STOP)
626 ",
627 "AB",
628 vec![
629 Char('A'),
630 Ligature(L {
631 c: '1',
632 original: "".into(),
633 includes_left_boundary: false,
634 includes_right_boundary: false,
635 }),
636 Kern(Scaled::ONE * 3),
637 Char('B'),
638 ],
639 ),
640 // AB -> A1^B
641 (
642 single_lig_8,
643 "
644 (LABEL C A)
645 (/LIG/>> C B C 1)
646 (KRN C 1 R 0.1)
647 (STOP)
648
649 (LABEL C 1)
650 (KRN C B R 0.3)
651 (STOP)
652 ",
653 "AB",
654 vec![
655 Char('A'),
656 Ligature(L {
657 c: '1',
658 original: "".into(),
659 includes_left_boundary: false,
660 includes_right_boundary: false,
661 }),
662 Char('B'),
663 ],
664 ),
665 // AB -> A^B
666 // This is the same as single_lig_5, but the replacement character
667 // is the same as the character that is removed. In theory lig(A, A)
668 // could be replaced by char(A), and this test verifies that it is not.
669 (
670 no_op_lig,
671 "
672 (LABEL C A)
673 (LIG/> C B C A)
674 (STOP)
675 ",
676 "AB",
677 vec![
678 Ligature(L {
679 c: 'A',
680 original: "A".into(),
681 includes_left_boundary: false,
682 includes_right_boundary: false,
683 }),
684 Char('B'),
685 ],
686 ),
687 // AB -> ^1, 1C -> ^2
688 (
689 multiple_lig_1,
690 "
691 (LABEL C A)
692 (LIG C B C 1)
693
694 (LABEL C 1)
695 (LIG C C C 2)
696 (STOP)
697 ",
698 "ABC",
699 vec![Ligature(L {
700 c: '2',
701 original: "ABC".into(),
702 includes_left_boundary: false,
703 includes_right_boundary: false,
704 }),],
705 ),
706 // AB -> ^A1B, 1B -> 2
707 (
708 multiple_lig_2,
709 "
710 (LABEL C A)
711 (/LIG/ C B C 1)
712 (LABEL C 1)
713 (LIG C B C 2)
714 (STOP)
715 ",
716 "AB",
717 vec![
718 Char('A'),
719 Ligature(L {
720 c: '2',
721 original: "B".into(),
722 includes_left_boundary: false,
723 includes_right_boundary: false,
724 }),
725 ],
726 ),
727 // AA -> 1^A multiple times
728 (
729 multiple_lig_3,
730 "
731 (LABEL C A)
732 (LIG/ C A C 1)
733 (STOP)
734 ",
735 "AAAAA",
736 vec![
737 Ligature(L {
738 c: '1',
739 original: "A".into(),
740 includes_left_boundary: false,
741 includes_right_boundary: false,
742 }),
743 Ligature(L {
744 c: '1',
745 original: "A".into(),
746 includes_left_boundary: false,
747 includes_right_boundary: false,
748 }),
749 Ligature(L {
750 c: '1',
751 original: "A".into(),
752 includes_left_boundary: false,
753 includes_right_boundary: false,
754 }),
755 Ligature(L {
756 c: '1',
757 original: "A".into(),
758 includes_left_boundary: false,
759 includes_right_boundary: false,
760 }),
761 Char('A'),
762 ],
763 ),
764 // AA -> ^A multiple times
765 (
766 multiple_lig_4,
767 "
768 (LABEL C A)
769 (LIG C A C A)
770 (STOP)
771 ",
772 "AAAAAA",
773 vec![Ligature(L {
774 c: 'A',
775 original: "AAAAAA".into(),
776 includes_left_boundary: false,
777 includes_right_boundary: false,
778 }),],
779 ),
780 // AB -> ^A1, A1 -> ^21
781 (
782 multiple_lig_5,
783 "
784 (LABEL C A)
785 (/LIG C B C 1)
786 (LIG/ C 1 C 2)
787 (STOP)
788 ",
789 "AB",
790 vec![
791 Ligature(L {
792 c: '2',
793 original: "A".into(),
794 includes_left_boundary: false,
795 includes_right_boundary: false,
796 }),
797 Ligature(L {
798 c: '1',
799 original: "B".into(),
800 includes_left_boundary: false,
801 includes_right_boundary: false,
802 }),
803 ],
804 ),
805 // AB -> ^A1, A1 -> ^21, 21 -> 3
806 (
807 multiple_lig_6,
808 "
809 (LABEL C A)
810 (/LIG C B C 1)
811 (LIG/ C 1 C 2)
812 (LABEL C 2)
813 (LIG C 1 C 3)
814 (STOP)
815 ",
816 "AB",
817 vec![Ligature(L {
818 c: '3',
819 original: "AB".into(),
820 includes_left_boundary: false,
821 includes_right_boundary: false,
822 })],
823 ),
824 // AB -> ^1, 1C -> 12^C
825 (
826 multiple_lig_7,
827 "
828 (LABEL C A)
829 (LIG C B C 1)
830 (STOP)
831
832 (LABEL C 1)
833 (/LIG/>> C C C 2)
834 (STOP)
835 ",
836 "ABC",
837 vec![
838 Ligature(L {
839 c: '1',
840 original: "AB".into(),
841 includes_left_boundary: false,
842 includes_right_boundary: false,
843 }),
844 Ligature(L {
845 c: '2',
846 original: "".into(),
847 includes_left_boundary: false,
848 includes_right_boundary: false,
849 }),
850 Char('C'),
851 ],
852 ),
853 (
854 kern_after_lig_1,
855 "
856 (LABEL C A)
857 (LIG C B C 1)
858 (STOP)
859
860 (LABEL C 1)
861 (KRN C C R 0.1)
862 ",
863 "ABC",
864 vec![
865 Ligature(L {
866 c: '1',
867 original: "AB".into(),
868 includes_left_boundary: false,
869 includes_right_boundary: false,
870 }),
871 Kern(Scaled::ONE),
872 Char('C'),
873 ],
874 ),
875 (
876 kern_after_lig_2,
877 "
878 (LABEL C A)
879 (LIG C B C 1)
880 (STOP)
881
882 (LABEL C 1)
883 (KRN C A R 0.1)
884 ",
885 "ABAB",
886 vec![
887 Ligature(L {
888 c: '1',
889 original: "AB".into(),
890 includes_left_boundary: false,
891 includes_right_boundary: false,
892 }),
893 Kern(Scaled::ONE),
894 Ligature(L {
895 c: '1',
896 original: "AB".into(),
897 includes_left_boundary: false,
898 includes_right_boundary: false,
899 }),
900 ],
901 ),
902 (
903 left_boundary_char_1,
904 "
905 (LABEL BOUNDARYCHAR)
906 (LIG C A C 1)
907 ",
908 "A",
909 vec![Ligature(L {
910 c: '1',
911 original: "|A".into(),
912 includes_left_boundary: true,
913 includes_right_boundary: false,
914 }),],
915 ),
916 (
917 left_boundary_char_2,
918 "
919 (LABEL BOUNDARYCHAR)
920 (/LIG/ C A C 1)
921 (/LIG/ C 1 C 2)
922 ",
923 "A",
924 vec![
925 Ligature(L {
926 c: '2',
927 original: "|".into(),
928 includes_left_boundary: true,
929 includes_right_boundary: false,
930 }),
931 Ligature(L {
932 c: '1',
933 original: "".into(),
934 includes_left_boundary: false,
935 includes_right_boundary: false,
936 }),
937 Char('A'),
938 ],
939 ),
940 (
941 left_boundary_char_3,
942 "
943 (LABEL BOUNDARYCHAR)
944 (/LIG/ C A C 1)
945 ",
946 "A",
947 vec![
948 Ligature(L {
949 c: '1',
950 original: "|".into(),
951 includes_left_boundary: true,
952 includes_right_boundary: false,
953 }),
954 Char('A'),
955 ],
956 ),
957 /*
958 (
959 right_boundary_char_1,
960 "
961 (LABEL C M)
962 (/LIG/ C L C N)
963 (STOP)
964 ",
965 "N",
966 vec![Char('N'), Ligature(L { c: 'Q', original: "|".into() }),],
967 ),
968 (
969 right_boundary_char_2,
970 "
971 (LABEL C N)
972 (/LIG/ C L C Q)
973 (STOP)
974 ",
975 "M",
976 vec![
977 Char('M'),
978 Ligature(L { c: 'N', original: "".into() }),
979 Ligature(L { c: 'Q', original: "|".into() }),
980 ],
981 ),
982 */
983 (
984 right_boundary_char_3,
985 "
986 (LABEL C A)
987 (LIG C L C 1)
988 (STOP)
989 ",
990 "A",
991 vec![Ligature(L {
992 c: '1',
993 original: "A|".into(),
994 includes_left_boundary: false,
995 includes_right_boundary: true,
996 }),],
997 ),
998 (
999 right_boundary_char_4,
1000 "
1001 (LABEL C A)
1002 (KRN C L R 1)
1003 (STOP)
1004 ",
1005 "A",
1006 vec![Char('A'), Kern(Scaled::ONE * 10),],
1007 ),
1008 );
1009}