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}