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 common::font;
85use common::font::TextItem;
86use std::collections::HashMap;
87pub mod lang;
88
89/// A compiled lig/kern program.
90///
91/// The default value is an empty program with no kerns or ligatures.
92#[derive(Clone, Debug, Default)]
93pub struct CompiledProgram {
94    right_boundary_char: Option<Char>,
95    replacements: HashMap<(Option<Char>, Char), compiler::Replacement>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
99enum IntermediateOp {
100    // Emit the kern.
101    Kern(common::Scaled),
102    // Emit the char in the payload.
103    C(compiler::C),
104}
105
106impl CompiledProgram {
107    /// Compile a lig/kern program.
108    pub fn compile(
109        program: &lang::Program,
110        design_size: FixWord,
111        kerns: &[FixWord],
112        entrypoints: HashMap<Char, u16>,
113    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
114        compiler::compile(program, design_size, kerns, &entrypoints)
115    }
116
117    /// Compile a lig/kern program from a PL file.
118    pub fn compile_from_pl_file(
119        pl_file: &super::pl::File,
120    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
121        let entrypoints = pl_file.lig_kern_entrypoints(false);
122        // The kerns array, which is used for KernAtIndex operations, is empty
123        // because PL files do not have such operations.
124        CompiledProgram::compile(
125            &pl_file.lig_kern_program,
126            pl_file.header.design_size,
127            &[],
128            entrypoints,
129        )
130    }
131
132    /// Compile a lig/kern program from a TFM file.
133    pub fn compile_from_tfm_file(
134        tfm_file: &mut super::File,
135    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
136        let entrypoints: HashMap<Char, u16> = tfm_file
137            .lig_kern_entrypoints()
138            .into_iter()
139            .filter_map(|(c, e)| {
140                tfm_file
141                    .lig_kern_program
142                    .unpack_entrypoint(e)
143                    .ok()
144                    .map(|e| (c, e))
145            })
146            .collect();
147        CompiledProgram::compile(
148            &tfm_file.lig_kern_program,
149            tfm_file.header.design_size,
150            &tfm_file.kerns,
151            entrypoints,
152        )
153    }
154
155    pub fn has_replacement(&self, left_char: Option<char>, right_char: Option<char>) -> bool {
156        self.get_replacement_utf8(left_char, right_char).is_some()
157    }
158
159    fn get_replacement(
160        &self,
161        left_char: Option<Char>,
162        right_char: Option<Char>,
163    ) -> Option<&Replacement> {
164        let right_char = match right_char {
165            None => self.right_boundary_char?,
166            Some(c) => c,
167        };
168        self.replacements.get(&(left_char, right_char))
169    }
170
171    fn get_replacement_utf8(
172        &self,
173        left_char: Option<char>,
174        right_char: Option<char>,
175    ) -> Option<&Replacement> {
176        let left_char = match left_char {
177            None => None,
178            Some(c) => {
179                let Ok(c) = c.try_into() else {
180                    return None;
181                };
182                Some(c)
183            }
184        };
185        let right_char = match right_char {
186            None => None,
187            Some(c) => {
188                let Ok(c) = c.try_into() else {
189                    return None;
190                };
191                Some(c)
192            }
193        };
194        self.get_replacement(left_char, right_char)
195    }
196
197    /// Returns an iterator over all pairs `(char,char)` that have a replacement
198    ///     specified in the lig/kern program.
199    pub fn all_pairs_with_replacements(&self) -> Vec<(Option<Char>, Char)> {
200        let mut v: Vec<(Option<Char>, Char)> = self.replacements.keys().copied().collect();
201        v.sort();
202        v
203    }
204
205    /// Returns whether this program is seven-bit safe.
206    ///
207    /// A lig/kern program is seven-bit safe if the replacement for any
208    ///     pair of seven-bit safe characters
209    ///     consists only of seven-bit characters.
210    /// Conversely a program is seven-bit unsafe if there is a
211    ///     pair of seven-bit characters whose replacement
212    ///     contains a non-seven-bit character.
213    pub fn is_seven_bit_safe(&self) -> bool {
214        self.all_pairs_with_replacements()
215            .into_iter()
216            .filter(|(l, r)| l.map(|c| c.is_seven_bit()).unwrap_or(true) && r.is_seven_bit())
217            .flat_map(|(l, r)| self.get_replacement(l, Some(r)))
218            .all(|rep| {
219                rep.0.iter().all(|op| match op {
220                    IntermediateOp::Kern(_) => true,
221                    IntermediateOp::C(c) => c.c.is_seven_bit(),
222                }) && rep.1.c.is_seven_bit()
223            })
224    }
225
226    /// Run the lig/kern program for the provided word.
227    pub fn run<'a>(&'a self, word: &'a str) -> RunIter<'a, std::str::Chars<'a>> {
228        self.run_with_options(word.chars(), Default::default())
229    }
230
231    /// Run the lig/kern program for the provided word with the provided options.
232    ///
233    /// This method also supports providing an arbitrary iterator over characters, rather
234    /// than a string slice. This facility is used in Boxworks to avoid allocations.
235    pub fn run_with_options<'a, Word>(
236        &'a self,
237        mut word: Word,
238        options: RunOptions,
239    ) -> RunIter<'a, Word>
240    where
241        Word: Iterator<Item = char>,
242    {
243        let next_left = if options.disable_left_boundary {
244            match word.next() {
245                Some(c) => NextLeft::Char(c),
246                None => NextLeft::None,
247            }
248        } else {
249            NextLeft::Boundary
250        };
251        RunIter {
252            program: self,
253            word,
254            consumes_left: true,
255            intermediate_ops: &[],
256            next_left,
257            ligature: None,
258            left: None,
259            right_boundary_override: options.right_boundary_override,
260        }
261    }
262}
263
264/// Options for the [CompiledProgram::run_with_options] method
265#[derive(Default, Debug)]
266pub struct RunOptions {
267    pub disable_left_boundary: bool,
268    pub right_boundary_override: Option<char>,
269}
270
271#[derive(Default)]
272struct PendingLigature {
273    s: String,
274    includes_left_boundary: bool,
275    includes_right_boundary: bool,
276}
277
278impl PendingLigature {
279    fn into_text_item(self, c: char) -> TextItem {
280        TextItem::Ligature {
281            c,
282            original: self.s.into(),
283            includes_left_boundary: self.includes_left_boundary,
284            includes_right_boundary: self.includes_right_boundary,
285        }
286    }
287}
288
289/// Iterator over the replacement elements of a word in a lig/kern program.
290pub struct RunIter<'a, Word> {
291    // First two fields are fixed for the iteration.
292    program: &'a CompiledProgram,
293    right_boundary_override: Option<char>,
294
295    // The word we're iterating over: gets consumed as we iterate.
296    word: Word,
297
298    // A ligature can be built up from multiple lig/kern passes - e.g.
299    // repeated LIG commands. This field stores information about the pending
300    // ligature before it is written out (e.g. by a LIG/> or the absence of
301    // a replacement).
302    ligature: Option<PendingLigature>,
303
304    // Intermediate ops: if the slice is non-empty the next step is to
305    // process is.
306    // consumes_left=true if the first character or ligature should be
307    // marked as consuming the left character used in the replacement
308    // (or left hand boundary).
309    // left is the character that was used as the left-hand side in the
310    // replacement, or None if it was the left hand boundary.
311    intermediate_ops: &'a [IntermediateOp],
312    consumes_left: bool,
313    left: Option<char>,
314
315    // If intermediate ops is empty, the next step is to evaluate the
316    // lig/kern program with the left character as specified in the enum.
317    next_left: NextLeft,
318}
319
320#[derive(Debug)]
321enum NextLeft {
322    Boundary,
323    Char(char),
324    Lig(char, char),
325    FinalLig(char),
326    None,
327}
328
329impl<'a, Word> RunIter<'a, Word> {
330    pub fn is_separation_point(&self) -> bool {
331        self.intermediate_ops.is_empty()
332            && !matches!(self.next_left, NextLeft::Lig(_, _) | NextLeft::FinalLig(_))
333    }
334}
335
336impl<'a, Word> font::TextIter for RunIter<'a, Word>
337where
338    Word: Iterator<Item = char>,
339{
340    fn is_separation_point(&self) -> bool {
341        self.intermediate_ops.is_empty()
342            && !matches!(self.next_left, NextLeft::Lig(_, _) | NextLeft::FinalLig(_))
343    }
344}
345
346impl<'a, Word> Iterator for RunIter<'a, Word>
347where
348    Word: Iterator<Item = char>,
349{
350    type Item = TextItem;
351
352    fn next(&mut self) -> Option<Self::Item> {
353        if let Some((op, tail)) = self.intermediate_ops.split_first() {
354            self.intermediate_ops = tail;
355            return Some(match op {
356                IntermediateOp::Kern(kern) => TextItem::Kern(*kern),
357                IntermediateOp::C(compiler::C { c, is_lig: false }) => {
358                    self.consumes_left = false;
359                    match self.ligature.take() {
360                        Some(l) => {
361                            // This happens when left is a ligature.
362                            l.into_text_item((*c).into())
363                        }
364                        None => TextItem::Char((*c).into()),
365                    }
366                }
367                IntermediateOp::C(compiler::C { c, is_lig: true }) => {
368                    let mut s = self.ligature.take().unwrap_or_default();
369                    if self.consumes_left {
370                        if let Some(left) = self.left {
371                            s.s.push(left);
372                        } else {
373                            s.includes_left_boundary = true;
374                        }
375                    }
376                    self.consumes_left = false;
377                    s.includes_right_boundary = matches!(self.next_left, NextLeft::None)
378                        && matches!(
379                            (tail.first(), tail.get(1)),
380                            (None, None) | (Some(IntermediateOp::Kern(_)), None)
381                        );
382                    s.into_text_item((*c).into())
383                }
384            });
385        };
386        let (left, left_in_original): (Option<char>, bool) = match &self.next_left {
387            NextLeft::Boundary => (None, true),
388            NextLeft::Char(c) => (Some(*c), true),
389            NextLeft::Lig(c, right) => {
390                let s = self.ligature.get_or_insert_default();
391                if self.consumes_left {
392                    if let Some(left) = self.left {
393                        s.s.push(left);
394                    } else {
395                        s.includes_left_boundary = true;
396                    }
397                }
398                s.s.push(*right);
399                (Some(*c), false)
400            }
401            NextLeft::FinalLig(c) => {
402                let mut s = self.ligature.take().unwrap_or_default();
403                if self.consumes_left {
404                    if let Some(left) = self.left {
405                        s.s.push(left);
406                    } else {
407                        s.includes_left_boundary = true;
408                    }
409                }
410                s.includes_right_boundary = true;
411                let lig = s.into_text_item(*c);
412                self.next_left = NextLeft::None;
413                return Some(lig);
414            }
415            NextLeft::None => return None,
416        };
417        let right = self.word.next();
418        if left.is_none() && right.is_none() {
419            // TODO: remove this check?
420            return None;
421        }
422        let right_for_lookup = match right {
423            Some(r) => Some(r),
424            None => self.right_boundary_override,
425        };
426        // self.left is None if we're at the left boundary
427        // self.right is None if we're at the right boundary
428        match self.program.get_replacement_utf8(left, right_for_lookup) {
429            Some(replacement) => {
430                self.left = left;
431                self.consumes_left = left_in_original;
432                self.intermediate_ops = &replacement.0;
433                self.next_left = match (replacement.1.is_lig, right) {
434                    (false, None) => NextLeft::None,
435                    (false, Some(_)) => NextLeft::Char(replacement.1.c.into()),
436                    (true, Some(right)) => NextLeft::Lig(replacement.1.c.into(), right),
437                    (true, None) => NextLeft::FinalLig(replacement.1.c.into()),
438                };
439            }
440            None => {
441                self.next_left = match right {
442                    None => NextLeft::None,
443                    Some(right) => NextLeft::Char(right),
444                };
445                if let Some(left) = left {
446                    return Some(match self.ligature.take() {
447                        Some(l) => l.into_text_item(left),
448                        None => TextItem::Char(left),
449                    });
450                }
451            }
452        }
453        self.next()
454    }
455}
456
457/// An error returned from lig/kern compilation.
458///
459/// TODO: rename Cycle everywhere including the docs
460#[derive(Clone, Debug, PartialEq, Eq)]
461pub struct InfiniteLoopError {
462    /// The pair of characters the starts the infinite loop.
463    pub starting_pair: (Option<Char>, Char),
464}
465
466impl InfiniteLoopError {
467    pub fn pltotf_message(&self) -> String {
468        let left = match self.starting_pair.0 {
469            Some(c) => format!["'{:03o}", c.0],
470            None => "boundary".to_string(),
471        };
472        format!(
473            "Infinite ligature loop starting with {} and '{:03o}!",
474            left, self.starting_pair.1 .0
475        )
476    }
477    pub fn pltotf_section(&self) -> u8 {
478        125
479    }
480}
481
482/// One step in a lig/kern infinite loop.
483///
484/// A vector of these steps is returned in a [`InfiniteLoopError`].
485#[derive(Clone, Debug, PartialEq, Eq)]
486pub struct InfiniteLoopStep {
487    /// The index of the instruction to apply in this step.
488    pub instruction_index: usize,
489    /// The replacement text after applying this step.
490    ///
491    /// The boolean specifies whether the replacement begins with the
492    /// left boundary char.
493    pub post_replacement: (bool, Vec<Char>),
494    /// The position of the cursor after applying this step.
495    pub post_cursor_position: usize,
496}
497
498#[cfg(test)]
499mod tests {
500    use common::Scaled;
501
502    use super::*;
503    use pretty_assertions::assert_eq;
504
505    const LIGAROO: &'static str = include_str!["ligaroo.plst"];
506
507    fn run_test(program: &str, input: &str, want: Vec<TextItem>) {
508        let source = LIGAROO.replace("(LIGTABLE", &format!["(LIGTABLE\n{program}"]);
509        let pl_file = crate::pl::File::from_pl_source_code(&source).0;
510
511        if std::env::var("TEXCRAFT_VERIFY").unwrap_or_default() == "tex" {
512            verify_against_tex(pl_file, input, want);
513            return;
514        }
515
516        let program = CompiledProgram::compile_from_pl_file(&pl_file).0;
517        let got: Vec<TextItem> = program.run(input).collect();
518        assert_eq!(got, want);
519    }
520
521    /// Verify the expected output of a test case against a real TeX engine.
522    ///
523    /// The property list file is converted to a TFM file, TeX builds an
524    /// `\hbox` from the input word set in a font backed by that TFM file,
525    /// and the box contents (dumped with `\showbox`) are parsed back into
526    /// run items and compared to the expected ones.
527    fn verify_against_tex(pl_file: crate::pl::File, input: &str, want: Vec<TextItem>) {
528        let tfm_file: crate::File = pl_file.into();
529        let stdout = run_tex(&tfm_file.serialize(), input);
530        let got = parse_showbox_output(&stdout);
531        let want: Vec<TextItem> = want.into_iter().map(normalize_for_tex).collect();
532        assert_eq!(got, want);
533    }
534
535    /// Run the `tex` binary on an `\hbox` containing the input word, set in
536    /// a font backed by the provided TFM bytes, and return the terminal
537    /// output which includes a `\showbox` dump of the box.
538    fn run_tex(tfm_bytes: &[u8], input: &str) -> String {
539        let mut dir = std::env::temp_dir();
540        dir.push("texcraft_tfm_ligkern");
541        // The thread name is the name of the unit test being run, so tests
542        // running in parallel get distinct directories.
543        dir.push(
544            std::thread::current()
545                .name()
546                .unwrap_or("texcraft_unknown_thread_name")
547                .replace("::", "__"),
548        );
549        std::fs::create_dir_all(&dir).unwrap();
550        std::fs::write(dir.join("ligaroo.tfm"), tfm_bytes).unwrap();
551
552        let tex_source = format!(
553            r"\nonstopmode
554\tracingonline=1
555\showboxbreadth=1000000
556\showboxdepth=100
557\font\ligkernfont=ligaroo
558\ligkernfont
559\setbox253=\hbox{{{input}}}
560\showbox253
561\end
562"
563        );
564        let input_path = dir.join("tex-input.tex");
565        std::fs::write(&input_path, tex_source).unwrap();
566
567        let output = std::process::Command::new("tex")
568            .current_dir(&dir)
569            // By default TeX wraps terminal output at 79 characters, which
570            // would split long box dump lines.
571            .env("max_print_line", "10000")
572            .arg(&input_path)
573            .output()
574            .expect("failed to run the `tex` binary");
575        // The exit status is not checked: `\showbox` counts as an error in
576        // TeX's book-keeping ("! OK.") and makes the exit status non-zero.
577        String::from_utf8(output.stdout).expect("stdout of TeX is utf-8")
578    }
579
580    /// Parse the contents of the box dumped with `\showbox253` into run items.
581    fn parse_showbox_output(stdout: &str) -> Vec<TextItem> {
582        let mut lines = stdout.lines();
583        for line in lines.by_ref() {
584            if line.starts_with(r"> \box253=") {
585                break;
586            }
587        }
588        let dimens_line = lines.next().expect(r"box dump contains an \hbox line");
589        assert!(
590            dimens_line.starts_with(r"\hbox("),
591            r"expected an \hbox dimensions line, got {dimens_line:?}"
592        );
593        // The box contents are the following lines, prefixed with a period
594        // because they are one level deep in the dumped box.
595        lines
596            .map_while(|line| line.strip_prefix('.'))
597            .map(parse_run_item)
598            .collect()
599    }
600
601    /// Parse one line of the box dump into a run item.
602    fn parse_run_item(line: &str) -> TextItem {
603        // Kern lines look like `\kern1.0` (or `\kern 1.0` for explicit kerns).
604        if let Some(width) = line.strip_prefix(r"\kern") {
605            let width = common::Scaled::parse_no_units(width.trim())
606                .expect("kern width fits in a scaled number");
607            return TextItem::Kern(width);
608        }
609        // Character lines look like `\ligkernfont A`, and ligature lines
610        // like `\ligkernfont 1 (ligature AB)`.
611        let tail = line
612            .strip_prefix(r"\ligkernfont ")
613            .expect(r"box content line is a \kern or a character in the test font");
614        let parse_char = |s: &str| {
615            let mut chars = s.chars();
616            let c = chars.next().expect("a character follows the font name");
617            assert_eq!(chars.next(), None, "expected a single character in {s:?}");
618            c
619        };
620        match tail.split_once(" (ligature ") {
621            None => TextItem::Char(parse_char(tail)),
622            Some((c, original)) => {
623                let original = original
624                    .strip_suffix(')')
625                    .expect("ligature original chars end with `)`");
626                TextItem::Ligature {
627                    c: parse_char(c),
628                    original: original.into(),
629                    includes_left_boundary: false,
630                    includes_right_boundary: false,
631                }
632            }
633        }
634    }
635
636    /// Normalize a run item to the form in which it appears in TeX's box dump.
637    ///
638    /// TeX's box display marks a boundary character in a ligature's original
639    /// characters with a `|`. A `|` next to an empty original is ambiguous
640    /// between the left and the right boundary, so instead of parsing the
641    /// markers back into the boundary fields, the comparison happens on the
642    /// marked-up original characters with the boundary fields cleared.
643    fn normalize_for_tex(item: TextItem) -> TextItem {
644        match item {
645            TextItem::Ligature {
646                c,
647                original: lig_original,
648                includes_left_boundary,
649                includes_right_boundary,
650            } => {
651                let mut original = String::new();
652                if includes_left_boundary {
653                    original.push('|');
654                }
655                original.push_str(&lig_original);
656                if includes_right_boundary {
657                    original.push('|');
658                }
659                TextItem::Ligature {
660                    c,
661                    original: original.into(),
662                    includes_left_boundary: false,
663                    includes_right_boundary: false,
664                }
665            }
666            item => item,
667        }
668    }
669
670    macro_rules! tests {
671        ( $(
672            ($name: ident, $program: expr, $input: expr, $want: expr, ),
673        )+ ) => { $(
674            #[test]
675            fn $name() {
676                #[allow(unused_imports)]
677                use TextItem::*;
678                let program = $program;
679                let input = $input;
680                let want = $want;
681                run_test(program, input, want);
682            }
683        )+ };
684    }
685
686    tests!(
687        (empty_input, "", "", vec![],),
688        // AB -> ^1
689        (
690            single_lig_1,
691            "
692                (LABEL C A)
693                (LIG C B C 1)
694                (KRN C 1 R 0.1)
695                (STOP)
696
697                (LABEL C 1)
698                (KRN C B R 0.3)
699                (STOP)
700            ",
701            "AB",
702            vec![Ligature {
703                c: '1',
704                original: "AB".into(),
705                includes_left_boundary: false,
706                includes_right_boundary: false,
707            }],
708        ),
709        // AB -> ^A1
710        (
711            single_lig_2,
712            "
713                (LABEL C A)
714                (/LIG C B C 1)
715                (KRN C 1 R 0.1)
716                (STOP)
717
718                (LABEL C 1)
719                (KRN C B R 0.3)
720                (STOP)
721            ",
722            "AB",
723            vec![
724                Char('A'),
725                Kern(Scaled::ONE),
726                Ligature {
727                    c: '1',
728                    original: "B".into(),
729                    includes_left_boundary: false,
730                    includes_right_boundary: false,
731                }
732            ],
733        ),
734        // AB -> A^1
735        (
736            single_lig_3,
737            "
738                (LABEL C A)
739                (/LIG> C B C 1)
740                (KRN C 1 R 0.1)
741                (STOP)
742
743                (LABEL C 1)
744                (KRN C B R 0.3)
745                (STOP)
746            ",
747            "AB",
748            vec![
749                Char('A'),
750                Ligature {
751                    c: '1',
752                    original: "B".into(),
753                    includes_left_boundary: false,
754                    includes_right_boundary: false,
755                },
756            ],
757        ),
758        // AB -> ^1B
759        (
760            single_lig_4,
761            "
762                (LABEL C A)
763                (LIG/ C B C 1)
764                (KRN C 1 R 0.1)
765                (STOP)
766
767                (LABEL C 1)
768                (KRN C B R 0.3)
769                (STOP)
770            ",
771            "AB",
772            vec![
773                Ligature {
774                    c: '1',
775                    original: "A".into(),
776                    includes_left_boundary: false,
777                    includes_right_boundary: false,
778                },
779                Kern(Scaled::ONE * 3),
780                Char('B'),
781            ],
782        ),
783        // AB -> 1^B
784        (
785            single_lig_5,
786            "
787                (LABEL C A)
788                (LIG/> C B C 1)
789                (KRN C 1 R 0.1)
790                (STOP)
791
792                (LABEL C 1)
793                (KRN C B R 0.3)
794                (STOP)
795            ",
796            "AB",
797            vec![
798                Ligature {
799                    c: '1',
800                    original: "A".into(),
801                    includes_left_boundary: false,
802                    includes_right_boundary: false,
803                },
804                Char('B'),
805            ],
806        ),
807        // AB -> ^A1B
808        (
809            single_lig_6,
810            "
811                (LABEL C A)
812                (/LIG/ C B C 1)
813                (KRN C 1 R 0.1)
814                (STOP)
815
816                (LABEL C 1)
817                (KRN C B R 0.3)
818                (STOP)
819            ",
820            "AB",
821            vec![
822                Char('A'),
823                Kern(Scaled::ONE),
824                Ligature {
825                    c: '1',
826                    original: "".into(),
827                    includes_left_boundary: false,
828                    includes_right_boundary: false,
829                },
830                Kern(Scaled::ONE * 3),
831                Char('B'),
832            ],
833        ),
834        // AB -> A^1B
835        (
836            single_lig_7,
837            "
838                (LABEL C A)
839                (/LIG/> C B C 1)
840                (KRN C 1 R 0.1)
841                (STOP)
842
843                (LABEL C 1)
844                (KRN C B R 0.3)
845                (STOP)
846            ",
847            "AB",
848            vec![
849                Char('A'),
850                Ligature {
851                    c: '1',
852                    original: "".into(),
853                    includes_left_boundary: false,
854                    includes_right_boundary: false,
855                },
856                Kern(Scaled::ONE * 3),
857                Char('B'),
858            ],
859        ),
860        // AB -> A1^B
861        (
862            single_lig_8,
863            "
864                (LABEL C A)
865                (/LIG/>> C B C 1)
866                (KRN C 1 R 0.1)
867                (STOP)
868
869                (LABEL C 1)
870                (KRN C B R 0.3)
871                (STOP)
872            ",
873            "AB",
874            vec![
875                Char('A'),
876                Ligature {
877                    c: '1',
878                    original: "".into(),
879                    includes_left_boundary: false,
880                    includes_right_boundary: false,
881                },
882                Char('B'),
883            ],
884        ),
885        // AB -> A^B
886        // This is the same as single_lig_5, but the replacement character
887        // is the same as the character that is removed. In theory lig(A, A)
888        // could be replaced by char(A), and this test verifies that it is not.
889        (
890            no_op_lig,
891            "
892                (LABEL C A)
893                (LIG/> C B C A)
894                (STOP)
895            ",
896            "AB",
897            vec![
898                Ligature {
899                    c: 'A',
900                    original: "A".into(),
901                    includes_left_boundary: false,
902                    includes_right_boundary: false,
903                },
904                Char('B'),
905            ],
906        ),
907        // AB -> ^1, 1C -> ^2
908        (
909            multiple_lig_1,
910            "
911                (LABEL C A)
912                (LIG C B C 1)
913
914                (LABEL C 1)
915                (LIG C C C 2)
916                (STOP)
917            ",
918            "ABC",
919            vec![Ligature {
920                c: '2',
921                original: "ABC".into(),
922                includes_left_boundary: false,
923                includes_right_boundary: false,
924            },],
925        ),
926        // AB -> ^A1B, 1B -> 2
927        (
928            multiple_lig_2,
929            "
930                (LABEL C A)
931                (/LIG/ C B C 1)
932                (LABEL C 1)
933                (LIG C B C 2)
934                (STOP)
935            ",
936            "AB",
937            vec![
938                Char('A'),
939                Ligature {
940                    c: '2',
941                    original: "B".into(),
942                    includes_left_boundary: false,
943                    includes_right_boundary: false,
944                },
945            ],
946        ),
947        // AA -> 1^A multiple times
948        (
949            multiple_lig_3,
950            "
951                (LABEL C A)
952                (LIG/ C A C 1)
953                (STOP)
954            ",
955            "AAAAA",
956            vec![
957                Ligature {
958                    c: '1',
959                    original: "A".into(),
960                    includes_left_boundary: false,
961                    includes_right_boundary: false,
962                },
963                Ligature {
964                    c: '1',
965                    original: "A".into(),
966                    includes_left_boundary: false,
967                    includes_right_boundary: false,
968                },
969                Ligature {
970                    c: '1',
971                    original: "A".into(),
972                    includes_left_boundary: false,
973                    includes_right_boundary: false,
974                },
975                Ligature {
976                    c: '1',
977                    original: "A".into(),
978                    includes_left_boundary: false,
979                    includes_right_boundary: false,
980                },
981                Char('A'),
982            ],
983        ),
984        // AA -> ^A multiple times
985        (
986            multiple_lig_4,
987            "
988                (LABEL C A)
989                (LIG C A C A)
990                (STOP)
991            ",
992            "AAAAAA",
993            vec![Ligature {
994                c: 'A',
995                original: "AAAAAA".into(),
996                includes_left_boundary: false,
997                includes_right_boundary: false,
998            },],
999        ),
1000        // AB -> ^A1, A1 -> ^21
1001        (
1002            multiple_lig_5,
1003            "
1004                (LABEL C A)
1005                (/LIG C B C 1)
1006                (LIG/ C 1 C 2)
1007                (STOP)
1008            ",
1009            "AB",
1010            vec![
1011                Ligature {
1012                    c: '2',
1013                    original: "A".into(),
1014                    includes_left_boundary: false,
1015                    includes_right_boundary: false,
1016                },
1017                Ligature {
1018                    c: '1',
1019                    original: "B".into(),
1020                    includes_left_boundary: false,
1021                    includes_right_boundary: false,
1022                },
1023            ],
1024        ),
1025        // AB -> ^A1, A1 -> ^21, 21 -> 3
1026        (
1027            multiple_lig_6,
1028            "
1029                (LABEL C A)
1030                (/LIG C B C 1)
1031                (LIG/ C 1 C 2)
1032                (LABEL C 2)
1033                (LIG C 1 C 3)
1034                (STOP)
1035            ",
1036            "AB",
1037            vec![Ligature {
1038                c: '3',
1039                original: "AB".into(),
1040                includes_left_boundary: false,
1041                includes_right_boundary: false,
1042            }],
1043        ),
1044        // AB -> ^1, 1C -> 12^C
1045        (
1046            multiple_lig_7,
1047            "
1048                (LABEL C A)
1049                (LIG C B C 1)
1050                (STOP)
1051
1052                (LABEL C 1)
1053                (/LIG/>> C C C 2)
1054                (STOP)
1055            ",
1056            "ABC",
1057            vec![
1058                Ligature {
1059                    c: '1',
1060                    original: "AB".into(),
1061                    includes_left_boundary: false,
1062                    includes_right_boundary: false,
1063                },
1064                Ligature {
1065                    c: '2',
1066                    original: "".into(),
1067                    includes_left_boundary: false,
1068                    includes_right_boundary: false,
1069                },
1070                Char('C'),
1071            ],
1072        ),
1073        (
1074            kern_after_lig_1,
1075            "
1076                (LABEL C A)
1077                (LIG C B C 1)
1078                (STOP)
1079
1080                (LABEL C 1)
1081                (KRN C C R 0.1)
1082            ",
1083            "ABC",
1084            vec![
1085                Ligature {
1086                    c: '1',
1087                    original: "AB".into(),
1088                    includes_left_boundary: false,
1089                    includes_right_boundary: false,
1090                },
1091                Kern(Scaled::ONE),
1092                Char('C'),
1093            ],
1094        ),
1095        (
1096            kern_after_lig_2,
1097            "
1098                (LABEL C A)
1099                (LIG C B C 1)
1100                (STOP)
1101
1102                (LABEL C 1)
1103                (KRN C A R 0.1)
1104            ",
1105            "ABAB",
1106            vec![
1107                Ligature {
1108                    c: '1',
1109                    original: "AB".into(),
1110                    includes_left_boundary: false,
1111                    includes_right_boundary: false,
1112                },
1113                Kern(Scaled::ONE),
1114                Ligature {
1115                    c: '1',
1116                    original: "AB".into(),
1117                    includes_left_boundary: false,
1118                    includes_right_boundary: false,
1119                },
1120            ],
1121        ),
1122        (
1123            left_boundary_char_1,
1124            "
1125                (LABEL BOUNDARYCHAR)
1126                (LIG C A C 1)
1127            ",
1128            "A",
1129            vec![Ligature {
1130                c: '1',
1131                original: "A".into(),
1132                includes_left_boundary: true,
1133                includes_right_boundary: false,
1134            },],
1135        ),
1136        (
1137            left_boundary_char_2,
1138            "
1139                (LABEL BOUNDARYCHAR)
1140                (/LIG/ C A C 1)
1141                (/LIG/ C 1 C 2)
1142            ",
1143            "A",
1144            vec![
1145                Ligature {
1146                    c: '2',
1147                    original: "".into(),
1148                    includes_left_boundary: true,
1149                    includes_right_boundary: false,
1150                },
1151                Ligature {
1152                    c: '1',
1153                    original: "".into(),
1154                    includes_left_boundary: false,
1155                    includes_right_boundary: false,
1156                },
1157                Char('A'),
1158            ],
1159        ),
1160        (
1161            left_boundary_char_3,
1162            "
1163                (LABEL BOUNDARYCHAR)
1164                (/LIG/ C A C 1)
1165            ",
1166            "A",
1167            vec![
1168                Ligature {
1169                    c: '1',
1170                    original: "".into(),
1171                    includes_left_boundary: true,
1172                    includes_right_boundary: false,
1173                },
1174                Char('A'),
1175            ],
1176        ),
1177        // |A -> ^|1
1178        (
1179            left_boundary_char_4,
1180            "
1181                (LABEL BOUNDARYCHAR)
1182                (/LIG C A C 1)
1183            ",
1184            "A",
1185            vec![Ligature {
1186                c: '1',
1187                original: "A".into(),
1188                includes_left_boundary: true,
1189                includes_right_boundary: false,
1190            },],
1191        ),
1192        // |A -> |^1
1193        (
1194            left_boundary_char_5,
1195            "
1196                (LABEL BOUNDARYCHAR)
1197                (/LIG> C A C 1)
1198            ",
1199            "A",
1200            vec![Ligature {
1201                c: '1',
1202                original: "A".into(),
1203                includes_left_boundary: true,
1204                includes_right_boundary: false,
1205            },],
1206        ),
1207        // |A -> ^1A
1208        (
1209            left_boundary_char_6,
1210            "
1211                (LABEL BOUNDARYCHAR)
1212                (LIG/ C A C 1)
1213            ",
1214            "A",
1215            vec![
1216                Ligature {
1217                    c: '1',
1218                    original: "".into(),
1219                    includes_left_boundary: true,
1220                    includes_right_boundary: false,
1221                },
1222                Char('A'),
1223            ],
1224        ),
1225        // |A -> 1^A
1226        (
1227            left_boundary_char_7,
1228            "
1229                (LABEL BOUNDARYCHAR)
1230                (LIG/> C A C 1)
1231            ",
1232            "A",
1233            vec![
1234                Ligature {
1235                    c: '1',
1236                    original: "".into(),
1237                    includes_left_boundary: true,
1238                    includes_right_boundary: false,
1239                },
1240                Char('A'),
1241            ],
1242        ),
1243        // |A -> |^1A
1244        (
1245            left_boundary_char_8,
1246            "
1247                (LABEL BOUNDARYCHAR)
1248                (/LIG/> C A C 1)
1249            ",
1250            "A",
1251            vec![
1252                Ligature {
1253                    c: '1',
1254                    original: "".into(),
1255                    includes_left_boundary: true,
1256                    includes_right_boundary: false,
1257                },
1258                Char('A'),
1259            ],
1260        ),
1261        // |A -> |1^A
1262        (
1263            left_boundary_char_9,
1264            "
1265                (LABEL BOUNDARYCHAR)
1266                (/LIG/>> C A C 1)
1267            ",
1268            "A",
1269            vec![
1270                Ligature {
1271                    c: '1',
1272                    original: "".into(),
1273                    includes_left_boundary: true,
1274                    includes_right_boundary: false,
1275                },
1276                Char('A'),
1277            ],
1278        ),
1279        (
1280            right_boundary_char_lig_1,
1281            "
1282                (LABEL C A)
1283                (LIG C R C 1)
1284                (STOP)
1285            ",
1286            "A",
1287            vec![Ligature {
1288                c: '1',
1289                original: "A".into(),
1290                includes_left_boundary: false,
1291                includes_right_boundary: true,
1292            },],
1293        ),
1294        // AR -> ^AB
1295        (
1296            right_boundary_char_lig_2,
1297            "
1298                (LABEL C A)
1299                (/LIG C R C B)
1300                (STOP)
1301            ",
1302            "A",
1303            vec![
1304                Char('A'),
1305                Ligature {
1306                    c: 'B',
1307                    original: "".into(),
1308                    includes_left_boundary: false,
1309                    includes_right_boundary: true,
1310                },
1311            ],
1312        ),
1313        // AR -> A^B
1314        (
1315            right_boundary_char_lig_3,
1316            "
1317                (LABEL C A)
1318                (/LIG> C R C B)
1319                (STOP)
1320            ",
1321            "A",
1322            vec![
1323                Char('A'),
1324                Ligature {
1325                    c: 'B',
1326                    original: "".into(),
1327                    includes_left_boundary: false,
1328                    includes_right_boundary: true,
1329                },
1330            ],
1331        ),
1332        (
1333            right_boundary_char_lig_4,
1334            "
1335                (LABEL C A)
1336                (/LIG/ C R C B)
1337                (STOP)
1338            ",
1339            "A",
1340            vec![
1341                Char('A'),
1342                Ligature {
1343                    c: 'B',
1344                    original: "".into(),
1345                    includes_left_boundary: false,
1346                    includes_right_boundary: true,
1347                },
1348            ],
1349        ),
1350        // AR -> ^BR
1351        (
1352            right_boundary_char_lig_5,
1353            "
1354                (LABEL C A)
1355                (LIG/ C R C B)
1356                (STOP)
1357            ",
1358            "A",
1359            vec![Ligature {
1360                c: 'B',
1361                original: "A".into(),
1362                includes_left_boundary: false,
1363                includes_right_boundary: true,
1364            },],
1365        ),
1366        // AR -> BR^
1367        (
1368            right_boundary_char_lig_6,
1369            "
1370                (LABEL C A)
1371                (LIG/> C R C B)
1372                (STOP)
1373            ",
1374            "A",
1375            vec![Ligature {
1376                c: 'B',
1377                original: "A".into(),
1378                includes_left_boundary: false,
1379                includes_right_boundary: true,
1380            },],
1381        ),
1382        // AR -> A^BR
1383        (
1384            right_boundary_char_lig_7,
1385            "
1386                (LABEL C A)
1387                (/LIG/> C R C B)
1388                (STOP)
1389            ",
1390            "A",
1391            vec![
1392                Char('A'),
1393                Ligature {
1394                    c: 'B',
1395                    original: "".into(),
1396                    includes_left_boundary: false,
1397                    includes_right_boundary: true,
1398                },
1399            ],
1400        ),
1401        // AR -> AB^R
1402        (
1403            right_boundary_char_lig_8,
1404            "
1405                (LABEL C A)
1406                (/LIG/>> C R C B)
1407                (STOP)
1408            ",
1409            "A",
1410            vec![
1411                Char('A'),
1412                Ligature {
1413                    c: 'B',
1414                    original: "".into(),
1415                    includes_left_boundary: false,
1416                    includes_right_boundary: true,
1417                },
1418            ],
1419        ),
1420        (
1421            right_boundary_char_lig_9,
1422            "
1423                (LABEL C A)
1424                (LIG C R C B)
1425                (LABEL C B)
1426                (LIG C R C C)
1427                (STOP)
1428            ",
1429            "A",
1430            vec![Ligature {
1431                c: 'B',
1432                original: "A".into(),
1433                includes_left_boundary: false,
1434                includes_right_boundary: true,
1435            },],
1436        ),
1437        (
1438            right_boundary_char_lig_10,
1439            "
1440                (LABEL C A)
1441                (LIG/ C R C B)
1442                (LABEL C B)
1443                (LIG/ C R C C)
1444                (STOP)
1445            ",
1446            "A",
1447            vec![Ligature {
1448                c: 'C',
1449                original: "A".into(),
1450                includes_left_boundary: false,
1451                includes_right_boundary: true,
1452            },],
1453        ),
1454        (
1455            right_boundary_char_lig_11,
1456            "
1457                (LABEL C A)
1458                (LIG C R C B)
1459                (LABEL C B)
1460                (LIG/ C R C C)
1461                (STOP)
1462            ",
1463            "A",
1464            vec![Ligature {
1465                c: 'B',
1466                original: "A".into(),
1467                includes_left_boundary: false,
1468                includes_right_boundary: true,
1469            },],
1470        ),
1471        (
1472            right_boundary_char_lig_12,
1473            "
1474                (LABEL C A)
1475                (LIG/ C R C B)
1476                (LABEL C B)
1477                (LIG C R C C)
1478                (STOP)
1479            ",
1480            "A",
1481            vec![Ligature {
1482                c: 'C',
1483                original: "A".into(),
1484                includes_left_boundary: false,
1485                includes_right_boundary: true,
1486            },],
1487        ),
1488        (
1489            right_boundary_char_kern_1,
1490            "
1491                (LABEL C A)
1492                (KRN C R R 1)
1493                (STOP)
1494            ",
1495            "A",
1496            vec![Char('A'), Kern(Scaled::ONE * 10),],
1497        ),
1498        (
1499            right_boundary_char_kern_2,
1500            "
1501                (LABEL C A)
1502                (LIG/ C R C B)
1503                (LABEL C B)
1504                (LIG/ C R C C)
1505                (LABEL C C)
1506                (KRN C R R 1)
1507                (STOP)
1508            ",
1509            "A",
1510            vec![
1511                Ligature {
1512                    c: 'C',
1513                    original: "A".into(),
1514                    includes_left_boundary: false,
1515                    includes_right_boundary: true,
1516                },
1517                Kern(Scaled::ONE * 10),
1518            ],
1519        ),
1520        (
1521            right_boundary_char_kern_3,
1522            "
1523                (LABEL C A)
1524                (LIG C B C C)
1525                (LABEL C C)
1526                (KRN C R R 1)
1527                (STOP)
1528            ",
1529            "AB",
1530            vec![
1531                Ligature {
1532                    c: 'C',
1533                    original: "AB".into(),
1534                    includes_left_boundary: false,
1535                    includes_right_boundary: false,
1536                },
1537                Kern(Scaled::ONE * 10),
1538            ],
1539        ),
1540    );
1541}