Files
wordcraft/crates/layout/src/headers.rs
T
Harrison BoothandMartin Ball 7855a2d814 fix(fields): quoted-argument backslash escapes; body PAGE in a cardinal-text section (#4579)
* fix(fields): quoted-argument backslash escapes and body PAGE in a cardinal-text section

A backslash inside a quoted field argument makes the next character literal, so TITLE "x \* Upper" shows x * Upper as Word does. A body PAGE field now uses the section's page number format (cardinal text shows words), as headers and footers already did.

* Fields: PAGE in a body table follows the section number format; parity docs

* fmt

* docs: clarify quoted field escape rule

* Fix page fields in fitted text boxes

* Move fitted cache test after layout helpers

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 02:08:54 +01:00

406 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::boxes::{BoxInput, layout_box};
use super::cache::{Totals, hash_of};
use super::ctx::{Ctx, with_pct_base, with_pin};
use super::flow::again;
use super::page::{PageBuilder, PageFrame, page_border_rules};
use super::placed::{HfSlot, Placed};
use super::{
Arc, Document, HashMap, HashSet, HeaderSet, InlineObject, Page, Path, Rect, SectionProps, SectionStart, StoryRef, ViewMode, fields, styleref,
work,
};
/// Whether a section that starts on a new page needs a blank page first so it lands on the right
/// side, and the page number it restarts at. `last` is the number of the page before it and
/// `pages` how many pages there are so far.
/// - An odd- or even-page break counts on from the previous section's numbers. A section that
/// restarts its numbering gets no blank page unless the document prints both sides (different
/// odd and even headers, or mirror margins); a restart at the wrong parity takes the next number.
/// - With different odd and even headers, a next-page break that restarts the numbers at an odd
/// (even) number starts on an odd (even) sheet.
///
/// (Word's output for docxide-pdf's fixtures: bush_fires_act_comparison has a blank page 2.)
pub(crate) fn section_side(start: SectionStart, restart: Option<u32>, last: u32, pages: usize, even_odd: bool, mirror: bool) -> (bool, Option<u32>) {
match start {
SectionStart::OddPage | SectionStart::EvenPage => {
let want_odd = start == SectionStart::OddPage;
let continuing_odd = last.saturating_add(1) % 2 == 1;
let filler = pages > 0 && continuing_odd != want_odd && (restart.is_none() || even_odd || mirror);
let restart = restart.map(|n| if (n % 2 == 1) == want_odd { n } else { n.saturating_add(1) });
(filler, restart)
}
SectionStart::NextPage => {
let filler = even_odd && pages > 0 && restart.is_some_and(|n| (n % 2 == 1) != ((pages + 1) % 2 == 1));
(filler, restart)
}
SectionStart::Continuous | SectionStart::NextColumn => (false, restart),
}
}
/// The document's sections with their headers and footers (Word's "link to previous"), each
/// section's resolved once for the whole layout in document order rather than by looking back
/// over the sections before it whenever one is needed.
pub(crate) struct HfLinks<'a> {
pub(crate) sections: Vec<(usize, &'a SectionProps)>,
pub(crate) heads: Linked,
pub(crate) feet: Linked,
}
impl<'a> HfLinks<'a> {
pub(crate) fn new(sections: Vec<(usize, &'a SectionProps)>) -> Self {
let heads = Linked::new(&sections, |s| s.headers);
let feet = Linked::new(&sections, |s| s.footers);
HfLinks { sections, heads, feet }
}
/// The text width of the section that defines header (or, `footer`, footer) `id`, when
/// section `si` inherits it rather than naming it (`None` when it names it). Measured in
/// Word: an inherited part's percentage-width tables are sized by that section, though it's
/// laid out on `si`'s pages.
pub(crate) fn defining_width(&self, footer: bool, si: usize, id: Option<u32>) -> Option<f32> {
let id = id?;
work::steps(1);
let linked = if footer { &self.feet } else { &self.heads };
let own = self.sections.get(si)?.1;
if linked.own.get(si).is_some_and(|h| [h.default, h.first, h.even].contains(&Some(id))) {
return None;
}
// The last section before `si` naming it.
let namers = linked.namers.get(&id)?;
let k = *namers.get(namers.partition_point(|&k| k < si).checked_sub(1)?)?;
self.sections.get(k).map(|(_, s)| s.text_width()).filter(|w| (w - own.text_width()).abs() > 0.01)
}
}
/// The headers (or footers) of every section: those it names, and with those it leaves out
/// inherited from the sections before it.
pub(crate) struct Linked {
own: Vec<HeaderSet>,
linked: Vec<HeaderSet>,
/// The sections naming each part, in order.
namers: HashMap<u32, Vec<usize>>,
}
impl Linked {
fn new(sections: &[(usize, &SectionProps)], get: fn(&SectionProps) -> HeaderSet) -> Self {
let own: Vec<HeaderSet> = sections.iter().map(|(_, s)| get(s)).collect();
let mut linked = Vec::with_capacity(own.len());
let mut namers: HashMap<u32, Vec<usize>> = HashMap::new();
let mut prev = HeaderSet::default();
for (si, o) in own.iter().enumerate() {
work::steps(1);
prev = HeaderSet { default: o.default.or(prev.default), first: o.first.or(prev.first), even: o.even.or(prev.even) };
linked.push(prev);
for id in [o.default, o.first, o.even].into_iter().flatten() {
let v = namers.entry(id).or_default();
if v.last() != Some(&si) {
v.push(si);
}
}
}
Linked { own, linked, namers }
}
/// Section `si`'s headers (or footers), those it leaves out inherited.
pub(crate) fn set(&self, si: usize) -> HeaderSet {
self.linked.get(si).copied().unwrap_or_default()
}
}
/// Header rows are re-placed on every page a table spills onto, so a table is O(header items ×
/// pages) placed items — and an exact-height header row keeps all its overflowing text without
/// growing, so a few KB of document could otherwise fill memory. Once this many items (a row
/// counts at least one) have been repeated across the whole layout (every table together), later pages carry no header rows;
/// pagination still finishes. Far above any real table (three ordinary header rows over some
/// 5 000 pages).
pub(crate) const MAX_HEADER_ITEMS: usize = 500_000;
#[cfg(test)]
thread_local! {
/// A smaller [`MAX_HEADER_ITEMS`] for tests, so a short table can reach it.
pub(crate) static HEADER_ITEMS_CAP: std::cell::Cell<usize> = const { std::cell::Cell::new(MAX_HEADER_ITEMS) };
}
/// [`MAX_HEADER_ITEMS`], or the test cap ([`HEADER_ITEMS_CAP`]) in tests.
pub(crate) fn header_items_cap() -> usize {
#[cfg(test)]
return HEADER_ITEMS_CAP.with(|c| c.get());
#[cfg(not(test))]
MAX_HEADER_ITEMS
}
/// What re-placing `it` costs against [`MAX_HEADER_ITEMS`] (and laying it out against
/// [`textbox::MAX_FLOW_WORK`]): one, and for a clipped group (a floating Drawing Canvas) every
/// member below it, nested groups included, as placing clones them all. Counted with an explicit
/// stack, and counting stops once the total passes `limit` (the result is then just over it), so
/// neither deep nesting nor a huge group makes the count itself costly.
pub(crate) fn placed_cost(it: &Placed, limit: usize) -> usize {
placed_cost_by(it, limit, |_| 1)
}
/// [`placed_cost`] with each item (the group itself and every member) costing `cost` of it.
pub(crate) fn placed_cost_by(it: &Placed, limit: usize, cost: impl Fn(&Placed) -> usize) -> usize {
let mut count = 0usize;
let mut stack: Vec<std::slice::Iter<Placed>> = Vec::new();
let mut next = Some(it);
loop {
let cur = match next.take() {
Some(c) => c,
None => loop {
match stack.last_mut() {
None => return count,
Some(iter) => match iter.next() {
Some(c) => break c,
None => {
stack.pop();
}
},
}
},
};
count = count.saturating_add(cost(cur));
if count > limit {
return count;
}
if let Placed::Clip { items, .. } = cur {
stack.push(items.iter());
}
}
}
/// Header tables can displace a non-overlapping body table. Page-positioned drawings keep
/// their own wrapping rules, so only table areas are retained from the header layout.
pub(crate) fn header_table_areas(ctx: &mut Ctx, pb: &PageBuilder) -> Vec<Rect> {
if pb.web || ctx.opts.view == ViewMode::Read {
return Vec::new();
}
let Some(page) = pb.pages.last() else { return Vec::new() };
let first = pb.pages.iter().rev().nth(1).is_none_or(|prev| prev.section != page.section);
let key = (page.section, page.number, first);
if let Some(areas) = ctx.header_tables.get(&key) {
return areas.clone();
}
let hf = Arc::clone(&ctx.hf);
let Some((_, sect)) = hf.sections.get(page.section) else { return Vec::new() };
if sect.text_direction.is_turned() {
return Vec::new();
}
let set = hf.heads.set(page.section);
let id = if sect.title_page && first {
set.first
} else if ctx.doc.settings.even_odd_headers && page.number.is_multiple_of(2) {
set.even.or(set.default)
} else {
set.default
};
let Some(id) = id else { return Vec::new() };
let Some(part) = ctx.doc.parts.get(&id) else { return Vec::new() };
let blocks = part.blocks.clone();
let x = sect.margin_left + sect.gutter;
let frame = PageFrame { sect, margin_top: Some(page.body.y), origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let base = hf.defining_width(false, page.section, Some(id));
let counters = std::mem::take(&mut ctx.counters);
let boxes = std::mem::take(&mut ctx.boxes);
let chains = std::mem::take(&mut ctx.chains);
let fields = ctx.fields.clone();
let grid = std::mem::take(&mut ctx.grid);
let track = std::mem::replace(&mut ctx.track_totals, false);
ctx.fields.page_format = sect.page_num_format;
ctx.fields.page = page.number;
let mut areas = again(ctx, 0, |ctx| {
with_pct_base(ctx, base, |ctx| layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, Some(frame)))).table_areas
});
ctx.counters = counters;
ctx.boxes = boxes;
ctx.chains = chains;
ctx.fields = fields;
ctx.grid = grid;
ctx.track_totals = track;
for r in &mut areas {
r.x += x;
r.y += sect.header;
}
ctx.header_tables.insert(key, areas.clone());
areas
}
/// Whether a header, a footer or a text box shows a `PAGEREF` field, which they resolve from the
/// paginated body (see [`bookmark_pages`]).
pub(crate) fn headers_footers_refer_to_pages(doc: &Document) -> bool {
use wordcraft_doc::PartKind;
doc.parts.iter().filter(|(_, part)| matches!(part.kind, PartKind::Header | PartKind::Footer | PartKind::TextBox)).any(|(id, _)| {
let story = StoryRef::Part(*id);
doc.para_paths(story)
.iter()
.filter_map(|path| doc.para(story, path))
.any(|p| p.objects.iter().any(|o| matches!(o, InlineObject::Field { instr, .. } if fields::field_name(instr) == "PAGEREF")))
})
}
/// Where each bookmark in the body starts: the number and number format of the page it is placed
/// on (with two of the same name, the first in the document).
pub(crate) fn bookmark_pages(doc: &Document, pages: &[Page], sections: &[(usize, &SectionProps)]) -> fields::BookmarkPages {
// Each paragraph's bookmarks: name, offset, and its line in the paragraph layout last looked
// up (the pieces of a paragraph usually share one).
let mut wanted: HashMap<Path, Vec<(String, usize, Option<(usize, usize)>)>> = HashMap::new();
let mut seen = HashSet::new();
for path in doc.para_paths(StoryRef::Body) {
let Some(p) = doc.para(StoryRef::Body, &path) else { continue };
if !p.objects.iter().any(|o| matches!(o, InlineObject::BookmarkStart { .. })) {
continue;
}
for (off, o) in p.object_offsets().into_iter().zip(&p.objects) {
if let InlineObject::BookmarkStart { name } = o
&& seen.insert(name.as_str())
{
wanted.entry(path.clone()).or_default().push((name.clone(), off, None));
}
}
}
let mut out = fields::BookmarkPages::new();
for page in pages {
if wanted.is_empty() {
break;
}
let format = sections.get(page.section).map(|(_, s)| s.page_num_format).unwrap_or_default();
for it in &page.items {
let Placed::Lines { story: StoryRef::Body, path, para, l0, l1, .. } = it else { continue };
let Some(marks) = wanted.get_mut(path) else { continue };
let layout = Arc::as_ptr(para) as usize;
marks.retain_mut(|(name, off, line)| {
let li = match *line {
Some((k, li)) if k == layout => li,
_ => {
let li = para.line_of(*off);
*line = Some((layout, li));
li
}
};
if !(*l0..*l1).contains(&li) {
return true;
}
out.insert(std::mem::take(name), (page.number, format));
false
});
if marks.is_empty() {
wanted.remove(path);
}
}
}
out
}
/// A key for caches standing for `marks` (0 for none).
pub(crate) fn marks_key(marks: &fields::BookmarkPages) -> u64 {
if marks.is_empty() {
return 0;
}
let mut v: Vec<_> = marks.iter().collect();
v.sort_by(|a, b| a.0.cmp(b.0));
hash_of(&v)
}
pub(crate) fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(usize, &SectionProps)], totals: &Totals) {
let total = pages.len() as u32;
let hf = Arc::clone(&ctx.hf);
let even_odd = ctx.doc.settings.even_odd_headers;
let styles = styleref::headers_use_styleref(ctx.doc).then(|| ctx.cache.style_index(ctx.doc));
let mut first_of_section: HashSet<usize> = HashSet::new();
let mut seen = HashSet::new();
for (i, p) in pages.iter().enumerate() {
if seen.insert(p.section) {
first_of_section.insert(i);
}
}
for (i, page) in pages.iter_mut().enumerate() {
let Some((_, sect)) = sections.get(page.section) else { continue };
// Inherit header/footer references from earlier sections (Word's "link to previous").
let pick = |linked: &Linked| -> Option<u32> {
let set = linked.set(page.section);
if sect.title_page && first_of_section.contains(&i) {
return set.first;
}
if even_odd && page.number % 2 == 0 {
return set.even.or(set.default);
}
set.default
};
let hid = pick(&hf.heads);
let fid = pick(&hf.feet);
page.hf_slot = if sect.title_page && first_of_section.contains(&i) {
HfSlot::First
} else if even_odd && page.number % 2 == 0 {
HfSlot::Even
} else {
HfSlot::Default
};
ctx.fields.page = page.number;
ctx.fields.page_format = sect.page_num_format;
ctx.fields.pages = total;
ctx.fields.section_pages = totals.section(page.section);
ctx.fields.section = page.section as u32 + 1;
// STYLEREF: the body paragraphs on this page.
ctx.fields.styleref = styles.as_ref().map(|ix| {
let paths = page.items.iter().filter_map(|it| match it {
Placed::Lines { story: StoryRef::Body, path, .. } => Some(path),
_ => None,
});
(ix.clone(), ix.page_at(paths, page.first_block))
});
let w = sect.text_width();
let x = sect.margin_left + sect.gutter;
if let Some(id) = hid
&& let Some(part) = ctx.doc.parts.get(&id)
{
let blocks = part.blocks.clone();
let frame = PageFrame { sect, margin_top: Some(page.body.y), origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let base = hf.defining_width(false, page.section, hid);
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, frame.margin_top, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items)
});
for it in &mut items {
it.translate(x, sect.header);
}
page.header = behind.into_iter().chain(items).chain(front).collect();
page.header_story = Some(id);
}
if let Some(id) = fid
&& let Some(part) = ctx.doc.parts.get(&id)
{
let blocks = part.blocks.clone();
let base = hf.defining_width(true, page.section, fid);
// The footer's height fixes where it sits, which page-relative floats need.
let (h, ..) = with_pin(ctx, id, sect, page.number, None, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, None)).height)
});
let frame = PageFrame { sect, margin_top: None, origin: (x, sect.page_h - sect.footer - h), number: page.number, mirror: ctx.mirror };
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, frame.margin_top, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items)
});
for it in &mut items {
it.translate(x, sect.page_h - sect.footer - h);
}
page.footer = behind.into_iter().chain(items).chain(front).collect();
page.footer_story = Some(id);
}
}
ctx.fields.styleref = None;
}
/// Mirrored margins: even pages have the inside margin (and gutter) on the right, so their
/// header, text, notes and footer move to start at the outside margin. Page borders, measured
/// from the page edge, stay.
pub(crate) fn mirror_even_pages(pages: &mut [Page], sections: &[(usize, &SectionProps)]) {
for page in pages.iter_mut().filter(|p| p.number % 2 == 0) {
let Some((_, s)) = sections.get(page.section) else { continue };
let dx = s.margin_right - s.margin_left - s.gutter;
if dx.abs() < 0.001 || !dx.is_finite() {
continue;
}
let borders = page_border_rules(s).len();
for it in page.items.iter_mut().skip(borders).chain(page.header.iter_mut()).chain(page.footer.iter_mut()) {
it.translate(dx, 0.0);
}
page.body.x += dx;
}
}