1use 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
218struct ActiveNode {
219 diffs: Diffs,
222 fitness_class: FitnessClass,
223 hyphenated: bool,
224 line_number: usize,
225 node_index: usize,
227 total_demerits: i32,
228}
229
230#[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 elem: usize,
242 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 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 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 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 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 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 inner_list.push(math.into());
324 }
325 Glue(_) => {
326 }
330 Kern(mut kern) => {
331 kern.width = Scaled::ZERO;
332 inner_list.push(kern.into());
333 }
334 Penalty(penalty) => {
335 inner_list.push(penalty.into());
337 }
338 _ => {
339 unreachable!("node cannot appear as a breakpoint: {break_point_node:?}");
340 }
341 }
342 }
343
344 inner_list.push(
347 ds::Glue {
348 value: self.params.right_skip,
349 kind: ds::GlueKind::Normal,
350 }
351 .into(),
352 );
353
354 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 if !v_list.is_empty() {
372 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 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 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 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 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 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 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 let mut diffs: Diffs = Default::default();
513 for i in 0..=list.len() {
515 let elem = list.get(i);
516 use ds::Horizontal::*;
517 let mut disc_width = Scaled::ZERO;
518 let (mut penalty, hyphenated) = match elem {
524 None => {
525 (EJECT_PENALTY, true)
530 }
531 Some(elem) => match elem {
532 Char(ds::Char { char, font }) | Ligature(ds::Ligature { char, font, .. }) => {
533 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 continue;
547 }
548 Discretionary(discretionary) => {
549 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 }
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 (0, false)
579 } else {
580 continue;
581 }
582 }
583 Glue(glue) => {
584 if auto_breaking && i > 0 && list[i - 1].precedes_break() {
586 (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 (0, false)
606 } else {
607 diffs.width += kern.width;
608 continue;
609 }
610 }
611 Penalty(penalty) => {
612 (penalty.0, false)
615 }
616 },
617 };
618
619 if penalty >= INFINITE_PENALTY {
621 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 #[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 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 let line_diffs = diffs.clone() - active_node.diffs.clone() + background.clone();
655 let line_width = self
656 .line_widths
657 .get(active_node.line_number)
660 .copied()
661 .unwrap_or(*self.line_widths.last().unwrap());
662
663 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 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 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 let (deactivate, allowable_break, artificial_demerits) =
705 if badness > INFINITE_BADNESS || penalty == EJECT_PENALTY {
706 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 if allowable_break {
722 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 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 active_nodes.push_back(active_node);
766 }
767 }
768
769 if minimum_demerits < AWFUL_BAD {
771 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 match elem {
781 Discretionary(discretionary) => {
782 let mut j = i + 1;
784 while j < i + 1 + discretionary.replace_count as usize {
785 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 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 }
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 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 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 let Some(elem) = elem else { break };
877 match elem {
878 Char(_) | HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Adjust(_)
879 | Ligature(_) => {
880 }
883 Discretionary(_) => {
884 }
886 Whatsit(_whatsit) => todo!(),
887 Math(_) => {
888 }
890 Glue(glue) => {
891 diffs.update_from_glue(&glue.value);
892 }
893 Kern(kern) => {
894 diffs.width += kern.width;
895 }
896 Penalty(_) => {
897 }
899 }
900 }
901
902 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 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 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 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 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 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 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
1014const AWFUL_BAD: i32 = 0o7_777_777_777;
1016const INFINITE_BADNESS: i32 = 10000;
1017const INFINITE_PENALTY: i32 = 10000;
1018const EJECT_PENALTY: i32 = -10000;
1019
1020fn 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 #[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 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 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 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 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 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 (
1289 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 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 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 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: ¶ms,
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 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 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}