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}