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}