fix: naming a page style is a page break, same master or not

Repeating the current master was read as "no switch, no break", so a book
whose chapter heading style names its own page style ran the chapters
together — a fixture lost the break before every chapter. An explicitly empty
name now ends the parent walk, so nothing inherits a break it never asked
for; the section itself still starts only where the master changes.
This commit is contained in:
Steffen Becker committed 2026-08-11 07:30:45 +00:00
1 parent 361ce14864
commit 12f2e7a623
3 files changed
+13 -8

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ The **Open .odt** button (`App.svelte`) parses an uploaded file with `importOdt(
- **Content controls** (`inlineChildren`, DOCX): a `w:sdt` inside a paragraph — Word wraps every citation and bibliography entry in one — is walked through to its `w:sdtContent`, or the runs inside it never reach the paragraph (the thesis fixture lost every `[7]`-style reference and broke its lines one page short).
- **Formulas** (`convertFormulaFrame`): a `<draw:frame>` whose `<draw:object>` resolves to a sub-document with a `math:math` root (or holds one inline) → a `formula` node. Our own `<annotation encoding="application/x-tex">` is preferred; LibreOffice strips it, so the MathML is parsed back to LaTeX instead. **`display` comes from the paragraph** (`aloneInParagraph`), not the object — LibreOffice writes `display="block"` on every formula it re-saves. DOCX: `m:oMath`/`m:oMathPara` sit beside the `w:r` runs in their own namespace, so `convertInline` probes them ahead of its `namespaceURI !== W` guard (that guard silently dropped every Word formula). See `docs/architecture/formulas.md`.
- **Bookmarks & cross-references**: `text:bookmark-start`/`-end` (ODF) and `w:bookmarkStart`/`End` (DOCX) become a `bookmark` **mark** on the text they cover; the open ranges live in `Ctx` because Word writes them beside paragraphs as well as beside runs, and a range may close in a later paragraph. A **point** bookmark has no range to mark and is dropped, `_GoBack` with it. `text:bookmark-ref` and `REF`/`PAGEREF` fields (the `fldChar` machine and `w:fldSimple` alike, including a field Word left unresolved) become `crossRef` nodes carrying the producer's cached text. A `w:hyperlink w:anchor` with no `r:id` is an internal link → `#name`. See `docs/architecture/formatting.md`.
- **Page geometry is the *body's* master page**, not the one named `Standard`: the importer counts the blocks each `style:master-page-name` governs and re-reads geometry (and the app's own header/footer) from the winner. A document whose Standard master no paragraph references would otherwise take margins — and a footer band — no page of it has. A **master switch always breaks the page** (LibreOffice's "page break with page style"), except for the master the document opens with; `fo:break-after="page"` becomes the next block's `breakBefore`, the only break the editor stores. A master with a `style:next-style-name` is itself the different-first-page flag: its own (possibly absent) zones govern page one.
- **Page geometry is the *body's* master page**, not the one named `Standard`: the importer counts the blocks each `style:master-page-name` governs and re-reads geometry (and the app's own header/footer) from the winner. A document whose Standard master no paragraph references would otherwise take margins — and a footer band — no page of it has. **Naming a master breaks the page** (LibreOffice's "page break with page style") — probed: even a style naming the master the page already uses breaks, which is how a book starts each chapter fresh, so only the document's first block is exempt. The *section* (its own header/footer set) starts only where the master really changes. An empty `style:master-page-name=""` is LibreOffice's explicit "no change" and ends the parent-chain walk, so an ancestor's master can't break every child style. `fo:break-after="page"` becomes the next block's `breakBefore`, the only break the editor stores. A master with a `style:next-style-name` is itself the different-first-page flag: its own (possibly absent) zones govern page one.
- **A page-anchored frame** (`text:anchor-type="page"`) is out of the flow: it rides a paragraph of its own that collapses to nothing, and the node view places it from its page's corner behind the text (`anchorPage`, `image.ts`). In a cell it falls back to an ordinary floating frame.
- **A header/footer zone is fitted to its schema** (`fitZoneSchema`): the zone editor has fewer marks and nodes than the body, and one it doesn't know makes `generateHTML` throw — which renders the whole zone blank. Unknown marks are dropped, unknown nodes leave their text.
- **A hoisted text box keeps what its anchor paragraph gave it**: an as-char figure frame is centred by the paragraph it sits in, so `wrapAlign` carries that (or the frame's own `style:horizontal-pos`) onto the box. Its `fo:padding` is read too — the editor's own 0.15cm ring is only the default.
+9 -6
View File
@@ -1250,12 +1250,15 @@ function convertParaLike(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault =
// A style:master-page-name switches the page master, which is how ODF gives a section
// its own header/footer; the block that does it opens that section.
const master = kind === 'body' ? resolver.masterPageOf(styleName) : null;
if (master && ctx.masterPages[ctx.masterPages.length - 1] !== master) {
ctx.masterPages.push(master);
attrs.sectionBreak = true;
// Switching the page master starts a new page in LibreOffice, always — except for
// the master the document opens with, which is where the text already is.
if (ctx.masterPages.length > 1) attrs.breakBefore = 'page';
if (master) {
// Naming a master *is* a page break, even where the page already uses that one
// (probed) — which is how a book starts every chapter on a fresh page. Only the
// document's first block has nothing above it to break from.
if (ctx.masterBlocks.size) attrs.breakBefore = 'page';
if (ctx.masterPages[ctx.masterPages.length - 1] !== master) {
ctx.masterPages.push(master);
attrs.sectionBreak = true;
}
}
// '' = the file's own default master, which the blocks before the first switch use.
const governing = kind === 'body' ? ctx.masterPages[ctx.masterPages.length - 1] ?? '' : null;
+3 -1
View File
@@ -519,8 +519,10 @@ export class StyleResolver {
while (cur && !seen.has(cur)) {
seen.add(cur);
const el = this.paraStyleEls.get(cur);
// Present but empty is LibreOffice's explicit "no page style change", so it ends
// the walk instead of letting an ancestor's break through to every child style.
const name = el?.getAttributeNS(NS.style, 'master-page-name');
if (name) return name;
if (name != null) return name || null;
cur = this.styles.get(`paragraph\0${cur}`)?.parent ?? null;
}
return null;