1use 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
219struct ActiveNode {
220 diffs: Diffs,
223 fitness_class: FitnessClass,
224 hyphenated: bool,
225 line_number: usize,
226 node_index: usize,
228 total_demerits: i32,
229}
230
231#[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 elem: usize,
243 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 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 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 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 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 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 inner_list.push(math.into());
319 }
320 Glue(_) => {
321 }
325 Kern(mut kern) => {
326 kern.width = Scaled::ZERO;
327 inner_list.push(kern.into());
328 }
329 Penalty(penalty) => {
330 inner_list.push(penalty.into());
332 }
333 _ => {
334 unreachable!("node cannot appear as a breakpoint: {break_point_node:?}");
335 }
336 }
337 }
338
339 inner_list.push(
342 ds::Glue {
343 value: self.params.right_skip,
344 kind: ds::GlueKind::Normal,
345 }
346 .into(),
347 );
348
349 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 if !v_list.is_empty() {
368 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 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 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 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 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 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 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 let mut diffs: Diffs = Default::default();
503 for i in 0..=list.len() {
505 let elem = list.get(i);
506 use ds::Horizontal::*;
507 let mut disc_width = Scaled::ZERO;
508 let (mut penalty, hyphenated) = match elem {
514 None => {
515 (EJECT_PENALTY, true)
520 }
521 Some(elem) => match elem {
522 Char(ds::Char { char, font }) | Ligature(ds::Ligature { char, font, .. }) => {
523 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 continue;
541 }
542 Discretionary(discretionary) => {
543 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 }
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 (0, false)
573 } else {
574 continue;
575 }
576 }
577 Glue(glue) => {
578 if auto_breaking && i > 0 && list[i - 1].precedes_break() {
580 (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 (0, false)
600 } else {
601 diffs.width += kern.width;
602 continue;
603 }
604 }
605 Penalty(penalty) => {
606 (penalty.0, false)
609 }
610 },
611 };
612
613 if penalty >= INFINITE_PENALTY {
615 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 #[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 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 let line_diffs = diffs.clone() - active_node.diffs.clone() + background.clone();
649 let line_width = self
650 .line_widths
651 .get(active_node.line_number)
654 .copied()
655 .unwrap_or(*self.line_widths.last().unwrap());
656
657 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 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 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 let (deactivate, allowable_break, artificial_demerits) =
699 if badness > INFINITE_BADNESS || penalty == EJECT_PENALTY {
700 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 if allowable_break {
716 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 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 active_nodes.push_back(active_node);
760 }
761 }
762
763 if minimum_demerits < AWFUL_BAD {
765 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 match elem {
775 Discretionary(discretionary) => {
776 let mut j = i + 1;
778 while j < i + 1 + discretionary.replace_count as usize {
779 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 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 }
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 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 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 let Some(elem) = elem else { break };
875 match elem {
876 Char(_) | HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Adjust(_)
877 | Ligature(_) => {
878 }
881 Discretionary(_) => {
882 }
884 Whatsit(_whatsit) => todo!(),
885 Math(_) => {
886 }
888 Glue(glue) => {
889 diffs.update_from_glue(&glue.value);
890 }
891 Kern(kern) => {
892 diffs.width += kern.width;
893 }
894 Penalty(_) => {
895 }
897 }
898 }
899
900 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 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 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 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 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 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 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
1012const AWFUL_BAD: i32 = 0o7_777_777_777;
1014const INFINITE_BADNESS: i32 = 10000;
1015const INFINITE_PENALTY: i32 = 10000;
1016const EJECT_PENALTY: i32 = -10000;
1017
1018fn 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 #[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 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 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 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 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 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 (
1287 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 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 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 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: ¶ms,
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 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 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}