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}