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    pub fn has_replacement(&self, left_char: Option<char>, right_char: Option<char>) -> bool {
155        self.get_replacement_utf8(left_char, right_char).is_some()
156    }
157
158    fn get_replacement(
159        &self,
160        left_char: Option<Char>,
161        right_char: Option<Char>,
162    ) -> Option<&Replacement> {
163        let right_char = match right_char {
164            None => self.right_boundary_char?,
165            Some(c) => c,
166        };
167        self.replacements.get(&(left_char, right_char))
168    }
169
170    fn get_replacement_utf8(
171        &self,
172        left_char: Option<char>,
173        right_char: Option<char>,
174    ) -> Option<&Replacement> {
175        let left_char = match left_char {
176            None => None,
177            Some(c) => {
178                let Ok(c) = c.try_into() else {
179                    return None;
180                };
181                Some(c)
182            }
183        };
184        let right_char = match right_char {
185            None => None,
186            Some(c) => {
187                let Ok(c) = c.try_into() else {
188                    return None;
189                };
190                Some(c)
191            }
192        };
193        self.get_replacement(left_char, right_char)
194    }
195
196    /// Returns an iterator over all pairs `(char,char)` that have a replacement
197    ///     specified in the lig/kern program.
198    pub fn all_pairs_with_replacements(&self) -> Vec<(Option<Char>, Char)> {
199        let mut v: Vec<(Option<Char>, Char)> = self.replacements.keys().copied().collect();
200        v.sort();
201        v
202    }
203
204    /// Returns whether this program is seven-bit safe.
205    ///
206    /// A lig/kern program is seven-bit safe if the replacement for any
207    ///     pair of seven-bit safe characters
208    ///     consists only of seven-bit characters.
209    /// Conversely a program is seven-bit unsafe if there is a
210    ///     pair of seven-bit characters whose replacement
211    ///     contains a non-seven-bit character.
212    pub fn is_seven_bit_safe(&self) -> bool {
213        self.all_pairs_with_replacements()
214            .into_iter()
215            .filter(|(l, r)| l.map(|c| c.is_seven_bit()).unwrap_or(true) && r.is_seven_bit())
216            .flat_map(|(l, r)| self.get_replacement(l, Some(r)))
217            .all(|rep| {
218                rep.0.iter().all(|op| match op {
219                    IntermediateOp::Kern(_) => true,
220                    IntermediateOp::C(c) => c.c.is_seven_bit(),
221                }) && rep.1.c.is_seven_bit()
222            })
223    }
224
225    /// Run this lig/kern program.
226    pub fn run<T: Emitter>(&self, word: &str, emitter: &mut T) {
227        struct PendingLigature {
228            s: String,
229            includes_left_boundary: bool,
230            includes_right_boundary: bool,
231        }
232        impl PendingLigature {
233            fn into_ligature(self, c: char) -> Ligature {
234                Ligature {
235                    c,
236                    original: self.s.into(),
237                    includes_left_boundary: self.includes_left_boundary,
238                    includes_right_boundary: self.includes_right_boundary,
239                }
240            }
241        }
242        let mut ligature: Option<PendingLigature> = None;
243        let mut iter = word.chars();
244        let mut left: Option<char> = None;
245        let mut left_in_original = true;
246        loop {
247            let right = iter.next();
248            if left.is_none() && right.is_none() {
249                break;
250            }
251
252            match self.get_replacement_utf8(left, right) {
253                Some(replacement) => {
254                    for op in &replacement.0 {
255                        match op {
256                            IntermediateOp::Kern(kern) => emitter.emit_kern(*kern),
257                            IntermediateOp::C(compiler::C {
258                                c,
259                                is_lig: false,
260                                consumes_left,
261                                consumes_right,
262                            }) => {
263                                debug_assert!(consumes_left);
264                                debug_assert!(!consumes_right);
265                                match ligature.take() {
266                                    Some(l) => {
267                                        // This happens when left is a ligature.
268                                        emitter.emit_ligature(l.into_ligature((*c).into()));
269                                    }
270                                    None => {
271                                        emitter.emit_character((*c).into());
272                                    }
273                                }
274                            }
275                            IntermediateOp::C(compiler::C {
276                                c,
277                                is_lig: true,
278                                consumes_left,
279                                consumes_right,
280                            }) => {
281                                let mut s = ligature.take().unwrap_or(PendingLigature {
282                                    s: Default::default(),
283                                    includes_left_boundary: false,
284                                    includes_right_boundary: false,
285                                });
286                                if *consumes_left && left_in_original {
287                                    // TODO: figure out where in TeX the '|' comes from!
288                                    s.s.push(left.unwrap_or('|'));
289                                    s.includes_left_boundary = left.is_none();
290                                }
291                                if *consumes_right {
292                                    s.s.push(right.unwrap_or('|'));
293                                    s.includes_right_boundary = right.is_none();
294                                }
295                                emitter.emit_ligature(s.into_ligature((*c).into()));
296                            }
297                        }
298                    }
299                    match replacement.1 {
300                        compiler::C {
301                            c,
302                            is_lig: false,
303                            consumes_left: _,
304                            consumes_right,
305                        } => {
306                            debug_assert!(consumes_right);
307                            if right.is_none() {
308                                left = None;
309                            } else {
310                                left = Some(c.into());
311                                left_in_original = true; // = consumes_right;
312                            }
313                        }
314                        compiler::C {
315                            c,
316                            is_lig: true,
317                            consumes_left,
318                            consumes_right,
319                        } => {
320                            let s = ligature.get_or_insert(PendingLigature {
321                                s: Default::default(),
322                                includes_left_boundary: false,
323                                includes_right_boundary: false,
324                            });
325                            if consumes_left && left_in_original {
326                                s.s.push(left.unwrap_or('|'));
327                                s.includes_left_boundary = left.is_none();
328                            }
329                            if consumes_right {
330                                s.s.push(right.unwrap_or('|'));
331                                s.includes_right_boundary = right.is_none();
332                            }
333                            left = Some(c.into());
334                            left_in_original = false;
335                        }
336                    }
337                }
338                None => {
339                    if let Some(left) = left {
340                        match ligature.take() {
341                            Some(l) => {
342                                emitter.emit_ligature(l.into_ligature(left));
343                            }
344                            None => {
345                                emitter.emit_character(left);
346                            }
347                        }
348                    }
349                    left = right;
350                    left_in_original = true;
351                }
352            }
353        }
354    }
355
356    pub fn run_iter<'a>(
357        &'a self,
358        word: &'a str,
359        right_boundary_override: Option<char>,
360    ) -> RunIter<'a> {
361        RunIter {
362            program: self,
363            word: word.chars(),
364            intermediate_ops: &[],
365            terminal_op: None,
366            ligature: None,
367            left_in_original: true,
368            left: None,
369            right: None,
370            right_boundary_override,
371        }
372    }
373}
374
375#[derive(PartialEq, Debug)]
376pub enum RunItem {
377    Char(char),
378    Kern(common::Scaled),
379    Ligature(Ligature),
380}
381
382struct PendingLigature {
383    s: String,
384    includes_left_boundary: bool,
385    includes_right_boundary: bool,
386}
387impl PendingLigature {
388    fn into_ligature(self, c: char) -> Ligature {
389        Ligature {
390            c,
391            original: self.s.into(),
392            includes_left_boundary: self.includes_left_boundary,
393            includes_right_boundary: self.includes_right_boundary,
394        }
395    }
396}
397
398pub struct RunIter<'a> {
399    program: &'a CompiledProgram,
400    word: std::str::Chars<'a>,
401    intermediate_ops: &'a [IntermediateOp],
402    terminal_op: Option<compiler::C>,
403    ligature: Option<PendingLigature>,
404    left_in_original: bool, // default true
405    left: Option<char>,
406    right: Option<char>,
407    right_boundary_override: Option<char>,
408}
409
410impl<'a> RunIter<'a> {
411    pub fn is_separation_point(&self) -> bool {
412        self.intermediate_ops.is_empty() && self.left_in_original
413    }
414}
415
416impl<'a> Iterator for RunIter<'a> {
417    type Item = RunItem;
418
419    fn next(&mut self) -> Option<Self::Item> {
420        let next = if let Some((op, tail)) = self.intermediate_ops.split_first() {
421            self.intermediate_ops = tail;
422            Some(match op {
423                IntermediateOp::Kern(kern) => RunItem::Kern(*kern),
424                IntermediateOp::C(compiler::C {
425                    c,
426                    is_lig: false,
427                    consumes_left,
428                    consumes_right,
429                }) => {
430                    debug_assert!(consumes_left);
431                    debug_assert!(!consumes_right);
432                    match self.ligature.take() {
433                        Some(l) => {
434                            // This happens when left is a ligature.
435                            RunItem::Ligature(l.into_ligature((*c).into()))
436                        }
437                        None => RunItem::Char((*c).into()),
438                    }
439                }
440                IntermediateOp::C(compiler::C {
441                    c,
442                    is_lig: true,
443                    consumes_left,
444                    consumes_right,
445                }) => {
446                    debug_assert!(!consumes_right);
447                    let mut s = self.ligature.take().unwrap_or(PendingLigature {
448                        s: Default::default(),
449                        includes_left_boundary: false,
450                        includes_right_boundary: false,
451                    });
452                    if *consumes_left && self.left_in_original {
453                        // TODO: figure out where in TeX the '|' comes from!
454                        s.s.push(self.left.unwrap_or('|'));
455                        s.includes_left_boundary = self.left.is_none();
456                    }
457                    if *consumes_right {
458                        s.s.push(self.right.unwrap_or('|'));
459                        s.includes_right_boundary = self.right.is_none();
460                    }
461                    RunItem::Ligature(s.into_ligature((*c).into()))
462                }
463            })
464        } else {
465            None
466        };
467        if self.intermediate_ops.is_empty() {
468            if let Some(op) = self.terminal_op.take() {
469                match op {
470                    compiler::C {
471                        c,
472                        is_lig: false,
473                        consumes_left: _,
474                        consumes_right,
475                    } => {
476                        debug_assert!(consumes_right);
477                        if self.right.is_none() {
478                            self.left = None;
479                        } else {
480                            self.left = Some(c.into());
481                            self.left_in_original = true; // = consumes_right;
482                        }
483                    }
484                    compiler::C {
485                        c,
486                        is_lig: true,
487                        consumes_left,
488                        consumes_right,
489                    } => {
490                        debug_assert!(consumes_right);
491                        let s = self.ligature.get_or_insert(PendingLigature {
492                            s: Default::default(),
493                            includes_left_boundary: false,
494                            includes_right_boundary: false,
495                        });
496                        if consumes_left && self.left_in_original {
497                            s.s.push(self.left.unwrap_or('|'));
498                            s.includes_left_boundary = self.left.is_none();
499                        }
500                        if consumes_right {
501                            s.s.push(self.right.unwrap_or('|'));
502                            s.includes_right_boundary = self.right.is_none();
503                        }
504                        self.left = Some(c.into());
505                        self.left_in_original = false;
506                    }
507                }
508            }
509        }
510        if let Some(next) = next {
511            return Some(next);
512        }
513        self.right = self.word.next();
514        if self.left.is_none() && self.right.is_none() {
515            return None;
516        }
517        let right_for_lookup = match self.right {
518            Some(r) => Some(r),
519            None => self.right_boundary_override,
520        };
521        // self.left is None if we're at the left boundary
522        // self.right is None if we're at the right boundary
523        match self
524            .program
525            .get_replacement_utf8(self.left, right_for_lookup)
526        {
527            Some(replacement) => {
528                self.intermediate_ops = &replacement.0;
529                self.terminal_op = Some(replacement.1.clone());
530            }
531            None => {
532                let old_left = self.left;
533                self.left = self.right;
534                self.left_in_original = true;
535                if let Some(left) = old_left {
536                    return Some(match self.ligature.take() {
537                        Some(l) => RunItem::Ligature(l.into_ligature(left)),
538                        None => RunItem::Char(left),
539                    });
540                }
541            }
542        }
543        self.next()
544    }
545}
546
547/// Implementations of this trait determine how characters, kerns and ligatures
548/// are handled when running a lig/kern program.
549pub trait Emitter {
550    fn emit_character(&mut self, c: char);
551    fn emit_kern(&mut self, kern: common::Scaled);
552    fn emit_ligature(&mut self, ligature: Ligature);
553}
554
555#[derive(PartialEq, Debug)]
556pub struct Ligature {
557    pub c: char,
558    pub original: Rc<str>,
559    includes_left_boundary: bool,
560    includes_right_boundary: bool,
561}
562
563/// An error returned from lig/kern compilation.
564///
565/// TODO: rename Cycle everywhere including the docs
566#[derive(Clone, Debug, PartialEq, Eq)]
567pub struct InfiniteLoopError {
568    /// The pair of characters the starts the infinite loop.
569    pub starting_pair: (Option<Char>, Char),
570}
571
572impl InfiniteLoopError {
573    pub fn pltotf_message(&self) -> String {
574        let left = match self.starting_pair.0 {
575            Some(c) => format!["'{:03o}", c.0],
576            None => "boundary".to_string(),
577        };
578        format!(
579            "Infinite ligature loop starting with {} and '{:03o}!",
580            left, self.starting_pair.1 .0
581        )
582    }
583    pub fn pltotf_section(&self) -> u8 {
584        125
585    }
586}
587
588/// One step in a lig/kern infinite loop.
589///
590/// A vector of these steps is returned in a [`InfiniteLoopError`].
591#[derive(Clone, Debug, PartialEq, Eq)]
592pub struct InfiniteLoopStep {
593    /// The index of the instruction to apply in this step.
594    pub instruction_index: usize,
595    /// The replacement text after applying this step.
596    ///
597    /// The boolean specifies whether the replacement begins with the
598    /// left boundary char.
599    pub post_replacement: (bool, Vec<Char>),
600    /// The position of the cursor after applying this step.
601    pub post_cursor_position: usize,
602}
603
604#[cfg(test)]
605mod tests {
606    use common::Scaled;
607
608    use super::Ligature as L;
609    use super::*;
610
611    const LIGAROO: &'static str = include_str!["ligaroo.plst"];
612
613    #[derive(Default)]
614    struct ElementEmitter(Vec<RunItem>);
615
616    impl Emitter for ElementEmitter {
617        fn emit_character(&mut self, c: char) {
618            self.0.push(RunItem::Char(c))
619        }
620
621        fn emit_kern(&mut self, kern: common::Scaled) {
622            self.0.push(RunItem::Kern(kern))
623        }
624
625        fn emit_ligature(&mut self, ligature: L) {
626            self.0.push(RunItem::Ligature(ligature))
627        }
628    }
629
630    fn run_test(program: &str, input: &str, want: Vec<RunItem>) {
631        let source = LIGAROO.replace("(LIGTABLE", &format!["(LIGTABLE\n{program}"]);
632        let pl_file = crate::pl::File::from_pl_source_code(&source).0;
633        let program = CompiledProgram::compile_from_pl_file(&pl_file).0;
634        let mut emitter: ElementEmitter = Default::default();
635        program.run(input, &mut emitter);
636        assert_eq!(emitter.0, want);
637
638        let got: Vec<RunItem> = program.run_iter(input, None).collect();
639        assert_eq!(got, want);
640    }
641
642    macro_rules! tests {
643        ( $(
644            ($name: ident, $program: expr, $input: expr, $want: expr, ),
645        )+ ) => { $(
646            #[test]
647            fn $name() {
648                use RunItem::*;
649                let program = $program;
650                let input = $input;
651                let want = $want;
652                run_test(program, input, want);
653            }
654        )+ };
655    }
656
657    tests!(
658        // AB -> ^1
659        (
660            single_lig_1,
661            "
662                (LABEL C A)
663                (LIG C B C 1)
664                (KRN C 1 R 0.1)
665                (STOP)
666
667                (LABEL C 1)
668                (KRN C B R 0.3)
669                (STOP)
670            ",
671            "AB",
672            vec![Ligature(L {
673                c: '1',
674                original: "AB".into(),
675                includes_left_boundary: false,
676                includes_right_boundary: false,
677            })],
678        ),
679        // AB -> ^A1
680        (
681            single_lig_2,
682            "
683                (LABEL C A)
684                (/LIG C B C 1)
685                (KRN C 1 R 0.1)
686                (STOP)
687
688                (LABEL C 1)
689                (KRN C B R 0.3)
690                (STOP)
691            ",
692            "AB",
693            vec![
694                Char('A'),
695                Kern(Scaled::ONE),
696                Ligature(L {
697                    c: '1',
698                    original: "B".into(),
699                    includes_left_boundary: false,
700                    includes_right_boundary: false,
701                })
702            ],
703        ),
704        // AB -> A^1
705        (
706            single_lig_3,
707            "
708                (LABEL C A)
709                (/LIG> C B C 1)
710                (KRN C 1 R 0.1)
711                (STOP)
712
713                (LABEL C 1)
714                (KRN C B R 0.3)
715                (STOP)
716            ",
717            "AB",
718            vec![
719                Char('A'),
720                Ligature(L {
721                    c: '1',
722                    original: "B".into(),
723                    includes_left_boundary: false,
724                    includes_right_boundary: false,
725                }),
726            ],
727        ),
728        // AB -> ^1B
729        (
730            single_lig_4,
731            "
732                (LABEL C A)
733                (LIG/ C B C 1)
734                (KRN C 1 R 0.1)
735                (STOP)
736
737                (LABEL C 1)
738                (KRN C B R 0.3)
739                (STOP)
740            ",
741            "AB",
742            vec![
743                Ligature(L {
744                    c: '1',
745                    original: "A".into(),
746                    includes_left_boundary: false,
747                    includes_right_boundary: false,
748                }),
749                Kern(Scaled::ONE * 3),
750                Char('B'),
751            ],
752        ),
753        // AB -> 1^B
754        (
755            single_lig_5,
756            "
757                (LABEL C A)
758                (LIG/> C B C 1)
759                (KRN C 1 R 0.1)
760                (STOP)
761
762                (LABEL C 1)
763                (KRN C B R 0.3)
764                (STOP)
765            ",
766            "AB",
767            vec![
768                Ligature(L {
769                    c: '1',
770                    original: "A".into(),
771                    includes_left_boundary: false,
772                    includes_right_boundary: false,
773                }),
774                Char('B'),
775            ],
776        ),
777        // AB -> ^A1B
778        (
779            single_lig_6,
780            "
781                (LABEL C A)
782                (/LIG/ C B C 1)
783                (KRN C 1 R 0.1)
784                (STOP)
785
786                (LABEL C 1)
787                (KRN C B R 0.3)
788                (STOP)
789            ",
790            "AB",
791            vec![
792                Char('A'),
793                Kern(Scaled::ONE),
794                Ligature(L {
795                    c: '1',
796                    original: "".into(),
797                    includes_left_boundary: false,
798                    includes_right_boundary: false,
799                }),
800                Kern(Scaled::ONE * 3),
801                Char('B'),
802            ],
803        ),
804        // AB -> A^1B
805        (
806            single_lig_7,
807            "
808                (LABEL C A)
809                (/LIG/> C B C 1)
810                (KRN C 1 R 0.1)
811                (STOP)
812
813                (LABEL C 1)
814                (KRN C B R 0.3)
815                (STOP)
816            ",
817            "AB",
818            vec![
819                Char('A'),
820                Ligature(L {
821                    c: '1',
822                    original: "".into(),
823                    includes_left_boundary: false,
824                    includes_right_boundary: false,
825                }),
826                Kern(Scaled::ONE * 3),
827                Char('B'),
828            ],
829        ),
830        // AB -> A1^B
831        (
832            single_lig_8,
833            "
834                (LABEL C A)
835                (/LIG/>> C B C 1)
836                (KRN C 1 R 0.1)
837                (STOP)
838
839                (LABEL C 1)
840                (KRN C B R 0.3)
841                (STOP)
842            ",
843            "AB",
844            vec![
845                Char('A'),
846                Ligature(L {
847                    c: '1',
848                    original: "".into(),
849                    includes_left_boundary: false,
850                    includes_right_boundary: false,
851                }),
852                Char('B'),
853            ],
854        ),
855        // AB -> A^B
856        // This is the same as single_lig_5, but the replacement character
857        // is the same as the character that is removed. In theory lig(A, A)
858        // could be replaced by char(A), and this test verifies that it is not.
859        (
860            no_op_lig,
861            "
862                (LABEL C A)
863                (LIG/> C B C A)
864                (STOP)
865            ",
866            "AB",
867            vec![
868                Ligature(L {
869                    c: 'A',
870                    original: "A".into(),
871                    includes_left_boundary: false,
872                    includes_right_boundary: false,
873                }),
874                Char('B'),
875            ],
876        ),
877        // AB -> ^1, 1C -> ^2
878        (
879            multiple_lig_1,
880            "
881                (LABEL C A)
882                (LIG C B C 1)
883
884                (LABEL C 1)
885                (LIG C C C 2)
886                (STOP)
887            ",
888            "ABC",
889            vec![Ligature(L {
890                c: '2',
891                original: "ABC".into(),
892                includes_left_boundary: false,
893                includes_right_boundary: false,
894            }),],
895        ),
896        // AB -> ^A1B, 1B -> 2
897        (
898            multiple_lig_2,
899            "
900                (LABEL C A)
901                (/LIG/ C B C 1)
902                (LABEL C 1)
903                (LIG C B C 2)
904                (STOP)
905            ",
906            "AB",
907            vec![
908                Char('A'),
909                Ligature(L {
910                    c: '2',
911                    original: "B".into(),
912                    includes_left_boundary: false,
913                    includes_right_boundary: false,
914                }),
915            ],
916        ),
917        // AA -> 1^A multiple times
918        (
919            multiple_lig_3,
920            "
921                (LABEL C A)
922                (LIG/ C A C 1)
923                (STOP)
924            ",
925            "AAAAA",
926            vec![
927                Ligature(L {
928                    c: '1',
929                    original: "A".into(),
930                    includes_left_boundary: false,
931                    includes_right_boundary: false,
932                }),
933                Ligature(L {
934                    c: '1',
935                    original: "A".into(),
936                    includes_left_boundary: false,
937                    includes_right_boundary: false,
938                }),
939                Ligature(L {
940                    c: '1',
941                    original: "A".into(),
942                    includes_left_boundary: false,
943                    includes_right_boundary: false,
944                }),
945                Ligature(L {
946                    c: '1',
947                    original: "A".into(),
948                    includes_left_boundary: false,
949                    includes_right_boundary: false,
950                }),
951                Char('A'),
952            ],
953        ),
954        // AA -> ^A multiple times
955        (
956            multiple_lig_4,
957            "
958                (LABEL C A)
959                (LIG C A C A)
960                (STOP)
961            ",
962            "AAAAAA",
963            vec![Ligature(L {
964                c: 'A',
965                original: "AAAAAA".into(),
966                includes_left_boundary: false,
967                includes_right_boundary: false,
968            }),],
969        ),
970        // AB -> ^A1, A1 -> ^21
971        (
972            multiple_lig_5,
973            "
974                (LABEL C A)
975                (/LIG C B C 1)
976                (LIG/ C 1 C 2)
977                (STOP)
978            ",
979            "AB",
980            vec![
981                Ligature(L {
982                    c: '2',
983                    original: "A".into(),
984                    includes_left_boundary: false,
985                    includes_right_boundary: false,
986                }),
987                Ligature(L {
988                    c: '1',
989                    original: "B".into(),
990                    includes_left_boundary: false,
991                    includes_right_boundary: false,
992                }),
993            ],
994        ),
995        // AB -> ^A1, A1 -> ^21, 21 -> 3
996        (
997            multiple_lig_6,
998            "
999                (LABEL C A)
1000                (/LIG C B C 1)
1001                (LIG/ C 1 C 2)
1002                (LABEL C 2)
1003                (LIG C 1 C 3)
1004                (STOP)
1005            ",
1006            "AB",
1007            vec![Ligature(L {
1008                c: '3',
1009                original: "AB".into(),
1010                includes_left_boundary: false,
1011                includes_right_boundary: false,
1012            })],
1013        ),
1014        // AB -> ^1, 1C -> 12^C
1015        (
1016            multiple_lig_7,
1017            "
1018                (LABEL C A)
1019                (LIG C B C 1)
1020                (STOP)
1021
1022                (LABEL C 1)
1023                (/LIG/>> C C C 2)
1024                (STOP)
1025            ",
1026            "ABC",
1027            vec![
1028                Ligature(L {
1029                    c: '1',
1030                    original: "AB".into(),
1031                    includes_left_boundary: false,
1032                    includes_right_boundary: false,
1033                }),
1034                Ligature(L {
1035                    c: '2',
1036                    original: "".into(),
1037                    includes_left_boundary: false,
1038                    includes_right_boundary: false,
1039                }),
1040                Char('C'),
1041            ],
1042        ),
1043        (
1044            kern_after_lig_1,
1045            "
1046                (LABEL C A)
1047                (LIG C B C 1)
1048                (STOP)
1049
1050                (LABEL C 1)
1051                (KRN C C R 0.1)
1052            ",
1053            "ABC",
1054            vec![
1055                Ligature(L {
1056                    c: '1',
1057                    original: "AB".into(),
1058                    includes_left_boundary: false,
1059                    includes_right_boundary: false,
1060                }),
1061                Kern(Scaled::ONE),
1062                Char('C'),
1063            ],
1064        ),
1065        (
1066            kern_after_lig_2,
1067            "
1068                (LABEL C A)
1069                (LIG C B C 1)
1070                (STOP)
1071
1072                (LABEL C 1)
1073                (KRN C A R 0.1)
1074            ",
1075            "ABAB",
1076            vec![
1077                Ligature(L {
1078                    c: '1',
1079                    original: "AB".into(),
1080                    includes_left_boundary: false,
1081                    includes_right_boundary: false,
1082                }),
1083                Kern(Scaled::ONE),
1084                Ligature(L {
1085                    c: '1',
1086                    original: "AB".into(),
1087                    includes_left_boundary: false,
1088                    includes_right_boundary: false,
1089                }),
1090            ],
1091        ),
1092        (
1093            left_boundary_char_1,
1094            "
1095                (LABEL BOUNDARYCHAR)
1096                (LIG C A C 1)
1097            ",
1098            "A",
1099            vec![Ligature(L {
1100                c: '1',
1101                original: "|A".into(),
1102                includes_left_boundary: true,
1103                includes_right_boundary: false,
1104            }),],
1105        ),
1106        (
1107            left_boundary_char_2,
1108            "
1109                (LABEL BOUNDARYCHAR)
1110                (/LIG/ C A C 1)
1111                (/LIG/ C 1 C 2)
1112            ",
1113            "A",
1114            vec![
1115                Ligature(L {
1116                    c: '2',
1117                    original: "|".into(),
1118                    includes_left_boundary: true,
1119                    includes_right_boundary: false,
1120                }),
1121                Ligature(L {
1122                    c: '1',
1123                    original: "".into(),
1124                    includes_left_boundary: false,
1125                    includes_right_boundary: false,
1126                }),
1127                Char('A'),
1128            ],
1129        ),
1130        (
1131            left_boundary_char_3,
1132            "
1133                (LABEL BOUNDARYCHAR)
1134                (/LIG/ C A C 1)
1135            ",
1136            "A",
1137            vec![
1138                Ligature(L {
1139                    c: '1',
1140                    original: "|".into(),
1141                    includes_left_boundary: true,
1142                    includes_right_boundary: false,
1143                }),
1144                Char('A'),
1145            ],
1146        ),
1147        /*
1148        (
1149            right_boundary_char_1,
1150            "
1151                (LABEL C M)
1152                (/LIG/ C L C N)
1153                (STOP)
1154            ",
1155            "N",
1156            vec![Char('N'), Ligature(L { c: 'Q', original: "|".into() }),],
1157        ),
1158        (
1159            right_boundary_char_2,
1160            "
1161                (LABEL C N)
1162                (/LIG/ C L C Q)
1163                (STOP)
1164            ",
1165            "M",
1166            vec![
1167                Char('M'),
1168                Ligature(L { c: 'N', original: "".into() }),
1169                Ligature(L { c: 'Q', original: "|".into() }),
1170            ],
1171        ),
1172        */
1173        (
1174            right_boundary_char_3,
1175            "
1176                (LABEL C A)
1177                (LIG C L C 1)
1178                (STOP)
1179            ",
1180            "A",
1181            vec![Ligature(L {
1182                c: '1',
1183                original: "A|".into(),
1184                includes_left_boundary: false,
1185                includes_right_boundary: true,
1186            }),],
1187        ),
1188        (
1189            right_boundary_char_4,
1190            "
1191                (LABEL C A)
1192                (KRN C L R 1)
1193                (STOP)
1194            ",
1195            "A",
1196            vec![Char('A'), Kern(Scaled::ONE * 10),],
1197        ),
1198    );
1199}