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