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