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>
This commit is contained in:
dfgodvan
2026-10-18 16:55:08 -04:00
committed by GitHub
co-authored by dfgodvan
parent 4b18b307be
commit 99ff497180
3 changed files with 41 additions and 3 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -503,7 +503,7 @@ struct Key {
rev: u64,
width: u32,
label: Option<String>,
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,
+38
View File
@@ -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);
}
}