1#![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#[derive(Clone, Debug, Default, PartialEq)]
12pub struct Fragment {
13 pub surface: Surface,
15 pub nodes: Vec<LayoutNode>,
17 pub source_map: SourceMap,
19 pub metadata: FragmentMetadata,
21}
22
23impl Fragment {
24 #[must_use]
26 pub fn node(&self, node: NodeId) -> Option<&LayoutNode> {
27 self.nodes.iter().find(|candidate| candidate.id == node)
28 }
29
30 pub fn source_entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
32 self.source_map.entries_for_node(node)
33 }
34
35 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 #[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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct SourceOrigin<'a> {
96 pub node: NodeId,
98 pub source: &'a SourceFile,
100 pub span: ByteSpan,
102 pub role: SourceRole,
104}
105
106#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub struct FragmentMetadata {
109 pub engine_profile: String,
111 pub format_id: String,
113 pub fragment_kind: FragmentKind,
115}
116
117#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
119#[non_exhaustive]
120pub enum FragmentKind {
121 #[default]
123 MathInline,
124 MathDisplay,
126 Text,
128}
129
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub struct Surface {
133 pub width: Length,
135 pub height: Length,
137 pub baseline: Length,
139}
140
141#[derive(Clone, Debug, PartialEq)]
143pub struct LayoutNode {
144 pub id: NodeId,
146 pub origin: Point,
148 pub bounds: Rect,
150 pub primary_source: Option<SourceRange>,
152 pub style: Style,
154 pub kind: LayoutNodeKind,
156}
157
158impl LayoutNode {
159 #[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#[derive(Clone, Debug, PartialEq)]
179#[non_exhaustive]
180pub enum LayoutNodeKind {
181 Box(LayoutBox),
183 List(LayoutList),
185 Group {
187 children: Vec<NodeId>,
189 },
190 GlyphRun(GlyphRun),
192 Rule(Rule),
194 Glue(Glue),
196 Kern(Kern),
198 Drawing(Drawing),
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct LayoutBox {
205 pub kind: BoxKind,
207 pub metrics: BoxMetrics,
209 pub children: Vec<NodeId>,
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[non_exhaustive]
216pub enum BoxKind {
217 Horizontal,
219 Vertical,
221 Math,
223 Text,
225}
226
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
229pub struct BoxMetrics {
230 pub width: Length,
232 pub height: Length,
234 pub depth: Length,
236 pub shift: Length,
238}
239
240impl BoxMetrics {
241 #[must_use]
243 pub const fn baseline_offset(&self) -> Length {
244 self.height
245 }
246
247 #[must_use]
249 pub const fn total_height(&self) -> Length {
250 Length(self.height.0 + self.depth.0)
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct LayoutList {
257 pub kind: ListKind,
259 pub children: Vec<NodeId>,
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum ListKind {
267 Horizontal,
269 Vertical,
271 Math,
273 Paragraph,
275 Text,
277}
278
279#[derive(Clone, Debug, PartialEq)]
281pub struct GlyphRun {
282 pub font: FontRef,
284 pub direction: Direction,
286 pub script: Option<Tag>,
288 pub language: Option<String>,
290 pub glyphs: Vec<PositionedGlyph>,
292}
293
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
296pub struct PositionedGlyph {
297 pub glyph_id: GlyphId,
299 pub offset: Point,
301 pub advance: Point,
303 pub cluster: Option<ByteSpan>,
305}
306
307#[derive(Clone, Debug, PartialEq, Eq)]
309pub struct FontRef {
310 pub id: FontId,
312 pub name: String,
314 pub size: Length,
316 pub features: Vec<FontFeature>,
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct FontFeature {
323 pub tag: Tag,
325 pub value: u32,
327}
328
329#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
331pub struct Rule {
332 pub size: Size,
334 pub color: Color,
336}
337
338#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
340pub struct Glue {
341 pub amount: Length,
343}
344
345#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
347pub struct Kern {
348 pub amount: Length,
350}
351
352#[derive(Clone, Debug, Default, PartialEq, Eq)]
354pub struct Drawing {
355 pub commands: Vec<DrawCommand>,
357 pub stroke: Option<Stroke>,
359 pub fill: Option<Color>,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365#[non_exhaustive]
366pub enum DrawCommand {
367 MoveTo(Point),
369 LineTo(Point),
371 CubicTo {
373 ctrl1: Point,
375 ctrl2: Point,
377 to: Point,
379 },
380 Close,
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub struct Stroke {
387 pub color: Color,
389 pub width: Length,
391}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq)]
395pub struct Style {
396 pub foreground: Color,
398 pub background: Option<Color>,
400 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
416pub struct Alpha(pub u8);
417
418impl Alpha {
419 pub const TRANSPARENT: Self = Self(0);
421 pub const OPAQUE: Self = Self(255);
423}
424
425impl Default for Alpha {
426 fn default() -> Self {
427 Self::OPAQUE
428 }
429}
430
431#[derive(Clone, Debug, Default, PartialEq, Eq)]
433pub struct SourceMap {
434 pub sources: Vec<SourceFile>,
436 pub entries: Vec<SourceMapEntry>,
438}
439
440impl SourceMap {
441 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 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 pub fn add_entry(&mut self, node: NodeId, range: SourceRange, role: SourceRole) {
462 self.entries.push(SourceMapEntry { node, range, role });
463 }
464
465 #[must_use]
467 pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
468 self.sources.iter().find(|source| source.id == id)
469 }
470
471 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 #[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#[derive(Clone, Debug, PartialEq, Eq)]
487pub struct SourceFile {
488 pub id: SourceId,
490 pub name: String,
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub struct SourceMapEntry {
497 pub node: NodeId,
499 pub range: SourceRange,
501 pub role: SourceRole,
503}
504
505#[derive(Clone, Copy, Debug, PartialEq, Eq)]
507pub struct SourceRange {
508 pub source: SourceId,
510 pub span: ByteSpan,
512}
513
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516#[non_exhaustive]
517pub enum SourceRole {
518 Primary,
520 EnclosingConstruct,
522 MacroExpansion,
524 ResourceRequest,
526 Package,
528 FontDefinition,
530 Resource,
532}
533
534#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
536pub struct ByteSpan {
537 pub start: u32,
539 pub end: u32,
541}
542
543#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
545pub struct Point {
546 pub x: Length,
548 pub y: Length,
550}
551
552#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
554pub struct Size {
555 pub width: Length,
557 pub height: Length,
559}
560
561#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
563pub struct Rect {
564 pub origin: Point,
566 pub size: Size,
568}
569
570#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
572pub struct Length(pub i32);
573
574impl Length {
575 pub const ZERO: Self = Self(0);
577
578 #[must_use]
580 pub const fn from_scaled_points(value: i32) -> Self {
581 Self(value)
582 }
583}
584
585#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
587pub struct NodeId(pub u32);
588
589#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
591pub struct SourceId(pub u32);
592
593#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
595pub struct FontId(pub u32);
596
597#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
599pub struct GlyphId(pub u32);
600
601#[derive(Clone, Copy, Debug, PartialEq)]
603pub enum OutlineCommand {
604 MoveTo {
606 x: f32,
608 y: f32,
610 },
611 LineTo {
613 x: f32,
615 y: f32,
617 },
618 QuadTo {
620 cx: f32,
622 cy: f32,
624 x: f32,
626 y: f32,
628 },
629 CurveTo {
631 c1x: f32,
633 c1y: f32,
635 c2x: f32,
637 c2y: f32,
639 x: f32,
641 y: f32,
643 },
644 Close,
646}
647
648#[derive(Clone, Debug, Default, PartialEq)]
650pub struct GlyphOutline {
651 pub units_per_em: u16,
653 pub commands: Vec<OutlineCommand>,
655}
656
657#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
659pub struct Tag(pub [u8; 4]);
660
661#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
663#[non_exhaustive]
664pub enum Direction {
665 #[default]
667 LeftToRight,
668 RightToLeft,
670 TopToBottom,
672}
673
674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub struct Color {
677 pub r: u8,
679 pub g: u8,
681 pub b: u8,
683 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}