Skip to main content

mathtex_ir/
lib.rs

1//! Renderer neutral layout IR for one typeset TeX fragment, with source spans.
2#![forbid(unsafe_code)]
3
4use std::fmt;
5use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
6
7/// One typeset fragment: a tree of layout nodes rooted at `root`, stored densely by id.
8#[derive(Clone, Debug, Default, PartialEq)]
9pub struct Fragment {
10    /// The root node, placed at (0, 0) with its reference point on the surface baseline.
11    pub root: NodeId,
12    /// Every node of the tree, `nodes[i].id == NodeId(i)`.
13    pub nodes: Vec<LayoutNode>,
14    /// Source files and enclosing construct spans.
15    pub source_map: SourceMap,
16    /// How the fragment was produced.
17    pub metadata: FragmentMetadata,
18    /// Extent of the root box, `surface.baseline == root.height`.
19    pub surface: Surface,
20}
21
22impl Fragment {
23    /// Builds a fragment, deriving the surface from the root and checking every invariant.
24    pub fn new(
25        root: NodeId,
26        nodes: Vec<LayoutNode>,
27        source_map: SourceMap,
28        metadata: FragmentMetadata,
29    ) -> Result<Self, FragmentError> {
30        let surface = nodes
31            .get(root.index())
32            .map_or_else(Surface::default, Surface::of_root);
33        let fragment = Self {
34            root,
35            nodes,
36            source_map,
37            metadata,
38            surface,
39        };
40        fragment.validate()?;
41        Ok(fragment)
42    }
43
44    /// Checks dense ids, a single tree under the root, the root placement, the surface and source ids.
45    pub fn validate(&self) -> Result<(), FragmentError> {
46        for (index, node) in self.nodes.iter().enumerate() {
47            if node.id.index() != index {
48                return Err(FragmentError::IdMismatch { index, id: node.id });
49            }
50        }
51        if self.nodes.is_empty() {
52            return Ok(());
53        }
54        let root = self
55            .nodes
56            .get(self.root.index())
57            .ok_or(FragmentError::RootOutOfRange(self.root))?;
58        if root.origin != Point::ORIGIN {
59            return Err(FragmentError::RootNotAtOrigin);
60        }
61        if self.surface != Surface::of_root(root) {
62            return Err(FragmentError::SurfaceMismatch);
63        }
64        let mut has_parent = vec![false; self.nodes.len()];
65        for node in &self.nodes {
66            for &child in node.children() {
67                let slot =
68                    has_parent
69                        .get_mut(child.index())
70                        .ok_or(FragmentError::ChildOutOfRange {
71                            parent: node.id,
72                            child,
73                        })?;
74                if *slot || child == self.root {
75                    return Err(FragmentError::MultipleParents(child));
76                }
77                *slot = true;
78            }
79        }
80        let mut reached = vec![false; self.nodes.len()];
81        let mut stack = vec![self.root];
82        while let Some(id) = stack.pop() {
83            if std::mem::replace(&mut reached[id.index()], true) {
84                return Err(FragmentError::MultipleParents(id));
85            }
86            stack.extend_from_slice(self.nodes[id.index()].children());
87        }
88        if let Some(index) = reached.iter().position(|reached| !reached) {
89            return Err(FragmentError::Unreachable(self.nodes[index].id));
90        }
91        for (index, file) in self.source_map.sources.iter().enumerate() {
92            if file.id.index() != index {
93                return Err(FragmentError::SourceIdMismatch { index, id: file.id });
94            }
95        }
96        let known = |range: &SourceRange| self.source_map.source(range.source).is_some();
97        for node in &self.nodes {
98            if let Some(range) = &node.primary_source {
99                if !known(range) {
100                    return Err(FragmentError::UnknownSource {
101                        node: node.id,
102                        source: range.source,
103                    });
104                }
105            }
106        }
107        for entry in &self.source_map.entries {
108            if entry.node.index() >= self.nodes.len() {
109                return Err(FragmentError::Unreachable(entry.node));
110            }
111            if !known(&entry.range) {
112                return Err(FragmentError::UnknownSource {
113                    node: entry.node,
114                    source: entry.range.source,
115                });
116            }
117        }
118        Ok(())
119    }
120
121    /// Returns the node with the given id in constant time.
122    #[must_use]
123    pub fn node(&self, id: NodeId) -> Option<&LayoutNode> {
124        self.nodes.get(id.index()).filter(|node| node.id == id)
125    }
126
127    /// Returns the root node, `None` for an empty fragment.
128    #[must_use]
129    pub fn root_node(&self) -> Option<&LayoutNode> {
130        self.node(self.root)
131    }
132
133    /// Returns the children of a node, empty for leaves and unknown ids.
134    #[must_use]
135    pub fn children(&self, id: NodeId) -> &[NodeId] {
136        self.node(id).map_or(&[], LayoutNode::children)
137    }
138
139    /// Iterates the enclosing construct entries of a node, innermost first.
140    pub fn source_entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
141        self.source_map.entries_for_node(node)
142    }
143
144    /// Iterates the node's primary origin followed by its enclosing construct origins.
145    pub fn source_origins_for_node(&self, node: NodeId) -> impl Iterator<Item = SourceOrigin<'_>> {
146        let primary = self
147            .primary_source_for_node(node)
148            .and_then(|range| self.origin(node, range, SourceRole::Primary));
149        let enclosing = self.source_entries_for_node(node).filter_map(move |entry| {
150            self.origin(node, entry.range, SourceRole::EnclosingConstruct)
151        });
152        primary.into_iter().chain(enclosing)
153    }
154
155    /// Returns the primary source range of a node.
156    #[must_use]
157    pub fn primary_source_for_node(&self, node: NodeId) -> Option<SourceRange> {
158        self.node(node)?.primary_source
159    }
160
161    /// Resolves a glyph's cluster span to a range in the node's primary source file.
162    #[must_use]
163    pub fn glyph_source_range(&self, node: NodeId, glyph_index: usize) -> Option<SourceRange> {
164        let node = self.node(node)?;
165        let LayoutNodeKind::GlyphRun(run) = &node.kind else {
166            return None;
167        };
168        let span = run.glyphs.get(glyph_index)?.cluster?;
169        Some(SourceRange {
170            source: node.primary_source?.source,
171            span,
172        })
173    }
174
175    /// Resolves a glyph's cluster span to a full source origin.
176    #[must_use]
177    pub fn glyph_source_origin(
178        &self,
179        node: NodeId,
180        glyph_index: usize,
181    ) -> Option<SourceOrigin<'_>> {
182        let range = self.glyph_source_range(node, glyph_index)?;
183        self.origin(node, range, SourceRole::Primary)
184    }
185
186    /// Absolute glyphs and rules in tree order, y down, origin at the surface top left.
187    #[must_use]
188    pub fn flatten(&self) -> Vec<Placed<'_>> {
189        let mut out = Vec::new();
190        if self.root_node().is_none() {
191            return out;
192        }
193        let mut visited = vec![false; self.nodes.len()];
194        let mut stack = vec![(self.root, Point::new(Length::ZERO, self.surface.baseline))];
195        while let Some((id, parent)) = stack.pop() {
196            let Some(node) = self.node(id) else {
197                continue;
198            };
199            if std::mem::replace(&mut visited[id.index()], true) {
200                continue;
201            }
202            let at = parent + node.origin;
203            match &node.kind {
204                LayoutNodeKind::Box(layout_box) => {
205                    stack.extend(layout_box.children.iter().rev().map(|&child| (child, at)));
206                }
207                LayoutNodeKind::GlyphRun(run) => {
208                    for (index, glyph) in run.glyphs.iter().enumerate() {
209                        let source = self.glyph_source_range(id, index).or(node.primary_source);
210                        out.push(Placed::Glyph {
211                            node: id,
212                            font: &run.font,
213                            glyph_id: glyph.glyph_id,
214                            x: at.x + glyph.offset.x,
215                            y: at.y + glyph.offset.y,
216                            source,
217                        });
218                    }
219                }
220                LayoutNodeKind::Rule => {
221                    let height = node.height + node.depth;
222                    // TeX draws a rule only when both its width and total height are positive.
223                    if node.width > Length::ZERO && height > Length::ZERO {
224                        out.push(Placed::Rule {
225                            node: id,
226                            x: at.x,
227                            y: at.y - node.height,
228                            width: node.width,
229                            height,
230                            source: node.primary_source,
231                        });
232                    }
233                }
234                LayoutNodeKind::Glue(_) | LayoutNodeKind::Kern(_) => {}
235            }
236        }
237        out
238    }
239
240    fn origin(
241        &self,
242        node: NodeId,
243        range: SourceRange,
244        role: SourceRole,
245    ) -> Option<SourceOrigin<'_>> {
246        Some(SourceOrigin {
247            node,
248            source: self.source_map.source(range.source)?,
249            span: range.span,
250            role,
251        })
252    }
253}
254
255/// A drawable item from [`Fragment::flatten`], lengths absolute from the surface top left, y down.
256#[derive(Clone, Copy, Debug, PartialEq)]
257pub enum Placed<'a> {
258    /// One glyph, `(x, y)` is its origin on the baseline.
259    Glyph {
260        /// The glyph run node the glyph belongs to.
261        node: NodeId,
262        /// Font of the glyph run.
263        font: &'a FontRef,
264        /// Glyph index, or the character code for TFM fonts.
265        glyph_id: GlyphId,
266        /// Horizontal position of the glyph origin.
267        x: Length,
268        /// Baseline position of the glyph origin.
269        y: Length,
270        /// The glyph's cluster range, or the run's primary source.
271        source: Option<SourceRange>,
272    },
273    /// One filled rectangle, `(x, y)` is its top left corner.
274    Rule {
275        /// The rule node.
276        node: NodeId,
277        /// Left edge.
278        x: Length,
279        /// Top edge.
280        y: Length,
281        /// Horizontal extent.
282        width: Length,
283        /// Vertical extent, the rule's height plus depth.
284        height: Length,
285        /// The rule's primary source.
286        source: Option<SourceRange>,
287    },
288}
289
290/// Why a fragment breaks the IR invariants.
291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
292#[non_exhaustive]
293pub enum FragmentError {
294    /// A node is stored at an index other than its id.
295    IdMismatch {
296        /// Position in `nodes`.
297        index: usize,
298        /// Id stored there.
299        id: NodeId,
300    },
301    /// The root id indexes no node.
302    RootOutOfRange(NodeId),
303    /// The root origin is not (0, 0).
304    RootNotAtOrigin,
305    /// The surface does not match the root's width, height and depth.
306    SurfaceMismatch,
307    /// A box lists a child id that indexes no node.
308    ChildOutOfRange {
309        /// The box listing the child.
310        parent: NodeId,
311        /// The missing child id.
312        child: NodeId,
313    },
314    /// A node is listed as a child more than once, or the root is listed as a child.
315    MultipleParents(NodeId),
316    /// A node is not reachable from the root.
317    Unreachable(NodeId),
318    /// A source file is stored at an index other than its id.
319    SourceIdMismatch {
320        /// Position in `sources`.
321        index: usize,
322        /// Id stored there.
323        id: SourceId,
324    },
325    /// A node or entry refers to an unregistered source file.
326    UnknownSource {
327        /// The node carrying the range.
328        node: NodeId,
329        /// The unknown source id.
330        source: SourceId,
331    },
332}
333
334impl fmt::Display for FragmentError {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        match self {
337            Self::IdMismatch { index, id } => {
338                write!(f, "node at index {index} has id {}", id.0)
339            }
340            Self::RootOutOfRange(id) => write!(f, "root id {} indexes no node", id.0),
341            Self::RootNotAtOrigin => f.write_str("root origin is not (0, 0)"),
342            Self::SurfaceMismatch => f.write_str("surface does not match the root box"),
343            Self::ChildOutOfRange { parent, child } => {
344                write!(f, "node {} lists missing child {}", parent.0, child.0)
345            }
346            Self::MultipleParents(id) => write!(f, "node {} has more than one parent", id.0),
347            Self::Unreachable(id) => write!(f, "node {} is not reachable from the root", id.0),
348            Self::SourceIdMismatch { index, id } => {
349                write!(f, "source at index {index} has id {}", id.0)
350            }
351            Self::UnknownSource { node, source } => {
352                write!(f, "node {} refers to unknown source {}", node.0, source.0)
353            }
354        }
355    }
356}
357
358impl std::error::Error for FragmentError {}
359
360/// A resolved source relationship of a node.
361#[derive(Clone, Copy, Debug, PartialEq, Eq)]
362pub struct SourceOrigin<'a> {
363    /// The node.
364    pub node: NodeId,
365    /// The source file holding the span.
366    pub source: &'a SourceFile,
367    /// Byte span within that file.
368    pub span: ByteSpan,
369    /// Whether the span is the node's own source or an enclosing construct.
370    pub role: SourceRole,
371}
372
373/// How a fragment was produced.
374#[derive(Clone, Debug, Default, PartialEq, Eq)]
375pub struct FragmentMetadata {
376    /// Format identifier, such as `plain`, `latex` or a host chosen name.
377    pub format_id: String,
378    /// Mode the fragment was typeset in.
379    pub fragment_kind: FragmentKind,
380}
381
382/// Mode a fragment was typeset in.
383#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
384#[non_exhaustive]
385pub enum FragmentKind {
386    /// Inline math.
387    #[default]
388    MathInline,
389    /// Display math.
390    MathDisplay,
391    /// Horizontal mode text.
392    Text,
393}
394
395/// Extent of a fragment, the root box's width, total height and height above the baseline.
396#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
397pub struct Surface {
398    /// Root width.
399    pub width: Length,
400    /// Root height plus depth.
401    pub height: Length,
402    /// Distance from the top edge down to the baseline, the root height.
403    pub baseline: Length,
404}
405
406impl Surface {
407    /// The surface a root node spans.
408    #[must_use]
409    pub fn of_root(root: &LayoutNode) -> Self {
410        Self {
411            width: root.width,
412            height: root.height + root.depth,
413            baseline: root.height,
414        }
415    }
416}
417
418/// A node of the layout tree.
419#[derive(Clone, Debug, PartialEq)]
420pub struct LayoutNode {
421    /// Id, equal to the node's index in [`Fragment::nodes`].
422    pub id: NodeId,
423    /// Reference point relative to the parent's reference point, the box's left baseline point, y down.
424    pub origin: Point,
425    /// Horizontal extent to the right of the reference point.
426    pub width: Length,
427    /// Extent above the reference point.
428    pub height: Length,
429    /// Extent below the reference point.
430    pub depth: Length,
431    /// The source span this node was typeset from.
432    pub primary_source: Option<SourceRange>,
433    /// What the node is.
434    pub kind: LayoutNodeKind,
435}
436
437impl LayoutNode {
438    /// Returns the children of a box, empty for every other node.
439    #[must_use]
440    pub fn children(&self) -> &[NodeId] {
441        match &self.kind {
442            LayoutNodeKind::Box(layout_box) => &layout_box.children,
443            _ => &[],
444        }
445    }
446}
447
448/// What a layout node is.
449#[derive(Clone, Debug, PartialEq)]
450#[non_exhaustive]
451pub enum LayoutNodeKind {
452    /// A box with positioned children.
453    Box(LayoutBox),
454    /// Glyphs of one font.
455    GlyphRun(GlyphRun),
456    /// A filled rectangle spanning the node's width, height and depth.
457    Rule,
458    /// Glue after glue setting.
459    Glue(Glue),
460    /// A kern.
461    Kern(Kern),
462}
463
464/// A TeX box.
465#[derive(Clone, Debug, PartialEq, Eq)]
466pub struct LayoutBox {
467    /// Horizontal or vertical list.
468    pub kind: BoxKind,
469    /// Children in list order.
470    pub children: Vec<NodeId>,
471}
472
473/// Whether a box holds a horizontal or a vertical list.
474#[derive(Clone, Copy, Debug, PartialEq, Eq)]
475#[non_exhaustive]
476pub enum BoxKind {
477    /// An hbox.
478    Horizontal,
479    /// A vbox.
480    Vertical,
481}
482
483/// Direction a glue or kern moves the list position.
484#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
485pub enum Axis {
486    /// Along a horizontal list.
487    #[default]
488    Horizontal,
489    /// Along a vertical list.
490    Vertical,
491}
492
493/// Glyphs of one font.
494#[derive(Clone, Debug, PartialEq, Eq)]
495pub struct GlyphRun {
496    /// Font of every glyph in the run.
497    pub font: FontRef,
498    /// Glyphs in visual order.
499    pub glyphs: Vec<PositionedGlyph>,
500}
501
502/// A glyph placed relative to its run.
503#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
504pub struct PositionedGlyph {
505    /// Glyph index in an OpenType font, or the character code for TFM fonts.
506    pub glyph_id: GlyphId,
507    /// Offset of the glyph origin from the run's reference point, y down.
508    pub offset: Point,
509    /// Source bytes the glyph was shaped from, in the run's primary source file.
510    pub cluster: Option<ByteSpan>,
511}
512
513/// A font as the IR names it.
514#[derive(Clone, Debug, PartialEq, Eq, Hash)]
515pub struct FontRef {
516    /// The key the host's font loader returned, `None` for TFM fonts.
517    pub key: Option<FontKey>,
518    /// The XeTeX font spec of a native font, the TFM name, or empty for host box runs.
519    pub spec: String,
520    /// Size the font was loaded at.
521    pub size: Length,
522}
523
524/// Host chosen identity of a loaded font face.
525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
526pub struct FontKey(pub u64);
527
528/// Glue after glue setting.
529#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
530pub struct Glue {
531    /// Set size along the axis, natural size plus the stretch or shrink the parent box applied.
532    pub amount: Length,
533    /// List direction the glue lies in.
534    pub axis: Axis,
535}
536
537/// A kern.
538#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
539pub struct Kern {
540    /// Size along the axis.
541    pub amount: Length,
542    /// List direction the kern lies in.
543    pub axis: Axis,
544}
545
546/// Source files and the enclosing construct spans of nodes.
547#[derive(Clone, Debug, Default, PartialEq, Eq)]
548pub struct SourceMap {
549    /// Registered source files, `sources[i].id == SourceId(i)`.
550    pub sources: Vec<SourceFile>,
551    /// Enclosing construct spans, innermost first per node.
552    pub entries: Vec<SourceMapEntry>,
553}
554
555impl SourceMap {
556    /// Registers a source file and returns its id.
557    pub fn add_source(&mut self, name: impl Into<String>) -> SourceId {
558        let id = SourceId(u32::try_from(self.sources.len()).unwrap_or(u32::MAX));
559        self.sources.push(SourceFile {
560            id,
561            name: name.into(),
562        });
563        id
564    }
565
566    /// Returns the id of a source file by name, registering it when new.
567    pub fn intern_source(&mut self, name: impl Into<String>) -> SourceId {
568        let name = name.into();
569        if let Some(source) = self.sources.iter().find(|source| source.name == name) {
570            return source.id;
571        }
572        self.add_source(name)
573    }
574
575    /// Records an enclosing construct span of a node.
576    pub fn add_entry(&mut self, node: NodeId, range: SourceRange) {
577        self.entries.push(SourceMapEntry { node, range });
578    }
579
580    /// Returns a registered source file.
581    #[must_use]
582    pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
583        self.sources
584            .get(id.index())
585            .filter(|source| source.id == id)
586    }
587
588    /// Iterates the enclosing construct entries of a node.
589    pub fn entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
590        self.entries.iter().filter(move |entry| entry.node == node)
591    }
592}
593
594/// A source file, such as the host's input.
595#[derive(Clone, Debug, PartialEq, Eq)]
596pub struct SourceFile {
597    /// Id, equal to the file's index in [`SourceMap::sources`].
598    pub id: SourceId,
599    /// Name the engine read the file under.
600    pub name: String,
601}
602
603/// An enclosing construct span of a node, such as a macro call around it.
604#[derive(Clone, Copy, Debug, PartialEq, Eq)]
605pub struct SourceMapEntry {
606    /// The node.
607    pub node: NodeId,
608    /// The enclosing span.
609    pub range: SourceRange,
610}
611
612/// A span within a registered source file.
613#[derive(Clone, Copy, Debug, PartialEq, Eq)]
614pub struct SourceRange {
615    /// The file.
616    pub source: SourceId,
617    /// Byte span in that file.
618    pub span: ByteSpan,
619}
620
621/// How a source span relates to a node.
622#[derive(Clone, Copy, Debug, PartialEq, Eq)]
623#[non_exhaustive]
624pub enum SourceRole {
625    /// The span the node was typeset from.
626    Primary,
627    /// A construct enclosing the node's own span.
628    EnclosingConstruct,
629}
630
631/// A half open span of UTF-8 byte offsets.
632#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
633pub struct ByteSpan {
634    /// First byte.
635    pub start: u32,
636    /// One past the last byte.
637    pub end: u32,
638}
639
640/// A point in scaled points, y down.
641#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
642pub struct Point {
643    /// Horizontal coordinate.
644    pub x: Length,
645    /// Vertical coordinate, growing downward.
646    pub y: Length,
647}
648
649impl Point {
650    /// The point (0, 0).
651    pub const ORIGIN: Self = Self {
652        x: Length::ZERO,
653        y: Length::ZERO,
654    };
655
656    /// Builds a point.
657    #[must_use]
658    pub const fn new(x: Length, y: Length) -> Self {
659        Self { x, y }
660    }
661}
662
663impl Add for Point {
664    type Output = Self;
665
666    fn add(self, rhs: Self) -> Self {
667        Self::new(self.x + rhs.x, self.y + rhs.y)
668    }
669}
670
671/// A TeX dimension in scaled points, 65536 per point, arithmetic saturates.
672#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
673pub struct Length(pub i32);
674
675impl Length {
676    /// Zero length.
677    pub const ZERO: Self = Self(0);
678    /// Scaled points per TeX point.
679    pub const SP_PER_PT: i32 = 65_536;
680
681    /// Builds a length from scaled points.
682    #[must_use]
683    pub const fn from_scaled_points(value: i32) -> Self {
684        Self(value)
685    }
686
687    /// Builds a length from TeX points, rounded to the nearest scaled point.
688    #[must_use]
689    pub fn from_pt(points: f64) -> Self {
690        Self((points * f64::from(Self::SP_PER_PT)).round() as i32)
691    }
692
693    /// Returns the length in TeX points.
694    #[must_use]
695    pub fn to_pt(self) -> f64 {
696        f64::from(self.0) / f64::from(Self::SP_PER_PT)
697    }
698}
699
700impl Add for Length {
701    type Output = Self;
702
703    fn add(self, rhs: Self) -> Self {
704        Self(self.0.saturating_add(rhs.0))
705    }
706}
707
708impl AddAssign for Length {
709    fn add_assign(&mut self, rhs: Self) {
710        *self = *self + rhs;
711    }
712}
713
714impl Sub for Length {
715    type Output = Self;
716
717    fn sub(self, rhs: Self) -> Self {
718        Self(self.0.saturating_sub(rhs.0))
719    }
720}
721
722impl SubAssign for Length {
723    fn sub_assign(&mut self, rhs: Self) {
724        *self = *self - rhs;
725    }
726}
727
728impl Neg for Length {
729    type Output = Self;
730
731    fn neg(self) -> Self {
732        Self(self.0.saturating_neg())
733    }
734}
735
736impl Mul<i32> for Length {
737    type Output = Self;
738
739    fn mul(self, rhs: i32) -> Self {
740        Self(self.0.saturating_mul(rhs))
741    }
742}
743
744impl Mul<f64> for Length {
745    type Output = Self;
746
747    fn mul(self, rhs: f64) -> Self {
748        Self((f64::from(self.0) * rhs).round() as i32)
749    }
750}
751
752/// Node id, the node's index in [`Fragment::nodes`].
753#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
754pub struct NodeId(pub u32);
755
756impl NodeId {
757    /// Returns the id as a vector index.
758    #[must_use]
759    pub const fn index(self) -> usize {
760        self.0 as usize
761    }
762}
763
764/// Source file id, the file's index in [`SourceMap::sources`].
765#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
766pub struct SourceId(pub u32);
767
768impl SourceId {
769    /// Returns the id as a vector index.
770    #[must_use]
771    pub const fn index(self) -> usize {
772        self.0 as usize
773    }
774}
775
776/// Glyph index in an OpenType font, or the character code for TFM fonts.
777#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
778pub struct GlyphId(pub u32);
779
780/// Glyph outline command in font design units, y up.
781#[derive(Clone, Copy, Debug, PartialEq)]
782pub enum OutlineCommand {
783    /// Starts a contour.
784    MoveTo {
785        /// Horizontal position.
786        x: f32,
787        /// Vertical position.
788        y: f32,
789    },
790    /// Straight segment.
791    LineTo {
792        /// Horizontal position.
793        x: f32,
794        /// Vertical position.
795        y: f32,
796    },
797    /// Quadratic Bezier segment.
798    QuadTo {
799        /// Control point horizontal position.
800        cx: f32,
801        /// Control point vertical position.
802        cy: f32,
803        /// End point horizontal position.
804        x: f32,
805        /// End point vertical position.
806        y: f32,
807    },
808    /// Cubic Bezier segment.
809    CurveTo {
810        /// First control point horizontal position.
811        c1x: f32,
812        /// First control point vertical position.
813        c1y: f32,
814        /// Second control point horizontal position.
815        c2x: f32,
816        /// Second control point vertical position.
817        c2y: f32,
818        /// End point horizontal position.
819        x: f32,
820        /// End point vertical position.
821        y: f32,
822    },
823    /// Closes the contour.
824    Close,
825}
826
827/// Glyph outline in font design units, y up, empty for blank glyphs such as spaces.
828#[derive(Clone, Debug, Default, PartialEq)]
829pub struct GlyphOutline {
830    /// Design units per em of the font.
831    pub units_per_em: u16,
832    /// Contour commands.
833    pub commands: Vec<OutlineCommand>,
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839
840    const PT: i32 = Length::SP_PER_PT;
841
842    fn node(
843        id: u32,
844        origin: (i32, i32),
845        size: (i32, i32, i32),
846        kind: LayoutNodeKind,
847    ) -> LayoutNode {
848        LayoutNode {
849            id: NodeId(id),
850            origin: Point::new(Length(origin.0 * PT), Length(origin.1 * PT)),
851            width: Length(size.0 * PT),
852            height: Length(size.1 * PT),
853            depth: Length(size.2 * PT),
854            primary_source: None,
855            kind,
856        }
857    }
858
859    fn hbox(children: &[u32]) -> LayoutNodeKind {
860        LayoutNodeKind::Box(LayoutBox {
861            kind: BoxKind::Horizontal,
862            children: children.iter().copied().map(NodeId).collect(),
863        })
864    }
865
866    fn run(glyphs: &[(u32, i32, i32)]) -> LayoutNodeKind {
867        LayoutNodeKind::GlyphRun(GlyphRun {
868            font: FontRef {
869                key: Some(FontKey(3)),
870                spec: "[test.otf]".into(),
871                size: Length(10 * PT),
872            },
873            glyphs: glyphs
874                .iter()
875                .map(|&(glyph, x, y)| PositionedGlyph {
876                    glyph_id: GlyphId(glyph),
877                    offset: Point::new(Length(x * PT), Length(y * PT)),
878                    cluster: None,
879                })
880                .collect(),
881        })
882    }
883
884    fn sample() -> Fragment {
885        let nodes = vec![
886            node(0, (0, 0), (20, 8, 2), hbox(&[1, 3])),
887            node(1, (3, -2), (5, 4, 0), hbox(&[2])),
888            node(2, (1, 0), (4, 4, 0), run(&[(7, 0, 0), (8, 2, 1)])),
889            node(3, (10, 0), (6, 3, 1), LayoutNodeKind::Rule),
890        ];
891        Fragment::new(
892            NodeId(0),
893            nodes,
894            SourceMap::default(),
895            FragmentMetadata::default(),
896        )
897        .expect("valid fragment")
898    }
899
900    #[test]
901    fn surface_derives_from_the_root() {
902        let fragment = sample();
903        assert_eq!(fragment.surface.width, Length(20 * PT));
904        assert_eq!(fragment.surface.height, Length(10 * PT));
905        assert_eq!(fragment.surface.baseline, Length(8 * PT));
906    }
907
908    #[test]
909    fn flatten_accumulates_parent_relative_origins_from_the_baseline() {
910        let fragment = sample();
911        let placed = fragment.flatten();
912        let pt = |value: i32| Length(value * PT);
913        assert_eq!(placed.len(), 3);
914        // Run origin is (3 + 1, 8 - 2), the second glyph adds its offset of (2, 1).
915        assert!(
916            matches!(placed[0], Placed::Glyph { glyph_id: GlyphId(7), x, y, .. } if x == pt(4) && y == pt(6))
917        );
918        assert!(
919            matches!(placed[1], Placed::Glyph { glyph_id: GlyphId(8), x, y, .. } if x == pt(6) && y == pt(7))
920        );
921        // The rule's top is its baseline minus its height, its extent is height plus depth.
922        assert_eq!(
923            placed[2],
924            Placed::Rule {
925                node: NodeId(3),
926                x: pt(10),
927                y: pt(5),
928                width: pt(6),
929                height: pt(4),
930                source: None,
931            }
932        );
933    }
934
935    #[test]
936    fn validate_rejects_broken_trees() {
937        let mut fragment = sample();
938        fragment.nodes.swap(1, 2);
939        assert!(matches!(
940            fragment.validate(),
941            Err(FragmentError::IdMismatch { index: 1, .. })
942        ));
943
944        let mut fragment = sample();
945        if let LayoutNodeKind::Box(root) = &mut fragment.nodes[0].kind {
946            root.children.push(NodeId(2));
947        }
948        assert_eq!(
949            fragment.validate(),
950            Err(FragmentError::MultipleParents(NodeId(2)))
951        );
952
953        let mut fragment = sample();
954        if let LayoutNodeKind::Box(root) = &mut fragment.nodes[0].kind {
955            root.children.pop();
956        }
957        assert_eq!(
958            fragment.validate(),
959            Err(FragmentError::Unreachable(NodeId(3)))
960        );
961
962        let mut fragment = sample();
963        fragment.nodes[0].origin.x = Length(1);
964        assert_eq!(fragment.validate(), Err(FragmentError::RootNotAtOrigin));
965
966        let mut fragment = sample();
967        fragment.surface.baseline = Length::ZERO;
968        assert_eq!(fragment.validate(), Err(FragmentError::SurfaceMismatch));
969    }
970
971    #[test]
972    fn source_origins_list_the_primary_span_then_enclosing_spans() {
973        let mut fragment = sample();
974        let input = fragment.source_map.add_source("input");
975        let package = fragment.source_map.add_source("amsmath.sty");
976        assert_eq!(fragment.source_map.intern_source("input"), input);
977        fragment.nodes[2].primary_source = Some(SourceRange {
978            source: input,
979            span: ByteSpan { start: 1, end: 5 },
980        });
981        fragment.source_map.add_entry(
982            NodeId(2),
983            SourceRange {
984                source: package,
985                span: ByteSpan { start: 10, end: 20 },
986            },
987        );
988        if let LayoutNodeKind::GlyphRun(run) = &mut fragment.nodes[2].kind {
989            run.glyphs[1].cluster = Some(ByteSpan { start: 2, end: 4 });
990        }
991        fragment.validate().expect("valid");
992
993        let origins = fragment
994            .source_origins_for_node(NodeId(2))
995            .collect::<Vec<_>>();
996        assert_eq!(origins.len(), 2);
997        assert_eq!(origins[0].role, SourceRole::Primary);
998        assert_eq!(origins[0].span, ByteSpan { start: 1, end: 5 });
999        assert_eq!(origins[1].source.name, "amsmath.sty");
1000        assert_eq!(origins[1].role, SourceRole::EnclosingConstruct);
1001
1002        assert_eq!(fragment.glyph_source_range(NodeId(2), 0), None);
1003        let cluster = SourceRange {
1004            source: input,
1005            span: ByteSpan { start: 2, end: 4 },
1006        };
1007        assert_eq!(fragment.glyph_source_range(NodeId(2), 1), Some(cluster));
1008        let placed = fragment.flatten();
1009        assert!(
1010            matches!(placed[0], Placed::Glyph { source: Some(range), .. } if range.span == ByteSpan { start: 1, end: 5 })
1011        );
1012        assert!(matches!(placed[1], Placed::Glyph { source: Some(range), .. } if range == cluster));
1013
1014        fragment.nodes[3].primary_source = Some(SourceRange {
1015            source: SourceId(9),
1016            span: ByteSpan::default(),
1017        });
1018        assert!(matches!(
1019            fragment.validate(),
1020            Err(FragmentError::UnknownSource { .. })
1021        ));
1022    }
1023
1024    #[test]
1025    fn length_arithmetic_saturates_and_converts_points() {
1026        assert_eq!(Length::from_pt(1.5), Length(98_304));
1027        assert_eq!(Length(98_304).to_pt(), 1.5);
1028        assert_eq!(Length(3) + Length(4) - Length(10), Length(-3));
1029        assert_eq!(-Length(5), Length(-5));
1030        assert_eq!(Length(5) * 3, Length(15));
1031        assert_eq!(Length(10) * 0.25, Length(3));
1032        assert_eq!(Length(i32::MAX) + Length(1), Length(i32::MAX));
1033        assert_eq!(-Length(i32::MIN), Length(i32::MAX));
1034    }
1035}