#![forbid(unsafe_code)]
use std::fmt;
use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Fragment {
pub root: NodeId,
pub nodes: Vec<LayoutNode>,
pub source_map: SourceMap,
pub metadata: FragmentMetadata,
pub surface: Surface,
}
impl Fragment {
pub fn new(
root: NodeId,
nodes: Vec<LayoutNode>,
source_map: SourceMap,
metadata: FragmentMetadata,
) -> Result<Self, FragmentError> {
let surface = nodes
.get(root.index())
.map_or_else(Surface::default, Surface::of_root);
let fragment = Self {
root,
nodes,
source_map,
metadata,
surface,
};
fragment.validate()?;
Ok(fragment)
}
pub fn validate(&self) -> Result<(), FragmentError> {
for (index, node) in self.nodes.iter().enumerate() {
if node.id.index() != index {
return Err(FragmentError::IdMismatch { index, id: node.id });
}
}
if self.nodes.is_empty() {
return Ok(());
}
let root = self
.nodes
.get(self.root.index())
.ok_or(FragmentError::RootOutOfRange(self.root))?;
if root.origin != Point::ORIGIN {
return Err(FragmentError::RootNotAtOrigin);
}
if self.surface != Surface::of_root(root) {
return Err(FragmentError::SurfaceMismatch);
}
let mut has_parent = vec![false; self.nodes.len()];
for node in &self.nodes {
for &child in node.children() {
let slot =
has_parent
.get_mut(child.index())
.ok_or(FragmentError::ChildOutOfRange {
parent: node.id,
child,
})?;
if *slot || child == self.root {
return Err(FragmentError::MultipleParents(child));
}
*slot = true;
}
}
let mut reached = vec![false; self.nodes.len()];
let mut stack = vec![self.root];
while let Some(id) = stack.pop() {
if std::mem::replace(&mut reached[id.index()], true) {
return Err(FragmentError::MultipleParents(id));
}
stack.extend_from_slice(self.nodes[id.index()].children());
}
if let Some(index) = reached.iter().position(|reached| !reached) {
return Err(FragmentError::Unreachable(self.nodes[index].id));
}
for (index, file) in self.source_map.sources.iter().enumerate() {
if file.id.index() != index {
return Err(FragmentError::SourceIdMismatch { index, id: file.id });
}
}
let known = |range: &SourceRange| self.source_map.source(range.source).is_some();
for node in &self.nodes {
if let Some(range) = &node.primary_source {
if !known(range) {
return Err(FragmentError::UnknownSource {
node: node.id,
source: range.source,
});
}
}
}
for entry in &self.source_map.entries {
if entry.node.index() >= self.nodes.len() {
return Err(FragmentError::Unreachable(entry.node));
}
if !known(&entry.range) {
return Err(FragmentError::UnknownSource {
node: entry.node,
source: entry.range.source,
});
}
}
Ok(())
}
#[must_use]
pub fn node(&self, id: NodeId) -> Option<&LayoutNode> {
self.nodes.get(id.index()).filter(|node| node.id == id)
}
#[must_use]
pub fn root_node(&self) -> Option<&LayoutNode> {
self.node(self.root)
}
#[must_use]
pub fn children(&self, id: NodeId) -> &[NodeId] {
self.node(id).map_or(&[], LayoutNode::children)
}
pub fn source_entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
self.source_map.entries_for_node(node)
}
pub fn source_origins_for_node(&self, node: NodeId) -> impl Iterator<Item = SourceOrigin<'_>> {
let primary = self
.primary_source_for_node(node)
.and_then(|range| self.origin(node, range, SourceRole::Primary));
let enclosing = self.source_entries_for_node(node).filter_map(move |entry| {
self.origin(node, entry.range, SourceRole::EnclosingConstruct)
});
primary.into_iter().chain(enclosing)
}
#[must_use]
pub fn primary_source_for_node(&self, node: NodeId) -> Option<SourceRange> {
self.node(node)?.primary_source
}
#[must_use]
pub fn glyph_source_range(&self, node: NodeId, glyph_index: usize) -> Option<SourceRange> {
let node = self.node(node)?;
let LayoutNodeKind::GlyphRun(run) = &node.kind else {
return None;
};
let span = run.glyphs.get(glyph_index)?.cluster?;
Some(SourceRange {
source: node.primary_source?.source,
span,
})
}
#[must_use]
pub fn glyph_source_origin(
&self,
node: NodeId,
glyph_index: usize,
) -> Option<SourceOrigin<'_>> {
let range = self.glyph_source_range(node, glyph_index)?;
self.origin(node, range, SourceRole::Primary)
}
#[must_use]
pub fn flatten(&self) -> Vec<Placed<'_>> {
let mut out = Vec::new();
if self.root_node().is_none() {
return out;
}
let mut visited = vec![false; self.nodes.len()];
let mut stack = vec![(self.root, Point::new(Length::ZERO, self.surface.baseline))];
while let Some((id, parent)) = stack.pop() {
let Some(node) = self.node(id) else {
continue;
};
if std::mem::replace(&mut visited[id.index()], true) {
continue;
}
let at = parent + node.origin;
match &node.kind {
LayoutNodeKind::Box(layout_box) => {
stack.extend(layout_box.children.iter().rev().map(|&child| (child, at)));
}
LayoutNodeKind::GlyphRun(run) => {
for (index, glyph) in run.glyphs.iter().enumerate() {
let source = self.glyph_source_range(id, index).or(node.primary_source);
out.push(Placed::Glyph {
node: id,
font: &run.font,
glyph_id: glyph.glyph_id,
x: at.x + glyph.offset.x,
y: at.y + glyph.offset.y,
source,
});
}
}
LayoutNodeKind::Rule => {
let height = node.height + node.depth;
if node.width > Length::ZERO && height > Length::ZERO {
out.push(Placed::Rule {
node: id,
x: at.x,
y: at.y - node.height,
width: node.width,
height,
source: node.primary_source,
});
}
}
LayoutNodeKind::Glue(_) | LayoutNodeKind::Kern(_) => {}
}
}
out
}
fn origin(
&self,
node: NodeId,
range: SourceRange,
role: SourceRole,
) -> Option<SourceOrigin<'_>> {
Some(SourceOrigin {
node,
source: self.source_map.source(range.source)?,
span: range.span,
role,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Placed<'a> {
Glyph {
node: NodeId,
font: &'a FontRef,
glyph_id: GlyphId,
x: Length,
y: Length,
source: Option<SourceRange>,
},
Rule {
node: NodeId,
x: Length,
y: Length,
width: Length,
height: Length,
source: Option<SourceRange>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FragmentError {
IdMismatch {
index: usize,
id: NodeId,
},
RootOutOfRange(NodeId),
RootNotAtOrigin,
SurfaceMismatch,
ChildOutOfRange {
parent: NodeId,
child: NodeId,
},
MultipleParents(NodeId),
Unreachable(NodeId),
SourceIdMismatch {
index: usize,
id: SourceId,
},
UnknownSource {
node: NodeId,
source: SourceId,
},
}
impl fmt::Display for FragmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IdMismatch { index, id } => {
write!(f, "node at index {index} has id {}", id.0)
}
Self::RootOutOfRange(id) => write!(f, "root id {} indexes no node", id.0),
Self::RootNotAtOrigin => f.write_str("root origin is not (0, 0)"),
Self::SurfaceMismatch => f.write_str("surface does not match the root box"),
Self::ChildOutOfRange { parent, child } => {
write!(f, "node {} lists missing child {}", parent.0, child.0)
}
Self::MultipleParents(id) => write!(f, "node {} has more than one parent", id.0),
Self::Unreachable(id) => write!(f, "node {} is not reachable from the root", id.0),
Self::SourceIdMismatch { index, id } => {
write!(f, "source at index {index} has id {}", id.0)
}
Self::UnknownSource { node, source } => {
write!(f, "node {} refers to unknown source {}", node.0, source.0)
}
}
}
}
impl std::error::Error for FragmentError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceOrigin<'a> {
pub node: NodeId,
pub source: &'a SourceFile,
pub span: ByteSpan,
pub role: SourceRole,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FragmentMetadata {
pub format_id: String,
pub fragment_kind: FragmentKind,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum FragmentKind {
#[default]
MathInline,
MathDisplay,
Text,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Surface {
pub width: Length,
pub height: Length,
pub baseline: Length,
}
impl Surface {
#[must_use]
pub fn of_root(root: &LayoutNode) -> Self {
Self {
width: root.width,
height: root.height + root.depth,
baseline: root.height,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutNode {
pub id: NodeId,
pub origin: Point,
pub width: Length,
pub height: Length,
pub depth: Length,
pub primary_source: Option<SourceRange>,
pub kind: LayoutNodeKind,
}
impl LayoutNode {
#[must_use]
pub fn children(&self) -> &[NodeId] {
match &self.kind {
LayoutNodeKind::Box(layout_box) => &layout_box.children,
_ => &[],
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum LayoutNodeKind {
Box(LayoutBox),
GlyphRun(GlyphRun),
Rule,
Glue(Glue),
Kern(Kern),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LayoutBox {
pub kind: BoxKind,
pub children: Vec<NodeId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BoxKind {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Axis {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GlyphRun {
pub font: FontRef,
pub glyphs: Vec<PositionedGlyph>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PositionedGlyph {
pub glyph_id: GlyphId,
pub offset: Point,
pub cluster: Option<ByteSpan>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FontRef {
pub key: Option<FontKey>,
pub spec: String,
pub size: Length,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontKey(pub u64);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Glue {
pub amount: Length,
pub axis: Axis,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Kern {
pub amount: Length,
pub axis: Axis,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SourceMap {
pub sources: Vec<SourceFile>,
pub entries: Vec<SourceMapEntry>,
}
impl SourceMap {
pub fn add_source(&mut self, name: impl Into<String>) -> SourceId {
let id = SourceId(u32::try_from(self.sources.len()).unwrap_or(u32::MAX));
self.sources.push(SourceFile {
id,
name: name.into(),
});
id
}
pub fn intern_source(&mut self, name: impl Into<String>) -> SourceId {
let name = name.into();
if let Some(source) = self.sources.iter().find(|source| source.name == name) {
return source.id;
}
self.add_source(name)
}
pub fn add_entry(&mut self, node: NodeId, range: SourceRange) {
self.entries.push(SourceMapEntry { node, range });
}
#[must_use]
pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
self.sources
.get(id.index())
.filter(|source| source.id == id)
}
pub fn entries_for_node(&self, node: NodeId) -> impl Iterator<Item = &SourceMapEntry> {
self.entries.iter().filter(move |entry| entry.node == node)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceFile {
pub id: SourceId,
pub name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceMapEntry {
pub node: NodeId,
pub range: SourceRange,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceRange {
pub source: SourceId,
pub span: ByteSpan,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SourceRole {
Primary,
EnclosingConstruct,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ByteSpan {
pub start: u32,
pub end: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Point {
pub x: Length,
pub y: Length,
}
impl Point {
pub const ORIGIN: Self = Self {
x: Length::ZERO,
y: Length::ZERO,
};
#[must_use]
pub const fn new(x: Length, y: Length) -> Self {
Self { x, y }
}
}
impl Add for Point {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.x + rhs.x, self.y + rhs.y)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Length(pub i32);
impl Length {
pub const ZERO: Self = Self(0);
pub const SP_PER_PT: i32 = 65_536;
#[must_use]
pub const fn from_scaled_points(value: i32) -> Self {
Self(value)
}
#[must_use]
pub fn from_pt(points: f64) -> Self {
Self((points * f64::from(Self::SP_PER_PT)).round() as i32)
}
#[must_use]
pub fn to_pt(self) -> f64 {
f64::from(self.0) / f64::from(Self::SP_PER_PT)
}
}
impl Add for Length {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0))
}
}
impl AddAssign for Length {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl Sub for Length {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl SubAssign for Length {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
impl Neg for Length {
type Output = Self;
fn neg(self) -> Self {
Self(self.0.saturating_neg())
}
}
impl Mul<i32> for Length {
type Output = Self;
fn mul(self, rhs: i32) -> Self {
Self(self.0.saturating_mul(rhs))
}
}
impl Mul<f64> for Length {
type Output = Self;
fn mul(self, rhs: f64) -> Self {
Self((f64::from(self.0) * rhs).round() as i32)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeId(pub u32);
impl NodeId {
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceId(pub u32);
impl SourceId {
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlyphId(pub u32);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OutlineCommand {
MoveTo {
x: f32,
y: f32,
},
LineTo {
x: f32,
y: f32,
},
QuadTo {
cx: f32,
cy: f32,
x: f32,
y: f32,
},
CurveTo {
c1x: f32,
c1y: f32,
c2x: f32,
c2y: f32,
x: f32,
y: f32,
},
Close,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GlyphOutline {
pub units_per_em: u16,
pub commands: Vec<OutlineCommand>,
}
#[cfg(test)]
mod tests {
use super::*;
const PT: i32 = Length::SP_PER_PT;
fn node(
id: u32,
origin: (i32, i32),
size: (i32, i32, i32),
kind: LayoutNodeKind,
) -> LayoutNode {
LayoutNode {
id: NodeId(id),
origin: Point::new(Length(origin.0 * PT), Length(origin.1 * PT)),
width: Length(size.0 * PT),
height: Length(size.1 * PT),
depth: Length(size.2 * PT),
primary_source: None,
kind,
}
}
fn hbox(children: &[u32]) -> LayoutNodeKind {
LayoutNodeKind::Box(LayoutBox {
kind: BoxKind::Horizontal,
children: children.iter().copied().map(NodeId).collect(),
})
}
fn run(glyphs: &[(u32, i32, i32)]) -> LayoutNodeKind {
LayoutNodeKind::GlyphRun(GlyphRun {
font: FontRef {
key: Some(FontKey(3)),
spec: "[test.otf]".into(),
size: Length(10 * PT),
},
glyphs: glyphs
.iter()
.map(|&(glyph, x, y)| PositionedGlyph {
glyph_id: GlyphId(glyph),
offset: Point::new(Length(x * PT), Length(y * PT)),
cluster: None,
})
.collect(),
})
}
fn sample() -> Fragment {
let nodes = vec![
node(0, (0, 0), (20, 8, 2), hbox(&[1, 3])),
node(1, (3, -2), (5, 4, 0), hbox(&[2])),
node(2, (1, 0), (4, 4, 0), run(&[(7, 0, 0), (8, 2, 1)])),
node(3, (10, 0), (6, 3, 1), LayoutNodeKind::Rule),
];
Fragment::new(
NodeId(0),
nodes,
SourceMap::default(),
FragmentMetadata::default(),
)
.expect("valid fragment")
}
#[test]
fn surface_derives_from_the_root() {
let fragment = sample();
assert_eq!(fragment.surface.width, Length(20 * PT));
assert_eq!(fragment.surface.height, Length(10 * PT));
assert_eq!(fragment.surface.baseline, Length(8 * PT));
}
#[test]
fn flatten_accumulates_parent_relative_origins_from_the_baseline() {
let fragment = sample();
let placed = fragment.flatten();
let pt = |value: i32| Length(value * PT);
assert_eq!(placed.len(), 3);
assert!(
matches!(placed[0], Placed::Glyph { glyph_id: GlyphId(7), x, y, .. } if x == pt(4) && y == pt(6))
);
assert!(
matches!(placed[1], Placed::Glyph { glyph_id: GlyphId(8), x, y, .. } if x == pt(6) && y == pt(7))
);
assert_eq!(
placed[2],
Placed::Rule {
node: NodeId(3),
x: pt(10),
y: pt(5),
width: pt(6),
height: pt(4),
source: None,
}
);
}
#[test]
fn validate_rejects_broken_trees() {
let mut fragment = sample();
fragment.nodes.swap(1, 2);
assert!(matches!(
fragment.validate(),
Err(FragmentError::IdMismatch { index: 1, .. })
));
let mut fragment = sample();
if let LayoutNodeKind::Box(root) = &mut fragment.nodes[0].kind {
root.children.push(NodeId(2));
}
assert_eq!(
fragment.validate(),
Err(FragmentError::MultipleParents(NodeId(2)))
);
let mut fragment = sample();
if let LayoutNodeKind::Box(root) = &mut fragment.nodes[0].kind {
root.children.pop();
}
assert_eq!(
fragment.validate(),
Err(FragmentError::Unreachable(NodeId(3)))
);
let mut fragment = sample();
fragment.nodes[0].origin.x = Length(1);
assert_eq!(fragment.validate(), Err(FragmentError::RootNotAtOrigin));
let mut fragment = sample();
fragment.surface.baseline = Length::ZERO;
assert_eq!(fragment.validate(), Err(FragmentError::SurfaceMismatch));
}
#[test]
fn source_origins_list_the_primary_span_then_enclosing_spans() {
let mut fragment = sample();
let input = fragment.source_map.add_source("input");
let package = fragment.source_map.add_source("amsmath.sty");
assert_eq!(fragment.source_map.intern_source("input"), input);
fragment.nodes[2].primary_source = Some(SourceRange {
source: input,
span: ByteSpan { start: 1, end: 5 },
});
fragment.source_map.add_entry(
NodeId(2),
SourceRange {
source: package,
span: ByteSpan { start: 10, end: 20 },
},
);
if let LayoutNodeKind::GlyphRun(run) = &mut fragment.nodes[2].kind {
run.glyphs[1].cluster = Some(ByteSpan { start: 2, end: 4 });
}
fragment.validate().expect("valid");
let origins = fragment
.source_origins_for_node(NodeId(2))
.collect::<Vec<_>>();
assert_eq!(origins.len(), 2);
assert_eq!(origins[0].role, SourceRole::Primary);
assert_eq!(origins[0].span, ByteSpan { start: 1, end: 5 });
assert_eq!(origins[1].source.name, "amsmath.sty");
assert_eq!(origins[1].role, SourceRole::EnclosingConstruct);
assert_eq!(fragment.glyph_source_range(NodeId(2), 0), None);
let cluster = SourceRange {
source: input,
span: ByteSpan { start: 2, end: 4 },
};
assert_eq!(fragment.glyph_source_range(NodeId(2), 1), Some(cluster));
let placed = fragment.flatten();
assert!(
matches!(placed[0], Placed::Glyph { source: Some(range), .. } if range.span == ByteSpan { start: 1, end: 5 })
);
assert!(matches!(placed[1], Placed::Glyph { source: Some(range), .. } if range == cluster));
fragment.nodes[3].primary_source = Some(SourceRange {
source: SourceId(9),
span: ByteSpan::default(),
});
assert!(matches!(
fragment.validate(),
Err(FragmentError::UnknownSource { .. })
));
}
#[test]
fn length_arithmetic_saturates_and_converts_points() {
assert_eq!(Length::from_pt(1.5), Length(98_304));
assert_eq!(Length(98_304).to_pt(), 1.5);
assert_eq!(Length(3) + Length(4) - Length(10), Length(-3));
assert_eq!(-Length(5), Length(-5));
assert_eq!(Length(5) * 3, Length(15));
assert_eq!(Length(10) * 0.25, Length(3));
assert_eq!(Length(i32::MAX) + Length(1), Length(i32::MAX));
assert_eq!(-Length(i32::MIN), Length(i32::MAX));
}
}