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