use super::Span;
use super::action::ActionDef;
use super::asset::AssetBlock;
use super::block_style::BlockStyle;
use super::brand::BrandContract;
use super::library::LibraryDef;
use super::node::Node;
use super::policy::DiagnosticPolicy;
use super::provenance::ProvenanceDef;
use super::recipe::RecipeDef;
use super::style::StyleBlock;
use super::token::TokenBlock;
use super::value::Dimension;
use super::value::PropertyValue;
use super::variant::VariantDef;
#[derive(Debug, Clone, PartialEq)]
pub struct Project {
pub id: String,
pub name: String,
pub author: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Page {
pub id: String,
pub name: Option<String>,
pub width: Dimension,
pub height: Dimension,
pub background: Option<PropertyValue>,
pub bleed: Option<Dimension>,
pub margin_inner: Option<Dimension>,
pub margin_outer: Option<Dimension>,
pub margin_top: Option<Dimension>,
pub margin_bottom: Option<Dimension>,
pub baseline_grid: Option<Dimension>,
pub line_jumps: Option<String>,
pub safe_zones: Vec<SafeZone>,
pub folds: Vec<Fold>,
pub block_styles: Vec<BlockStyle>,
pub parity: Option<String>,
pub master: Option<String>,
pub children: Vec<Node>,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SafeZoneType {
Exclusion,
Required,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SafeZone {
pub id: String,
pub zone_type: SafeZoneType,
pub x: Dimension,
pub y: Dimension,
pub w: Dimension,
pub h: Dimension,
pub label: Option<String>,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fold {
pub id: String,
pub orientation: String,
pub position: Option<Dimension>,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DocumentBody {
pub id: String,
pub title: Option<String>,
pub block_styles: Vec<BlockStyle>,
pub pages: Vec<Page>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComponentDef {
pub id: String,
pub children: Vec<super::node::Node>,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MasterDef {
pub id: String,
pub children: Vec<super::node::Node>,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SectionDef {
pub id: String,
pub name: String,
pub folio_start: Option<usize>,
pub folio_style: Option<String>,
pub start_page: String,
pub source_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
pub version: u32,
pub colorspace: Option<String>,
pub doc_id: Option<String>,
pub mirror_margins: Option<bool>,
pub page_progression: Option<String>,
pub page_parity_start: Option<String>,
pub facing_pages: Option<bool>,
pub spread_gutter: Option<Dimension>,
pub margin_inner: Option<Dimension>,
pub margin_outer: Option<Dimension>,
pub margin_top: Option<Dimension>,
pub margin_bottom: Option<Dimension>,
pub project: Option<Project>,
pub assets: AssetBlock,
pub libraries: Vec<LibraryDef>,
pub actions: Vec<ActionDef>,
pub tokens: TokenBlock,
pub styles: StyleBlock,
pub components: Vec<ComponentDef>,
pub masters: Vec<MasterDef>,
pub sections: Vec<SectionDef>,
pub provenance: Vec<ProvenanceDef>,
pub variants: Vec<VariantDef>,
pub recipes: Vec<RecipeDef>,
pub diagnostic_policy: DiagnosticPolicy,
pub brand_contract: BrandContract,
pub body: DocumentBody,
}
impl Document {
pub fn page_is_recto(&self, page: &Page, page_index_1based: usize) -> bool {
if let Some(p) = page.parity.as_deref() {
return !p.eq_ignore_ascii_case("verso");
}
let base_recto = page_index_1based % 2 == 1;
match self.page_parity_start.as_deref() {
Some(s) if s.eq_ignore_ascii_case("verso") => !base_recto,
_ => base_recto,
}
}
pub fn effective_margins(
&self,
page: &Page,
) -> (
Option<Dimension>,
Option<Dimension>,
Option<Dimension>,
Option<Dimension>,
) {
(
page.margin_inner
.clone()
.or_else(|| self.margin_inner.clone()),
page.margin_outer
.clone()
.or_else(|| self.margin_outer.clone()),
page.margin_top.clone().or_else(|| self.margin_top.clone()),
page.margin_bottom
.clone()
.or_else(|| self.margin_bottom.clone()),
)
}
}
#[cfg(test)]
mod parity_tests {
use super::*;
use crate::ast::value::Dimension;
use crate::ast::value::Unit;
fn px(v: f64) -> Dimension {
Dimension {
value: v,
unit: Unit::Px,
}
}
fn page(id: &str, parity: Option<&str>) -> Page {
Page {
id: id.to_owned(),
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: parity.map(str::to_owned),
master: None,
safe_zones: Vec::new(),
folds: Vec::new(),
block_styles: Vec::new(),
children: Vec::new(),
source_span: None,
}
}
fn doc(start: Option<&str>) -> Document {
Document {
version: 1,
colorspace: None,
doc_id: None,
mirror_margins: None,
facing_pages: None,
spread_gutter: None,
page_progression: None,
page_parity_start: start.map(str::to_owned),
margin_inner: None,
margin_outer: None,
margin_top: None,
margin_bottom: None,
project: None,
assets: AssetBlock::default(),
libraries: Vec::new(),
actions: Vec::new(),
tokens: TokenBlock::default(),
styles: StyleBlock::default(),
components: Vec::new(),
masters: Vec::new(),
sections: Vec::new(),
provenance: Vec::new(),
variants: Vec::new(),
recipes: Vec::new(),
diagnostic_policy: DiagnosticPolicy::default(),
brand_contract: BrandContract::default(),
body: DocumentBody {
id: "body".to_owned(),
title: None,
block_styles: Vec::new(),
pages: Vec::new(),
},
}
}
#[test]
fn default_page_one_recto_page_two_verso() {
let d = doc(None);
assert!(d.page_is_recto(&page("p1", None), 1), "page 1 is recto");
assert!(!d.page_is_recto(&page("p2", None), 2), "page 2 is verso");
assert!(d.page_is_recto(&page("p3", None), 3), "page 3 is recto");
}
#[test]
fn start_verso_flips_the_sequence() {
let d = doc(Some("verso"));
assert!(!d.page_is_recto(&page("p1", None), 1), "page 1 is verso");
assert!(d.page_is_recto(&page("p2", None), 2), "page 2 is recto");
}
#[test]
fn start_recto_matches_default() {
let d = doc(Some("recto"));
assert!(d.page_is_recto(&page("p1", None), 1));
assert!(!d.page_is_recto(&page("p2", None), 2));
}
#[test]
fn page_override_verso_wins_over_start() {
let d = doc(None);
assert!(!d.page_is_recto(&page("p1", Some("verso")), 1));
let d2 = doc(Some("verso"));
assert!(d2.page_is_recto(&page("p1", Some("recto")), 1));
}
#[test]
fn page_override_recto_on_even_page() {
let d = doc(None);
assert!(
d.page_is_recto(&page("p2", Some("recto")), 2),
"page 2 forced recto"
);
}
#[test]
fn invalid_start_falls_back_to_default() {
let d = doc(Some("sideways"));
assert!(d.page_is_recto(&page("p1", None), 1), "page 1 stays recto");
assert!(!d.page_is_recto(&page("p2", None), 2));
}
#[test]
fn invalid_page_parity_treated_as_recto() {
let d = doc(None);
assert!(
d.page_is_recto(&page("p2", Some("nonsense")), 2),
"an invalid override is treated as recto"
);
}
#[test]
fn effective_margins_page_value_wins_when_both_set() {
let mut d = doc(None);
d.margin_inner = Some(px(10.0));
d.margin_outer = Some(px(20.0));
d.margin_top = Some(px(30.0));
d.margin_bottom = Some(px(40.0));
let mut p = page("p", None);
p.margin_inner = Some(px(1.0));
p.margin_outer = Some(px(2.0));
p.margin_top = Some(px(3.0));
p.margin_bottom = Some(px(4.0));
let (i, o, t, b) = d.effective_margins(&p);
assert_eq!(i, Some(px(1.0)));
assert_eq!(o, Some(px(2.0)));
assert_eq!(t, Some(px(3.0)));
assert_eq!(b, Some(px(4.0)));
}
#[test]
fn effective_margins_doc_default_used_when_page_none() {
let mut d = doc(None);
d.margin_inner = Some(px(10.0));
d.margin_outer = Some(px(20.0));
d.margin_top = Some(px(30.0));
d.margin_bottom = Some(px(40.0));
let p = page("p", None);
let (i, o, t, b) = d.effective_margins(&p);
assert_eq!(i, Some(px(10.0)));
assert_eq!(o, Some(px(20.0)));
assert_eq!(t, Some(px(30.0)));
assert_eq!(b, Some(px(40.0)));
}
#[test]
fn effective_margins_mixed_override() {
let mut d = doc(None);
d.margin_inner = Some(px(10.0));
d.margin_outer = Some(px(20.0));
d.margin_top = Some(px(30.0));
d.margin_bottom = Some(px(40.0));
let mut p = page("p", None);
p.margin_inner = Some(px(99.0));
let (i, o, t, b) = d.effective_margins(&p);
assert_eq!(i, Some(px(99.0)));
assert_eq!(o, Some(px(20.0)));
assert_eq!(t, Some(px(30.0)));
assert_eq!(b, Some(px(40.0)));
}
#[test]
fn effective_margins_none_when_both_none() {
let d = doc(None);
let p = page("p", None);
assert_eq!(d.effective_margins(&p), (None, None, None, None));
}
#[test]
fn effective_margins_default_off_is_page_values_verbatim() {
let d = doc(None);
let mut p = page("p", None);
p.margin_inner = Some(px(225.0));
p.margin_top = Some(px(210.0));
let (i, o, t, b) = d.effective_margins(&p);
assert_eq!(i, p.margin_inner);
assert_eq!(o, p.margin_outer);
assert_eq!(t, p.margin_top);
assert_eq!(b, p.margin_bottom);
}
#[test]
fn default_is_byte_identical_to_index_parity() {
let d = doc(None);
for idx in 1..=64usize {
assert_eq!(
d.page_is_recto(&page("p", None), idx),
idx % 2 == 1,
"default parity must equal index%2==1 at index {idx}"
);
}
}
}