diff --git a/ROADMAP.md b/ROADMAP.md index e49a22d70..2fee5036f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,6 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-30** · Parent pages: a running-header variable shows its name in angle brackets on the parent itself (it was blank), as InDesign shows it; on the document pages, and in text with no page, it shows its text as before. No readiness percentage change. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. - **2026-10-30** · File > Package opens a dialog, as the reference app's: a Summary of fonts, links and images and inks, pages for each, a Printing Instructions step, and a PDF made with the options of a preset (#1514, #1515). - **2026-10-30** · IDML import reads TOC separators and entry styles written with the `$ID/` prefix or as `Properties` children, and the TOC separator notation (`^t`, `^y`, `^m`, `^>`) is shared by the dialog and IDML, with a special-character menu on the separator field (#1249, #1255). diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index b19833ff1..227dad654 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -581,8 +581,10 @@ pub struct FrameSpec { pub page_name: Option, /// Marker text of the section the frame's page is in (None on parent pages). pub section_marker: Option, - /// Absolute document page the frame is on (None on parent pages). + /// Absolute document page the frame is on (None on parent pages and the pasteboard). pub page: Option, + /// The frame is on a parent spread. + pub parent: bool, /// Baseline grid in inner space: (first grid line y, increment). pub grid: Option<(f64, f64)>, /// The frame is on a left page (for towards/away-from-spine alignment). @@ -817,6 +819,7 @@ pub(crate) fn frame_specs_placed(doc: &Document, sid: StoryId, placed: &[(ItemId page_name, section_marker, page, + parent: matches!(spread_ref, Some(designcraft_doc::SpreadRef::Parent(_))), grid, left_page, right_page, @@ -862,7 +865,7 @@ struct SubstitutionInput<'a> { frame: Option<&'a FrameSpec>, opts: &'a ComposeOptions, var_story: bool, - var_cache: &'a mut HashMap, Arc>>, + var_cache: &'a mut HashMap<(Option, bool), Arc>>, notes: &'a mut Notes, } @@ -870,7 +873,9 @@ fn paragraph_substitution(input: SubstitutionInput<'_>) -> SubstCtx { let SubstitutionInput { doc, story, range: prange, props: pp, frame: cur_frame, opts, var_story, var_cache, notes } = input; let var_values = if var_story { let page = opts.page.or_else(|| cur_frame.and_then(|f| f.page)); - var_cache.entry(page).or_insert_with(|| std::sync::Arc::new(vars::values(doc, page, opts.running.as_deref()))).clone() + // A parent page itself (not composed for one of the pages it applies to). + let parent = page.is_none() && cur_frame.is_some_and(|f| f.parent); + var_cache.entry((page, parent)).or_insert_with(|| std::sync::Arc::new(vars::values_shown(doc, page, parent, opts.running.as_deref()))).clone() } else { Default::default() }; @@ -2681,7 +2686,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let var_story = vars::has_vars(story); let mut notes = Notes::new(doc, story); let mut note_snaps: Vec<(u32, Option, usize)> = Vec::with_capacity(np); - let mut var_cache: std::collections::HashMap, std::sync::Arc>> = Default::default(); + let mut var_cache: std::collections::HashMap<(Option, bool), std::sync::Arc>> = Default::default(); // Column balancing above spanning paragraphs: settled and trial bottoms by band, the search // in progress, and a budget for the re-lays. let mut limits: Rc> = Rc::default(); @@ -5977,6 +5982,7 @@ mod column_allocation_safety_tests { page_name: None, section_marker: None, page: None, + parent: false, grid: None, left_page: false, right_page: false, diff --git a/crates/compose/src/notes.rs b/crates/compose/src/notes.rs index a385a9093..1e1c852a5 100644 --- a/crates/compose/src/notes.rs +++ b/crates/compose/src/notes.rs @@ -176,6 +176,7 @@ impl Notes { page_name: f.page_name.clone(), section_marker: f.section_marker.clone(), page: f.page, + parent: f.parent, grid: None, left_page: f.left_page, right_page: f.right_page, diff --git a/crates/compose/src/table.rs b/crates/compose/src/table.rs index dc579d8a7..a124b1fce 100644 --- a/crates/compose/src/table.rs +++ b/crates/compose/src/table.rs @@ -132,6 +132,7 @@ fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, (lef page_name: opts.page_name.clone(), section_marker: opts.section_marker.clone(), page: opts.page, + parent: false, grid: None, left_page, right_page, diff --git a/crates/compose/src/vars.rs b/crates/compose/src/vars.rs index 508ac4a3e..bd9e32ccd 100644 --- a/crates/compose/src/vars.rs +++ b/crates/compose/src/vars.rs @@ -160,10 +160,18 @@ impl RunningIndex { /// Values of every variable on `page`. pub fn values(doc: &Document, page: Option, running: Option<&RunningIndex>) -> Vec { + values_shown(doc, page, false, running) +} + +/// [`values`] as text shows them: on a parent page itself (`parent`, with no `page`), a running +/// header has no entries of its own and shows which variable sits there, ``, as InDesign +/// does. Elsewhere without a page (the pasteboard), it is its text before and after. +pub fn values_shown(doc: &Document, page: Option, parent: bool, running: Option<&RunningIndex>) -> Vec { doc.text_variables .iter() .enumerate() .map(|(i, v)| match &v.kind { + VarKind::RunningHeader { .. } if parent && page.is_none() => format!("<{}>", v.name), VarKind::RunningHeader { style, use_, character } => { let body = match (running, page) { (Some(r), Some(p)) => r.value(style, *character, *use_, p), diff --git a/crates/engine/src/cmd/variables/tests.rs b/crates/engine/src/cmd/variables/tests.rs index 75c4bcaed..9093f31dd 100644 --- a/crates/engine/src/cmd/variables/tests.rs +++ b/crates/engine/src/cmd/variables/tests.rs @@ -1,5 +1,10 @@ use super::*; +fn shown_on_parent(s: &Session, d: &designcraft_doc::Document, sid: designcraft_doc::StoryId) -> usize { + let cs = s.cache.get(d, sid, None); + cs.frames.iter().flat_map(|f| f.lines.iter()).map(|l| l.glyphs.iter().filter(|g| g.visible && g.adv > 0.0).count()).sum() +} + /// A parent-page page-number marker follows Numbering & Section Options: changing a section's /// start number renames its pages without changing their index or the number of sections, and /// the marker composed for a page shows the new name. @@ -350,6 +355,14 @@ fn running_header_on_parent_page_follows_headings() { assert_eq!(idx.value("Heading", false, Default::default(), 2), "Epsilon"); let list = s.execute("variables.list", &json!({})).unwrap(); assert!(list.as_array().unwrap().iter().any(|v| v["name"] == "Last Page Number" && v["value"] == "3")); + // On the parent page itself the header shows which variable it holds, not a blank. Text with + // no page that isn't on a parent shows the variable's text before and after, as before. + let head = d.variable("Chapter Head").unwrap(); + assert_eq!(crate::compose::vars::values_shown(&d, None, true, Some(&idx))[head], ""); + assert_eq!(crate::compose::vars::values_shown(&d, None, false, Some(&idx))[head], ""); + assert_eq!(crate::compose::vars::values(&d, None, Some(&idx))[head], "", "values() is the no-parent case"); + assert_eq!(crate::compose::vars::values_shown(&d, Some(2), true, Some(&idx))[head], "Epsilon", "composed for a page: its entry"); + assert!(shown_on_parent(&s, &d, sid) > p1, "the parent shows , longer than Alpha"); // Deleting a variable flattens its instances to text. s.execute("variables.delete", &json!({"name": "Last Page Number"})).unwrap(); assert!(s.doc().unwrap().doc.story(sid).unwrap().text.ends_with(" · 3"));