boxworks_hyphenate/
lib.rs

1use boxworks::ds;
2
3pub struct Hyphenator {
4    // TODOs:
5    // (1) Don't depend on the tfm crate. There should be kind of abstraction here
6    // so that this works with all font types. Maybe the solution is to have a ligkern
7    // crate that contains the non-tfm logic for lig/kern programs. Or some boxworks
8    // abstractions.
9    // (2) Support changing the font!
10    pub lig_kern_program: tfm::ligkern::CompiledProgram,
11    pub hyphenator: hyphenate::Hyphenator,
12    pub left_hyphen_min: i32,
13    pub right_hyphen_min: i32,
14}
15
16impl Hyphenator {
17    /// Creates a hyphenator with plain TeX's English patterns and defaults.
18    ///
19    /// The lig/kern program of the font being hyphenated is required because
20    /// hyphenation breaks ligatures apart to insert discretionaries, and the
21    /// program is needed to reconstitute ligatures and kerns in the result.
22    /// Passing the wrong program (e.g. an empty one) silently produces
23    /// un-ligatured output.
24    pub fn plain_tex_en_us(lig_kern_program: tfm::ligkern::CompiledProgram) -> Self {
25        Self {
26            lig_kern_program,
27            hyphenator: hyphenate::Hyphenator::plain_tex_en_us(),
28            left_hyphen_min: 2,
29            right_hyphen_min: 3,
30        }
31    }
32}
33
34impl boxworks::Hyphenator for Hyphenator {
35    fn hyphenate(&self, list: &mut Vec<ds::Horizontal>) {
36        let out = hyphenate_impl(self, list);
37        *list = out;
38    }
39}
40
41fn hyphenate_impl(hyphenater: &Hyphenator, list: &[ds::Horizontal]) -> Vec<ds::Horizontal> {
42    let lower_caser = hyphenate::AsciiLowerCaser::default();
43    let mut out = vec![];
44    let mut i = 0;
45    while let Some(elem) = list.get(i) {
46        i += 1;
47        out.push(elem.clone());
48        // Hyphenation starts only at glue nodes, as per TeX.2021.866.
49        if !matches!(elem, ds::Horizontal::Glue(_)) {
50            continue;
51        }
52
53        // TODO: hyf_char needs to be read from somewhere using the font numbers
54        // in char and lig nodes.
55        let hyf_char = 3_i32;
56
57        // Find the place to start hyphenating
58        // TeX.2021.896
59        let hyphenation_font: Option<u32> = loop {
60            let Some(elem) = list.get(i) else { break None };
61            enum Action {
62                Start { font: u32 },
63                Continue,
64                // Equivalent to done1 in Knuth's TeX.
65                Abort,
66            }
67            use ds::Horizontal::*;
68            let action = match elem {
69                Char(char) => {
70                    // The hyf_char logic runs in the label done2.
71                    // TODO: this code assumes \uchyph=true (uppercase letters are hyphenated)
72                    // and that \lccode has its PlainTeX values (has lower case character iff
73                    // ASCII alphabetic). We should remove these assumptions by plumbing in some
74                    // parameters.
75                    if char.char.is_ascii_alphabetic() {
76                        if hyf_char > 0 && hyf_char <= 255 {
77                            Action::Start { font: char.font }
78                        } else {
79                            Action::Abort
80                        }
81                    } else {
82                        Action::Continue
83                    }
84                }
85                Ligature(ligature) => {
86                    match ligature.original_chars.chars().next() {
87                        None => Action::Continue,
88                        Some(char) => {
89                            // TODO: all of the TODOs in the Char arm.
90                            if char.is_ascii_alphabetic() {
91                                if hyf_char > 0 && hyf_char <= 255 {
92                                    Action::Start {
93                                        font: ligature.font,
94                                    }
95                                } else {
96                                    Action::Abort
97                                }
98                            } else {
99                                Action::Continue
100                            }
101                        }
102                    }
103                }
104                Whatsit(whatsit) => {
105                    // TeX.2021.1363 but given how we've architected the code, the logic in TeX.2021.1382
106                    // (which changes the current language) should run here.
107                    whatsit.hyphenation_hook();
108                    Action::Continue
109                }
110                Kern(kern) => match kern.kind {
111                    ds::KernKind::Normal => Action::Continue,
112                    _ => Action::Abort,
113                },
114                _ => Action::Abort,
115            };
116            match action {
117                Action::Start { font } => {
118                    break Some(font);
119                }
120                Action::Continue => {
121                    i += 1;
122                    out.push(elem.clone());
123                }
124                Action::Abort => {
125                    i += 1;
126                    out.push(elem.clone());
127                    break None;
128                }
129            }
130        };
131        let Some(hyphenation_font) = hyphenation_font else {
132            continue;
133        };
134        // It's still possible we won't hyphenate based on the node that ends the
135        // string of characters. So we save this `i` value here; if hyphenation is skipped
136        // we set `i` back to this.
137        let hyphenation_start_i = i;
138
139        let mut s = String::new();
140        // Accumulate the word to be hyphenated.
141        // TeX.2021.897
142        let right_boundary: Option<char> =
143            loop {
144                let Some(elem) = list.get(i) else { break None };
145                use ds::Horizontal::*;
146                match elem {
147                    Char(char) => {
148                        if char.font != hyphenation_font {
149                            // TODO: add tests for this case including left/right boundary behaviour.
150                            break None;
151                        }
152                        // TODO: plumb in \lccode and change this check.
153                        if !char.char.is_ascii_alphabetic() {
154                            break Some(char.char);
155                        }
156                        if s.len() + char.char.len_utf8() >= 64 {
157                            // TeX only hyphenates words up to 64 bytes.
158                            // TODO: this check is not quite right: unicode values in the range [128, 255)
159                            // should count as 1 only.
160                            break Some(char.char);
161                        }
162                        s.push(char.char);
163                    }
164                    Ligature(ligature) => {
165                        // TeX.2021.898
166                        if ligature.font != hyphenation_font {
167                            // TODO: add tests for this case including left/right boundary behaviour.
168                            break None;
169                        }
170                        if !ligature
171                            .original_chars
172                            .chars()
173                            .all(|c| c.is_ascii_alphabetic())
174                        {
175                            break Some(ligature.original_chars.chars().next().expect(
176                                "there must be at least one char for this branch to execute",
177                            ));
178                        }
179                        if s.len() + ligature.original_chars.len() >= 64 {
180                            // TeX only hyphenates words up to 64 bytes.
181                            // TODO: this check is not quite right: unicode values in the range [128, 255)
182                            // should count as 1 only.
183                            break Some(ligature.original_chars.chars().next().expect(
184                                "there must be at least one char for this branch to execute",
185                            ));
186                        }
187                        s.push_str(&ligature.original_chars);
188                    }
189                    Kern(kern) => match kern.kind {
190                        ds::KernKind::Normal => {
191                            // TODO: set up the lig/kern program correctly.
192                        }
193                        _ => break None,
194                    },
195                    _ => break None,
196                }
197                // Consume the node whose characters have just been placed in s (or the normal kern).
198                i += 1;
199            };
200        // The first char node that triggered the word search will have been put in s.
201        assert!(!s.is_empty());
202
203        // Check if the word can be hyphenated based on the terminating node.
204        // TeX.2021.899
205        // We use a different index to iterate as all of the elements here still need to be
206        // copied to the output list.
207        let mut j = i;
208        let should_hyphenate = loop {
209            let Some(elem) = list.get(j) else { break true };
210            use ds::Horizontal::*;
211            match elem {
212                Char(_) | Ligature(_) => {
213                    // This can happen if the font is different, or the 64 byte limit is already
214                    // reached.
215                }
216                Kern(kern) => match kern.kind {
217                    ds::KernKind::Normal => {
218                        // TODO: set up the lig/kern program correctly.
219                    }
220                    _ => break true,
221                },
222                Whatsit(_) | Glue(_) | Penalty(_) | Insertion(_) | Adjust(_) | Mark(_) => {
223                    break true;
224                }
225                HBox(_) | VBox(_) | Rule(_) | Discretionary(_) | Math(_) => {
226                    // done1 in Knuth's TeX.f
227                    break false;
228                }
229            }
230            j += 1;
231        };
232        if !should_hyphenate {
233            i = hyphenation_start_i;
234            continue;
235        }
236
237        let l = s.chars().count();
238
239        let mut indices = {
240            let indices = hyphenater.hyphenator.calculate_indices(&lower_caser, &s);
241            // TeX.2021.1200
242            let left_hyphen_min: usize = match hyphenater.left_hyphen_min.try_into() {
243                Ok(0) | Err(_) => 1,
244                Ok(i) => i,
245            };
246            let right_hyphen_min: usize = match hyphenater.right_hyphen_min.try_into() {
247                Ok(0) | Err(_) => 1,
248                Ok(i) => i,
249            };
250            let hyph_max = l.saturating_sub(right_hyphen_min);
251            IndexIter::new(indices, left_hyphen_min, hyph_max)
252        };
253        let mut next_or = indices.next();
254
255        let mut main_iter = hyphenater.lig_kern_program.run_iter(&s, right_boundary);
256        use tfm::ligkern::RunItem;
257        let mut chars_pushed = 0;
258        // This corresponds to the loop in TeX.2021.913 but not 1-1.
259        //
260        // TeX's loop is across all cut prefixes whereas this loop is over each
261        // individual lig/kern element that is emitted.
262        //
263        // Knuth's reconstitute method sets hyphen_passed>0 if either the main lig/kern
264        // program ran over a hyphen while processing the cut prefix,
265        // or if the hyphen lig/kern program is non-trivial and
266        // thus needs to run. His reconstitute method does *not* consider the regular case where the
267        // hyphen does not interact with the lig/kern program; this case is handled in the
268        // body of section 913.
269        while let Some(elem) = main_iter.next() {
270            let prev_chars_pushed = chars_pushed;
271            // TODO: don't have 3 matches...
272            chars_pushed += match &elem {
273                RunItem::Char(_) => 1,
274                RunItem::Ligature(ligature) => ligature.original.chars().count(),
275                RunItem::Kern(_) => 0,
276            };
277            let last_char = match &elem {
278                RunItem::Char(c) => Some(*c),
279                RunItem::Ligature(ligature) => ligature.original.chars().last(),
280                RunItem::Kern(_) => None,
281            };
282            let original_elem: ds::Horizontal = match elem {
283                RunItem::Char(c) => ds::Char {
284                    char: c,
285                    font: hyphenation_font,
286                }
287                .into(),
288                RunItem::Kern(scaled) => ds::Kern {
289                    width: scaled,
290                    kind: ds::KernKind::Normal,
291                }
292                .into(),
293                RunItem::Ligature(ligature) => ds::Ligature {
294                    included_left_boundary: false,
295                    included_right_boundary: false,
296                    char: ligature.c,
297                    font: hyphenation_font,
298                    original_chars: ligature.original,
299                }
300                .into(),
301            };
302            // TODO: debug assert this is equal to the original element in the list
303            out.push(original_elem);
304
305            let hyph_next = next_or.unwrap_or(usize::MAX);
306            if hyph_next > chars_pushed {
307                // No hyphen here.
308                continue;
309            }
310
311            // TODO: I'm sure we can remove this expect by piping the char from the matches above.
312            let last_char =
313                last_char.expect("we are seeing this hyphen because we have passed a character");
314            let is_hyphen_rule = hyphenater
315                .lig_kern_program
316                .has_replacement(Some(last_char), Some('-'));
317
318            let (mut pop_before_disc, mut pre_break_text) = if hyph_next == chars_pushed
319                && main_iter.is_separation_point()
320                && !is_hyphen_rule
321            {
322                // regime 1: the hyphen is exactly on a lig/kern atom boundary, there is no hyphen lig/kern program.
323                // In this case we put the element now, and then insert the discretionary.
324                (0, "-".into())
325            } else {
326                // regime 2: either the hyphen was in the middle of the lig element OR there is a hyphen lig/kern
327                // program. In this case we need to put the element in afterwards.
328                // TODO: is this enough or do we need to reverse the atom entirely?
329                // TODO: we are indexing string using chars bad bad bad.
330                // Some simple unicode tests will show this :)
331                (1, format!["{}-", &s[prev_chars_pushed..hyph_next]])
332            };
333
334            // This is the loop in TeX.2021.344.
335            loop {
336                let pre_break: Vec<ds::DiscretionaryElem> = hyphenater
337                    .lig_kern_program
338                    .run_iter(&pre_break_text, None)
339                    .map(|elem| {
340                        let d: ds::DiscretionaryElem = match elem {
341                            RunItem::Char(c) => ds::Char {
342                                char: c,
343                                font: hyphenation_font,
344                            }
345                            .into(),
346                            RunItem::Kern(scaled) => ds::Kern {
347                                width: scaled,
348                                kind: ds::KernKind::Normal,
349                            }
350                            .into(),
351                            RunItem::Ligature(ligature) => ds::Ligature {
352                                char: ligature.c,
353                                font: hyphenation_font,
354                                included_left_boundary: false,  // TODO
355                                included_right_boundary: false, // TODO
356                                original_chars: ligature.original.clone(),
357                            }
358                            .into(),
359                        };
360                        d
361                    })
362                    .collect();
363
364                let hyph_next = next_or.unwrap_or(usize::MAX);
365                let post_break_text = &s[hyph_next..];
366                let mut post_break_iter = hyphenater
367                    .lig_kern_program
368                    .run_iter(post_break_text, right_boundary);
369                let mut post_break: Vec<ds::DiscretionaryElem> = vec![];
370                let mut post_chars_pushed = hyph_next;
371
372                let mut post_disc: Vec<ds::Horizontal> = vec![];
373                // TODO: if pop_before_disc is only ever in [0,1], change it to a boolean and simplify this code.
374                // This all hinges on the other TODO about whether we need to actually add all the elements
375                // since the last synchronization pt.
376                for _ in 0..pop_before_disc {
377                    post_disc.push(out.pop().unwrap());
378                    post_disc.reverse();
379                }
380
381                // This is the loop in TeX.2021.916.
382                // We want to achieve synchronization for the two iterators.
383                loop {
384                    if post_chars_pushed == chars_pushed
385                        && post_break_iter.is_separation_point()
386                        && main_iter.is_separation_point()
387                    {
388                        break;
389                    }
390                    if post_chars_pushed < chars_pushed {
391                        while let Some(elem) = post_break_iter.next() {
392                            post_chars_pushed += match &elem {
393                                RunItem::Char(_) => 1,
394                                RunItem::Kern(_) => 0,
395                                RunItem::Ligature(ligature) => ligature.original.chars().count(),
396                            };
397                            post_break.push(match elem {
398                                RunItem::Char(c) => ds::Char {
399                                    char: c,
400                                    font: hyphenation_font,
401                                }
402                                .into(),
403                                RunItem::Kern(scaled) => ds::Kern {
404                                    width: scaled,
405                                    kind: ds::KernKind::Normal,
406                                }
407                                .into(),
408                                RunItem::Ligature(ligature) => ds::Ligature {
409                                    char: ligature.c,
410                                    font: hyphenation_font,
411                                    included_left_boundary: false, // TODO
412                                    included_right_boundary: false, // TODO
413                                    original_chars: ligature.original.clone(),
414                                }
415                                .into(),
416                            });
417                            if post_break_iter.is_separation_point() {
418                                break;
419                            }
420                        }
421                    } else {
422                        while let Some(elem) = main_iter.next() {
423                            chars_pushed += match &elem {
424                                RunItem::Char(_) => 1,
425                                RunItem::Kern(_) => 0,
426                                RunItem::Ligature(ligature) => ligature.original.chars().count(),
427                            };
428                            post_disc.push(match elem {
429                                RunItem::Char(c) => ds::Char {
430                                    char: c,
431                                    font: hyphenation_font,
432                                }
433                                .into(),
434                                RunItem::Kern(scaled) => ds::Kern {
435                                    width: scaled,
436                                    kind: ds::KernKind::Normal,
437                                }
438                                .into(),
439                                RunItem::Ligature(ligature) => ds::Ligature {
440                                    char: ligature.c,
441                                    font: hyphenation_font,
442                                    included_left_boundary: false, // TODO
443                                    included_right_boundary: false, // TODO
444                                    original_chars: ligature.original,
445                                }
446                                .into(),
447                            });
448                            if main_iter.is_separation_point() {
449                                break;
450                            }
451                        }
452                    }
453                }
454                out.push(
455                    ds::Discretionary {
456                        pre_break,
457                        post_break,
458                        replace_count: post_disc.len().try_into().unwrap(),
459                    }
460                    .into(),
461                );
462                out.append(&mut post_disc);
463
464                // We increment the hyphen index at least once to account for the hyphen we have just inserted.
465                next_or = indices.next();
466                // When performing synchronization we may have passed over some hyphens.
467                while next_or.unwrap_or(usize::MAX) < chars_pushed {
468                    next_or = indices.next();
469                }
470                if next_or.unwrap_or(usize::MAX) > chars_pushed {
471                    // We need to consume more characters before trying more hyphens
472                    break;
473                }
474                pop_before_disc = 0;
475                pre_break_text = "-".into();
476            }
477        }
478    }
479    out
480}
481
482struct IndexIter<I> {
483    inner: I,
484    min: usize,
485    max: usize,
486}
487
488impl<I: Iterator<Item = usize>> IndexIter<I> {
489    fn new(inner: I, min: usize, max: usize) -> Self {
490        Self { inner, min, max }
491    }
492}
493
494impl<I: Iterator<Item = usize>> Iterator for IndexIter<I> {
495    type Item = usize;
496
497    fn next(&mut self) -> Option<Self::Item> {
498        let n = self.inner.next()?;
499        if n >= self.min && n <= self.max {
500            return Some(n);
501        }
502        self.next()
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use std::collections::HashMap;
509    use std::path::PathBuf;
510
511    use super::*;
512    use boxworks::TextPreprocessor;
513    use boxworks_testing::assert_box_eq;
514    use boxworks_text as bwt;
515
516    const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
517
518    fn run_hyphenation_test(tc: TestCase) {
519        let unhyphenated: String = tc.input.chars().filter(|c| *c != '-').collect();
520        let hyphenation_patterns = tc.hyphenation_patterns.unwrap_or(&tc.input);
521
522        // TeX does not hyphenate the first word of a paragraph so we need to put
523        // another word before the word of interest
524        let tex_input = format!["x {unhyphenated}"];
525
526        let mut tfm_file = tfm::File::deserialize(TFM_CMR10).0.unwrap();
527        {
528            let (p, e) = tfm::ligkern::lang::Program::parse_compact(tc.lig_kern_program).unwrap();
529            tfm_file.replace_lig_kern_program(p, e);
530        }
531
532        if std::env::var("TEXCRAFT_VERIFY").unwrap_or("".to_string()) == "tex" {
533            let tfm_bytes = tfm_file.serialize();
534            let mut auxiliary_files: HashMap<PathBuf, Vec<u8>> = Default::default();
535            auxiliary_files.insert("specialFont.tfm".into(), tfm_bytes);
536
537            let preamble = format![
538                r"
539                    \font \customFont specialFont
540                    
541                    \hyphenation{{{}}}
542                    \lefthyphenmin={}
543                    \righthyphenmin=0
544                    
545                    \customFont
546                ",
547                hyphenation_patterns,
548                tc.left_hyphen_min.unwrap_or(1),
549            ];
550            let mut tex_engine = boxworks::tex::new_tex_engine_binary("tex".to_string()).unwrap();
551            let (_, tex_got) = boxworks::tex::build_horizontal_lists(
552                tex_engine.as_mut(),
553                &auxiliary_files,
554                &preamble,
555                &mut vec![tex_input.clone()].iter(),
556                /*hyphenate=*/ true,
557            );
558            let tex_got: Vec<boxworks::ds::Horizontal> =
559                tex_got[0].list[2..].iter().cloned().collect();
560
561            assert_box_eq!(tc.want, tex_got);
562            return;
563        }
564
565        let lig_kern_program =
566            tfm::ligkern::CompiledProgram::compile_from_tfm_file(&mut tfm_file).0;
567        let mut tp = bwt::TextPreprocessorImpl::new(bwt::Params::plain_tex_defaults());
568        tp.register_font(0, &tfm_file, lig_kern_program.clone());
569        tp.activate_font(0);
570        let mut list = vec![];
571        for word in tex_input.split_ascii_whitespace() {
572            tp.add_word(word.trim_matches(' '), &mut list);
573            tp.add_space(&mut list);
574        }
575        list.pop();
576
577        let mut font_repo: bwt::TfmFontRepo = Default::default();
578        font_repo.register_font(0, tfm_file);
579
580        let mut hyphenator = Hyphenator::plain_tex_en_us(lig_kern_program);
581        hyphenator
582            .hyphenator
583            .insert_exceptions(hyphenation_patterns);
584        hyphenator.left_hyphen_min = tc.left_hyphen_min.unwrap_or(1);
585        hyphenator.right_hyphen_min = 1;
586        {
587            use boxworks::Hyphenator;
588            hyphenator.hyphenate(&mut list)
589        }
590
591        let tex_got: Vec<boxworks::ds::Horizontal> = list[2..].iter().cloned().collect();
592        assert_box_eq!(tc.want, tex_got);
593    }
594
595    macro_rules! hyphenation_tests {
596        ( $( {
597            $name: ident,
598            TestCase {
599                $( $field: ident: $value: expr, )*
600            }
601        }, )* ) => {
602            $(
603                #[test]
604                fn $name() {
605                    let test_case = TestCase {
606                        $( $field: $value, )*
607                        ..Default::default()
608                    };
609                    run_hyphenation_test(test_case);
610                }
611            )*
612        };
613    }
614
615    #[derive(Default)]
616    struct TestCase {
617        input: &'static str,
618        lig_kern_program: &'static str,
619        want: &'static str,
620        hyphenation_patterns: Option<&'static str>,
621        left_hyphen_min: Option<i32>,
622    }
623
624    hyphenation_tests![
625        {
626            no_hyphens,
627            TestCase {
628                input: "mint",
629                lig_kern_program: "",
630                want: r#"
631                    chars("mint")
632                "#,
633            }
634        },
635        {
636            most_simple_case,
637            TestCase {
638                input: "a-b",
639                lig_kern_program: "",
640                want: r#"
641                    chars("a")
642                    disc(
643                      pre_break=[
644                        chars("-")
645                      ],
646                    )
647                    chars("b")
648                "#,
649            }
650        },
651        {
652            lig_1,
653            TestCase {
654                input: "a-b",
655                lig_kern_program: "
656                    ab -> axb^
657                ",
658                want: r#"
659                    disc(
660                      pre_break=[
661                        chars("a-")
662                      ],
663                      replace_count=2,
664                    )
665                    chars("a")
666                    lig("x", "")
667                    chars("b")
668                "#,
669            }
670        },
671        {
672            lig_with_hyphen,
673            TestCase {
674                input: "a-b",
675                lig_kern_program: "
676                    a- -> ax-^
677                ",
678                want: r#"
679                    disc(
680                      pre_break=[
681                        chars("a")
682                        lig("x", "")
683                        chars("-")
684                      ],
685                      replace_count=1,
686                    )
687                    chars("a")
688                    chars("b")
689                "#,
690            }
691        },
692        {
693            lig_with_hyphen_and_letters,
694            TestCase {
695                input: "a-b",
696                lig_kern_program: "
697                    a- -> ax-^
698                    ab -> ac^_
699                ",
700                want: r#"
701                    disc(
702                      pre_break=[
703                        chars("a")
704                        lig("x", "")
705                        chars("-")
706                      ],
707                      post_break=[
708                        chars("b")
709                      ],
710                      replace_count=2,
711                    )
712                    chars("a")
713                    lig("c", "b")
714                "#,
715            }
716        },
717        /* TODO: fix the bug
718        {
719            // BUG: handled in TeX.2021.916. If there is a left boundary char
720            // synchronization cannot be a no-op as it is currently.
721            left_boundary_char,
722            TestCase {
723                input: "a-b",
724                lig_kern_program: "
725                    |b -> |c^_
726                ",
727                want: r#"
728                    chars("a")
729                    disc(
730                      pre_break=[
731                        chars("-")
732                      ],
733                      post_break=[
734                        chars("c")
735                      ],
736                      replace_count=1,
737                    )
738                    chars("b")
739                "#,
740            }
741        },
742        {
743            // BUG: we're running the hyphen lig kern program without disabling
744            // left char behaviour. Need to change run_iter to factor this in.
745            left_boundary_char_does_not_change_hyphen,
746            TestCase {
747                input: "a-b",
748                lig_kern_program: "
749                    |- -> |c^_
750                ",
751                want: r#"
752                    chars("a")
753                    disc(
754                      pre_break=[
755                        chars("-")
756                      ],
757                    )
758                    chars("b")
759                "#,
760            }
761        },
762        */
763        /*
764        TODO: the original_chars in the lig doesn't match TeX.
765        {
766            right_boundary_char_after_hyphen,
767            TestCase {
768                input: "a-b",
769                lig_kern_program: "
770                    -| -> -c^|
771                ",
772                want: r#"
773                    chars("a")
774                    disc(
775                      pre_break=[
776                        chars("-")
777                        lig("c", "")
778                      ],
779                    )
780                    chars("b")
781                "#,
782            }
783        },
784        */
785        {
786            big_lig_1,
787            TestCase {
788                input: "a-bc",
789                lig_kern_program: "
790                    ab -> _x^_
791                    xc -> _y^_
792                ",
793                want: r#"
794                    disc(
795                      pre_break=[
796                        chars("a")
797                        chars("-")
798                      ],
799                      post_break=[
800                        chars("b")
801                        chars("c")
802                      ],
803                      replace_count=1,
804                    )
805                    lig("y", "abc")
806                "#,
807            }
808        },
809        {
810            big_lig_2,
811            TestCase {
812                input: "a-bc",
813                lig_kern_program: "
814                    ab -> _x^_
815                    xc -> _y^_
816                    bc -> _z^_
817                ",
818                want: r#"
819                    disc(
820                      pre_break=[
821                        chars("a")
822                        chars("-")
823                      ],
824                      post_break=[
825                        lig("z", "bc")
826                      ],
827                      replace_count=1,
828                    )
829                    lig("y", "abc")
830                "#,
831            }
832        },
833        {
834            big_lig_3,
835            TestCase {
836                input: "ab-c",
837                lig_kern_program: "
838                    ab -> _x^_
839                    xc -> _y^_
840                ",
841                want: r#"
842                    disc(
843                      pre_break=[
844                        lig("x", "ab")
845                        chars("-")
846                      ],
847                      post_break=[
848                        chars("c")
849                      ],
850                      replace_count=1,
851                    )
852                    lig("y", "abc")
853                "#,
854            }
855        },
856        {
857            big_lig_4,
858            TestCase {
859                input: "ab-c",
860                lig_kern_program: "
861                    ab -> ax^_
862                ",
863                want: r#"
864                    chars("a")
865                    lig("x", "b")
866                    disc(
867                      pre_break=[
868                        chars("-")
869                      ],
870                      replace_count=0,
871                    )
872                    chars("c")
873                "#,
874            }
875        },
876        /*
877         * TODO: fix the bug
878        {
879            // BUG: probably how the simple case looks for the lig rule "b-" rather than "x-" but
880            // I'm not sure.
881            big_lig_with_hyphen,
882            TestCase {
883                input: "ab-c",
884                lig_kern_program: "
885                    ab -> ax^_
886                    x- -> xy^-
887                ",
888                want: r#"
889                    disc(
890                      pre_break=[
891                        chars("axy")
892                        chars("-")
893                      ],
894                      replace_count=2,
895                    )
896                    chars("a")
897                    lig("x", "b")
898                    chars("c")
899                "#,
900            }
901        },
902        */
903        {
904            big_lig_with_hyphen_2,
905            TestCase {
906                input: "ab-c",
907                lig_kern_program: "
908                    ab -> ax^b
909                    x- -> xy^-
910                ",
911                want: r#"
912                    chars("a")
913                    lig("x", "")
914                    chars("b")
915                    disc(
916                      pre_break=[
917                        chars("-")
918                      ],
919                      replace_count=0,
920                    )
921                    chars("c")
922                "#,
923            }
924        },
925        {
926            empty_lig_before,
927            TestCase {
928                input: "a-b",
929                lig_kern_program: "
930                    ab -> ax^b
931                ",
932                want: r#"
933                    disc(
934                      pre_break=[
935                        chars("a")
936                        chars("-")
937                      ],
938                      replace_count=2,
939                    )
940                    chars("a")
941                    lig("x", "")
942                    chars("b")
943                "#,
944            }
945        },
946        {
947            simple_kern,
948            TestCase {
949                input: "a-b",
950                lig_kern_program: "
951                    ab -> a[100]b
952                ",
953                want: r#"
954                    disc(
955                      pre_break=[
956                        chars("a")
957                        chars("-")
958                      ],
959                      replace_count=2,
960                    )
961                    chars("a")
962                    kern(0.00095pt)
963                    chars("b")
964                "#,
965            }
966        },
967        {
968            same_kern,
969            TestCase {
970                input: "a-b",
971                lig_kern_program: "
972                    ab -> a[100]b
973                    a- -> a[100]-
974                ",
975                want: r#"
976                    disc(
977                      pre_break=[
978                        chars("a")
979                        kern(0.00095pt)
980                        chars("-")
981                      ],
982                      replace_count=2,
983                    )
984                    chars("a")
985                    kern(0.00095pt)
986                    chars("b")
987                "#,
988            }
989        },
990        {
991            synchronization_1,
992            TestCase {
993                input: "a-bcdefgh",
994                lig_kern_program: "
995                    ab -> _x^_
996                    bc -> _y^_
997                    cd -> _z^_
998                    de -> _w^_
999                    ef -> _v^_
1000                ",
1001                want: r#"
1002                    disc(
1003                      pre_break=[
1004                        chars("a-")
1005                      ],
1006                      post_break=[
1007                        lig("y", "bc")
1008                        lig("w", "de")
1009                        chars("f")
1010                      ],
1011                      replace_count=3,
1012                    )
1013                    lig("x", "ab")
1014                    lig("z", "cd")
1015                    lig("v", "ef")
1016                    # synchronization point
1017                    chars("gh", font=0)
1018                "#,
1019            }
1020        },
1021        {
1022            synchronization_2,
1023            TestCase {
1024                input: "a-bcd-ef-gh",
1025                lig_kern_program: "
1026                    ab -> _x^_
1027                    bc -> _y^_
1028                    cd -> _z^_
1029                    de -> _w^_
1030                    ef -> _v^_
1031                ",
1032                want: r#"
1033                    disc(
1034                      pre_break=[
1035                        chars("a-")
1036                      ],
1037                      post_break=[
1038                        lig("y", "bc")
1039                        lig("w", "de")
1040                        chars("f")
1041                      ],
1042                      replace_count=3,
1043                    )
1044                    lig("x", "ab")
1045                    lig("z", "cd")
1046                    # the hyphen here is skipped
1047                    lig("v", "ef")
1048                    # synchronization point
1049                    disc(
1050                      pre_break=[
1051                        chars("-")
1052                      ],
1053                      post_break=[
1054                      ],
1055                    )
1056                    chars("gh", font=0)
1057                "#,
1058            }
1059        },
1060        {
1061            synchronization_3,
1062            TestCase {
1063                input: "a-bcde",
1064                lig_kern_program: "
1065                    ab -> _x^_
1066                    bc -> _y^_
1067                    xc -> _y^_
1068                    yd -> yzd^
1069                ",
1070                want: r#"
1071                    disc(
1072                      pre_break=[
1073                        chars("a-")
1074                      ],
1075                      post_break=[
1076                        lig("y", "bc")
1077                        lig("z", "")
1078                      ],
1079                      replace_count=2,
1080                    )
1081                    lig("y", "abc")
1082                    lig("z", "")
1083                    chars("de", font=0)
1084                "#,
1085            }
1086        },
1087        {
1088            word_ends_in_comma_1,
1089            TestCase {
1090                input: "baby,",
1091                lig_kern_program: "
1092                    y, -> y[100],
1093                    y| -> y[200]|
1094                ",
1095                want: r#"
1096                    chars("baby")
1097                    kern(0.00095pt)
1098                    chars(",")
1099                "#,
1100                hyphenation_patterns: Some("baby"),
1101            }
1102        },
1103        {
1104            word_ends_in_comma_2,
1105            TestCase {
1106                input: "baby,",
1107                lig_kern_program: "
1108                    y, -> y[100],
1109                    y| -> y[200]|
1110                ",
1111                want: r#"
1112                    chars("ba")
1113                    disc(
1114                      pre_break=[
1115                        chars("-")
1116                      ],
1117                    )
1118                    chars("by")
1119                    kern(0.00095pt)
1120                    chars(",")
1121                "#,
1122                hyphenation_patterns: Some("ba-by"),
1123            }
1124        },
1125        /* TODO: fix the bugs
1126        {
1127            right_boundary_char_override_1,
1128            TestCase {
1129                input: "ba-by",
1130                lig_kern_program: "
1131                    y| -> y.^|
1132                ",
1133                want: r#"
1134                    chars("ba")
1135                    disc(
1136                      pre_break=[
1137                        chars("-")
1138                      ],
1139                    )
1140                    chars("by")
1141                    lig(".", "|")
1142                "#,
1143            }
1144        },
1145        {
1146            right_boundary_char_override_2,
1147            TestCase {
1148                input: "ab.",
1149                lig_kern_program: "
1150                    |b -> |c^_
1151                    c. -> c,^_
1152                ",
1153                want: r#"
1154                    chars("a")
1155                    disc(
1156                      pre_break=[
1157                        chars("-")
1158                      ],
1159                      post_break=[
1160                        chars("c,")
1161                      ],
1162                      replace_count=1,
1163                    )
1164                    chars("b.")
1165                "#,
1166                hyphenation_patterns: Some("a-b"),
1167            }
1168        },
1169        {
1170            right_boundary_char_override_3,
1171            TestCase {
1172                input: "journey.",
1173                lig_kern_program: "
1174                    y. -> y^,_
1175                    ,| -> ,?^|
1176                ",
1177                want: r#"
1178                    chars("jour")
1179                    disc(
1180                      pre_break=[
1181                        chars("-")
1182                      ],
1183                    )
1184                    chars("ney")
1185                    lig(",", "|")
1186                    lig(",", ".")
1187                    lig("?", "|")
1188                "#,
1189                hyphenation_patterns: Some(""),
1190            }
1191        },
1192        */
1193        {
1194            right_boundary_char_override_4,
1195            TestCase {
1196                input: "journey.",
1197                lig_kern_program: "
1198                    y. -> y^,_
1199                    y, -> y^?_
1200                ",
1201                want: r#"
1202                    chars("jour")
1203                    disc(
1204                      pre_break=[
1205                        chars("-")
1206                      ],
1207                    )
1208                    chars("ney")
1209                    lig("?", "|")
1210                    lig("?", ".")
1211                "#,
1212                hyphenation_patterns: Some(""),
1213            }
1214        },
1215
1216        {
1217            right_boundary_char_override_5,
1218            TestCase {
1219                input: "journey.",
1220                lig_kern_program: "
1221                    y. -> y,^_
1222                ",
1223                want: r#"
1224                    chars("jour")
1225                    disc(
1226                      pre_break=[
1227                        chars("-")
1228                      ],
1229                    )
1230                    chars("ney")
1231                    lig(",", "|")
1232                    lig(",", ".")
1233                "#,
1234                hyphenation_patterns: Some(""),
1235            }
1236        },
1237        {
1238            right_boundary_char_override_6,
1239            TestCase {
1240                input: "journey.",
1241                lig_kern_program: "
1242                    y. -> y^,_
1243                    y, -> y^?,
1244                ",
1245                want: r#"
1246                    chars("jour")
1247                    disc(
1248                      pre_break=[
1249                        chars("-")
1250                      ],
1251                    )
1252                    chars("ney")
1253                    lig("?", "")
1254                    lig(",", "|")
1255                    lig(",", ".")
1256                "#,
1257                hyphenation_patterns: Some(""),
1258            }
1259        },
1260        {
1261            sneezing,
1262            TestCase {
1263                input: "sneezing",
1264                lig_kern_program: "
1265                    y. -> y^,_
1266                    y, -> y^?,
1267                ",
1268                want: r#"
1269                    chars("sneez")
1270                    disc(
1271                      pre_break=[
1272                        chars("-")
1273                      ],
1274                    )
1275                    chars("ing")
1276                "#,
1277                hyphenation_patterns: Some(""),
1278                left_hyphen_min: Some(3),
1279            }
1280        },
1281        {
1282            difficult,
1283            TestCase {
1284                input: "d-if-fi-cult",
1285                lig_kern_program: "
1286                    ff -> _0^_
1287                    0i -> _1^_
1288                ",
1289                want: r#"
1290                    chars("di")
1291                    disc(
1292                      pre_break=[
1293                       chars("f-")
1294                      ],
1295                      post_break=[
1296                        chars("fi")
1297                      ],
1298                      replace_count=1,
1299                    )
1300                    lig("1", "ffi")
1301                    disc(
1302                      pre_break=[
1303                        chars("-")
1304                      ],
1305                    )
1306                    chars("cult")
1307                "#,
1308                hyphenation_patterns: Some(""),
1309                left_hyphen_min: Some(3),
1310            }
1311        },
1312    ];
1313}