From 99ff4971809ea0a9880e44b203c7404eaeceb77c Mon Sep 17 00:00:00 2001 From: dfgodvan Date: Sun, 18 Oct 2026 23:55:08 +0300 Subject: [PATCH] Layout: a roman page number in the footer stays roman when the body shows the page count (#2966) A paragraph with a PAGE field is cached by the page number, the totals, the section and the bookmarks, but not by the number format the field shows the page number in. Paginating lays the footer out to know where the body ends, before the section's number format is set in the field context, so as decimal. When the body shows NUMPAGES or SECTIONPAGES, the pass that has the right totals made that layout's key equal to the page's own footer's, and the cached decimal one was drawn: page 2 of a section numbered in lower-case roman showed "2", not "ii". The number format is now part of the key. Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> --- crates/layout/src/fields.rs | 4 ++-- crates/layout/src/lib.rs | 2 +- crates/layout/src/tests_pages.rs | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/layout/src/fields.rs b/crates/layout/src/fields.rs index d4946c2f18..2b56405a9f 100644 --- a/crates/layout/src/fields.rs +++ b/crates/layout/src/fields.rs @@ -63,8 +63,8 @@ impl FieldCtx { self.page_from.iter().rev().find(|(from, _)| *from <= off).map_or(self.page, |(_, page)| *page) } /// A key for caches: the parts of the context a page-dependent paragraph depends on. - pub fn page_key(&self) -> (u32, u32, u32, u32, u64) { - (self.page, self.pages, self.section_pages, self.section, self.bookmarks_key) + pub fn page_key(&self) -> (u32, u32, u32, u32, u64, NumFormat) { + (self.page, self.pages, self.section_pages, self.section, self.bookmarks_key, self.page_format) } /// A key for caches of paragraph `p` with STYLEREF fields (0 when they show their stored /// result): a hash of the results its STYLEREF fields have on this page, so an edit to body diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 62724bd8bc..1bc00495ad 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -503,7 +503,7 @@ struct Key { rev: u64, width: u32, label: Option, - page: Option<(u32, u32, u32, u32, u64)>, + page: Option<(u32, u32, u32, u32, u64, wordcraft_doc::section::NumFormat)>, /// The pages a split paragraph's fields are on after its first (`FieldCtx::page_from`). page_from: u64, table: u64, diff --git a/crates/layout/src/tests_pages.rs b/crates/layout/src/tests_pages.rs index b577d1a5a0..cda6e1a96f 100644 --- a/crates/layout/src/tests_pages.rs +++ b/crates/layout/src/tests_pages.rs @@ -530,3 +530,41 @@ fn footer_border_limits_body() { assert!(page.body.bottom() <= top + 0.01, "body {}, footer stroke {top}", page.body.bottom()); } } + +/// A section numbering its pages in lower-case roman shows them so in its footer when the body +/// shows the page count (`NUMPAGES`): the footer laid out while paginating, before the section's +/// number format is known, isn't taken for the page's own footer. +#[test] +fn a_roman_page_number_in_the_footer_stays_roman_with_the_page_count_in_the_body() { + let field = |instr: &str| InlineObject::Field { + instr: instr.into(), + result: String::new(), + locked: false, + dirty: false, + code: false, + form: String::new(), + }; + let mut d = Document::new(); + let mut fp = Paragraph::new(); + fp.insert_object(0, field("PAGE"), &Default::default()).unwrap(); + let id = d.add_part(PartKind::Footer, vec![para_block(fp)]); + d.last_section.footers.default = Some(id); + let mut np = Paragraph::with_text(" pages", Default::default()); + np.insert_object(0, field("NUMPAGES"), &Default::default()).unwrap(); + d.body = vec![para_block(np)]; + d.body.extend(paras(&"word ".repeat(40), 40)); + let mut end = Paragraph::with_text("end", Default::default()); + end.section = Some(Box::new(d.last_section.clone())); + d.body.push(para_block(end)); + d.body.extend(paras(&"word ".repeat(40), 120)); + d.last_section.page_num_start = Some(1); + d.last_section.page_num_format = wordcraft_doc::section::NumFormat::LowerRoman; + let l = lay(&d); + let roman: Vec<_> = l.pages.iter().filter(|p| p.section == 1).collect(); + assert!(roman.len() > 5); + for p in roman { + let glyphs: usize = p.footer.iter().map(|it| if let Placed::Lines { para, .. } = it { para.glyphs.len() } else { 0 }).sum(); + let want = wordcraft_doc::section::NumFormat::LowerRoman.format(p.number); + assert_eq!(glyphs, want.chars().count(), "page {}: the footer should show {want}", p.number); + } +}