Skip to main content

mathtex_ir/
lib.rs

1//! Renderer neutral intermediate representation for a laid out TeX/LaTeX fragment.
2#![cfg_attr(not(feature = "std"), no_std)]
3#![forbid(unsafe_code)]
4
5extern crate alloc;
6
7use alloc::string::String;
8use alloc::vec::Vec;
9
10/// A complete renderer neutral layout result for one TeX/LaTeX fragment.
11#[derive(Clone, Debug, Default, PartialEq)]
12pub struct Fragment {
13    /// Fragment coordinate surface dimensions.
14    pub surface: Surface,
15    /// All layout nodes in this fragment.
16    pub nodes: Vec<LayoutNode>,
17    /// Source map relating nodes back to input positions.
18    pub source_map: SourceMap,
19    /// Metadata about the engine and format that produced this fragment.
20    pub metadata: FragmentMetadata,
21}
22
23impl Fragment {
24    /// Returns the layout node with the given identifier, if present.
25    #[must_use]
26    pub fn node(&self, node: NodeId) -> Option<&LayoutNode> {
27        self.nodes.iter().find(|candidate| candidate.id == node)
28    }
29
30    /// Iterates all source map entries for the given node.
31    pub fn source_entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
32        self.source_map.entries_for_node(node)
33    }
34
35    /// Iterates resolved source origins for the given node.
36    pub fn source_origins_for_node(&self, node: NodeId) -> impl Iterator<Item = SourceOrigin<'_>> {
37        self.source_entries_for_node(node)
38            .filter_map(|entry| self.source_origin_for_entry(entry))
39    }
40
41    /// Resolves a source map entry to its origin metadata.
42    #[must_use]
43    pub fn source_origin_for_entry(&self, entry: &SourceMapEntry) -> Option<SourceOrigin<'_>> {
44        let source = self.source_map.source(entry.range.source)?;
45        Some(SourceOrigin {
46            node: entry.node,
47            source,
48            span: entry.range.span,
49            role: entry.role,
50        })
51    }
52
53    /// Returns the primary source range for the given node, if any.
54    #[must_use]
55    pub fn primary_source_for_node(&self, node: NodeId) -> Option<SourceRange> {
56        self.source_map.primary_range_for_node(node)
57    }
58
59    /// Resolves the glyph cluster byte span to a SourceRange using the node's primary source.
60    #[must_use]
61    pub fn glyph_source_range(&self, node: NodeId, glyph_index: usize) -> Option<SourceRange> {
62        let node = self.node(node)?;
63        let LayoutNodeKind::GlyphRun(run) = &node.kind else {
64            return None;
65        };
66        let span = run.glyphs.get(glyph_index)?.cluster?;
67        let source = node
68            .primary_source
69            .or_else(|| self.primary_source_for_node(node.id))?
70            .source;
71
72        Some(SourceRange { source, span })
73    }
74
75    /// Resolves a glyph's source cluster to a full SourceOrigin.
76    #[must_use]
77    pub fn glyph_source_origin(
78        &self,
79        node: NodeId,
80        glyph_index: usize,
81    ) -> Option<SourceOrigin<'_>> {
82        let range = self.glyph_source_range(node, glyph_index)?;
83        let source = self.source_map.source(range.source)?;
84        Some(SourceOrigin {
85            node,
86            source,
87            span: range.span,
88            role: SourceRole::Primary,
89        })
90    }
91}
92
93/// A resolved source map relationship with source metadata.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct SourceOrigin<'a> {
96    /// The layout node this origin relates to.
97    pub node: NodeId,
98    /// The source file containing the originating text.
99    pub source: &'a SourceFile,
100    /// Byte span within that source file.
101    pub span: ByteSpan,
102    /// How this source range contributed to the node.
103    pub role: SourceRole,
104}
105
106/// Metadata describing how a fragment was produced.
107#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub struct FragmentMetadata {
109    /// Engine profile identifier, such as `tex`, `etex`, or `xetex`.
110    pub engine_profile: String,
111    /// Format image identifier, such as `plain`, `latex`, or a custom format.
112    pub format_id: String,
113    /// Whether this fragment is inline math, display math, or text.
114    pub fragment_kind: FragmentKind,
115}
116
117/// Classifies the mode in which a fragment was typeset.
118#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
119#[non_exhaustive]
120pub enum FragmentKind {
121    /// Inline math, typeset in text style.
122    #[default]
123    MathInline,
124    /// Display math, typeset centered on its own line.
125    MathDisplay,
126    /// Paragraph text mode.
127    Text,
128}
129
130/// The top level coordinate surface for a rendered fragment.
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub struct Surface {
133    /// Horizontal extent of the fragment bounding box.
134    pub width: Length,
135    /// Vertical extent of the fragment bounding box.
136    pub height: Length,
137    /// Baseline offset from the top edge.
138    pub baseline: Length,
139}
140
141/// A positioned renderable or grouping node.
142#[derive(Clone, Debug, PartialEq)]
143pub struct LayoutNode {
144    /// Stable node identifier within the fragment.
145    pub id: NodeId,
146    /// Position of the node origin in fragment coordinates.
147    pub origin: Point,
148    /// Bounding rectangle in fragment coordinates.
149    pub bounds: Rect,
150    /// Primary source range when the node has one direct origin.
151    pub primary_source: Option<SourceRange>,
152    /// Visual style applied to this node.
153    pub style: Style,
154    /// Content payload of this node.
155    pub kind: LayoutNodeKind,
156}
157
158impl LayoutNode {
159    /// Returns the baseline y coordinate; boxes use origin.y + height, glyph runs use origin.y.
160    #[must_use]
161    pub fn baseline_y(&self) -> Option<Length> {
162        match &self.kind {
163            LayoutNodeKind::Box(layout_box) => Some(Length(
164                self.origin.y.0 + layout_box.metrics.baseline_offset().0,
165            )),
166            LayoutNodeKind::GlyphRun(_) => Some(self.origin.y),
167            LayoutNodeKind::List(_)
168            | LayoutNodeKind::Group { .. }
169            | LayoutNodeKind::Rule(_)
170            | LayoutNodeKind::Glue(_)
171            | LayoutNodeKind::Kern(_)
172            | LayoutNodeKind::Drawing(_) => None,
173        }
174    }
175}
176
177/// Renderer neutral node payloads.
178#[derive(Clone, Debug, PartialEq)]
179#[non_exhaustive]
180pub enum LayoutNodeKind {
181    /// TeX style box with width, height, depth, and positioned children.
182    Box(LayoutBox),
183    /// TeX style list before or after packaging into a box.
184    List(LayoutList),
185    /// A grouping of child nodes with no layout box.
186    Group {
187        /// Child node identifiers in this group.
188        children: Vec<NodeId>,
189    },
190    /// A sequence of shaped glyphs from one font.
191    GlyphRun(GlyphRun),
192    /// A solid rule, such as a fraction bar.
193    Rule(Rule),
194    /// Flexible spacing emitted after layout resolution.
195    Glue(Glue),
196    /// Fixed horizontal or vertical spacing between adjacent items.
197    Kern(Kern),
198    /// Drawing commands for extensible delimiters or geometry beyond glyphs and rules.
199    Drawing(Drawing),
200}
201
202/// TeX style box data retained in renderer neutral layout IR.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct LayoutBox {
205    /// Whether this is a horizontal, vertical, math, or text box.
206    pub kind: BoxKind,
207    /// TeX box metrics.
208    pub metrics: BoxMetrics,
209    /// Child nodes positioned relative to this box.
210    pub children: Vec<NodeId>,
211}
212
213/// Distinguishes the axis or mode of a TeX box.
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[non_exhaustive]
216pub enum BoxKind {
217    /// Horizontal box (hbox), items laid out left to right.
218    Horizontal,
219    /// Vertical box (vbox), items stacked top to bottom.
220    Vertical,
221    /// Math mode box.
222    Math,
223    /// Box produced by text layout inside math or normal text.
224    Text,
225}
226
227/// TeX box dimensions.
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
229pub struct BoxMetrics {
230    /// Horizontal extent.
231    pub width: Length,
232    /// Height above baseline.
233    pub height: Length,
234    /// Depth below baseline.
235    pub depth: Length,
236    /// Shift applied when this box is placed in a parent list.
237    pub shift: Length,
238}
239
240impl BoxMetrics {
241    /// Baseline offset from the top edge of the box.
242    #[must_use]
243    pub const fn baseline_offset(&self) -> Length {
244        self.height
245    }
246
247    /// Height plus depth, giving the total vertical extent.
248    #[must_use]
249    pub const fn total_height(&self) -> Length {
250        Length(self.height.0 + self.depth.0)
251    }
252}
253
254/// TeX style list data retained before or after box packaging.
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct LayoutList {
257    /// Whether this is a horizontal, vertical, math, paragraph, or text list.
258    pub kind: ListKind,
259    /// Child node identifiers in list order.
260    pub children: Vec<NodeId>,
261}
262
263/// Distinguishes the axis or mode of a TeX list.
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum ListKind {
267    /// Horizontal list (hlist), items laid out left to right.
268    Horizontal,
269    /// Vertical list (vlist), items stacked top to bottom.
270    Vertical,
271    /// Math mode list.
272    Math,
273    /// Paragraph list before line breaking.
274    Paragraph,
275    /// Text list, including text inside math.
276    Text,
277}
278
279/// A shaped sequence of glyphs from one font.
280#[derive(Clone, Debug, PartialEq)]
281pub struct GlyphRun {
282    /// Font identity and size for all glyphs in this run.
283    pub font: FontRef,
284    /// Text direction for rendering.
285    pub direction: Direction,
286    /// Optional script tag, such as `Latn` or `Arab`.
287    pub script: Option<Tag>,
288    /// Optional language tag following language tag conventions.
289    pub language: Option<String>,
290    /// Positioned glyphs in visual order.
291    pub glyphs: Vec<PositionedGlyph>,
292}
293
294/// A glyph with its layout position and optional source cluster.
295#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
296pub struct PositionedGlyph {
297    /// Backend independent glyph identifier in the referenced font.
298    pub glyph_id: GlyphId,
299    /// Offset from the run origin.
300    pub offset: Point,
301    /// Advance to the next glyph.
302    pub advance: Point,
303    /// Source byte cluster for this glyph.
304    pub cluster: Option<ByteSpan>,
305}
306
307/// Font identity and size for a glyph run.
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub struct FontRef {
310    /// Stable font identifier assigned by the engine or resource layer.
311    pub id: FontId,
312    /// Human readable family or resource name.
313    pub name: String,
314    /// Rendered size in scaled points.
315    pub size: Length,
316    /// OpenType feature requests that affected shaping.
317    pub features: Vec<FontFeature>,
318}
319
320/// An OpenType feature tag and value pair.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct FontFeature {
323    /// Four byte tag in OpenType format.
324    pub tag: Tag,
325    /// Feature activation value; 1 enables, 0 disables.
326    pub value: u32,
327}
328
329/// A solid rectangle in layout coordinates.
330#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
331pub struct Rule {
332    /// Width and height of the rule.
333    pub size: Size,
334    /// Fill color of the rule.
335    pub color: Color,
336}
337
338/// Flexible spacing after TeX layout has resolved it to a concrete width.
339#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
340pub struct Glue {
341    /// Resolved glue amount.
342    pub amount: Length,
343}
344
345/// A fixed spacing adjustment between adjacent layout items.
346#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
347pub struct Kern {
348    /// The amount of space to insert.
349    pub amount: Length,
350}
351
352/// Backend neutral drawing data using path commands.
353#[derive(Clone, Debug, Default, PartialEq, Eq)]
354pub struct Drawing {
355    /// Ordered path commands composing the drawing.
356    pub commands: Vec<DrawCommand>,
357    /// Stroke style, or None if the path is not stroked.
358    pub stroke: Option<Stroke>,
359    /// Fill color, or None if the path is not filled.
360    pub fill: Option<Color>,
361}
362
363/// A path drawing command in local layout coordinates.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365#[non_exhaustive]
366pub enum DrawCommand {
367    /// Move the current point without drawing.
368    MoveTo(Point),
369    /// Draw a straight line to the given point.
370    LineTo(Point),
371    /// Draw a cubic Bezier curve to the given point.
372    CubicTo {
373        /// First control point.
374        ctrl1: Point,
375        /// Second control point.
376        ctrl2: Point,
377        /// End point of the curve.
378        to: Point,
379    },
380    /// Close the current subpath.
381    Close,
382}
383
384/// Stroke style for a drawing path.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub struct Stroke {
387    /// Stroke color.
388    pub color: Color,
389    /// Stroke width.
390    pub width: Length,
391}
392
393/// Renderer neutral style state attached to layout nodes.
394#[derive(Clone, Copy, Debug, PartialEq, Eq)]
395pub struct Style {
396    /// Foreground color for text and rules.
397    pub foreground: Color,
398    /// Background color when one is explicitly active.
399    pub background: Option<Color>,
400    /// Overall opacity applied to this node.
401    pub opacity: Alpha,
402}
403
404impl Default for Style {
405    fn default() -> Self {
406        Self {
407            foreground: Color::default(),
408            background: None,
409            opacity: Alpha::OPAQUE,
410        }
411    }
412}
413
414/// Alpha channel represented independently from color payloads.
415#[derive(Clone, Copy, Debug, PartialEq, Eq)]
416pub struct Alpha(pub u8);
417
418impl Alpha {
419    /// Fully transparent (alpha = 0).
420    pub const TRANSPARENT: Self = Self(0);
421    /// Fully opaque (alpha = 255).
422    pub const OPAQUE: Self = Self(255);
423}
424
425impl Default for Alpha {
426    fn default() -> Self {
427        Self::OPAQUE
428    }
429}
430
431/// Maps layout nodes to regions in source input files.
432#[derive(Clone, Debug, Default, PartialEq, Eq)]
433pub struct SourceMap {
434    /// Registered source files.
435    pub sources: Vec<SourceFile>,
436    /// Node to source relationships. A node may have multiple entries.
437    pub entries: Vec<SourceMapEntry>,
438}
439
440impl SourceMap {
441    /// Registers a new source file by name and returns its identifier.
442    pub fn add_source(&mut self, name: impl Into<String>) -> SourceId {
443        let id = SourceId(self.sources.len() as u32);
444        self.sources.push(SourceFile {
445            id,
446            name: name.into(),
447        });
448        id
449    }
450
451    /// Returns the existing source identifier for a name, or registers a new one.
452    pub fn intern_source(&mut self, name: impl Into<String>) -> SourceId {
453        let name = name.into();
454        if let Some(source) = self.sources.iter().find(|source| source.name == name) {
455            return source.id;
456        }
457        self.add_source(name)
458    }
459
460    /// Appends a source map entry linking a node to a source range.
461    pub fn add_entry(&mut self, node: NodeId, range: SourceRange, role: SourceRole) {
462        self.entries.push(SourceMapEntry { node, range, role });
463    }
464
465    /// Returns the source file for the given identifier.
466    #[must_use]
467    pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
468        self.sources.iter().find(|source| source.id == id)
469    }
470
471    /// Iterates all entries for the given node.
472    pub fn entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
473        self.entries.iter().filter(move |entry| entry.node == node)
474    }
475
476    /// Returns the primary source range for the given node, if present.
477    #[must_use]
478    pub fn primary_range_for_node(&self, node: NodeId) -> Option<SourceRange> {
479        self.entries_for_node(node)
480            .find(|entry| entry.role == SourceRole::Primary)
481            .map(|entry| entry.range)
482    }
483}
484
485/// A source resource, such as user input or a loaded package file.
486#[derive(Clone, Debug, PartialEq, Eq)]
487pub struct SourceFile {
488    /// Stable identifier for this source file.
489    pub id: SourceId,
490    /// File name or description of this source.
491    pub name: String,
492}
493
494/// A single mapping from a layout node to a source range.
495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub struct SourceMapEntry {
497    /// The node this entry maps.
498    pub node: NodeId,
499    /// Source range within a registered source file.
500    pub range: SourceRange,
501    /// How this source range relates to the node.
502    pub role: SourceRole,
503}
504
505/// A span within a registered source file.
506#[derive(Clone, Copy, Debug, PartialEq, Eq)]
507pub struct SourceRange {
508    /// The registered source file containing this range.
509    pub source: SourceId,
510    /// Byte span in that resource.
511    pub span: ByteSpan,
512}
513
514/// How a source range contributed to a node.
515#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516#[non_exhaustive]
517pub enum SourceRole {
518    /// The primary user visible expression source.
519    Primary,
520    /// Enclosing construct or macro invocation range; entries are emitted innermost first.
521    EnclosingConstruct,
522    /// Macro expansion whose expansion produced this node.
523    MacroExpansion,
524    /// User source declaration that requested a resource, such as `\input` or `\usepackage`.
525    ResourceRequest,
526    /// Package file that defined a macro or environment used here.
527    Package,
528    /// TeX font definition source such as `.fd`.
529    FontDefinition,
530    /// Generic resource file referenced during typesetting.
531    Resource,
532}
533
534/// A half open byte span.
535#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
536pub struct ByteSpan {
537    /// Inclusive start byte.
538    pub start: u32,
539    /// Exclusive end byte.
540    pub end: u32,
541}
542
543/// A point in scaled layout units.
544#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
545pub struct Point {
546    /// Horizontal coordinate.
547    pub x: Length,
548    /// Vertical coordinate.
549    pub y: Length,
550}
551
552/// A two dimensional size in layout units.
553#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
554pub struct Size {
555    /// Horizontal extent.
556    pub width: Length,
557    /// Vertical extent.
558    pub height: Length,
559}
560
561/// A rectangle in layout coordinates.
562#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
563pub struct Rect {
564    /// Top left origin.
565    pub origin: Point,
566    /// Width and height of the rectangle.
567    pub size: Size,
568}
569
570/// TeX style scaled point value.
571#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
572pub struct Length(pub i32);
573
574impl Length {
575    /// Zero length.
576    pub const ZERO: Self = Self(0);
577
578    /// Constructs a length from a raw scaled point value.
579    #[must_use]
580    pub const fn from_scaled_points(value: i32) -> Self {
581        Self(value)
582    }
583}
584
585/// Stable node identifier.
586#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
587pub struct NodeId(pub u32);
588
589/// Stable source identifier.
590#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
591pub struct SourceId(pub u32);
592
593/// Stable font identifier.
594#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
595pub struct FontId(pub u32);
596
597/// Backend independent glyph identifier.
598#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
599pub struct GlyphId(pub u32);
600
601/// Glyph outline command in font design units, with y increasing upward.
602#[derive(Clone, Copy, Debug, PartialEq)]
603pub enum OutlineCommand {
604    /// Move the pen to a new position.
605    MoveTo {
606        /// Horizontal position.
607        x: f32,
608        /// Vertical position.
609        y: f32,
610    },
611    /// Draw a straight line to the given position.
612    LineTo {
613        /// Horizontal position.
614        x: f32,
615        /// Vertical position.
616        y: f32,
617    },
618    /// Draw a quadratic Bezier curve.
619    QuadTo {
620        /// Control point horizontal position.
621        cx: f32,
622        /// Control point vertical position.
623        cy: f32,
624        /// End point horizontal position.
625        x: f32,
626        /// End point vertical position.
627        y: f32,
628    },
629    /// Draw a cubic Bezier curve.
630    CurveTo {
631        /// First control point horizontal position.
632        c1x: f32,
633        /// First control point vertical position.
634        c1y: f32,
635        /// Second control point horizontal position.
636        c2x: f32,
637        /// Second control point vertical position.
638        c2y: f32,
639        /// End point horizontal position.
640        x: f32,
641        /// End point vertical position.
642        y: f32,
643    },
644    /// Close the current subpath.
645    Close,
646}
647
648/// Glyph outline in font design units, with empty commands for invisible glyphs such as spaces.
649#[derive(Clone, Debug, Default, PartialEq)]
650pub struct GlyphOutline {
651    /// The font units per em; outline coordinates are in these units.
652    pub units_per_em: u16,
653    /// Ordered outline path commands for this glyph.
654    pub commands: Vec<OutlineCommand>,
655}
656
657/// Four byte tag in OpenType format.
658#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
659pub struct Tag(pub [u8; 4]);
660
661/// Text direction for glyph runs.
662#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
663#[non_exhaustive]
664pub enum Direction {
665    /// Left to right, the default for Latin scripts.
666    #[default]
667    LeftToRight,
668    /// Right to left, used for Arabic and Hebrew.
669    RightToLeft,
670    /// Top to bottom, used for some vertical layouts.
671    TopToBottom,
672}
673
674/// Color with red, green, blue, and alpha channels.
675#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub struct Color {
677    /// Red channel.
678    pub r: u8,
679    /// Green channel.
680    pub g: u8,
681    /// Blue channel.
682    pub b: u8,
683    /// Alpha channel.
684    pub a: u8,
685}
686
687impl Default for Color {
688    fn default() -> Self {
689        Self {
690            r: 0,
691            g: 0,
692            b: 0,
693            a: 255,
694        }
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    #[test]
703    fn source_map_tracks_multiple_sources_for_node() {
704        let mut source_map = SourceMap::default();
705        let input = source_map.add_source("input");
706        let package = source_map.add_source("amsmath.sty");
707        let input_again = source_map.intern_source("input");
708
709        source_map.add_entry(
710            NodeId(7),
711            SourceRange {
712                source: input,
713                span: ByteSpan { start: 1, end: 5 },
714            },
715            SourceRole::Primary,
716        );
717        source_map.add_entry(
718            NodeId(7),
719            SourceRange {
720                source: package,
721                span: ByteSpan { start: 10, end: 20 },
722            },
723            SourceRole::Package,
724        );
725        source_map.add_entry(
726            NodeId(7),
727            SourceRange {
728                source: input,
729                span: ByteSpan { start: 0, end: 16 },
730            },
731            SourceRole::ResourceRequest,
732        );
733
734        let entries = source_map.entries_for_node(NodeId(7)).count();
735
736        assert_eq!(input_again, input);
737        assert_eq!(source_map.sources.len(), 2);
738        assert_eq!(entries, 3);
739        assert_eq!(
740            source_map.source(input).expect("input source").name,
741            "input"
742        );
743        assert_eq!(
744            source_map.primary_range_for_node(NodeId(7)),
745            Some(SourceRange {
746                source: input,
747                span: ByteSpan { start: 1, end: 5 },
748            })
749        );
750
751        let fragment = Fragment {
752            source_map,
753            ..Fragment::default()
754        };
755        let origins = fragment
756            .source_origins_for_node(NodeId(7))
757            .collect::<Vec<_>>();
758        assert_eq!(origins.len(), 3);
759        assert_eq!(origins[0].source.name, "input");
760        assert_eq!(origins[0].span, ByteSpan { start: 1, end: 5 });
761        assert_eq!(origins[1].source.name, "amsmath.sty");
762        assert_eq!(origins[1].role, SourceRole::Package);
763        assert_eq!(origins[2].role, SourceRole::ResourceRequest);
764    }
765
766    #[test]
767    fn fragment_resolves_glyph_clusters_to_original_source_ranges() {
768        let mut source_map = SourceMap::default();
769        let input = source_map.add_source("input");
770        let node = NodeId(4);
771        source_map.add_entry(
772            node,
773            SourceRange {
774                source: input,
775                span: ByteSpan { start: 0, end: 6 },
776            },
777            SourceRole::Primary,
778        );
779        let fragment = Fragment {
780            source_map,
781            nodes: alloc::vec![LayoutNode {
782                id: node,
783                origin: Point::default(),
784                bounds: Rect::default(),
785                primary_source: None,
786                style: Style::default(),
787                kind: LayoutNodeKind::GlyphRun(GlyphRun {
788                    font: FontRef {
789                        id: FontId(1),
790                        name: "test".into(),
791                        size: Length::ZERO,
792                        features: alloc::vec![],
793                    },
794                    direction: Direction::LeftToRight,
795                    script: None,
796                    language: None,
797                    glyphs: alloc::vec![PositionedGlyph {
798                        glyph_id: GlyphId(9),
799                        offset: Point::default(),
800                        advance: Point::default(),
801                        cluster: Some(ByteSpan { start: 2, end: 4 }),
802                    }],
803                }),
804            }],
805            ..Fragment::default()
806        };
807
808        assert_eq!(fragment.node(node).expect("node").id, node);
809        assert_eq!(
810            fragment.glyph_source_range(node, 0),
811            Some(SourceRange {
812                source: input,
813                span: ByteSpan { start: 2, end: 4 },
814            })
815        );
816        let origin = fragment
817            .glyph_source_origin(node, 0)
818            .expect("glyph source origin");
819        assert_eq!(origin.source.name, "input");
820        assert_eq!(origin.span, ByteSpan { start: 2, end: 4 });
821        assert_eq!(origin.role, SourceRole::Primary);
822        assert_eq!(fragment.glyph_source_range(node, 1), None);
823    }
824
825    #[test]
826    fn ir_represents_tex_boxes_and_lists_without_renderer_terms() {
827        let list = LayoutNode {
828            id: NodeId(1),
829            origin: Point::default(),
830            bounds: Rect::default(),
831            primary_source: None,
832            style: Style::default(),
833            kind: LayoutNodeKind::List(LayoutList {
834                kind: ListKind::Math,
835                children: alloc::vec![NodeId(2)],
836            }),
837        };
838        let boxed = LayoutNode {
839            id: NodeId(2),
840            origin: Point {
841                x: Length::ZERO,
842                y: Length::from_scaled_points(10_000),
843            },
844            bounds: Rect::default(),
845            primary_source: None,
846            style: Style::default(),
847            kind: LayoutNodeKind::Box(LayoutBox {
848                kind: BoxKind::Horizontal,
849                metrics: BoxMetrics {
850                    width: Length::from_scaled_points(65_536),
851                    height: Length::from_scaled_points(32_768),
852                    depth: Length::from_scaled_points(16_384),
853                    shift: Length::ZERO,
854                },
855                children: alloc::vec![],
856            }),
857        };
858        let glyphs = LayoutNode {
859            id: NodeId(3),
860            origin: Point {
861                x: Length::ZERO,
862                y: Length::from_scaled_points(50_000),
863            },
864            bounds: Rect::default(),
865            primary_source: None,
866            style: Style::default(),
867            kind: LayoutNodeKind::GlyphRun(GlyphRun {
868                font: FontRef {
869                    id: FontId(1),
870                    name: "test".into(),
871                    size: Length::ZERO,
872                    features: alloc::vec![],
873                },
874                direction: Direction::LeftToRight,
875                script: None,
876                language: None,
877                glyphs: alloc::vec![],
878            }),
879        };
880
881        let fragment = Fragment {
882            nodes: alloc::vec![list, boxed, glyphs],
883            ..Fragment::default()
884        };
885
886        assert_eq!(fragment.nodes[0].baseline_y(), None);
887        assert_eq!(
888            fragment.nodes[1].baseline_y(),
889            Some(Length::from_scaled_points(42_768))
890        );
891        assert_eq!(
892            fragment.nodes[2].baseline_y(),
893            Some(Length::from_scaled_points(50_000))
894        );
895
896        match &fragment.nodes[0].kind {
897            LayoutNodeKind::List(layout_list) => {
898                assert_eq!(layout_list.kind, ListKind::Math);
899                assert_eq!(layout_list.children, alloc::vec![NodeId(2)]);
900            }
901            other => panic!("unexpected node: {other:?}"),
902        }
903
904        match &fragment.nodes[1].kind {
905            LayoutNodeKind::Box(layout_box) => {
906                assert_eq!(layout_box.kind, BoxKind::Horizontal);
907                assert_eq!(layout_box.metrics.depth, Length::from_scaled_points(16_384));
908                assert_eq!(
909                    layout_box.metrics.baseline_offset(),
910                    Length::from_scaled_points(32_768)
911                );
912                assert_eq!(
913                    layout_box.metrics.total_height(),
914                    Length::from_scaled_points(49_152)
915                );
916            }
917            other => panic!("unexpected node: {other:?}"),
918        }
919    }
920}