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                hbox.glue_ratio = GlueRatio {
272                    num: excess,
273                    den: total_glue.shrink,
274                };
275            }
276            Equal => {
277                // Do nothing: hbox defaults cover this case.
278            }
279            Greater => {
280                // TeX.2021.658
281                if total_glue.stretch != common::Scaled::ZERO {
282                    hbox.glue_order = total_glue.stretch_order;
283                    hbox.glue_ratio = GlueRatio {
284                        num: excess,
285                        den: total_glue.stretch,
286                    };
287                }
288                if total_glue.stretch_order == GlueOrder::Normal {
289                    // TODO(TeX.2021.660): report an underfull box
290                }
291            }
292        }
293        hbox
294    }
295}
296
297/// Ratio by which glue should shrink or stretch.
298///
299/// This is one of the few (only?) places in Knuth's TeX where a floating point
300/// number is used.
301/// In general TeX uses fixed point integers to ensure that the results are
302/// the same on every computer/CPU.
303/// But the exact semantics of the glue ratio don't affect the output, so
304/// using a float is deemed okay by Knuth.
305///
306/// However we opt to use a real ratio: i.e., a numerator and a denominator.
307///
308/// Described in TeX.2021.109.
309#[derive(Copy, Clone, Debug, Eq)]
310pub struct GlueRatio {
311    pub num: common::Scaled,
312    pub den: common::Scaled,
313}
314
315impl PartialEq for GlueRatio {
316    fn eq(&self, other: &Self) -> bool {
317        // We would prefer to use:
318        // (self.num.0 as i64) * (other.den.0 as i64) == (other.num.0 as i64) * (self.den.0 as i64)
319        // but the maping from ratios to floats and back to ratios is unfortunately
320        // lossy. A better approach might be to "canonicalize" glue ratios when we construct
321        // them. This would be equivalent to writing the string and parsing it back in.
322        let lhs = format!["{}", self];
323        let rhs = format!["{}", other];
324        lhs == rhs
325    }
326}
327
328impl Default for GlueRatio {
329    fn default() -> Self {
330        Self {
331            num: common::Scaled(0),
332            den: common::Scaled(1),
333        }
334    }
335}
336
337impl GlueRatio {
338    pub fn as_float(&self) -> f32 {
339        (self.num.0 as f32) / (self.den.0 as f32)
340    }
341
342    pub fn from_float_str(s: &str) -> Option<Self> {
343        let s = format!("{s}pt");
344        let num = common::Scaled::parse_from_string(&s).ok()?;
345        Some(Self {
346            num,
347            den: common::Scaled::ONE,
348        })
349    }
350}
351
352impl std::fmt::Display for GlueRatio {
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        // TeX.2021.186
355        let g = self.as_float();
356        let g = g.abs();
357        let g = if g.abs() >= 20000.0 { 20000.0 } else { g };
358        let g = ((common::Scaled::ONE.0 as f32) * g).round() as i32;
359        write!(f, "{}", common::Scaled(g).display_no_units())
360    }
361}
362
363impl HBox {
364    /// Returns a hbox node corresponding to the TeX snippet `\hbox{}`.
365    ///
366    /// Described in TeX.2021.136.
367    pub fn new_null_box() -> Self {
368        Self {
369            height: Number::ZERO,
370            width: Number::ZERO,
371            depth: Number::ZERO,
372            shift_amount: Number::ZERO,
373            list: vec![],
374            glue_ratio: Default::default(),
375            glue_order: GlueOrder::Normal,
376        }
377    }
378}
379
380impl Default for HBox {
381    fn default() -> Self {
382        Self::new_null_box()
383    }
384}
385
386/// A box made from a vertical list.
387///
388/// This is the same as [HBox], except the list inside holds [Vertical] nodes
389/// instead of [Horizontal] nodes.
390///
391/// Described in TeX.2021.137.
392#[derive(Clone, Debug, Default, PartialEq)]
393pub struct VBox {
394    pub height: Number,
395    pub width: Number,
396    pub depth: Number,
397    pub shift_amount: Number,
398    pub list: Vec<Vertical>,
399    pub glue_ratio: GlueRatio,
400    pub glue_order: GlueOrder,
401}
402
403/// A rule stands for a solid black rectangle.
404///
405/// It has width, depth and height fields.
406/// However if any of these dimensions is -2^30, the actual value will be
407/// determined by running rule up to the boundary of the innermost, enclosing box.
408/// This is called a "running dimension".
409/// The width is never running in an hlist; the height and depth are never running
410/// in a vlist.
411///
412/// Described in TeX.2021.138.
413#[derive(Clone, Debug, PartialEq, Eq)]
414pub struct Rule {
415    pub height: Number,
416    pub width: Number,
417    pub depth: Number,
418}
419
420impl Rule {
421    pub const RUNNING: Number = Number(-2 << 30);
422
423    /// Creates a new rule.
424    ///
425    /// All of the dimensions are running.
426    ///
427    /// Described in TeX.2021.139.
428    pub fn new() -> Self {
429        Self {
430            height: Self::RUNNING,
431            width: Self::RUNNING,
432            depth: Self::RUNNING,
433        }
434    }
435}
436
437impl Default for Rule {
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443/// Vertical material to be inserted.
444///
445/// This node is related to the TeX primitive `\insert`.
446///
447/// Described in TeX.2021.140.
448#[derive(Clone, Debug, PartialEq)]
449pub struct Insertion {
450    pub box_number: u8,
451    /// Slightly misnamed: it actually holds the natural height plus depth
452    /// of the vertical list being inserted.
453    pub height: Number,
454    /// Used in case this insertion is split.
455    pub split_max_depth: Number,
456    pub split_top_skip: common::Glue,
457    /// Penalty to be used if this insertion floats to a subsequent
458    /// page after a split insertion of the same class.
459    pub float_penalty: u32,
460    pub vbox: Vec<Vertical>,
461}
462
463/// Contents of a user's `\mark` text.
464///
465/// TODO: At time of writing I don't know what to do with this node.
466/// In Knuth's TeX it references a token list, but I don't want Boxworks
467/// to depend on Texlang. So for the moment just leaving a dummy list.
468///
469/// Described in TeX.2021.141.
470#[derive(Clone, Debug, PartialEq, Eq)]
471pub struct Mark {
472    pub list: Vec<()>,
473}
474
475/// Specifies material that will be moved out into the surrounding vertical list.
476///
477/// E.g., used to implement the TeX primitive `\vadjust`.
478///
479/// Described in TeX.2021.142.
480#[derive(Clone, Debug, PartialEq)]
481pub struct Adjust {
482    pub list: Vec<Vertical>,
483}
484
485/// A ligature.
486///
487/// Described in TeX.2021.143.
488#[derive(Clone, Debug, PartialEq, Eq)]
489pub struct Ligature {
490    pub included_left_boundary: bool,
491    pub included_right_boundary: bool,
492    pub char: char,
493    pub font: u32,
494    /// The original characters that were replaced by the ligature.
495    /// This is used if the engine needs to break apart the ligature
496    /// in order to perform hyphenation.
497    ///
498    /// While most ligatures come from 2 characters (e.g. ff), TeX's
499    /// lig/kern programming language allows for a single ligature to come
500    /// from arbitrarily many characters.
501    pub original_chars: Rc<str>,
502}
503
504// Two constructors for ligature nodes are provided in TeX.2021.144
505// but they don't seem that useful so I'm omitting them.
506
507/// A discretionary break.
508///
509/// Described in TeX.2021.145.
510#[derive(Clone, Debug, PartialEq)]
511pub struct Discretionary {
512    /// Material to insert before this node, if the break occurs here.
513    pub pre_break: Vec<DiscretionaryElem>,
514    /// Material to insert after this node, if the break occurs here.
515    pub post_break: Vec<DiscretionaryElem>,
516    /// Number of subsequent nodes to skip if the break occurs here.
517    pub replace_count: u32,
518}
519
520impl Discretionary {
521    pub fn new() -> Self {
522        Self {
523            pre_break: vec![],
524            post_break: vec![],
525            replace_count: 0,
526        }
527    }
528}
529
530impl Default for Discretionary {
531    fn default() -> Self {
532        Self::new()
533    }
534}
535
536/// Element of a discretionary list.
537#[derive(Clone, Debug, PartialEq)]
538pub enum DiscretionaryElem {
539    Char(Char),
540    HBox(HBox),
541    VBox(VBox),
542    Rule(Rule),
543    Ligature(Ligature),
544    Kern(Kern),
545}
546
547impl From<DiscretionaryElem> for Horizontal {
548    fn from(value: DiscretionaryElem) -> Self {
549        use DiscretionaryElem as In;
550        use Horizontal as Out;
551        match value {
552            In::Char(char) => Out::Char(char),
553            In::HBox(hbox) => Out::HBox(hbox),
554            In::VBox(vbox) => Out::VBox(vbox),
555            In::Rule(rule) => Out::Rule(rule),
556            In::Ligature(ligature) => Out::Ligature(ligature),
557            In::Kern(kern) => Out::Kern(kern),
558        }
559    }
560}
561
562impl DiscretionaryElem {
563    pub fn width<F: super::FontRepo>(&self, font_width: &F) -> Number {
564        use DiscretionaryElem::*;
565        match self {
566            Char(char) => font_width
567                .width(char.char, char.font)
568                .unwrap_or(common::Scaled::ZERO),
569            HBox(hlist) => hlist.width,
570            VBox(vlist) => vlist.width,
571            Rule(rule) => rule.width,
572            Ligature(ligature) => font_width
573                .width(ligature.char, ligature.font)
574                .unwrap_or(common::Scaled::ZERO),
575            Kern(kern) => kern.width,
576        }
577    }
578}
579
580impl TryFrom<Horizontal> for DiscretionaryElem {
581    type Error = ();
582
583    fn try_from(value: Horizontal) -> Result<Self, Self::Error> {
584        use DiscretionaryElem as Out;
585        use Horizontal::*;
586        let out = match value {
587            Char(char) => Out::Char(char),
588            HBox(hlist) => Out::HBox(hlist),
589            VBox(vlist) => Out::VBox(vlist),
590            Rule(rule) => Out::Rule(rule),
591            Ligature(ligature) => Out::Ligature(ligature),
592            Kern(kern) => Out::Kern(kern),
593            _ => return Err(()),
594        };
595        Ok(out)
596    }
597}
598
599/// A whatsit node
600///
601/// This is used to facilitate extensions to TeX.
602/// It's unclear right now how what the API of it will be, though
603/// it can be figured out by reading the Chapter 53 Extensions of
604/// TeX.
605///
606/// Knuth uses this node type to implement both `\write` and `\special`
607/// so we'll eventually find out.
608///
609/// Described in TeX.2021.146.
610pub trait Whatsit: std::fmt::Debug {
611    // Invoked when this node is invoked when hyphenating.
612    //
613    // This is TeX.2021.1363 but given how we've architected the code, the logic in TeX.2021.1382
614    // (which changes the current language) should run here for \language whatsits.
615    fn hyphenation_hook(&self) {}
616}
617
618/// A marker placed before or after math mode.
619///
620/// Described in TeX.2021.147.
621///
622/// TODO: this also needs a width and so is wrong.
623#[derive(Clone, Copy, Debug, PartialEq, Eq)]
624pub enum Math {
625    Before,
626    After,
627}
628
629impl Horizontal {
630    /// Whether a glue node that comes after this node may be broken.
631    ///
632    /// For char nodes, this function is essentially undefined in Knuth's
633    /// TeX. More specifically, the value depends on the exact character code.
634    /// In TeX this function is never called for char nodes which is why this
635    /// is not a problem. Here, we return `true` for char nodes based on
636    /// my analysis of all places in Knuth's TeX where it is invoked:
637    ///
638    /// - TeX.2021.868: `precedes_break` is called on variable `cur_p` which
639    ///   is a pointer to a horizontal list. Before this call, the calling code
640    ///   first checks if the node is a character and if so follows the same
641    ///   code path. Thus returning `true` here is the right thing to do.
642    ///
643    /// - TeX.2021.973: the function is called on a variable `prev_p` which
644    ///   is a pointer to a vertical list and so the char case never arises.
645    ///
646    /// - TeX.2021.1000: same as the last case.
647    ///
648    /// This function is defined in TeX.2021.148.
649    pub fn precedes_break(&self) -> bool {
650        use Horizontal::*;
651        match self {
652            Char(_) | HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Adjust(_)
653            | Ligature(_) | Discretionary(_) | Whatsit(_) => true,
654            Kern(kern) => kern.kind != KernKind::Explicit,
655            Math(_) | Glue(_) | Penalty(_) => false,
656        }
657    }
658
659    /// Whether this node is discarded after a break.
660    ///
661    /// As with [Self::precedes_break], this function is essentially undefined
662    /// for char nodes in Knuth's TeX. However there is only one call site
663    /// (TeX.2021.879) and in that call site char nodes behave as if this
664    /// function returns true.
665    ///
666    /// This function is defined in TeX.2021.148.
667    pub fn non_discardable(&self) -> bool {
668        self.precedes_break()
669    }
670}
671
672impl Vertical {
673    /// Whether a glue node that comes after this node may be broken.
674    ///
675    /// This function is defined in TeX.2021.148.
676    pub fn precedes_break(&self) -> bool {
677        use Vertical::*;
678        matches!(
679            self,
680            HBox(_) | VBox(_) | Rule(_) | Mark(_) | Insertion(_) | Whatsit(_)
681        )
682    }
683}
684
685/// A piece of glue.
686///
687/// Described in TeX.2021.149.
688#[derive(Clone, Debug, PartialEq, Eq)]
689pub struct Glue {
690    pub value: common::Glue,
691    pub kind: GlueKind,
692}
693
694impl From<common::Glue> for Glue {
695    fn from(value: common::Glue) -> Self {
696        Self {
697            value,
698            kind: Default::default(),
699        }
700    }
701}
702
703/// The kind of a glue node.
704///
705/// Described in TeX.2021.149.
706#[derive(Clone, Debug, Default, PartialEq, Eq)]
707pub enum GlueKind {
708    #[default]
709    Normal,
710    ConditionalMath,
711    Math,
712    AlignedLeader,
713    CenteredLeader,
714    ExpandedLeader,
715}
716
717// TeX.2021.150 and TeX.2021.151 define the [font::Glue] type itself,
718// which is not in this crate.
719
720// Three constructors for glue nodes are provided in TeX.2021.152,
721// TeX.2021.153 and TeX.2021.154 but they don't seem that
722// useful so I'm omitting them.
723
724/// A kern.
725///
726/// Described in TeX.2021.155.
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub struct Kern {
729    pub width: Number,
730    pub kind: KernKind,
731}
732
733/// The kind of a kern node.
734///
735/// Described in TeX.2021.155.
736#[derive(Clone, Copy, Debug, PartialEq, Eq)]
737pub enum KernKind {
738    /// Inserted from font information or math mode calculations.
739    Normal,
740    /// Inserted using e.g. TeX's `\kern` primitive.
741    Explicit,
742    /// Inserted from non-math accents.
743    Accent,
744    /// Inserted from e.g. `\mkern` specifications in math formulas.
745    Math,
746}
747
748// A constructor for kern nodes is provided in TeX.2021.156,
749// but it doesn't seem useful.
750
751/// A penalty.
752///
753/// Described in TeX.2021.157.
754#[derive(Clone, Debug, PartialEq, Eq)]
755pub struct Penalty(pub i32);
756
757impl Penalty {
758    /// Any penalty bigger than this is considered infinite and no
759    /// break will be allowed for such high values.
760    pub const INFINITE: Penalty = Penalty(10000);
761
762    /// Any penalty smaller than this will result in a forced break.
763    pub const EJECT: Penalty = Penalty(-10000);
764}
765
766// A constructor for penalty nodes is provided in TeX.2021.157,
767// but it doesn't seem useful.
768
769// TODO: Unset node(s) in TeX.2021.159
770
771pub fn short_display_hlist(w: &mut dyn std::fmt::Write, hlist: &[Horizontal]) -> std::fmt::Result {
772    // TeX.2021.174
773    let mut i = 0;
774    while let Some(elem) = hlist.get(i) {
775        use Horizontal::*;
776        match elem {
777            Char(char) => write!(w, "{}", char.char)?,
778            HBox(_) | VBox(_) | Whatsit(_) | Mark(_) | Adjust(_) => write!(w, "[]")?,
779            Rule(_) => write!(w, "|")?,
780            Ligature(ligature) => write!(w, "{}", ligature.original_chars)?,
781            Discretionary(discretionary) => {
782                short_display_dlist(w, &discretionary.pre_break)?;
783                short_display_dlist(w, &discretionary.post_break)?;
784                i += discretionary.replace_count as usize;
785            }
786            Math(_) => write!(w, "$")?,
787            Glue(glue) => {
788                if !glue.value.is_zero() {
789                    write!(w, " ")?;
790                }
791            }
792            Kern(_) | Penalty(_) | Insertion(_) => {}
793        };
794        i += 1;
795    }
796    Ok(())
797}
798
799fn short_display_dlist(
800    w: &mut dyn std::fmt::Write,
801    dlist: &[DiscretionaryElem],
802) -> std::fmt::Result {
803    // TeX.2021.174
804    let mut i = 0;
805    while let Some(elem) = dlist.get(i) {
806        use DiscretionaryElem::*;
807        match elem {
808            Char(char) => write!(w, "{}", char.char)?,
809            HBox(_) | VBox(_) => write!(w, "[]")?,
810            Rule(_) => write!(w, "|")?,
811            Ligature(ligature) => write!(w, "{}", ligature.original_chars)?,
812            Kern(_) => {}
813        };
814        i += 1;
815    }
816    Ok(())
817}