fix: a title-page master governs one page, not the whole document
An ODF master page whose style:next-style-name names another master applies to a single page. Read as the section's running header, this file's full-page title background repeated on every page and its height pushed each block onto its own — 276 pages instead of 16. Its zones now become the first-page variants, the successor's the running ones, and a positioned frame in a zone is dropped as in DOCX.
This commit is contained in:
1 parent
28a39e017a
commit
2dcf8b8a20
3 files changed
+29
-21
No files matched your search
@@ -23,7 +23,7 @@ The **Open .odt** button (`App.svelte`) parses an uploaded file with `importOdt(
|
||||
|
||||
## Header/footer import
|
||||
|
||||
- **Import:** `StyleResolver.masterPageHF()` reads `style:header`/`style:footer` + the header/footer-style heights; `pageGeometry()` reconstructs the body margin (page margin + zone height + spacing) and `edgeDistancesCm()` returns the raw page margins as the zone distances. The header↔body spacing is only reserved when `style:dynamic-spacing="false"` — a `true` (LibreOffice's usual) means the gap collapses, so reserving it would shove the body down. `convertHfZone` → single-paragraph doc (extra paragraphs → hard breaks; a text-less zone that carries a background/border rule line is kept, box props merged with the bottom rule from the last paragraph); `convertInline(…, hfFields=true)` keeps `text:page-number`/`-count` as field nodes. Per-page variants (`header-first`/`-left`) are ignored with a warning. `hfIsEmpty` counts a rule-/shading-only paragraph as non-empty so it renders and exports.
|
||||
- **Import:** `StyleResolver.masterPageHF()` reads `style:header`/`style:footer` + the header/footer-style heights; `pageGeometry()` reconstructs the body margin (page margin + zone height + spacing) and `edgeDistancesCm()` returns the raw page margins as the zone distances. The header↔body spacing is only reserved when `style:dynamic-spacing="false"` — a `true` (LibreOffice's usual) means the gap collapses, so reserving it would shove the body down. `convertHfZone` → single-paragraph doc (extra paragraphs → hard breaks; a text-less zone that carries a background/border rule line is kept, box props merged with the bottom rule from the last paragraph); `convertInline(…, hfFields=true)` keeps `text:page-number`/`-count` as field nodes. A master page whose `style:next-style-name` points at *another* master governs one page only (the "different first page" idiom): its own zones become the first-page variants and the successor's the running ones — read as the running header instead, a title-page master repeats on every page of its section and its height pushes each block onto its own page. As in DOCX, a zone's positioned (non-`as-char`) frames are dropped with a warning: the one-paragraph zone can't place them, and a page-sized background reserves the whole page inline. `hfIsEmpty` counts a rule-/shading-only paragraph as non-empty so it renders and exports.
|
||||
|
||||
## Defaults on DOCX import
|
||||
|
||||
|
||||
@@ -1428,11 +1428,14 @@ function convertInline(root: Element, ctx: Ctx, baseProps: PropMap, defaults: Bl
|
||||
}
|
||||
if (e.namespaceURI === NS.draw) {
|
||||
const conv = convertDrawElement(e, ctx);
|
||||
// The one-paragraph header/footer schema holds inline images (forced
|
||||
// as-character) but not boxes — those are dropped with a warning.
|
||||
// The one-paragraph zone takes as-char images only — boxes and positioned
|
||||
// frames (a page-sized background reserves the whole page inline) are dropped
|
||||
// with a warning, as the DOCX importer does.
|
||||
if (hfFields) {
|
||||
if (conv?.inline?.type === 'image') out.push({ ...conv.inline, attrs: { ...conv.inline.attrs, wrap: 'inline' } });
|
||||
else if (conv) ctx.warnings.add('Drawings were removed');
|
||||
const wrap = conv?.inline?.attrs?.wrap;
|
||||
if (conv?.inline?.type === 'image' && (!wrap || wrap === 'inline')) {
|
||||
out.push({ ...conv.inline, attrs: { ...conv.inline.attrs, wrap: 'inline' } });
|
||||
} else if (conv) ctx.warnings.add('Drawings were removed');
|
||||
continue;
|
||||
}
|
||||
if (conv?.inline) out.push(conv.inline);
|
||||
|
||||
@@ -518,22 +518,27 @@ export class StyleResolver {
|
||||
const mp = this.masterPageEl(pageName);
|
||||
const layout = this.pageLayoutEl();
|
||||
|
||||
const zone = (local: string): Element | null => {
|
||||
if (!mp) return null;
|
||||
for (const child of Array.from(mp.children)) {
|
||||
if (child.namespaceURI === NS.style && child.localName === local) return child;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// header-first/footer-first (ODF 1.3 style: or older LibreOffice loext:) — match
|
||||
// by local name in either namespace so both producers round-trip.
|
||||
const firstZone = (local: string): Element | null => {
|
||||
if (!mp) return null;
|
||||
for (const child of Array.from(mp.children)) {
|
||||
// Match by local name in either namespace: header-first/footer-first are ODF 1.3
|
||||
// style: or older LibreOffice loext:, so both producers round-trip.
|
||||
const zoneIn = (owner: Element | null, local: string): Element | null => {
|
||||
if (!owner) return null;
|
||||
for (const child of Array.from(owner.children)) {
|
||||
if (child.localName === local && (child.namespaceURI === NS.style || child.namespaceURI === NS.loext)) return child;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// A master page naming a different style:next-style-name governs one page and then
|
||||
// hands over — the "different first page" idiom. Its own zones become the first-page
|
||||
// variants, the successor's the ones every later page in the section uses.
|
||||
const nextName = mp?.getAttributeNS(NS.style, 'next-style-name') ?? null;
|
||||
const successor = nextName && nextName !== mp?.getAttributeNS(NS.style, 'name')
|
||||
? this.masterPageEl(nextName)
|
||||
: null;
|
||||
const rest = successor ?? mp;
|
||||
|
||||
const zone = (local: string) => zoneIn(rest, local);
|
||||
const firstZone = (local: string): Element | null =>
|
||||
zoneIn(mp, `${local}-first`) ?? (successor ? zoneIn(mp, local) : null);
|
||||
const extraCm = (local: 'header-style' | 'footer-style', spacingAttr: 'margin-bottom' | 'margin-top'): number => {
|
||||
if (!layout) return 0;
|
||||
let props: Element | null = null;
|
||||
@@ -558,11 +563,11 @@ export class StyleResolver {
|
||||
return {
|
||||
header: zone('header'),
|
||||
footer: zone('footer'),
|
||||
headerFirst: firstZone('header-first'),
|
||||
footerFirst: firstZone('footer-first'),
|
||||
headerFirst: firstZone('header'),
|
||||
footerFirst: firstZone('footer'),
|
||||
// Even-page variant (Word odd/even). ODF header-left = the left (even) page.
|
||||
headerLeft: firstZone('header-left'),
|
||||
footerLeft: firstZone('footer-left'),
|
||||
headerLeft: zoneIn(rest, 'header-left'),
|
||||
footerLeft: zoneIn(rest, 'footer-left'),
|
||||
headerExtraCm: extraCm('header-style', 'margin-bottom'),
|
||||
footerExtraCm: extraCm('footer-style', 'margin-top'),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user