boxworks/ds.rs
1//! Core data structures for the typesetting engine.
2//!
3//! This module contains the fundamental data structures for the Boxworks typesetting engine.
4//! As in TeX, the Boxworks is based around various lists (horizontal, vertical, etc.)
5//! that contains elements (which themselves may be nested lists).
6//! The Rust representations of these lists and their elements are defined here.
7//!
8//! This module implements the entirety of TeX.2021 part 10, "data structures
9//! for boxes and their friends".
10
11use common::GlueOrder;
12use common::Scaled as Number;
13use std::rc::Rc;
14
15use crate::lang::convert::ToBoxLang;
16
17/// Element of a horizontal list.
18#[derive(Debug, Clone)]
19pub enum Horizontal {
20 Char(Char),
21 HBox(HBox),
22 VBox(VBox),
23 Rule(Rule),
24 Mark(Mark),
25 Insertion(Insertion),
26 Adjust(Adjust),
27 Ligature(Ligature),
28 Discretionary(Discretionary),
29 Whatsit(Rc<dyn Whatsit>),
30 Math(Math),
31 Glue(Glue),
32 Kern(Kern),
33 Penalty(Penalty),
34}
35
36macro_rules! horizontal_impl {
37 ( $( $variant: ident , )+ ) => {
38 impl PartialEq for Horizontal {
39 fn eq(&self, other: &Self) -> bool {
40 match (self, other) {
41 $(
42 (Self::$variant(l), Self::$variant(r)) => l == r,
43 )+
44 _ => false,
45 }
46 }
47 }
48 $(
49 impl From<$variant> for Horizontal {
50 fn from(value: $variant) -> Self {
51 Horizontal::$variant(value)
52 }
53 }
54 )+
55 };
56}
57
58horizontal_impl!(
59 Char,
60 HBox,
61 VBox,
62 Rule,
63 Mark,
64 Insertion,
65 Adjust,
66 Ligature,
67 Discretionary,
68 Math,
69 Glue,
70 Kern,
71 Penalty,
72);
73
74impl std::fmt::Display for Horizontal {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{}", self.to_box_lang())
77 }
78}
79
80/// Element of a vertical list.
81#[derive(Clone, Debug)]
82pub enum Vertical {
83 HBox(HBox),
84 VBox(VBox),
85 Rule(Rule),
86 Mark(Mark),
87 Insertion(Insertion),
88 Whatsit(Rc<dyn Whatsit>),
89 Math(Math),
90 Glue(Glue),
91 Kern(Kern),
92 Penalty(Penalty),
93}
94
95macro_rules! vertical_impl {
96 ( $( $variant: ident , )+ ) => {
97 impl PartialEq for Vertical {
98 fn eq(&self, other: &Self) -> bool {
99 match (self, other) {
100 $(
101 (Self::$variant(l), Self::$variant(r)) => l == r,
102 )+
103 _ => false,
104 }
105 }
106 }
107 $(
108 impl From<$variant> for Vertical {
109 fn from(value: $variant) -> Self {
110 Vertical::$variant(value)
111 }
112 }
113 )+
114 };
115}
116
117vertical_impl!(HBox, VBox, Rule, Mark, Insertion, Math, Glue, Kern, Penalty,);
118
119/// A character in a specific font.
120///
121/// This node can only appear in horizontal mode.
122///
123/// Described in TeX.2021.134.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct Char {
126 pub char: char,
127 pub font: common::FontId,
128}
129
130/// A box made from a horizontal list.
131///
132/// Described in TeX.2021.135.
133#[derive(Clone, Debug, PartialEq)]
134pub struct HBox {
135 pub height: Number,
136 pub width: Number,
137 pub depth: Number,
138 /// How much this box should be lowered (if it appears in a horizontal list),
139 /// or how much it should be moved to the right (if it appears in a vertical
140 /// list).
141 pub shift_amount: Number,
142 pub list: Vec<Horizontal>,
143 pub glue_ratio: GlueRatio,
144 pub glue_order: GlueOrder,
145}
146
147/// Pack width specifies how width is handled when packing
148pub enum PackWidth {
149 /// Make the box exactly this width, generally by stretching or shrinking
150 /// glue within the box.
151 Exact(common::Scaled),
152
153 /// Make the box its natural width, plus the additional width specified here.
154 Additional(common::Scaled),
155}
156
157impl HBox {
158 /// Create a horizontal from a vertical list.
159 ///
160 pub fn pack<F: super::FontRepo>(
161 font_repo: &F,
162 list: Vec<Horizontal>,
163 pack_width: PackWidth,
164 ) -> HBox {
165 // This function corresponds to hpack in TeX.2021.649.
166 let mut hbox = HBox {
167 list,
168 ..Default::default()
169 };
170 let mut total_glue = common::Glue::default();
171 let mut natural_width = common::Scaled::ZERO;
172 for elem in &hbox.list {
173 // TeX.2021.658
174 use Horizontal as H;
175 let [w, h, d] = match elem {
176 H::Ligature(Ligature { char, font, .. }) | H::Char(Char { char, font }) => {
177 // TeX.2021.654
178 let Some([w, h, d]) = font_repo.width_height_depth(*char, *font) else {
179 continue;
180 };
181 [w, h, d]
182 }
183 // The next 3 cases are TeX.2021.653.
184 H::HBox(HBox {
185 height,
186 width,
187 depth,
188 shift_amount,
189 ..
190 })
191 | H::VBox(VBox {
192 height,
193 width,
194 depth,
195 shift_amount,
196 ..
197 }) => [*height - *shift_amount, *width, *depth + *shift_amount],
198 H::Rule(Rule {
199 height,
200 width,
201 depth,
202 }) => [*height, *width, *depth],
203 // The next 3 cases are TeX.2021.655
204 H::Mark(_) | H::Insertion(_) | H::Adjust(_) => {
205 todo!("support more nodes here")
206 }
207 H::Discretionary(_discretionary) => {
208 // Do nothing. Discretionaries are only relevant if they are break points.
209 continue;
210 }
211 H::Whatsit(_whatsit) => {
212 // Do nothing for the moment. But maybe support a callback here.
213 // TeX.2021.1360.
214 continue;
215 }
216 H::Math(_math) => {
217 todo!("support math nodes here")
218 }
219 H::Glue(glue) => {
220 // TeX.2021.656
221 use std::cmp::Ordering::*;
222 match total_glue.shrink_order.cmp(&glue.value.shrink_order) {
223 Less => {
224 total_glue.shrink = glue.value.shrink;
225 total_glue.shrink_order = glue.value.shrink_order;
226 }
227 Equal => {
228 total_glue.shrink += glue.value.shrink;
229 }
230 Greater => {
231 // Do nothing.
232 // This glue has smaller order than some other glue in the box, so will
233 // not be used for shrinking.
234 }
235 }
236 match total_glue.stretch_order.cmp(&glue.value.stretch_order) {
237 Less => {
238 total_glue.stretch = glue.value.stretch;
239 total_glue.stretch_order = glue.value.stretch_order;
240 }
241 Equal => {
242 total_glue.stretch += glue.value.stretch;
243 }
244 Greater => {
245 // Do nothing.
246 // This glue has smaller order than some other glue in the box, so will
247 // not be used for stretching.
248 }
249 }
250 // TODO: implement leader support.
251 [glue.value.width, common::Scaled::ZERO, common::Scaled::ZERO]
252 }
253 H::Kern(kern) => [kern.width, common::Scaled::ZERO, common::Scaled::ZERO],
254 H::Penalty(_) => {
255 // Do nothing.
256 continue;
257 }
258 };
259 natural_width += w;
260 if h > hbox.height {
261 hbox.height = h;
262 }
263 if d > hbox.depth {
264 hbox.depth = d;
265 }
266 }
267
268 // TeX.2021.657
269 hbox.width = match pack_width {
270 PackWidth::Exact(exact) => exact,
271 PackWidth::Additional(additional) => natural_width + additional,
272 };
273 let excess = hbox.width - natural_width;
274 use std::cmp::Ordering::*;
275 match excess.cmp(&common::Scaled::ZERO) {
276 Less => {
277 // TeX.2021.664
278 hbox.glue_order = total_glue.shrink_order;
279 if total_glue.shrink_order == GlueOrder::Normal && total_glue.shrink < -excess {
280 // The box is overfull: the glue shrinks by exactly its
281 // shrinkability (TeX sets the glue ratio to unity) and
282 // the content overflows the box.
283 // TODO(TeX.2021.666): report the overfull box and append
284 // the \overfullrule rule.
285 if total_glue.shrink == common::Scaled::ZERO {
286 // It doesn't look like this case exists in Knuth, but it does, subtly.
287 // The key thing is that in the `[total_shrink]==0` branch, Knuth sets the
288 // glue_sign to be normal (i.e., not shrinking or stretching, so zero). This
289 // means that the assignment of 1 to the glue ratio in overfull branch does nothing
290 // because the glue_sign being zero means the ratio is always considered zero.
291 // This was discovered while debugging a failing unit test in the line
292 // breaker.
293 hbox.glue_ratio = GlueRatio {
294 num: common::Scaled::ZERO,
295 den: common::Scaled::ONE,
296 };
297 } else {
298 hbox.glue_ratio = GlueRatio {
299 num: common::Scaled::ONE,
300 den: common::Scaled::ONE,
301 };
302 }
303 } else if total_glue.shrink != common::Scaled::ZERO {
304 hbox.glue_ratio = GlueRatio {
305 num: excess,
306 den: total_glue.shrink,
307 };
308 } else {
309 hbox.glue_ratio = GlueRatio {
310 num: common::Scaled::ZERO,
311 den: common::Scaled::ONE,
312 }
313 }
314 }
315 Equal => {
316 // Do nothing: hbox defaults cover this case.
317 }
318 Greater => {
319 // TeX.2021.658
320 if total_glue.stretch != common::Scaled::ZERO {
321 hbox.glue_order = total_glue.stretch_order;
322 hbox.glue_ratio = GlueRatio {
323 num: excess,
324 den: total_glue.stretch,
325 };
326 }
327 if total_glue.stretch_order == GlueOrder::Normal {
328 // TODO(TeX.2021.660): report an underfull box
329 }
330 }
331 }
332 hbox
333 }
334}
335
336/// Ratio by which glue should shrink or stretch.
337///
338/// This is one of the few (only?) places in Knuth's TeX where a floating point
339/// number is used.
340/// In general TeX uses fixed point integers to ensure that the results are
341/// the same on every computer/CPU.
342/// But the exact semantics of the glue ratio don't affect the output, so
343/// using a float is deemed okay by Knuth.
344///
345/// However we opt to use a real ratio: i.e., a numerator and a denominator.
346///
347/// Described in TeX.2021.109.
348#[derive(Copy, Clone, Debug, Eq)]
349pub struct GlueRatio {
350 pub num: common::Scaled,
351 pub den: common::Scaled,
352}
353
354impl PartialEq for GlueRatio {
355 fn eq(&self, other: &Self) -> bool {
356 // We would prefer to use:
357 // (self.num.0 as i64) * (other.den.0 as i64) == (other.num.0 as i64) * (self.den.0 as i64)
358 // but the maping from ratios to floats and back to ratios is unfortunately
359 // lossy. A better approach might be to "canonicalize" glue ratios when we construct
360 // them. This would be equivalent to writing the string and parsing it back in.
361 let lhs = format!["{}", self];
362 let rhs = format!["{}", other];
363 lhs == rhs
364 }
365}
366
367impl Default for GlueRatio {
368 fn default() -> Self {
369 Self {
370 num: common::Scaled(0),
371 den: common::Scaled(1),
372 }
373 }
374}
375
376impl GlueRatio {
377 pub fn as_float(&self) -> f32 {
378 (self.num.0 as f32) / (self.den.0 as f32)
379 }
380
381 pub fn from_float_str(s: &str) -> Option<Self> {
382 let s = format!("{s}pt");
383 let num = common::Scaled::parse_from_string(&s).ok()?;
384 Some(Self {
385 num,
386 den: common::Scaled::ONE,
387 })
388 }
389}
390
391impl std::fmt::Display for GlueRatio {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 // TeX.2021.186
394 let g = self.as_float();
395 let g = g.abs();
396 let g = if g.abs() >= 20000.0 { 20000.0 } else { g };
397 let g = ((common::Scaled::ONE.0 as f32) * g).round() as i32;
398 write!(f, "{}", common::Scaled(g).display_no_units())
399 }
400}
401
402impl HBox {
403 /// Returns a hbox node corresponding to the TeX snippet `\hbox{}`.
404 ///
405 /// Described in TeX.2021.136.
406 pub fn new_null_box() -> Self {
407 Self {
408 height: Number::ZERO,
409 width: Number::ZERO,
410 depth: Number::ZERO,
411 shift_amount: Number::ZERO,
412 list: vec![],
413 glue_ratio: Default::default(),
414 glue_order: GlueOrder::Normal,
415 }
416 }
417}
418
419impl Default for HBox {
420 fn default() -> Self {
421 Self::new_null_box()
422 }
423}
424
425impl std::fmt::Display for HBox {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 use crate::lang::convert::ToBoxLang;
428 write!(f, "{}", self.to_box_lang())
429 }
430}
431
432/// A box made from a vertical list.
433///
434/// This is the same as [HBox], except the list inside holds [Vertical] nodes
435/// instead of [Horizontal] nodes.
436///
437/// Described in TeX.2021.137.
438#[derive(Clone, Debug, Default, PartialEq)]
439pub struct VBox {
440 pub height: Number,
441 pub width: Number,
442 pub depth: Number,
443 pub shift_amount: Number,
444 pub list: Vec<Vertical>,
445 pub glue_ratio: GlueRatio,
446 pub glue_order: GlueOrder,
447}
448
449impl std::fmt::Display for VBox {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 use crate::lang::convert::ToBoxLang;
452 write!(f, "{}", self.to_box_lang())
453 }
454}
455
456/// A rule stands for a solid black rectangle.
457///
458/// It has width, depth and height fields.
459/// However if any of these dimensions is -2^30, the actual value will be
460/// determined by running rule up to the boundary of the innermost, enclosing box.
461/// This is called a "running dimension".
462/// The width is never running in an hlist; the height and depth are never running
463/// in a vlist.
464///
465/// Described in TeX.2021.138.
466#[derive(Clone, Debug, PartialEq, Eq)]
467pub struct Rule {
468 pub height: Number,
469 pub width: Number,
470 pub depth: Number,
471}
472
473impl Rule {
474 pub const RUNNING: Number = Number(-2 << 30);
475
476 /// Creates a new rule.
477 ///
478 /// All of the dimensions are running.
479 ///
480 /// Described in TeX.2021.139.
481 pub fn new() -> Self {
482 Self {
483 height: Self::RUNNING,
484 width: Self::RUNNING,
485 depth: Self::RUNNING,
486 }
487 }
488}
489
490impl Default for Rule {
491 fn default() -> Self {
492 Self::new()
493 }
494}
495
496/// Vertical material to be inserted.
497///
498/// This node is related to the TeX primitive `\insert`.
499///
500/// Described in TeX.2021.140.
501#[derive(Clone, Debug, PartialEq)]
502pub struct Insertion {
503 pub box_number: u8,
504 /// Slightly misnamed: it actually holds the natural height plus depth
505 /// of the vertical list being inserted.
506 pub height: Number,
507 /// Used in case this insertion is split.
508 pub split_max_depth: Number,
509 pub split_top_skip: common::Glue,
510 /// Penalty to be used if this insertion floats to a subsequent
511 /// page after a split insertion of the same class.
512 pub float_penalty: u32,
513 pub vbox: Vec<Vertical>,
514}
515
516/// Contents of a user's `\mark` text.
517///
518/// TODO: At time of writing I don't know what to do with this node.
519/// In Knuth's TeX it references a token list, but I don't want Boxworks
520/// to depend on Texlang. So for the moment just leaving a dummy list.
521///
522/// Described in TeX.2021.141.
523#[derive(Clone, Debug, PartialEq, Eq)]
524pub struct Mark {
525 pub list: Vec<()>,
526}
527
528/// Specifies material that will be moved out into the surrounding vertical list.
529///
530/// E.g., used to implement the TeX primitive `\vadjust`.
531///
532/// Described in TeX.2021.142.
533#[derive(Clone, Debug, PartialEq)]
534pub struct Adjust {
535 pub list: Vec<Vertical>,
536}
537
538/// A ligature.
539///
540/// Described in TeX.2021.143.
541#[derive(Clone, Debug, PartialEq, Eq)]
542pub struct Ligature {
543 pub char: char,
544 pub font: common::FontId,
545 /// The original characters that were replaced by the ligature.
546 /// This is used if the engine needs to break apart the ligature
547 /// in order to perform hyphenation.
548 ///
549 /// While most ligatures come from 2 characters (e.g. ff), TeX's
550 /// lig/kern programming language allows for a single ligature to come
551 /// from arbitrarily many characters.
552 pub original_chars: Rc<str>,
553 pub includes_left_boundary: bool,
554 pub includes_right_boundary: bool,
555}
556
557impl Ligature {
558 /// Puts the ligature into a lossy standard form.
559 ///
560 /// What follows is the motivation for this method.
561 ///
562 /// TeX supports logging its internal typesetting data structures.
563 /// All of these data structures are reimplemented in this module,
564 /// and can be reconstructed from TeX's log output using the [Boxworks TeX log parsing logic](crate::tex).
565 /// This system used throughout Boxworks to verify that
566 /// Boxworks's typesetting code gives the identical results to TeX's.
567 ///
568 /// Unfortunately, however, TeX's display logic for ligatures specifically is
569 /// lossy. In the TeX's logging format the line
570 /// ```text
571 /// ..\tenrm a (ligature |)
572 /// ```
573 /// can mean one of three things:
574 /// - a ligature 'a' that replaces the character `|`,
575 /// - a ligature 'a' that replaces the left boundary, or
576 /// - a ligature 'a' that replaces the right boundary.
577 ///
578 /// This means that it's not possible to reconstruct fully the internal
579 /// ligature data structure from the logging output.
580 /// [Boxworks TeX log parsing logic](crate::tex) assumes the first
581 /// interpretation holds: the log line is parsed into the following value:
582 /// ```
583 /// # use boxworks::ds::Ligature;
584 /// Ligature {
585 /// char: 'a',
586 /// font: common::FontId::ONE,
587 /// original_chars: "|".into(),
588 /// includes_left_boundary: false,
589 /// includes_right_boundary: false,
590 /// };
591 /// ```
592 ///
593 /// This specifically presents issues when we want to verify TeX's output with
594 /// Boxworks where the correct value is, say,
595 /// ```
596 /// # use boxworks::ds::Ligature;
597 /// Ligature {
598 /// char: 'a',
599 /// font: common::FontId::ONE,
600 /// original_chars: "".into(),
601 /// includes_left_boundary: false,
602 /// includes_right_boundary: true,
603 /// };
604 /// ```
605 ///
606 /// A unit test that compares these outputs will fail.
607 ///
608 /// This method is designed to help write unit tests that pass.
609 /// It standardizes the ligature into the from parse from TeX.
610 /// We can then compare the standardized form with TeX's output.
611 /// This does mean that such unit tests can't verify which form is the right one.
612 pub fn standardize_lossy(&mut self) {
613 if !self.includes_left_boundary && !self.includes_right_boundary {
614 return;
615 }
616 self.original_chars = format![
617 "{}{}{}",
618 if self.includes_left_boundary { "|" } else { "" },
619 self.original_chars,
620 if self.includes_right_boundary {
621 "|"
622 } else {
623 ""
624 }
625 ]
626 .into();
627 self.includes_left_boundary = false;
628 self.includes_right_boundary = false;
629 }
630}
631
632// Two constructors for ligature nodes are provided in TeX.2021.144
633// but they don't seem that useful so I'm omitting them.
634
635/// A discretionary break.
636///
637/// Described in TeX.2021.145.
638#[derive(Clone, Debug, PartialEq)]
639pub struct Discretionary {
640 /// Material to insert before this node, if the break occurs here.
641 pub pre_break: Vec<DiscretionaryElem>,
642 /// Material to insert after this node, if the break occurs here.
643 pub post_break: Vec<DiscretionaryElem>,
644 /// Number of subsequent nodes to skip if the break occurs here.
645 pub replace_count: u32,
646}
647
648impl Discretionary {
649 pub fn new() -> Self {
650 Self {
651 pre_break: vec![],
652 post_break: vec![],
653 replace_count: 0,
654 }
655 }
656}
657
658impl Default for Discretionary {
659 fn default() -> Self {
660 Self::new()
661 }
662}
663
664/// Element of a discretionary list.
665#[derive(Clone, Debug, PartialEq)]
666pub enum DiscretionaryElem {
667 Char(Char),
668 HBox(HBox),
669 VBox(VBox),
670 Rule(Rule),
671 Ligature(Ligature),
672 Kern(Kern),
673}
674
675impl From<Char> for DiscretionaryElem {
676 fn from(value: Char) -> Self {
677 DiscretionaryElem::Char(value)
678 }
679}
680
681impl From<Kern> for DiscretionaryElem {
682 fn from(value: Kern) -> Self {
683 DiscretionaryElem::Kern(value)
684 }
685}
686
687impl From<Ligature> for DiscretionaryElem {
688 fn from(value: Ligature) -> Self {
689 DiscretionaryElem::Ligature(value)
690 }
691}
692
693impl From<DiscretionaryElem> for Horizontal {
694 fn from(value: DiscretionaryElem) -> Self {
695 use DiscretionaryElem as In;
696 use Horizontal as Out;
697 match value {
698 In::Char(char) => Out::Char(char),
699 In::HBox(hbox) => Out::HBox(hbox),
700 In::VBox(vbox) => Out::VBox(vbox),
701 In::Rule(rule) => Out::Rule(rule),
702 In::Ligature(ligature) => Out::Ligature(ligature),
703 In::Kern(kern) => Out::Kern(kern),
704 }
705 }
706}
707
708impl DiscretionaryElem {
709 pub fn width<F: super::FontRepo>(&self, font_width: &F) -> Number {
710 use DiscretionaryElem::*;
711 match self {
712 Char(char) => font_width
713 .width(char.char, char.font)
714 .unwrap_or(common::Scaled::ZERO),
715 HBox(hlist) => hlist.width,
716 VBox(vlist) => vlist.width,
717 Rule(rule) => rule.width,
718 Ligature(ligature) => font_width
719 .width(ligature.char, ligature.font)
720 .unwrap_or(common::Scaled::ZERO),
721 Kern(kern) => kern.width,
722 }
723 }
724}
725
726impl TryFrom<Horizontal> for DiscretionaryElem {
727 type Error = ();
728
729 fn try_from(value: Horizontal) -> Result<Self, Self::Error> {
730 use DiscretionaryElem as Out;
731 use Horizontal::*;
732 let out = match value {
733 Char(char) => Out::Char(char),
734 HBox(hlist) => Out::HBox(hlist),
735 VBox(vlist) => Out::VBox(vlist),
736 Rule(rule) => Out::Rule(rule),
737 Ligature(ligature) => Out::Ligature(ligature),
738 Kern(kern) => Out::Kern(kern),
739 _ => return Err(()),
740 };
741 Ok(out)
742 }
743}
744
745/// A whatsit node
746///
747/// This is used to facilitate extensions to TeX.
748/// It's unclear right now how what the API of it will be, though
749/// it can be figured out by reading the Chapter 53 Extensions of
750/// TeX.
751///
752/// Knuth uses this node type to implement both `\write` and `\special`
753/// so we'll eventually find out.
754///
755/// Described in TeX.2021.146.
756pub trait Whatsit: std::fmt::Debug {
757 // Invoked when this node is invoked when hyphenating.
758 //
759 // This is TeX.2021.1363 but given how we've architected the code, the logic in TeX.2021.1382
760 // (which changes the current language) should run here for \language whatsits.
761 fn hyphenation_hook(&self) {}
762}
763
764/// A marker placed before or after math mode.
765///
766/// Described in TeX.2021.147.
767///
768/// TODO: this also needs a width and so is wrong.
769#[derive(Clone, Copy, Debug, PartialEq, Eq)]
770pub enum Math {
771 Before,
772 After,
773}
774
775impl Horizontal {
776 /// Whether a glue node that comes after this node may be broken.
777 ///
778 /// For char nodes, this function is essentially undefined in Knuth's
779 /// TeX. More specifically, the value depends on the exact character code.
780 /// In TeX this function is never called for char nodes which is why this
781 /// is not a problem. Here, we return `true` for char nodes based on
782 /// my analysis of all places in Knuth's TeX where it is invoked:
783 ///
784 /// - TeX.2021.868: `precedes_break` is called on variable `cur_p` which
785 /// is a pointer to a horizontal list. Before this call, the calling code
786 /// first checks if the node is a character and if so follows the same
787 /// code path. Thus returning `true` here is the right thing to do.
788 ///
789 /// - TeX.2021.973: the function is called on a variable `prev_p` which
790 /// is a pointer to a vertical list and so the char case never arises.
791 ///
792 /// - TeX.2021.1000: same as the last case.
793 ///
794 /// This function is defined in TeX.2021.148.
795 pub fn precedes_break(&self) -> bool {
796 use Horizontal::*;
797 match self {
798 Char(_) | HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Adjust(_)
799 | Ligature(_) | Discretionary(_) | Whatsit(_) => true,
800 Kern(kern) => kern.kind != KernKind::Explicit,
801 Math(_) | Glue(_) | Penalty(_) => false,
802 }
803 }
804
805 /// Whether this node is discarded after a break.
806 ///
807 /// As with [Self::precedes_break], this function is essentially undefined
808 /// for char nodes in Knuth's TeX. However there is only one call site
809 /// (TeX.2021.879) and in that call site char nodes behave as if this
810 /// function returns true.
811 ///
812 /// This function is defined in TeX.2021.148.
813 pub fn non_discardable(&self) -> bool {
814 self.precedes_break()
815 }
816}
817
818impl Vertical {
819 /// Whether a glue node that comes after this node may be broken.
820 ///
821 /// This function is defined in TeX.2021.148.
822 pub fn precedes_break(&self) -> bool {
823 use Vertical::*;
824 matches!(
825 self,
826 HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Whatsit(_)
827 )
828 }
829}
830
831/// A piece of glue.
832///
833/// Described in TeX.2021.149.
834#[derive(Clone, Debug, PartialEq, Eq)]
835pub struct Glue {
836 pub value: common::Glue,
837 pub kind: GlueKind,
838}
839
840impl From<common::Glue> for Glue {
841 fn from(value: common::Glue) -> Self {
842 Self {
843 value,
844 kind: Default::default(),
845 }
846 }
847}
848
849/// The kind of a glue node.
850///
851/// Described in TeX.2021.149.
852#[derive(Clone, Debug, Default, PartialEq, Eq)]
853pub enum GlueKind {
854 #[default]
855 Normal,
856 ConditionalMath,
857 Math,
858 AlignedLeader,
859 CenteredLeader,
860 ExpandedLeader,
861}
862
863// TeX.2021.150 and TeX.2021.151 define the [font::Glue] type itself,
864// which is not in this crate.
865
866// Three constructors for glue nodes are provided in TeX.2021.152,
867// TeX.2021.153 and TeX.2021.154 but they don't seem that
868// useful so I'm omitting them.
869
870/// A kern.
871///
872/// Described in TeX.2021.155.
873#[derive(Clone, Debug, PartialEq, Eq)]
874pub struct Kern {
875 pub width: Number,
876 pub kind: KernKind,
877}
878
879/// The kind of a kern node.
880///
881/// Described in TeX.2021.155.
882#[derive(Clone, Copy, Debug, PartialEq, Eq)]
883pub enum KernKind {
884 /// Inserted from font information or math mode calculations.
885 Normal,
886 /// Inserted using e.g. TeX's `\kern` primitive.
887 Explicit,
888 /// Inserted from non-math accents.
889 Accent,
890 /// Inserted from e.g. `\mkern` specifications in math formulas.
891 Math,
892}
893
894// A constructor for kern nodes is provided in TeX.2021.156,
895// but it doesn't seem useful.
896
897/// A penalty.
898///
899/// Described in TeX.2021.157.
900#[derive(Clone, Debug, PartialEq, Eq)]
901pub struct Penalty(pub i32);
902
903impl Penalty {
904 /// Any penalty bigger than this is considered infinite and no
905 /// break will be allowed for such high values.
906 pub const INFINITE: Penalty = Penalty(10000);
907
908 /// Any penalty smaller than this will result in a forced break.
909 pub const EJECT: Penalty = Penalty(-10000);
910}
911
912// A constructor for penalty nodes is provided in TeX.2021.157,
913// but it doesn't seem useful.
914
915// TODO: Unset node(s) in TeX.2021.159
916
917pub fn short_display_hlist(w: &mut dyn std::fmt::Write, hlist: &[Horizontal]) -> std::fmt::Result {
918 // TeX.2021.174
919 let mut i = 0;
920 while let Some(elem) = hlist.get(i) {
921 use Horizontal::*;
922 match elem {
923 Char(char) => write!(w, "{}", char.char)?,
924 HBox(_) | VBox(_) | Whatsit(_) | Mark(_) | Adjust(_) => write!(w, "[]")?,
925 Rule(_) => write!(w, "|")?,
926 Ligature(ligature) => write!(w, "{}", ligature.original_chars)?,
927 Discretionary(discretionary) => {
928 short_display_dlist(w, &discretionary.pre_break)?;
929 short_display_dlist(w, &discretionary.post_break)?;
930 i += discretionary.replace_count as usize;
931 }
932 Math(_) => write!(w, "$")?,
933 Glue(glue) => {
934 if !glue.value.is_zero() {
935 write!(w, " ")?;
936 }
937 }
938 Kern(_) | Penalty(_) | Insertion(_) => {}
939 };
940 i += 1;
941 }
942 Ok(())
943}
944
945fn short_display_dlist(
946 w: &mut dyn std::fmt::Write,
947 dlist: &[DiscretionaryElem],
948) -> std::fmt::Result {
949 // TeX.2021.174
950 let mut i = 0;
951 while let Some(elem) = dlist.get(i) {
952 use DiscretionaryElem::*;
953 match elem {
954 Char(char) => write!(w, "{}", char.char)?,
955 HBox(_) | VBox(_) => write!(w, "[]")?,
956 Rule(_) => write!(w, "|")?,
957 Ligature(ligature) => write!(w, "{}", ligature.original_chars)?,
958 Kern(_) => {}
959 };
960 i += 1;
961 }
962 Ok(())
963}