mod anchor;
mod chain;
mod chart;
mod container;
mod crop;
mod ctx;
mod data_resolve;
mod field;
mod footnote;
mod image;
mod leaf;
mod line_jumps;
mod markdown_resolve;
mod paint;
mod pattern;
mod table;
mod table_flow;
mod text;
mod toc;
mod util;
use std::collections::BTreeMap;
use zenith_core::{
ComponentDef, DataContext, Diagnostic, Document, FontProvider, MasterDef, Node, PropertyValue,
Style, dim_to_px, resolve_tokens,
};
use zenith_layout::RustybuzzEngine;
use crate::ir::{Paint, Rect, Scene, SceneCommand};
use anchor::build_anchor_map;
use chain::resolve_chains_document;
use chart::compile_chart;
use container::{compile_frame, compile_group, compile_instance};
pub(in crate::compile) use ctx::NodeCtx;
use data_resolve::{scan_for_data_refs, substitute_data_refs};
use field::{
FieldCtx, build_node_boxes, build_page_index_map, build_section_assignments, compute_live_area,
resolve_field_to_text,
};
use image::compile_image;
use leaf::{
ConnectorEnv, RectEllipseEnv, ShapeCompileEnv, compile_connector, compile_ellipse,
compile_line, compile_polygon, compile_polyline, compile_rect, compile_shape,
};
use markdown_resolve::{resolve_markdown, scan_for_markdown_text};
use paint::{resolve_property_color, resolve_property_gradient};
use pattern::compile_pattern;
use table::{TableEmitCtx, compile_table};
use table_flow::resolve_table_flows;
use text::{TextCompileEnv, compile_code, compile_text, empty_md_blocks};
use toc::resolve_toc_to_text;
pub(super) type ComponentMap<'a> = BTreeMap<&'a str, &'a ComponentDef>;
pub(super) type MasterMap<'a> = BTreeMap<&'a str, &'a MasterDef>;
#[derive(Clone, Copy)]
pub(super) struct RenderCtx {
pub(super) opacity: f64,
pub(super) dx: f64,
pub(super) dy: f64,
pub(super) baseline_grid: Option<f64>,
}
impl RenderCtx {
fn root() -> Self {
RenderCtx {
opacity: 1.0,
dx: 0.0,
dy: 0.0,
baseline_grid: None,
}
}
pub(super) fn measure() -> Self {
RenderCtx {
opacity: 1.0,
dx: 0.0,
dy: 0.0,
baseline_grid: None,
}
}
fn root_offset(dx: f64, dy: f64) -> Self {
RenderCtx {
opacity: 1.0,
dx,
dy,
baseline_grid: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CompileResult {
pub scene: Scene,
pub diagnostics: Vec<Diagnostic>,
}
pub(super) fn style_prop<'a>(
style_ref: &Option<String>,
style_map: &'a BTreeMap<&str, &Style>,
key: &str,
) -> Option<&'a PropertyValue> {
let sid = style_ref.as_deref()?;
style_map.get(sid)?.properties.get(key)
}
pub fn compile(doc: &Document, fonts: &dyn FontProvider) -> CompileResult {
compile_page(doc, fonts, 0, None)
}
pub fn compile_page(
doc: &Document,
fonts: &dyn FontProvider,
page_index: usize,
data: Option<&DataContext>,
) -> CompileResult {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut md_blocks: markdown_resolve::MdBlockMap = markdown_resolve::MdBlockMap::new();
let owned_doc: Option<Document> = match data {
Some(ctx) => {
let mut cloned = doc.clone();
substitute_data_refs(&mut cloned, ctx, &mut diagnostics);
md_blocks = resolve_markdown(&mut cloned);
Some(cloned)
}
None => {
if scan_for_data_refs(doc) {
diagnostics.push(Diagnostic::advisory(
"data.no_context",
"document contains `(data)` references but no data context was \
provided at compile time; the references are left unresolved",
None,
None,
));
}
if scan_for_markdown_text(doc) {
let mut cloned = doc.clone();
md_blocks = resolve_markdown(&mut cloned);
Some(cloned)
} else {
None
}
}
};
let doc: &Document = owned_doc.as_ref().unwrap_or(doc);
let token_resolution = resolve_tokens(&doc.tokens);
diagnostics.extend(token_resolution.diagnostics);
let resolved = &token_resolution.resolved;
let style_map: BTreeMap<&str, &Style> = doc
.styles
.styles
.iter()
.map(|s| (s.id.as_str(), s))
.collect();
let mut component_map: ComponentMap = BTreeMap::new();
for comp in &doc.components {
component_map.entry(comp.id.as_str()).or_insert(comp);
}
let mut master_map: MasterMap = BTreeMap::new();
for master in &doc.masters {
master_map.entry(master.id.as_str()).or_insert(master);
}
let page_index_by_node_id = build_page_index_map(doc);
let Some(page) = doc.body.pages.get(page_index) else {
if doc.body.pages.is_empty() {
diagnostics.push(Diagnostic::advisory(
"scene.no_pages",
"document has no pages; an empty scene is returned",
None,
Some(doc.body.id.clone()),
));
} else {
diagnostics.push(Diagnostic::advisory(
"scene.page_out_of_range",
format!(
"page index {} is out of range; document has {} page(s)",
page_index,
doc.body.pages.len()
),
None,
Some(doc.body.id.clone()),
));
}
return CompileResult {
scene: Scene::new(0.0, 0.0),
diagnostics,
};
};
let page_w = match dim_to_px(page.width.value, &page.width.unit) {
Some(v) => v,
None => {
diagnostics.push(Diagnostic::advisory(
"scene.unsupported_unit",
format!(
"page '{}' width uses an unsupported unit; cannot compile scene",
page.id
),
page.source_span,
Some(page.id.clone()),
));
return CompileResult {
scene: Scene::new(0.0, 0.0),
diagnostics,
};
}
};
let page_h = match dim_to_px(page.height.value, &page.height.unit) {
Some(v) => v,
None => {
diagnostics.push(Diagnostic::advisory(
"scene.unsupported_unit",
format!(
"page '{}' height uses an unsupported unit; cannot compile scene",
page.id
),
page.source_span,
Some(page.id.clone()),
));
return CompileResult {
scene: Scene::new(0.0, 0.0),
diagnostics,
};
}
};
let bleed = page
.bleed
.as_ref()
.and_then(|d| dim_to_px(d.value, &d.unit))
.filter(|&px| px > 0.0)
.unwrap_or(0.0);
let media_w = page_w + 2.0 * bleed;
let media_h = page_h + 2.0 * bleed;
let mut scene = Scene::new(media_w, media_h);
scene.commands.push(SceneCommand::PushClip {
x: 0.0,
y: 0.0,
w: media_w,
h: media_h,
});
if let Some(bg_prop) = &page.background {
if let Some(gradient) = resolve_property_gradient(bg_prop, resolved, &page.id) {
scene.commands.push(SceneCommand::FillRect {
x: 0.0,
y: 0.0,
w: media_w,
h: media_h,
paint: Paint::Gradient(gradient),
});
} else if let Some(color) =
resolve_property_color(bg_prop, resolved, &mut diagnostics, &page.id)
{
scene.commands.push(SceneCommand::FillRect {
x: 0.0,
y: 0.0,
w: media_w,
h: media_h,
paint: Paint::solid(color),
});
}
}
let anchors = build_anchor_map(page, page_w, page_h, resolved);
let engine = RustybuzzEngine::new();
let mut chain_diags: Vec<Diagnostic> = Vec::new();
let chains = resolve_chains_document(
doc,
resolved,
&style_map,
fonts,
&engine,
&md_blocks,
&mut chain_diags,
);
let flows = resolve_table_flows(doc, resolved, &style_map, fonts, &engine, &mut chain_diags);
if page_index == 0 {
diagnostics.extend(chain_diags);
}
let page_index_1based = page_index + 1;
let is_recto = doc.page_is_recto(page, page_index_1based);
let mirror_margins = doc.mirror_margins.unwrap_or(false);
let rtl_book = doc.page_progression.as_deref() == Some("rtl");
let live_area = compute_live_area(
doc,
page,
page_w,
page_h,
is_recto,
mirror_margins,
rtl_book,
);
let footnote_markers = footnote::collect_footnote_markers(page);
let node_boxes = build_node_boxes(page, resolved);
let section_assignments = build_section_assignments(doc);
let section_assign = section_assignments.get(page_index).and_then(|opt| *opt);
let field_ctx = FieldCtx {
page_index_1based,
is_recto,
live_area,
page_index_by_node_id: &page_index_by_node_id,
footnote_markers: &footnote_markers,
node_boxes: &node_boxes,
total_pages: doc.body.pages.len(),
pages: &doc.body.pages,
section_page_index: section_assign.map(|a| a.page_index_in_section),
section_page_count: section_assign.map(|a| a.page_count),
section_folio_start: section_assign.map(|a| a.folio_start),
section_folio_style: section_assign.and_then(|a| a.folio_style),
section_name: section_assign.map(|a| a.name),
};
let node_cx = NodeCtx {
resolved,
style_map: &style_map,
components: &component_map,
fonts,
engine: &engine,
chains: &chains,
flows: &flows,
anchors: &anchors,
field_ctx: &field_ctx,
md_blocks: &md_blocks,
page_block_styles: &page.block_styles,
doc_block_styles: &doc.body.block_styles,
};
let baseline_grid: Option<f64> = page
.baseline_grid
.as_ref()
.and_then(|d| dim_to_px(d.value, &d.unit))
.filter(|g| g.is_finite() && *g > 0.0);
let mut root_ctx = if bleed > 0.0 {
RenderCtx::root_offset(bleed, bleed)
} else {
RenderCtx::root()
};
root_ctx.baseline_grid = baseline_grid;
let mut connector_strokes: Vec<usize> = Vec::new();
if let Some(master_id) = &page.master
&& let Some(master) = master_map.get(master_id.as_str())
{
let mut projected = master.children.clone();
let prefix = format!("{}/", page.id);
container::prefix_ids_in_children(&mut projected, &prefix);
for node in &projected {
compile_node(
node,
node_cx,
&mut scene.commands,
&mut diagnostics,
&mut connector_strokes,
root_ctx,
);
}
}
for node in &page.children {
compile_node(
node,
node_cx,
&mut scene.commands,
&mut diagnostics,
&mut connector_strokes,
root_ctx,
);
}
if let Some(mode) = page.line_jumps.as_deref()
&& (mode == "arc" || mode == "gap")
{
line_jumps::apply_line_jumps(&mut scene.commands, &connector_strokes, mode);
}
footnote::compile_footnote_zone(
page,
live_area,
footnote::FootnoteZoneEnv {
markers: &footnote_markers,
resolved,
style_map: &style_map,
fonts,
engine: &engine,
chains: &chains,
anchors: &anchors,
field_ctx: &field_ctx,
},
&mut scene.commands,
&mut diagnostics,
root_ctx,
);
scene.commands.push(SceneCommand::PopClip);
if bleed > 0.0 {
crop::emit_crop_marks(&mut scene.commands, bleed, page_w, page_h);
}
if bleed > 0.0 {
scene.trim = Some(Rect {
x: bleed,
y: bleed,
w: page_w,
h: page_h,
});
}
CompileResult { scene, diagnostics }
}
pub(super) fn node_role(node: &Node) -> Option<&str> {
match node {
Node::Rect(n) => n.role.as_deref(),
Node::Ellipse(n) => n.role.as_deref(),
Node::Line(n) => n.role.as_deref(),
Node::Text(n) => n.role.as_deref(),
Node::Code(n) => n.role.as_deref(),
Node::Frame(n) => n.role.as_deref(),
Node::Group(n) => n.role.as_deref(),
Node::Image(n) => n.role.as_deref(),
Node::Polygon(n) => n.role.as_deref(),
Node::Polyline(n) => n.role.as_deref(),
Node::Instance(n) => n.role.as_deref(),
Node::Field(n) => n.role.as_deref(),
Node::Toc(n) => n.role.as_deref(),
Node::Footnote(n) => n.role.as_deref(),
Node::Table(n) => n.role.as_deref(),
Node::Shape(n) => n.role.as_deref(),
Node::Connector(n) => n.role.as_deref(),
Node::Pattern(n) => n.role.as_deref(),
Node::Chart(n) => n.role.as_deref(),
Node::Unknown(_) => None,
}
}
pub(in crate::compile) fn compile_node(
node: &Node,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
connector_strokes: &mut Vec<usize>,
ctx: RenderCtx,
) -> f64 {
if node_role(node) == Some("guide") {
return 0.0;
}
let NodeCtx {
resolved,
style_map,
components,
fonts,
engine,
chains,
flows,
anchors,
field_ctx,
md_blocks,
page_block_styles,
doc_block_styles,
} = cx;
match node {
Node::Rect(rect) => {
compile_rect(
rect,
RectEllipseEnv {
resolved,
style_map,
anchors,
},
commands,
diagnostics,
ctx,
);
0.0
}
Node::Ellipse(ellipse) => {
compile_ellipse(
ellipse,
RectEllipseEnv {
resolved,
style_map,
anchors,
},
commands,
diagnostics,
ctx,
);
0.0
}
Node::Text(text) => compile_text(
text,
TextCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
md_blocks,
page_block_styles,
doc_block_styles,
},
commands,
diagnostics,
ctx,
),
Node::Line(line) => {
compile_line(line, resolved, style_map, commands, diagnostics, ctx);
0.0
}
Node::Frame(frame) => {
compile_frame(frame, cx, commands, diagnostics, connector_strokes, ctx);
0.0
}
Node::Group(group) => {
compile_group(group, cx, commands, diagnostics, connector_strokes, ctx);
0.0
}
Node::Instance(instance) => {
compile_instance(instance, cx, commands, diagnostics, connector_strokes, ctx);
0.0
}
Node::Field(field) => {
if let Some(text_node) = resolve_field_to_text(field, field_ctx) {
compile_text(
&text_node,
TextCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
md_blocks: empty_md_blocks(),
page_block_styles: &[],
doc_block_styles: &[],
},
commands,
diagnostics,
ctx,
);
}
0.0
}
Node::Toc(toc) => {
if let Some(text_node) =
resolve_toc_to_text(toc, field_ctx.pages, field_ctx.page_index_by_node_id)
{
compile_text(
&text_node,
TextCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
md_blocks: empty_md_blocks(),
page_block_styles: &[],
doc_block_styles: &[],
},
commands,
diagnostics,
ctx,
);
}
0.0
}
Node::Image(image) => {
compile_image(image, resolved, commands, diagnostics, anchors, ctx);
0.0
}
Node::Polygon(poly) => {
compile_polygon(poly, resolved, style_map, commands, diagnostics, ctx);
0.0
}
Node::Polyline(poly) => {
compile_polyline(poly, resolved, style_map, commands, diagnostics, ctx);
0.0
}
Node::Code(code) => compile_code(
code,
TextCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
md_blocks: empty_md_blocks(),
page_block_styles: &[],
doc_block_styles: &[],
},
commands,
diagnostics,
ctx,
),
Node::Table(table) => {
compile_table(
TableEmitCtx {
table,
resolved,
style_map,
components,
fonts,
engine,
chains,
flows,
anchors,
field_ctx,
},
commands,
diagnostics,
ctx,
);
0.0
}
Node::Shape(shape) => {
compile_shape(
shape,
commands,
diagnostics,
ShapeCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
ctx,
},
);
0.0
}
Node::Connector(connector) => {
let start = commands.len();
compile_connector(
connector,
commands,
diagnostics,
ConnectorEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers: field_ctx.footnote_markers,
node_boxes: field_ctx.node_boxes,
anchors,
ctx,
},
);
line_jumps::record_connector_stroke(commands, start, connector_strokes);
0.0
}
Node::Pattern(p) => compile_pattern(p, cx, commands, diagnostics, ctx),
Node::Chart(c) => compile_chart(c, cx, commands, diagnostics, ctx),
Node::Footnote(_) => {
0.0
}
Node::Unknown(unknown) => {
diagnostics.push(Diagnostic::advisory(
"scene.unsupported_node",
format!(
"unknown node kind '{}' cannot be compiled; the node is skipped \
(forward-compatibility: this kind may be supported in a later version)",
unknown.kind
),
unknown.source_span,
None,
));
0.0
}
}
}