boxworks_knuthplass/
lib.rs

1//! # Knuth-Plass line breaking algorithm.
2
3use std::{collections::VecDeque, ops::AddAssign};
4
5use boxworks::ds::{self, KernKind};
6use common::{font, GlueOrder, Scaled};
7pub mod debug;
8
9pub struct LineBreaker<'a, Font> {
10    pub params: &'a Params,
11    pub line_widths: &'a [Scaled],
12    pub line_indents: &'a [Scaled],
13    pub debug_logger: Option<&'a mut dyn debug::Logger>,
14    pub hyphenator: &'a dyn boxworks::Hyphenator,
15    pub font_repo: &'a font::Repo<Font>,
16}
17
18#[derive(Debug)]
19pub struct Params {
20    pub adj_demerits: i32,
21    pub broken_penalty: i32,
22    pub double_hyphen_demerits: i32,
23    pub club_penalty: i32,
24    pub emergency_stretch: Scaled,
25    pub ex_hyphen_penalty: i32,
26    pub final_hyphen_demerits: i32,
27    // TODO: this is actually different for each invocation. Make it so!
28    pub final_widow_penalty: i32,
29    pub hyphen_penalty: i32,
30    pub inter_line_penalty: i32,
31    pub left_skip: common::Glue,
32    pub line_penalty: i32,
33    pub looseness: i32,
34    pub par_fill_skip: common::Glue,
35    pub pre_tolerance: i32,
36    pub right_skip: common::Glue,
37    pub tolerance: i32,
38}
39
40impl Default for Params {
41    fn default() -> Self {
42        Self::plain_tex_defaults()
43    }
44}
45
46impl Params {
47    pub fn plain_tex_defaults() -> Self {
48        Self {
49            adj_demerits: 10000,
50            broken_penalty: 100,
51            double_hyphen_demerits: 10000,
52            club_penalty: 150,
53            emergency_stretch: Scaled::ZERO,
54            ex_hyphen_penalty: 50,
55            final_hyphen_demerits: 5000,
56            final_widow_penalty: 150,
57            hyphen_penalty: 50,
58            inter_line_penalty: 0,
59            left_skip: common::Glue::ZERO,
60            line_penalty: 10,
61            looseness: 0,
62            par_fill_skip: common::Glue {
63                width: Scaled::ZERO,
64                stretch: Scaled::ONE,
65                stretch_order: common::GlueOrder::Fil,
66                shrink: Scaled::ZERO,
67                shrink_order: Default::default(),
68            },
69            pre_tolerance: 100,
70            right_skip: common::Glue::ZERO,
71            tolerance: 200,
72        }
73    }
74
75    /// Output the parameters in TeX format.
76    pub fn tex(&self) -> String {
77        let Params {
78            adj_demerits,
79            broken_penalty,
80            double_hyphen_demerits,
81            club_penalty,
82            emergency_stretch,
83            ex_hyphen_penalty,
84            final_hyphen_demerits,
85            final_widow_penalty,
86            hyphen_penalty,
87            inter_line_penalty,
88            left_skip,
89            line_penalty,
90            looseness,
91            par_fill_skip,
92            pre_tolerance,
93            right_skip,
94            tolerance,
95        } = self;
96        format!(
97            r"
98            \adjdemerits={adj_demerits}
99            \brokenpenalty={broken_penalty}
100            \clubpenalty={club_penalty}
101            \doublehyphendemerits={double_hyphen_demerits}
102            \emergencystretch={emergency_stretch}
103            \exhyphenpenalty={ex_hyphen_penalty}
104            \finalhyphendemerits={final_hyphen_demerits}
105            \hyphenpenalty={hyphen_penalty}
106            \interlinepenalty={inter_line_penalty}
107            \leftskip={left_skip}
108            \linepenalty={line_penalty}
109            \looseness={looseness}
110            \parfillskip={par_fill_skip}
111            \pretolerance={pre_tolerance}
112            \rightskip={right_skip}
113            \tolerance={tolerance}
114            \widowpenalty={final_widow_penalty}
115        "
116        )
117    }
118}
119
120#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
121struct Scaled64(i64);
122
123impl std::ops::Add for Scaled64 {
124    type Output = Self;
125
126    fn add(self, rhs: Self) -> Self::Output {
127        Self(self.0 + rhs.0)
128    }
129}
130
131impl std::ops::Sub for Scaled64 {
132    type Output = Self;
133
134    fn sub(self, rhs: Self) -> Self::Output {
135        Self(self.0 - rhs.0)
136    }
137}
138
139impl std::ops::Neg for Scaled64 {
140    type Output = Self;
141
142    fn neg(self) -> Self::Output {
143        Self(-self.0)
144    }
145}
146
147impl AddAssign<Scaled> for Scaled64 {
148    fn add_assign(&mut self, rhs: Scaled) {
149        self.0 += rhs.0 as i64
150    }
151}
152
153impl std::ops::SubAssign<Scaled> for Scaled64 {
154    fn sub_assign(&mut self, rhs: Scaled) {
155        self.0 -= rhs.0 as i64
156    }
157}
158
159#[derive(Debug, Clone, Default, PartialEq, Eq)]
160struct Diffs {
161    width: Scaled64,
162    shrinkability: Scaled64,
163    stretchabilities: [Scaled64; 4],
164}
165
166impl Diffs {
167    fn update_from_glue(&mut self, glue: &common::Glue) {
168        self.width += glue.width;
169        // TODO: emit warning if the shrink order is not normal like in TeX.2021.825
170        self.shrinkability += glue.shrink;
171        self.stretchabilities[glue.stretch_order as usize] += glue.stretch;
172    }
173    fn infinitely_stretchable(&self) -> bool {
174        self.stretchabilities[GlueOrder::Fil as usize].0 != 0
175            || self.stretchabilities[GlueOrder::Fill as usize].0 != 0
176            || self.stretchabilities[GlueOrder::Filll as usize].0 != 0
177    }
178    fn finite_stretchability(&self) -> Scaled64 {
179        self.stretchabilities[GlueOrder::Normal as usize]
180    }
181}
182
183impl std::ops::Add for Diffs {
184    type Output = Diffs;
185
186    fn add(self, rhs: Self) -> Self::Output {
187        Diffs {
188            width: self.width + rhs.width,
189            shrinkability: self.shrinkability + rhs.shrinkability,
190            stretchabilities: [
191                self.stretchabilities[0] + rhs.stretchabilities[0],
192                self.stretchabilities[1] + rhs.stretchabilities[1],
193                self.stretchabilities[2] + rhs.stretchabilities[2],
194                self.stretchabilities[3] + rhs.stretchabilities[3],
195            ],
196        }
197    }
198}
199
200impl std::ops::Sub for Diffs {
201    type Output = Diffs;
202
203    fn sub(self, rhs: Self) -> Self::Output {
204        Diffs {
205            width: self.width - rhs.width,
206            shrinkability: self.shrinkability - rhs.shrinkability,
207            stretchabilities: [
208                self.stretchabilities[0] - rhs.stretchabilities[0],
209                self.stretchabilities[1] - rhs.stretchabilities[1],
210                self.stretchabilities[2] - rhs.stretchabilities[2],
211                self.stretchabilities[3] - rhs.stretchabilities[3],
212            ],
213        }
214    }
215}
216
217// TeX.2021.819
218#[derive(Clone, Debug, PartialEq, Eq)]
219struct ActiveNode {
220    // Deltas between this active node and the preceding element in
221    // the list of active nodes.
222    diffs: Diffs,
223    fitness_class: FitnessClass,
224    hyphenated: bool,
225    line_number: usize,
226    // Index of the passive node corresponding to this active node.
227    node_index: usize,
228    total_demerits: i32,
229}
230
231/// TeX.2021.817
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233enum FitnessClass {
234    VeryLoose = 0,
235    Loose = 1,
236    Decent = 2,
237    Tight = 3,
238}
239
240struct PassiveNode {
241    // Index of the element in the horizontal list
242    elem: usize,
243    // Index of the passive node corresponding to the previous break
244    // in the optimal path to this node.
245    previous_node_index: usize,
246}
247
248impl<'a, Font: font::Format> boxworks::LineBreaker for LineBreaker<'a, Font> {
249    fn break_line(mut self, v_list: &mut Vec<ds::Vertical>, h_list: &mut Vec<ds::Horizontal>) {
250        // This function is analogous to TeX.2021.815.
251
252        // TeX.2021.816
253        if matches!(h_list.last(), Some(ds::Horizontal::Glue(_))) {
254            h_list.pop();
255        }
256        h_list.push(ds::Horizontal::Penalty(ds::Penalty::INFINITE));
257        h_list.push(ds::Horizontal::Glue(ds::Glue {
258            kind: ds::GlueKind::Normal,
259            value: self.params.par_fill_skip,
260        }));
261
262        let break_points = self.break_line_all_attempts(self.hyphenator, h_list);
263        self.post_line_break(v_list, h_list, &break_points);
264    }
265}
266
267impl<'a, Font: font::Format> LineBreaker<'a, Font> {
268    fn post_line_break(
269        &self,
270        v_list: &mut Vec<ds::Vertical>,
271        h_list: &[ds::Horizontal],
272        break_points: &[usize],
273    ) {
274        let mut start_of_line = 0_usize;
275        let mut disc_post_break_nodes: Option<Vec<ds::DiscretionaryElem>> = None;
276        for (line_index, break_point) in break_points.iter().enumerate() {
277            let mut inner_list: Vec<ds::Horizontal> = vec![];
278
279            // TeX.2021.887
280            if !self.params.left_skip.is_zero() {
281                inner_list.push(
282                    ds::Glue {
283                        value: self.params.left_skip,
284                        kind: ds::GlueKind::Normal,
285                    }
286                    .into(),
287                );
288            }
289
290            // TeX.2021.884
291            if let Some(disc_nodes) = disc_post_break_nodes.take() {
292                for disc_node in disc_nodes {
293                    inner_list.push(disc_node.into());
294                }
295            }
296
297            inner_list.extend_from_slice(&h_list[start_of_line..*break_point]);
298            start_of_line = *break_point + 1;
299
300            // TeX.2021.881
301            // This is the check that `q != null` in Knuth's TeX.
302            // This logic does not run for the final breakpoint.
303            if let Some(break_point_node) = h_list.get(*break_point).cloned() {
304                use ds::Horizontal::*;
305                match break_point_node {
306                    Discretionary(discretionary) => {
307                        // TeX.2021.882
308                        // The empty discretionary survives in the list, funnily enough.
309                        inner_list.push(Discretionary(Default::default()));
310                        for pre_break_node in discretionary.pre_break {
311                            inner_list.push(pre_break_node.into());
312                        }
313                        disc_post_break_nodes = Some(discretionary.post_break);
314                        start_of_line += discretionary.replace_count as usize;
315                    }
316                    Math(math) => {
317                        // TODO: set the width of Math to zero
318                        inner_list.push(math.into());
319                    }
320                    Glue(_) => {
321                        // Do nothing. In TeX there is an "optimization" in which the glue is
322                        // modified to be \rightskip, but we don't do this. Instead it is inserted
323                        // below.
324                    }
325                    Kern(mut kern) => {
326                        kern.width = Scaled::ZERO;
327                        inner_list.push(kern.into());
328                    }
329                    Penalty(penalty) => {
330                        // Do nothing.
331                        inner_list.push(penalty.into());
332                    }
333                    _ => {
334                        unreachable!("node cannot appear as a breakpoint: {break_point_node:?}");
335                    }
336                }
337            }
338
339            // TeX.2021.886
340            // Unlike \leftskip, there is no check if the glue here is zero.
341            inner_list.push(
342                ds::Glue {
343                    value: self.params.right_skip,
344                    kind: ds::GlueKind::Normal,
345                }
346                .into(),
347            );
348
349            // TeX.2021.889
350            let width = self
351                .line_widths
352                .get(line_index)
353                .unwrap_or(self.line_widths.last().expect("non-empty line widths"));
354            let indent = self
355                .line_indents
356                .get(line_index)
357                .copied()
358                .unwrap_or(self.line_indents.last().copied().unwrap_or(Scaled::ZERO));
359            let h_box = {
360                let mut b =
361                    ds::HBox::pack(self.font_repo, inner_list, ds::PackWidth::Exact(*width));
362                b.shift_amount = indent;
363                b
364            };
365
366            // TeX.2021.888 and TeX.2021.679
367            if !v_list.is_empty() {
368                // TODO: make \baselineskip configurable
369                // TODO: implement `\lineskiplimit` `\lineskip`.
370                let mut baseline_skip = common::Glue {
371                    width: common::Scaled::ONE * 12,
372                    ..Default::default()
373                };
374                baseline_skip.width -= h_box.height;
375                let mut j = v_list.len() - 1;
376                // TODO: this is probably way too wrong. And shouldn't be managed
377                // here: probably the v list should carry last_depth, like Knuth
378                // does.
379                let last_depth = loop {
380                    let elem = &v_list[j];
381                    use ds::Vertical::*;
382                    match elem {
383                        HBox(hbox) => break hbox.depth,
384                        VBox(vbox) => break vbox.depth,
385                        _ => {}
386                    };
387                    j = match j.checked_sub(1) {
388                        None => break common::Scaled::ZERO,
389                        Some(j) => j,
390                    };
391                };
392                baseline_skip.width -= last_depth;
393                v_list.push(
394                    ds::Glue {
395                        value: baseline_skip,
396                        kind: Default::default(),
397                    }
398                    .into(),
399                );
400            }
401            v_list.push(h_box.into());
402
403            // TeX.2021.890
404            // If this is not the last line, we consider penalties.
405            if line_index + 1 != break_points.len() {
406                let mut p = self.params.inter_line_penalty;
407                if line_index == 0 {
408                    p += self.params.club_penalty;
409                }
410                if line_index + 2 == break_points.len() {
411                    p += self.params.final_widow_penalty;
412                }
413                if disc_post_break_nodes.is_some() {
414                    p += self.params.broken_penalty;
415                }
416                if p != 0 {
417                    v_list.push(ds::Penalty(p).into());
418                }
419            }
420        }
421    }
422    pub fn break_line_all_attempts(
423        &mut self,
424        hyphenator: &dyn boxworks::Hyphenator,
425        h_list: &mut Vec<ds::Horizontal>,
426    ) -> Vec<usize> {
427        // We manually unroll the "loop" in TeX.2021.863.
428        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
429            debug_logger.log_attempt(debug::Attempt::First);
430        }
431        if let Some(v) = self.break_line_single_attempt(
432            h_list,
433            self.params.pre_tolerance,
434            common::Scaled::ZERO,
435            false,
436        ) {
437            return v;
438        }
439        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
440            debug_logger.log_attempt(debug::Attempt::Second);
441        }
442        hyphenator.hyphenate(h_list);
443        // The only difference between the second and third passes is that in
444        // the third pass emergency stretch is added. If the emergency stretch
445        // is zero, then the two passes are the same. In this case, as an
446        // optimization, we skip the third pass. This optimization is in Knuth's
447        // TeX as well.
448        let second_pass_is_final_pass = self.params.emergency_stretch.is_zero();
449        if let Some(v) = self.break_line_single_attempt(
450            h_list,
451            self.params.tolerance,
452            common::Scaled::ZERO,
453            second_pass_is_final_pass,
454        ) {
455            return v;
456        }
457        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
458            debug_logger.log_attempt(debug::Attempt::Emergency);
459        }
460        self.break_line_single_attempt(
461            h_list,
462            self.params.tolerance,
463            self.params.emergency_stretch,
464            true,
465        )
466        .expect("force_solution=true")
467    }
468
469    pub fn break_line_single_attempt(
470        &mut self,
471        list: &[ds::Horizontal],
472        tolerance: i32,
473        emergency_stretch: common::Scaled,
474        force_solution: bool,
475    ) -> Option<Vec<usize>> {
476        let mut auto_breaking = true;
477        let mut passive_nodes = vec![PassiveNode {
478            elem: 0,
479            previous_node_index: 0,
480        }];
481
482        let background = {
483            // TeX.2021.827
484            let mut b: Diffs = Default::default();
485            b.update_from_glue(&self.params.left_skip);
486            b.update_from_glue(&self.params.right_skip);
487            b.stretchabilities[common::GlueOrder::Normal as usize] += emergency_stretch;
488            b
489        };
490        let mut active_nodes = VecDeque::<ActiveNode>::from([
491            // TeX.2021.864
492            ActiveNode {
493                diffs: Default::default(),
494                fitness_class: FitnessClass::Decent,
495                hyphenated: false,
496                line_number: 0,
497                node_index: 0,
498                total_demerits: 0,
499            },
500        ]);
501        // TeX.2021.864
502        let mut diffs: Diffs = Default::default();
503        // This is the loop in TeX.2021.863
504        for i in 0..=list.len() {
505            let elem = list.get(i);
506            use ds::Horizontal::*;
507            let mut disc_width = Scaled::ZERO;
508            // This switch is TeX.2021.866. In TeX, Knuth invokes `try_break` inline
509            // at the relevant parts of the switch. We instead return the two arguments
510            // to `try_break` from the match, and the rest of this function implements
511            // the `try_break`. Switch cases for which `try_break` should not be invoked
512            // contain a continue statement.
513            let (mut penalty, hyphenated) = match elem {
514                None => {
515                    // This corresponds to the try_break call in TeX.202.873.
516                    // I'm guessing the last break is considered hyphenated so as to penalize
517                    // the second-to-last line being hyphenated. A hyphen doesn't look nice
518                    // in the bottom right of the paragraph.
519                    (EJECT_PENALTY, true)
520                }
521                Some(elem) => match elem {
522                    Char(ds::Char { char, font }) | Ligature(ds::Ligature { char, font, .. }) => {
523                        // TeX.2021.867 has an optimization in which subsequent chars are read
524                        // here. I'm not convinced it's worth it.
525                        diffs.width += self
526                            .font_repo
527                            .get(*font)
528                            .width(*char)
529                            .unwrap_or(Scaled::ZERO);
530                        continue;
531                    }
532                    HBox(ds::HBox { width, .. })
533                    | VBox(ds::VBox { width, .. })
534                    | Rule(ds::Rule { width, .. }) => {
535                        diffs.width += *width;
536                        continue;
537                    }
538                    Mark(_) | Insertion(_) | Adjust(_) => {
539                        // do nothing
540                        continue;
541                    }
542                    Discretionary(discretionary) => {
543                        // TeX.2021.869
544                        disc_width = discretionary
545                            .pre_break
546                            .iter()
547                            .map(|e| e.width(self.font_repo))
548                            .sum();
549                        (
550                            if discretionary.pre_break.is_empty() {
551                                self.params.ex_hyphen_penalty
552                            } else {
553                                self.params.hyphen_penalty
554                            },
555                            true,
556                        )
557                        // Knuth includes the following optimization, which we omit.
558                        // The discretionary node specifies that the following r
559                        // elements of the horizontal list should be removed if
560                        // a break occurs here. These elements must be one of the 6
561                        // types allowed in discretionary lists. None of these elements
562                        // can themselves be breakpoints. Thus, Knuth skips ahead
563                        // by r elements in the horizontal list just updating the widths.
564                        // It's unclear if this optimization is worth implementing...
565                    }
566                    Whatsit(_whatsit) => todo!(),
567                    Math(math) => {
568                        auto_breaking = *math == ds::Math::After;
569                        if auto_breaking && matches!(list.get(i + 1), Some(Glue(_))) {
570                            // List of allowable line breaks in TeXBook chapter 14 p96:
571                            // (c) at a math-off that is immediately followed by glue.
572                            (0, false)
573                        } else {
574                            continue;
575                        }
576                    }
577                    Glue(glue) => {
578                        // TeX.2021.868
579                        if auto_breaking && i > 0 && list[i - 1].precedes_break() {
580                            // List of allowable line breaks in TeXBook chapter 14 p96:
581                            // (a) at glue, provided that this glue is immediately preceded by
582                            // a non-discardable item, and that it is not part of a math formula
583                            // (i.e., not between math-on and math-off). A break "at glue" occurs
584                            // at the left edge of the glue space.
585                            (0, false)
586                        } else {
587                            diffs.update_from_glue(&glue.value);
588                            continue;
589                        }
590                    }
591                    Kern(kern) => {
592                        if kern.kind == KernKind::Explicit
593                            && auto_breaking
594                            && matches!(list.get(i + 1), Some(Glue(_)))
595                        {
596                            // List of allowable line breaks in TeXBook chapter 14 p96:
597                            // (b) at a kern, provided that this kern is immediately followed by glue,
598                            // and that it is not part of a math formula.
599                            (0, false)
600                        } else {
601                            diffs.width += kern.width;
602                            continue;
603                        }
604                    }
605                    Penalty(penalty) => {
606                        // List of allowable line breaks in TeXBook chapter 14 p96:
607                        // (d) at a penalty (which might have been inserted automatically in a formula).
608                        (penalty.0, false)
609                    }
610                },
611            };
612
613            // TeX.2021.831
614            if penalty >= INFINITE_PENALTY {
615                // TODO: For discretionary nodes we need to adjust the width here?
616                continue;
617            }
618            if penalty <= EJECT_PENALTY {
619                penalty = EJECT_PENALTY
620            }
621
622            let mut n = active_nodes.len();
623            while n > 0 {
624                let mut m = self.num_nodes_for_next_class(&active_nodes, n);
625                n -= m;
626
627                // TeX.2021.833
628                #[derive(Clone, Copy, Debug)]
629                struct Candidate {
630                    total_demerits: i32,
631                    previous_node_index: usize,
632                    line_number: usize,
633                    artificial_demerits: bool,
634                }
635                // TeX.2021.834
636                let mut candidates = [Candidate {
637                    total_demerits: AWFUL_BAD,
638                    previous_node_index: 0,
639                    line_number: 0,
640                    artificial_demerits: false,
641                }; 4];
642                let mut minimum_demerits = AWFUL_BAD;
643
644                while m > 0 {
645                    m -= 1;
646                    let active_node = active_nodes.pop_front().expect("active nodes to consider");
647                    // This is the key formula that essentially defines what we're doing with diffs.
648                    let line_diffs = diffs.clone() - active_node.diffs.clone() + background.clone();
649                    let line_width = self
650                        .line_widths
651                        // The index here is actually line_index = (line_number - 1)
652                        // = (previous_line_number + 1 - 1) = previous_line_number.
653                        .get(active_node.line_number)
654                        .copied()
655                        .unwrap_or(*self.line_widths.last().unwrap());
656
657                    // TeX.2021.851
658                    let shortfall = Scaled64(line_width.0 as i64)
659                        - line_diffs.width
660                        - Scaled64(disc_width.0 as i64);
661                    let (badness, fitness_class) = if shortfall.0 > 0 {
662                        // Stretching the line
663                        // TeX.2021.852
664                        if line_diffs.infinitely_stretchable() {
665                            (0, FitnessClass::Decent)
666                        } else {
667                            let b = badness(shortfall, line_diffs.finite_stretchability());
668                            (
669                                b,
670                                if b <= 12 {
671                                    FitnessClass::Decent
672                                } else if b <= 99 {
673                                    FitnessClass::Loose
674                                } else {
675                                    FitnessClass::VeryLoose
676                                },
677                            )
678                        }
679                    } else {
680                        // Shrinking the line
681                        // TeX.2021.853
682                        let b = if -shortfall > line_diffs.shrinkability {
683                            INFINITE_BADNESS + 1
684                        } else {
685                            badness(-shortfall, line_diffs.shrinkability)
686                        };
687                        (
688                            b,
689                            if b <= 12 {
690                                FitnessClass::Decent
691                            } else {
692                                FitnessClass::Tight
693                            },
694                        )
695                    };
696
697                    // The condition of the if statement is in TeX.2021.851
698                    let (deactivate, allowable_break, artificial_demerits) =
699                        if badness > INFINITE_BADNESS || penalty == EJECT_PENALTY {
700                            // TeX.2021.854
701                            if force_solution
702                                && minimum_demerits == AWFUL_BAD
703                                && active_nodes.is_empty()
704                            {
705                                (true, true, true)
706                            } else {
707                                (true, badness <= tolerance, false)
708                            }
709                        } else {
710                            (false, badness <= tolerance, false)
711                        };
712
713                    // Allowable break.
714                    // Add a candidate
715                    if allowable_break {
716                        // TeX.2021.855
717                        let demerits = if artificial_demerits {
718                            0_i32
719                        } else {
720                            self.demerits(
721                                badness,
722                                penalty,
723                                active_node.fitness_class,
724                                fitness_class,
725                                active_node.hyphenated && hyphenated,
726                                active_node.hyphenated && elem.is_none(),
727                            )
728                        };
729                        let total_demerits = demerits + active_node.total_demerits;
730                        // The logging here is implemented in TeX.2021.856
731                        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
732                            debug_logger.log_feasible_breakpoint(
733                                list,
734                                debug::FeasibleBreakpoint {
735                                    elem_index: i,
736                                    badness,
737                                    penalty,
738                                    demerits,
739                                    artificial_demerits,
740                                    previous_node_index: active_node.node_index,
741                                },
742                            );
743                        }
744                        let candidate = &mut candidates[fitness_class as usize];
745                        if total_demerits <= candidate.total_demerits {
746                            *candidate = Candidate {
747                                total_demerits,
748                                previous_node_index: active_node.node_index,
749                                line_number: active_node.line_number + 1,
750                                artificial_demerits,
751                            };
752                        }
753                        if total_demerits <= minimum_demerits {
754                            minimum_demerits = total_demerits;
755                        }
756                    }
757                    if !deactivate {
758                        // Our implementation of TeX.2021.860
759                        active_nodes.push_back(active_node);
760                    }
761                }
762
763                // TeX.2021.835
764                if minimum_demerits < AWFUL_BAD {
765                    // TeX.2021.836
766                    if self.params.adj_demerits.abs() >= AWFUL_BAD - minimum_demerits {
767                        minimum_demerits = AWFUL_BAD - 1;
768                    } else {
769                        minimum_demerits += self.params.adj_demerits.abs();
770                    }
771                    let mut diffs = diffs.clone();
772                    if let Some(elem) = elem {
773                        // TeX.2021.837
774                        match elem {
775                            Discretionary(discretionary) => {
776                                // TeX.2021.840
777                                let mut j = i + 1;
778                                while j < i + 1 + discretionary.replace_count as usize {
779                                    // TeX.2021.841
780                                    diffs.width += match &list[j] {
781                                        Char(ds::Char { char, font })
782                                        | Ligature(ds::Ligature { char, font, .. }) => self
783                                            .font_repo
784                                            .get(*font)
785                                            .width(*char)
786                                            .unwrap_or(Scaled::ZERO),
787                                        HBox(ds::HBox { width, .. })
788                                        | VBox(ds::VBox { width, .. })
789                                        | Rule(ds::Rule { width, .. })
790                                        | Kern(ds::Kern { width, .. }) => *width,
791                                        _ => {
792                                            eprintln!(
793                                                "invalid node {:?} in discretionary replacement list",
794                                                &list[j]
795                                            );
796                                            Scaled::ZERO
797                                        }
798                                    };
799                                    j += 1;
800                                }
801                                for elem in &discretionary.post_break {
802                                    // TeX.2021.842
803                                    use ds::DiscretionaryElem::*;
804                                    diffs.width -= match elem {
805                                        Char(ds::Char { char, font })
806                                        | Ligature(ds::Ligature { char, font, .. }) => self
807                                            .font_repo
808                                            .get(*font)
809                                            .width(*char)
810                                            .unwrap_or(Scaled::ZERO),
811                                        HBox(ds::HBox { width, .. })
812                                        | VBox(ds::VBox { width, .. })
813                                        | Rule(ds::Rule { width, .. })
814                                        | Kern(ds::Kern { width, .. }) => *width,
815                                    }
816                                }
817                            }
818                            Math(_math) => {
819                                // TODO when math node is fixed in boxworks crate.
820                            }
821                            Glue(glue) => {
822                                diffs.update_from_glue(&glue.value);
823                            }
824                            Kern(kern) => {
825                                if kern.kind == ds::KernKind::Explicit {
826                                    diffs.width -= kern.width;
827                                }
828                            }
829                            _ => {}
830                        }
831                    }
832                    for fitness_class in [
833                        FitnessClass::VeryLoose,
834                        FitnessClass::Loose,
835                        FitnessClass::Decent,
836                        FitnessClass::Tight,
837                    ] {
838                        let candidate = candidates[fitness_class as usize];
839                        if candidate.total_demerits > minimum_demerits {
840                            continue;
841                        }
842                        // TeX.2021.845
843                        let active_node = ActiveNode {
844                            diffs: diffs.clone(),
845                            fitness_class,
846                            hyphenated,
847                            line_number: candidate.line_number,
848                            node_index: passive_nodes.len(),
849                            total_demerits: candidate.total_demerits,
850                        };
851
852                        // Logging here is TeX.2021.846
853                        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
854                            debug_logger.log_new_active_node(debug::NewActiveNode {
855                                node_index: active_node.node_index,
856                                line_number: active_node.line_number,
857                                fitness_class: active_node.fitness_class as u8,
858                                hyphenated: active_node.hyphenated,
859                                total_demerits: active_node.total_demerits,
860                                artificial_demerits: candidate.artificial_demerits,
861                                previous_node_index: candidate.previous_node_index,
862                            });
863                        }
864                        active_nodes.push_back(active_node);
865                        passive_nodes.push(PassiveNode {
866                            elem: i,
867                            previous_node_index: candidate.previous_node_index,
868                        });
869                    }
870                }
871            }
872
873            // At the end we update the widths.
874            let Some(elem) = elem else { break };
875            match elem {
876                Char(_) | HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Adjust(_)
877                | Ligature(_) => {
878                    // Unreachable because we've already handled these nodes in
879                    // the earlier switch and skipped the rest of the loop.
880                }
881                Discretionary(_) => {
882                    // diffs.width += -disc_width;
883                }
884                Whatsit(_whatsit) => todo!(),
885                Math(_) => {
886                    // Math nodes have no width.
887                }
888                Glue(glue) => {
889                    diffs.update_from_glue(&glue.value);
890                }
891                Kern(kern) => {
892                    diffs.width += kern.width;
893                }
894                Penalty(_) => {
895                    // Penalty nodes have no width.
896                }
897            }
898        }
899
900        // TeX.2021.874
901        // The following line exits early if there are no active nodes and thus no solution.
902        let mut best = active_nodes.front()?;
903        for active_node in &active_nodes {
904            if active_node.total_demerits < best.total_demerits {
905                best = active_node;
906            }
907        }
908
909        if self.params.looseness != 0 {
910            // TeX.2021.875
911            let looseness = self.params.looseness;
912            let best_line_number: i32 = best.line_number.try_into().unwrap();
913            let mut actual_looseness = 0;
914            for active_node in &active_nodes {
915                let line_number: i32 = active_node.line_number.try_into().unwrap();
916                let line_diff = line_number - best_line_number;
917
918                if (line_diff < actual_looseness && looseness <= line_diff)
919                    || (line_diff > actual_looseness && looseness >= line_diff)
920                {
921                    best = active_node;
922                    actual_looseness = line_diff;
923                } else if line_diff == actual_looseness
924                    && active_node.total_demerits < best.total_demerits
925                {
926                    best = active_node;
927                }
928            }
929            // If we don't get the desired looseness and this is not the final pass,
930            // we try again. This conditional is a negated version of the last conditional
931            // in TeX.2021.873.
932            if actual_looseness != looseness && !force_solution {
933                return None;
934            }
935        }
936
937        if let Some(debug_logger) = self.debug_logger.as_deref_mut() {
938            debug_logger.log_selected_node(best.node_index);
939        }
940
941        // TeX.2021.878
942        let mut v = vec![];
943        let mut index = best.node_index;
944        while index > 0 {
945            let passive_node = &passive_nodes[index];
946            v.push(passive_node.elem);
947            index = passive_node.previous_node_index;
948        }
949        v.reverse();
950        Some(v)
951    }
952
953    /// TeX.2021.859
954    fn demerits(
955        &self,
956        badness: i32,
957        penalty: i32,
958        previous_fitness_class: FitnessClass,
959        this_fitness_class: FitnessClass,
960        consecutive_hyphens: bool,
961        end_after_hyphen: bool,
962    ) -> i32 {
963        let mut d = self.params.line_penalty + badness;
964        if d.abs() >= 10_000 {
965            d = 10_000;
966        }
967        d = d * d;
968        if penalty > 0 {
969            d += penalty * penalty;
970        } else if penalty > EJECT_PENALTY {
971            d -= penalty * penalty;
972        }
973        if end_after_hyphen {
974            d += self.params.final_hyphen_demerits;
975        } else if consecutive_hyphens {
976            d += self.params.double_hyphen_demerits;
977        }
978        if (previous_fitness_class as isize - this_fitness_class as isize).abs() > 1 {
979            d += self.params.adj_demerits;
980        }
981        d
982    }
983
984    /// Returns the number of active nodes at the head of the list that will all create
985    /// new active nodes of the same line class.
986    ///
987    /// Note that the active nodes at the head grouped in this was may not all have the
988    /// same line class themselves. For example,
989    /// if the line widths are [5,4,3], then there are 3 line classes: line 1, line 2, and
990    /// remaining lines. Active nodes in line 1 create active nodes in line 2. But active
991    /// nodes in line 2 and remaining lines create active nodes in remaining lines. Thus in
992    /// the grouping here, active nodes for line 2 and remaining nodes are returned
993    /// together.
994    ///
995    /// This "subtlety" is covered by unit tests and was in fact discovered by a failing
996    /// unit test.
997    fn num_nodes_for_next_class(&self, active_nodes: &VecDeque<ActiveNode>, k: usize) -> usize {
998        let first_active_node = active_nodes.front().expect("active nodes are non-empty");
999        let prev_line_number = first_active_node.line_number;
1000        if self.params.looseness == 0 && prev_line_number + 2 >= self.line_widths.len() {
1001            // This covers the last class of active nodes whose widths are all the same.
1002            return k;
1003        }
1004        active_nodes
1005            .iter()
1006            .take(k)
1007            .take_while(|active_node| active_node.line_number == first_active_node.line_number)
1008            .count()
1009    }
1010}
1011
1012// TeX.2021.833
1013const AWFUL_BAD: i32 = 0o7_777_777_777;
1014const INFINITE_BADNESS: i32 = 10000;
1015const INFINITE_PENALTY: i32 = 10000;
1016const EJECT_PENALTY: i32 = -10000;
1017
1018/// TeX.2021.108
1019fn badness(shortfall: Scaled64, stretchability: Scaled64) -> i32 {
1020    let t = shortfall.0;
1021    let s = stretchability.0;
1022    if t == 0 {
1023        return 0;
1024    }
1025    if s <= 0 {
1026        return INFINITE_BADNESS;
1027    }
1028    let r = if t <= 7_230_584 {
1029        (t * 297) / s
1030    } else if s >= 1_663_497 {
1031        t / (s / 297)
1032    } else {
1033        t
1034    };
1035    if r > 1290 {
1036        INFINITE_BADNESS
1037    } else {
1038        ((r * r * r + 0o400_000) / 0o1_000_000)
1039            .try_into()
1040            .unwrap_or(INFINITE_BADNESS)
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use boxworks_text as bwt;
1048    use common::font;
1049    use pretty_assertions::assert_eq;
1050    use std::{cell::RefCell, rc::Rc};
1051
1052    const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
1053
1054    macro_rules! tests {
1055        (
1056            $( (
1057                $name: ident,
1058                $input: expr,
1059                $widths: expr,
1060                $( text_params: boxworks_text::Params {
1061                    $( $text_param_name: ident: $text_param_value: expr, )+
1062                }, )?
1063                $( params: Params {
1064                    $( $param_name: ident: $param_value: expr, )+
1065                }, )?
1066                $( typeset: $want: expr,
1067                    $( not_typeset: $not_want: expr, )?
1068                )?
1069                $( log: $want_log: expr, )?
1070            ), )+
1071        ) => {
1072            $(
1073                mod $name {
1074                    use super::*;
1075                    const INPUT: &'static str = include_str!(concat!("../testdata/", $input));
1076                    $(
1077                    #[test]
1078                    fn typeset() {
1079                        let input_file = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/", $want);
1080                        let want = include_str!(concat!("../testdata/", $want));
1081                        let widths = $widths;
1082                        run_test(
1083                            TFM_CMR10,
1084                            INPUT,
1085                            input_file,
1086                            want,
1087                            widths,
1088                            text_params(),
1089                            params(),
1090                        );
1091                    }
1092                    $(
1093                    /// Verifies that the parameters under test actually change
1094                    /// the output: the expected output must differ from the
1095                    /// expected output with default parameters (`not_typeset`).
1096                    #[test]
1097                    fn not_typeset() {
1098                        let want = include_str!(concat!("../testdata/", $want));
1099                        let not_want = include_str!(concat!("../testdata/", $not_want));
1100                        assert_typeset_ne(want, not_want);
1101                    }
1102                    )?
1103                    )?
1104                    $(
1105                    #[test]
1106                    fn log() {
1107                        let log_file = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/", $want_log);
1108                        let want_log = include_str!(concat!("../testdata/", $want_log));
1109                        let widths = $widths;
1110                        run_log_test(
1111                            TFM_CMR10,
1112                            INPUT,
1113                            log_file,
1114                            want_log,
1115                            widths,
1116                            text_params(),
1117                            params(),
1118                        );
1119                    }
1120                    )?
1121
1122                    fn text_params() -> boxworks_text::Params {
1123                        boxworks_text::Params {
1124                            $( $(
1125                                $text_param_name: $text_param_value,
1126                            )+ )?
1127                            .. boxworks_text::Params::plain_tex_defaults()
1128                        }
1129                    }
1130
1131                    fn params() -> Params {
1132                        Params {
1133                            $( $(
1134                                $param_name: $param_value,
1135                            )+ )?
1136                            .. Params::plain_tex_defaults()
1137                        }
1138                    }
1139                }
1140            )+
1141        };
1142    }
1143
1144    tests!(
1145        (
1146            wolf_hall_5in,
1147            "wolf_hall_input.txt",
1148            &["5in"],
1149            typeset: "wolf_hall_5in_want.txt",
1150            log: "wolf_hall_5in_log.txt",
1151        ),
1152        (
1153            wolf_hall_3in,
1154            "wolf_hall_input.txt",
1155            &["3in"],
1156            typeset: "wolf_hall_3in_want.txt",
1157            log: "wolf_hall_3in_log.txt",
1158        ),
1159        (
1160            wolf_hall_2in,
1161            "wolf_hall_input.txt",
1162            &["2in"],
1163            typeset: "wolf_hall_2in_want.txt",
1164            log: "wolf_hall_2in_log.txt",
1165        ),
1166        (
1167            // At this width the second pass fails and, since
1168            // \emergencystretch is zero by default, TeX gives up and emits
1169            // overfull/very loose lines. This test mainly exists so that its
1170            // want file can serve as the not_typeset baseline of the
1171            // emergency_stretch test below.
1172            wolf_hall_1in,
1173            "wolf_hall_input.txt",
1174            &["1in"],
1175            typeset: "wolf_hall_1in_want.txt",
1176            log: "wolf_hall_1in_log.txt",
1177        ),
1178        (
1179            // With a non-zero \emergencystretch, the failing second pass is
1180            // followed by a third pass in which every line gets the extra
1181            // stretchability.
1182            wolf_hall_emergency_stretch,
1183            "wolf_hall_input.txt",
1184            &["1in"],
1185            params: Params {
1186                emergency_stretch: common::Scaled::parse_from_string("10.0pt").unwrap(),
1187            },
1188            typeset: "wolf_hall_emergency_stretch_want.txt",
1189            not_typeset: "wolf_hall_1in_want.txt",
1190            log: "wolf_hall_emergency_stretch_log.txt",
1191        ),
1192    (
1193        // With a non-zero \emergencystretch, the failing second pass is
1194        // followed by a third pass in which every line gets the extra
1195        // stretchability.
1196        wolf_hall_emergency_stretch_2,
1197        "wolf_hall_input.txt",
1198        &["3in"],
1199        params: Params {
1200            emergency_stretch: common::Scaled::parse_from_string("10.0pt").unwrap(),
1201        },
1202        typeset: "wolf_hall_emergency_stretch_2_want.txt",
1203        // not_typeset: "wolf_hall_1in_want.txt",
1204        log: "wolf_hall_emergency_stretch_2_log.txt",
1205    ),
1206        (
1207            wolf_hall_variable_widths,
1208            "wolf_hall_input.txt",
1209            &["5in", "4in", "3in", "4in"],
1210            typeset: "wolf_hall_variable_widths_want.txt",
1211            log: "wolf_hall_variable_widths_log.txt",
1212        ),
1213        (
1214            farewell_to_arms_looseness_plus_1,
1215            "farewell_to_arms_input.txt",
1216            &["3in"],
1217            params: Params {
1218                looseness: 1,
1219            },
1220            typeset: "farewell_to_arms_looseness_plus_1_want.txt",
1221            // There is a bug here to do with the printing of post-break material.
1222            // Knuth prints the -fi after the discretionary. But right now I don't
1223            // know how he gets the ligature replacement text.
1224            log: "farewell_to_arms_looseness_plus_1_log.txt",
1225        ),
1226        (
1227            farewell_to_arms_looseness_minus_1,
1228            "farewell_to_arms_input.txt",
1229            &["5in"],
1230            params: Params {
1231                looseness: -1,
1232            },
1233            typeset: "farewell_to_arms_looseness_minus_1_want.txt",
1234            log: "farewell_to_arms_looseness_minus_1_log.txt",
1235        ),
1236        (
1237            wolf_hall_ragged_right,
1238            "wolf_hall_input.txt",
1239            &["5in"],
1240            text_params: boxworks_text::Params {
1241                space_skip: common::Glue {
1242                    width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
1243                    ..Default::default()
1244                },
1245                extra_space_skip: common::Glue {
1246                    width: common::Scaled::parse_from_string("5.0pt").unwrap(),
1247                    ..Default::default()
1248                },
1249            },
1250            params: Params {
1251                right_skip: common::Glue {
1252                    stretch: common::Scaled::parse_from_string("20.00003pt").unwrap(),
1253                    ..Default::default()
1254                },
1255            },
1256            typeset: "wolf_hall_ragged_right.txt",
1257            log: "wolf_hall_ragged_right_log.txt",
1258        ),
1259        (
1260            wolf_hall_ragged_right_margin,
1261            "wolf_hall_input.txt",
1262            &["5in"],
1263            text_params: boxworks_text::Params {
1264                space_skip: common::Glue {
1265                    width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
1266                    ..Default::default()
1267                },
1268                extra_space_skip: common::Glue {
1269                    width: common::Scaled::parse_from_string("5.0pt").unwrap(),
1270                    ..Default::default()
1271                },
1272            },
1273            params: Params {
1274                right_skip: common::Glue {
1275                    width: common::Scaled::parse_from_string("20.0pt").unwrap(),
1276                    stretch: common::Scaled::parse_from_string("20.00003pt").unwrap(),
1277                    ..Default::default()
1278                },
1279            },
1280            typeset: "wolf_hall_ragged_right_margin.txt",
1281        ),
1282        // The following tests each modify a single parameter from the plain TeX
1283        // defaults. In each case the parameter value is chosen such that the
1284        // typeset output differs from the output with default parameters at the
1285        // same line width.
1286        (
1287            // The optimal breaks for this text pay no adjacent-fitness demerits
1288            // at any of the standard widths, so no positive value of the
1289            // parameter changes the output. Instead a negative value is used to
1290            // reward fitness class jumps.
1291            // TODO: find a better (positive) parameter value, perhaps with a
1292            // non-standard line width.
1293            wolf_hall_adj_demerits,
1294            "wolf_hall_input.txt",
1295            &["3in"],
1296            params: Params {
1297                adj_demerits: -10000,
1298            },
1299            typeset: "wolf_hall_adj_demerits_want.txt",
1300            not_typeset: "wolf_hall_3in_want.txt",
1301            log: "wolf_hall_adj_demerits_log.txt",
1302        ),
1303        (
1304            wolf_hall_broken_penalty,
1305            "wolf_hall_input.txt",
1306            &["3in"],
1307            params: Params {
1308                broken_penalty: 500,
1309            },
1310            typeset: "wolf_hall_broken_penalty_want.txt",
1311            not_typeset: "wolf_hall_3in_want.txt",
1312            log: "wolf_hall_broken_penalty_log.txt",
1313        ),
1314        (
1315            wolf_hall_club_penalty,
1316            "wolf_hall_input.txt",
1317            &["3in"],
1318            params: Params {
1319                club_penalty: 1000,
1320            },
1321            typeset: "wolf_hall_club_penalty_want.txt",
1322            not_typeset: "wolf_hall_3in_want.txt",
1323            log: "wolf_hall_club_penalty_log.txt",
1324        ),
1325        (
1326            // The optimal breaks for this text contain no consecutive
1327            // hyphenated lines at any of the standard widths, so no positive
1328            // value of the parameter changes the output. Instead a negative
1329            // value is used to reward consecutive hyphenated lines.
1330            // TODO: find a better (positive) parameter value, perhaps with a
1331            // non-standard line width.
1332            wolf_hall_double_hyphen_demerits,
1333            "wolf_hall_input.txt",
1334            &["3in"],
1335            params: Params {
1336                double_hyphen_demerits: -100000,
1337            },
1338            typeset: "wolf_hall_double_hyphen_demerits_want.txt",
1339            not_typeset: "wolf_hall_3in_want.txt",
1340            log: "wolf_hall_double_hyphen_demerits_log.txt",
1341        ),
1342        (
1343            // Default parameters for the stone-eyed variant of the Wolf Hall
1344            // text (see the ex_hyphen_penalty test below). This test exists
1345            // mainly to keep the want file, which serves as the not_typeset
1346            // baseline of the ex_hyphen_penalty test, verifiable against TeX.
1347            wolf_hall_stone_eyed,
1348            "wolf_hall_stone_eyed_input.txt",
1349            &["3in"],
1350            typeset: "wolf_hall_stone_eyed_want.txt",
1351            log: "wolf_hall_stone_eyed_log.txt",
1352        ),
1353        (
1354            // This test uses a variant of the Wolf Hall text in which the
1355            // original hyphen in "stone-eyed" is restored, because
1356            // \exhyphenpenalty only applies at explicit hyphens.
1357            // The optimal breaks never break at that hyphen, so no positive
1358            // value of the parameter changes the output. Instead an eject
1359            // penalty is used to force a break there.
1360            // TODO: find a better (positive) parameter value.
1361            wolf_hall_ex_hyphen_penalty,
1362            "wolf_hall_stone_eyed_input.txt",
1363            &["3in"],
1364            params: Params {
1365                ex_hyphen_penalty: -10000,
1366            },
1367            typeset: "wolf_hall_ex_hyphen_penalty_want.txt",
1368            not_typeset: "wolf_hall_stone_eyed_want.txt",
1369            log: "wolf_hall_ex_hyphen_penalty_log.txt",
1370        ),
1371        (
1372            wolf_hall_final_hyphen_demerits,
1373            "wolf_hall_input.txt",
1374            &["3in"],
1375            params: Params {
1376                final_hyphen_demerits: 0,
1377            },
1378            typeset: "wolf_hall_final_hyphen_demerits_want.txt",
1379            not_typeset: "wolf_hall_3in_want.txt",
1380            log: "wolf_hall_final_hyphen_demerits_log.txt",
1381        ),
1382        (
1383            wolf_hall_final_widow_penalty,
1384            "wolf_hall_input.txt",
1385            &["3in"],
1386            params: Params {
1387                final_widow_penalty: 1000,
1388            },
1389            typeset: "wolf_hall_final_widow_penalty_want.txt",
1390            not_typeset: "wolf_hall_3in_want.txt",
1391            log: "wolf_hall_final_widow_penalty_log.txt",
1392        ),
1393        (
1394            wolf_hall_hyphen_penalty,
1395            "wolf_hall_input.txt",
1396            &["3in"],
1397            params: Params {
1398                hyphen_penalty: 10000,
1399            },
1400            typeset: "wolf_hall_hyphen_penalty_want.txt",
1401            not_typeset: "wolf_hall_3in_want.txt",
1402            log: "wolf_hall_hyphen_penalty_log.txt",
1403        ),
1404        (
1405            wolf_hall_inter_line_penalty,
1406            "wolf_hall_input.txt",
1407            &["3in"],
1408            params: Params {
1409                inter_line_penalty: 100,
1410            },
1411            typeset: "wolf_hall_inter_line_penalty_want.txt",
1412            not_typeset: "wolf_hall_3in_want.txt",
1413            log: "wolf_hall_inter_line_penalty_log.txt",
1414        ),
1415        (
1416            wolf_hall_left_skip,
1417            "wolf_hall_input.txt",
1418            &["3in"],
1419            params: Params {
1420                left_skip: common::Glue {
1421                    width: common::Scaled::parse_from_string("20.0pt").unwrap(),
1422                    ..Default::default()
1423                },
1424            },
1425            typeset: "wolf_hall_left_skip_want.txt",
1426            not_typeset: "wolf_hall_3in_want.txt",
1427            log: "wolf_hall_left_skip_log.txt",
1428        ),
1429        (
1430            wolf_hall_line_penalty,
1431            "wolf_hall_input.txt",
1432            &["3in"],
1433            params: Params {
1434                line_penalty: 100,
1435            },
1436            typeset: "wolf_hall_line_penalty_want.txt",
1437            not_typeset: "wolf_hall_3in_want.txt",
1438            log: "wolf_hall_line_penalty_log.txt",
1439        ),
1440        (
1441            wolf_hall_par_fill_skip,
1442            "wolf_hall_input.txt",
1443            &["3in"],
1444            params: Params {
1445                par_fill_skip: common::Glue::ZERO,
1446            },
1447            typeset: "wolf_hall_par_fill_skip_want.txt",
1448            not_typeset: "wolf_hall_3in_want.txt",
1449            log: "wolf_hall_par_fill_skip_log.txt",
1450        ),
1451        (
1452            wolf_hall_pre_tolerance,
1453            "wolf_hall_input.txt",
1454            &["3in"],
1455            params: Params {
1456                pre_tolerance: 10000,
1457            },
1458            typeset: "wolf_hall_pre_tolerance_want.txt",
1459            not_typeset: "wolf_hall_3in_want.txt",
1460            log: "wolf_hall_pre_tolerance_log.txt",
1461        ),
1462        (
1463            wolf_hall_right_skip,
1464            "wolf_hall_input.txt",
1465            &["3in"],
1466            params: Params {
1467                right_skip: common::Glue {
1468                    stretch: common::Scaled::parse_from_string("20.00003pt").unwrap(),
1469                    ..Default::default()
1470                },
1471            },
1472            typeset: "wolf_hall_right_skip_want.txt",
1473            not_typeset: "wolf_hall_3in_want.txt",
1474            log: "wolf_hall_right_skip_log.txt",
1475        ),
1476        (
1477            wolf_hall_tolerance,
1478            "wolf_hall_input.txt",
1479            &["3in"],
1480            params: Params {
1481                tolerance: 45,
1482            },
1483            typeset: "wolf_hall_tolerance_want.txt",
1484            not_typeset: "wolf_hall_3in_want.txt",
1485            log: "wolf_hall_tolerance_log.txt",
1486        ),
1487        (
1488            alice_paragraph_1_10in,
1489            "alice_paragraph_1.txt",
1490            &["10in"],
1491            typeset: "alice_paragraph_1_want.txt",
1492            log: "alice_paragraph_1_log.txt",
1493        ),
1494        (
1495            alice_paragraph_2_10in,
1496            "alice_paragraph_2.txt",
1497            &["10in"],
1498            typeset: "alice_paragraph_2_want.txt",
1499            log: "alice_paragraph_2_log.txt",
1500        ),
1501    );
1502
1503    fn run(
1504        tfm_bytes: &[u8],
1505        input: &str,
1506        widths: &[&str],
1507        text_params: boxworks_text::Params,
1508        params: Params,
1509    ) -> (ds::VBox, String) {
1510        let tfm_font = tfm::Font::build_from_bytes(tfm_bytes)
1511            .expect("tfm file is valid")
1512            .0;
1513
1514        let hyphenator_state = boxworks_hyphenate::State::plain_tex_en_us();
1515
1516        let mut font_repo: font::Repo<tfm::Font> = Default::default();
1517        let font_id = font_repo.register(tfm_font);
1518        let widths = parse_widths(widths);
1519
1520        let log: Rc<RefCell<String>> = Default::default();
1521        let mut logger = debug::TexLogger::new(log.clone());
1522
1523        let mut tp = bwt::TextPreprocessor::new(text_params);
1524        tp.activate_font(font_id);
1525        let mut list = vec![];
1526        for word in input.split_ascii_whitespace() {
1527            tp.add_word(&font_repo, word.trim_matches(' '), &mut list);
1528            tp.add_space(&font_repo, &mut list);
1529        }
1530
1531        let hyphenator = boxworks_hyphenate::Hyphenator {
1532            state: &hyphenator_state,
1533            font_repo: &font_repo,
1534        };
1535        let line_breaker = super::LineBreaker {
1536            params: &params,
1537            line_widths: &widths,
1538            line_indents: &[],
1539            debug_logger: Some(&mut logger),
1540            hyphenator: &hyphenator,
1541            font_repo: &font_repo,
1542        };
1543        let mut v_list = vec![];
1544        use boxworks::LineBreaker;
1545        line_breaker.break_line(&mut v_list, &mut list);
1546
1547        let v_box = ds::VBox {
1548            list: v_list,
1549            ..Default::default()
1550        };
1551        (v_box, log.take())
1552    }
1553
1554    fn run_test(
1555        tfm_bytes: &[u8],
1556        input: &str,
1557        input_file: &str,
1558        want: &str,
1559        widths: &[&str],
1560        text_params: boxworks_text::Params,
1561        params: Params,
1562    ) {
1563        if verify_with_tex() {
1564            let (vlist, _) = run_tex(tfm_bytes, input, widths, text_params, params);
1565            if std::env::var("TEXCRAFT_VERIFY_OVERWRITE").unwrap_or_default() == "true" {
1566                if !boxworks_testing::is_box_eq!(want, vlist.clone()) {
1567                    std::fs::write(input_file, format!["{vlist}"]).unwrap();
1568                }
1569            } else {
1570                boxworks_testing::assert_box_eq!(want, vlist);
1571            }
1572            return;
1573        }
1574        let (got, _) = run(tfm_bytes, input, widths, text_params, params);
1575        boxworks_testing::assert_box_eq!(want, got);
1576    }
1577
1578    fn run_log_test(
1579        tfm_bytes: &[u8],
1580        input: &str,
1581        log_file: &str,
1582        want_log: &str,
1583        widths: &[&str],
1584        text_params: boxworks_text::Params,
1585        params: Params,
1586    ) {
1587        if verify_with_tex() {
1588            let (_, stdout) = run_tex(tfm_bytes, input, widths, text_params, params);
1589            let trace = boxworks::tex::extract_paragraph_trace(&stdout);
1590            let want = normalize(want_log);
1591            let got = normalize(&trace);
1592            if std::env::var("TEXCRAFT_VERIFY_OVERWRITE").unwrap_or_default() == "true" {
1593                if want != got {
1594                    std::fs::write(log_file, got).unwrap();
1595                }
1596            } else {
1597                assert_eq!(want, got);
1598            }
1599            return;
1600        }
1601        let (_, got_log) = run(tfm_bytes, input, widths, text_params, params);
1602        assert_eq!(normalize(want_log), normalize(&got_log));
1603    }
1604
1605    /// Asserts that two expected typeset outputs are different, ignoring
1606    /// comment lines and blank lines.
1607    fn assert_typeset_ne(want: &str, not_want: &str) {
1608        fn normalize(s: &str) -> Vec<&str> {
1609            s.lines()
1610                .map(str::trim_end)
1611                .filter(|l| !l.is_empty() && !l.starts_with('#'))
1612                .collect()
1613        }
1614        assert_ne!(
1615            normalize(want),
1616            normalize(not_want),
1617            "expected the outputs in the typeset and not_typeset files to differ",
1618        );
1619    }
1620
1621    fn parse_widths(widths: &[&str]) -> Vec<common::Scaled> {
1622        widths
1623            .iter()
1624            .map(|w| common::Scaled::parse_from_string(w).unwrap())
1625            .collect()
1626    }
1627
1628    fn verify_with_tex() -> bool {
1629        std::env::var("TEXCRAFT_VERIFY").unwrap_or_default() == "tex"
1630    }
1631
1632    /// Runs TeX on the input and returns the resulting vertical list and the
1633    /// raw terminal output.
1634    fn run_tex(
1635        tfm_bytes: &[u8],
1636        input: &str,
1637        widths: &[&str],
1638        text_params: boxworks_text::Params,
1639        params: Params,
1640    ) -> (ds::VBox, String) {
1641        use std::collections::HashMap;
1642        use std::path::PathBuf;
1643
1644        let mut auxiliary_files: HashMap<PathBuf, Vec<u8>> = Default::default();
1645        auxiliary_files.insert("customFont.tfm".into(), tfm_bytes.to_vec());
1646        let preamble = format![
1647            "{}\n{}\n{}",
1648            boxworks::tex::diagnostic_preamble("customFont"),
1649            text_params.tex(),
1650            params.tex(),
1651        ];
1652
1653        let widths = parse_widths(widths);
1654        let mut engine = boxworks::tex::RecordingTexEngine::new(
1655            boxworks::tex::new_tex_engine_binary("tex".to_string()).unwrap(),
1656        );
1657        let texts = vec![boxworks::tex::prepend_looseness(
1658            params.looseness,
1659            input.trim(),
1660        )];
1661        let (_, vlists) = boxworks::tex::build_vertical_lists(
1662            &mut engine,
1663            &auxiliary_files,
1664            &preamble,
1665            &widths,
1666            &mut texts.iter(),
1667        );
1668        let [vlist]: [ds::VBox; 1] = vlists.try_into().expect("one text in, one vlist out");
1669        (vlist, engine.stdout().to_string())
1670    }
1671
1672    fn normalize(s: &str) -> String {
1673        let v: Vec<&str> = s
1674            .split('\n')
1675            .map(|l| l.trim())
1676            .map(|l| l.strip_prefix(r"\customFont ").unwrap_or(l))
1677            .filter(|l| !l.is_empty())
1678            .collect();
1679        v.join("\n")
1680    }
1681}