use std::collections::BTreeMap;
use zenith_core::{Node, PropertyValue, ResolvedToken};
use super::super::util::resolve_geometry_px;
pub(in crate::compile) fn build_page_index_map(
doc: &zenith_core::Document,
) -> BTreeMap<String, usize> {
let mut map: BTreeMap<String, usize> = BTreeMap::new();
for (page_idx0, page) in doc.body.pages.iter().enumerate() {
let page_index_1based = page_idx0 + 1;
index_nodes(&page.children, page_index_1based, &mut map);
}
map
}
fn index_nodes(children: &[Node], page_index_1based: usize, map: &mut BTreeMap<String, usize>) {
for child in children {
if let Some(id) = node_id(child) {
map.entry(id.to_owned()).or_insert(page_index_1based);
}
match child {
Node::Frame(f) => index_nodes(&f.children, page_index_1based, map),
Node::Group(g) => index_nodes(&g.children, page_index_1based, map),
Node::Table(t) => {
for row in &t.rows {
for cell in &row.cells {
index_nodes(&cell.children, page_index_1based, map);
}
}
}
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Light(_)
| Node::Mesh(_)
| Node::Unknown(_) => {}
}
}
}
pub(in crate::compile) fn build_node_boxes(
page: &zenith_core::Page,
resolved: &BTreeMap<String, ResolvedToken>,
) -> BTreeMap<String, (f64, f64, f64, f64)> {
let mut map: BTreeMap<String, (f64, f64, f64, f64)> = BTreeMap::new();
collect_node_boxes(&page.children, 0.0, 0.0, resolved, &mut map);
map
}
fn collect_node_boxes(
children: &[Node],
dx: f64,
dy: f64,
resolved: &BTreeMap<String, ResolvedToken>,
map: &mut BTreeMap<String, (f64, f64, f64, f64)>,
) {
for child in children {
if let Some(id) = node_id(child)
&& let Some((x, y, w, h)) = node_rect(child, resolved)
{
map.entry(id.to_owned()).or_insert((dx + x, dy + y, w, h));
}
match child {
Node::Frame(f) => collect_node_boxes(&f.children, dx, dy, resolved, map),
Node::Group(g) => {
let gx = resolve_geometry_px(g.x.as_ref(), resolved);
let gy = resolve_geometry_px(g.y.as_ref(), resolved);
collect_node_boxes(
&g.children,
dx + gx.unwrap_or(0.0),
dy + gy.unwrap_or(0.0),
resolved,
map,
);
}
Node::Table(_)
| Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Light(_)
| Node::Mesh(_)
| Node::Unknown(_) => {}
}
}
}
fn node_rect(
node: &Node,
resolved: &BTreeMap<String, ResolvedToken>,
) -> Option<(f64, f64, f64, f64)> {
let rect = |x: &Option<PropertyValue>,
y: &Option<PropertyValue>,
w: &Option<PropertyValue>,
h: &Option<PropertyValue>|
-> Option<(f64, f64, f64, f64)> {
let x = resolve_geometry_px(x.as_ref(), resolved)?;
let y = resolve_geometry_px(y.as_ref(), resolved)?;
let w = resolve_geometry_px(w.as_ref(), resolved)?;
let h = resolve_geometry_px(h.as_ref(), resolved)?;
Some((x, y, w, h))
};
match node {
Node::Rect(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Ellipse(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Text(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Code(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Frame(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Group(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Image(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Field(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Toc(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Table(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Shape(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Pattern(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Chart(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Light(_) => None,
Node::Mesh(n) => rect(&n.x, &n.y, &n.w, &n.h),
Node::Instance(_)
| Node::Line(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Footnote(_)
| Node::Connector(_)
| Node::Unknown(_) => None,
}
}
fn node_id(node: &Node) -> Option<&str> {
match node {
Node::Rect(n) => Some(&n.id),
Node::Ellipse(n) => Some(&n.id),
Node::Line(n) => Some(&n.id),
Node::Text(n) => Some(&n.id),
Node::Code(n) => Some(&n.id),
Node::Frame(n) => Some(&n.id),
Node::Group(n) => Some(&n.id),
Node::Image(n) => Some(&n.id),
Node::Polygon(n) => Some(&n.id),
Node::Polyline(n) => Some(&n.id),
Node::Instance(n) => Some(&n.id),
Node::Field(n) => Some(&n.id),
Node::Toc(n) => Some(&n.id),
Node::Footnote(n) => Some(&n.id),
Node::Table(n) => Some(&n.id),
Node::Shape(n) => Some(&n.id),
Node::Connector(n) => Some(&n.id),
Node::Pattern(n) => Some(&n.id),
Node::Chart(n) => Some(&n.id),
Node::Light(n) => Some(&n.id),
Node::Mesh(n) => Some(&n.id),
Node::Unknown(_) => None,
}
}
pub(in crate::compile) fn compute_live_area(
doc: &zenith_core::Document,
page: &zenith_core::Page,
page_w: f64,
page_h: f64,
is_recto: bool,
mirror_margins: bool,
rtl: bool,
) -> Option<(f64, f64, f64, f64)> {
use zenith_core::dim_to_px;
let (inner_opt, outer_opt, top_opt, bottom_opt) = doc.effective_margins(page);
let inner_dim = inner_opt.as_ref()?;
let outer_dim = outer_opt.as_ref()?;
let top_dim = top_opt.as_ref()?;
let bottom_dim = bottom_opt.as_ref()?;
let inner = dim_to_px(inner_dim.value, &inner_dim.unit)?;
let outer = dim_to_px(outer_dim.value, &outer_dim.unit)?;
let top = dim_to_px(top_dim.value, &top_dim.unit)?;
let bottom = dim_to_px(bottom_dim.value, &bottom_dim.unit)?;
let inner_on_right = if rtl { is_recto } else { !is_recto };
let left_inset = if mirror_margins && inner_on_right {
outer
} else {
inner
};
Some((
left_inset,
top,
page_w - inner - outer,
page_h - top - bottom,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compile::util::px;
use zenith_core::Document;
use zenith_core::Page;
fn margined_page() -> Page {
Page {
id: "p".to_owned(),
name: None,
width: px(1200.0),
height: px(1900.0),
background: None,
bleed: None,
margin_inner: Some(px(160.0)),
margin_outer: Some(px(100.0)),
margin_top: Some(px(80.0)),
margin_bottom: Some(px(80.0)),
baseline_grid: None,
line_jumps: None,
parity: None,
master: None,
safe_zones: Vec::new(),
folds: Vec::new(),
block_styles: Vec::new(),
children: Vec::new(),
source_span: None,
}
}
fn bare_page() -> Page {
let mut p = margined_page();
p.margin_inner = None;
p.margin_outer = None;
p.margin_top = None;
p.margin_bottom = None;
p
}
fn bare_doc() -> Document {
use zenith_core::{KdlAdapter, KdlSource};
KdlAdapter
.parse(b"zenith version=1 { document id=\"d\" { } }")
.expect("minimal test document must parse")
}
#[test]
fn live_area_recto_uses_inner_margin() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
true,
true,
false,
);
assert_eq!(la, Some((160.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_verso_mirrors_to_outer_margin() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
false,
true,
false,
);
assert_eq!(la, Some((100.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_unmirrored_verso_keeps_inner() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
false,
false,
false,
);
assert_eq!(la, Some((160.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_rtl_recto_mirrors_to_outer_margin() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
true,
true,
true,
);
assert_eq!(la, Some((100.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_rtl_verso_uses_inner_margin() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
false,
true,
true,
);
assert_eq!(la, Some((160.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_rtl_unmirrored_keeps_inner() {
let la = compute_live_area(
&bare_doc(),
&margined_page(),
1200.0,
1900.0,
true,
false,
true,
);
assert_eq!(la, Some((160.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_requires_all_four_margins() {
let mut page = margined_page();
page.margin_bottom = None;
assert_eq!(
compute_live_area(&bare_doc(), &page, 1200.0, 1900.0, true, true, false),
None,
"an incomplete margin set yields no live area"
);
}
#[test]
fn live_area_cascades_doc_margins_to_bare_page() {
let mut doc = bare_doc();
doc.margin_inner = Some(px(160.0));
doc.margin_outer = Some(px(100.0));
doc.margin_top = Some(px(80.0));
doc.margin_bottom = Some(px(80.0));
let la = compute_live_area(&doc, &bare_page(), 1200.0, 1900.0, true, true, false);
assert_eq!(la, Some((160.0, 80.0, 940.0, 1740.0)));
}
#[test]
fn live_area_page_inner_overrides_doc_default() {
let mut doc = bare_doc();
doc.margin_inner = Some(px(160.0));
doc.margin_outer = Some(px(100.0));
doc.margin_top = Some(px(80.0));
doc.margin_bottom = Some(px(80.0));
let mut page = bare_page();
page.margin_inner = Some(px(200.0));
let la = compute_live_area(&doc, &page, 1200.0, 1900.0, true, true, false);
assert_eq!(la, Some((200.0, 80.0, 900.0, 1740.0)));
}
}