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}