use zenith_core::Document;
#[derive(Clone, Copy)]
pub(in crate::compile) struct SectionAssignment<'a> {
pub(in crate::compile) page_index_in_section: usize,
pub(in crate::compile) page_count: usize,
pub(in crate::compile) folio_start: usize,
pub(in crate::compile) folio_style: Option<&'a str>,
pub(in crate::compile) name: &'a str,
}
pub(in crate::compile) fn build_section_assignments(
doc: &Document,
) -> Vec<Option<SectionAssignment<'_>>> {
use std::collections::BTreeMap;
let total_pages = doc.body.pages.len();
let page_index_map: BTreeMap<&str, usize> = doc
.body
.pages
.iter()
.enumerate()
.map(|(i, p)| (p.id.as_str(), i))
.collect();
let mut resolved: Vec<(usize, &zenith_core::SectionDef)> = doc
.sections
.iter()
.filter_map(|sec| {
let idx = page_index_map.get(sec.start_page.as_str()).copied()?;
Some((idx, sec))
})
.collect();
resolved.sort_by_key(|(idx, _)| *idx);
let mut out: Vec<Option<SectionAssignment<'_>>> = vec![None; total_pages];
for (i, &(start_idx, sec)) in resolved.iter().enumerate() {
let end_idx = resolved
.get(i + 1)
.map(|(next_start, _)| *next_start)
.unwrap_or(total_pages);
let page_count = end_idx.saturating_sub(start_idx);
let folio_start = sec.folio_start.unwrap_or(1);
let folio_style = sec.folio_style.as_deref();
let name = sec.name.as_str();
for page_idx in start_idx..end_idx {
if let Some(slot) = out.get_mut(page_idx) {
*slot = Some(SectionAssignment {
page_index_in_section: page_idx - start_idx,
page_count,
folio_start,
folio_style,
name,
});
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compile::util::px;
fn doc_with_pages_and_sections(
page_count: usize,
sections: Vec<zenith_core::SectionDef>,
) -> Document {
use zenith_core::{KdlAdapter, KdlSource};
let mut doc = KdlAdapter
.parse(b"zenith version=1 { document id=\"d\" { } }")
.expect("minimal test document must parse");
for i in 0..page_count {
doc.body.pages.push(zenith_core::Page {
id: format!("p{i}"),
name: None,
width: px(100.0),
height: px(100.0),
background: None,
bleed: None,
margin_inner: None,
margin_outer: None,
margin_top: None,
margin_bottom: None,
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,
});
}
doc.sections = sections;
doc
}
fn make_section(
id: &str,
name: &str,
start_page: &str,
folio_start: Option<usize>,
folio_style: Option<&str>,
) -> zenith_core::SectionDef {
zenith_core::SectionDef {
id: id.to_owned(),
name: name.to_owned(),
folio_start,
folio_style: folio_style.map(str::to_owned),
start_page: start_page.to_owned(),
source_span: None,
}
}
#[test]
fn build_section_assignments_two_sections_five_pages() {
let doc = doc_with_pages_and_sections(
5,
vec![
make_section(
"sec.front",
"Front Matter",
"p0",
Some(1),
Some("lower-roman"),
),
make_section("sec.body", "Body", "p2", Some(1), None),
],
);
let assignments = build_section_assignments(&doc);
assert_eq!(assignments.len(), 5);
let a0 = assignments[0].expect("p0 must have an assignment");
assert_eq!(a0.page_index_in_section, 0);
assert_eq!(a0.page_count, 2);
assert_eq!(a0.folio_start, 1);
assert_eq!(a0.folio_style, Some("lower-roman"));
assert_eq!(a0.name, "Front Matter");
let a1 = assignments[1].expect("p1 must have an assignment");
assert_eq!(a1.page_index_in_section, 1);
assert_eq!(a1.page_count, 2);
assert_eq!(a1.name, "Front Matter");
let a2 = assignments[2].expect("p2 must have an assignment");
assert_eq!(a2.page_index_in_section, 0);
assert_eq!(a2.page_count, 3);
assert_eq!(a2.folio_start, 1);
assert_eq!(a2.folio_style, None);
assert_eq!(a2.name, "Body");
let a4 = assignments[4].expect("p4 must have an assignment");
assert_eq!(a4.page_index_in_section, 2);
assert_eq!(a4.page_count, 3);
}
#[test]
fn build_section_assignments_page_before_first_section_is_none() {
let doc = doc_with_pages_and_sections(
4,
vec![make_section("sec.body", "Body", "p2", None, None)],
);
let assignments = build_section_assignments(&doc);
assert!(assignments[0].is_none(), "p0 is before the first section");
assert!(assignments[1].is_none(), "p1 is before the first section");
assert!(assignments[2].is_some(), "p2 starts the section");
assert!(assignments[3].is_some(), "p3 is in the section");
}
#[test]
fn build_section_assignments_unknown_start_page_is_skipped() {
let doc = doc_with_pages_and_sections(
2,
vec![make_section("sec.x", "X", "no-such-page", None, None)],
);
let assignments = build_section_assignments(&doc);
assert!(assignments[0].is_none());
assert!(assignments[1].is_none());
}
}