fix: read OOXML toggles as ST_OnOff, not as element presence

w:evenAndOddHeaders, w:titlePg and w:pageBreakBefore were treated as on
whenever the element existed, but w:val="false"/"0"/"off" turns them off
and both Word and the docx lib write that explicit form.

A file with <w:evenAndOddHeaders w:val="false"/> therefore took the
even-page header variant on every second page, which is empty — the
header and footer vanished from page 2 onwards.
This commit is contained in:
Steffen Becker committed 2026-08-06 19:53:47 +00:00
1 parent f48e5508a8
commit 3a68a144cb
2 files changed
+58 -3

No files matched your search

+5 -3
View File
@@ -776,7 +776,7 @@ function blockAttrs(ppr: Element | null, kind: BlockKind, headingLevel: number |
if (kind === 'body') {
const pb = fc(ppr, 'pageBreakBefore');
if (pb && wVal(pb) !== 'false' && wVal(pb) !== '0') attrs.breakBefore = 'page';
if (pb && onOff(pb)) attrs.breakBefore = 'page';
}
// Paragraph background ("colored field", w:shd) + per-side borders ("rule line", w:pBdr).
@@ -1461,7 +1461,8 @@ function docHasEvenOddHeaders(files: Record<string, Uint8Array>): boolean {
const bytes = files['word/settings.xml'];
if (!bytes) return false;
try {
return !!parseXml(strFromU8(bytes)).getElementsByTagNameNS(W, 'evenAndOddHeaders')[0];
const el = parseXml(strFromU8(bytes)).getElementsByTagNameNS(W, 'evenAndOddHeaders')[0];
return !!el && onOff(el);
} catch {
return false;
}
@@ -1489,7 +1490,8 @@ function parseSectPr(sect: Element | null, ctx: Ctx, oddEven = false): {
: null;
// Different first page: w:titlePg turns on the "first"-type refs for page 1.
const titlePg = !!fc(sect, 'titlePg');
const titlePgEl = fc(sect, 'titlePg');
const titlePg = !!titlePgEl && onOff(titlePgEl);
const refId = (type: string, variant: 'default' | 'first' | 'even' = 'default') => {
const ref = fcAll(sect, `${type}Reference`).find((r) => (r.getAttributeNS(W, 'type') ?? 'default') === variant);
return ref?.getAttributeNS(R, 'id') ?? null;
+53
View File
@@ -0,0 +1,53 @@
// OOXML toggles are ST_OnOff: the element being present does not mean "on", since
// w:val="false"/"0"/"off" turns it off. Word and the docx lib both write the explicit
// false form. jsdom (vitest `environment`) supplies the global DOMParser.
import { describe, it, expect } from 'vitest';
import { zipSync, strToU8 } from 'fflate';
import { importDocx } from '../../src/lib/import/docx';
const CT = `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>`;
const RELS = `<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`;
const DOC_RELS = `<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rH" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>
</Relationships>`;
const W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
const HEADER = `<?xml version="1.0"?><w:hdr ${W}><w:p><w:r><w:t>Running head</w:t></w:r></w:p></w:hdr>`;
const DOCUMENT = `<?xml version="1.0"?><w:document ${W} xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<w:body><w:p><w:r><w:t>Body</w:t></w:r></w:p>
<w:sectPr><w:headerReference w:type="default" r:id="rH"/><w:titlePg w:val="false"/></w:sectPr>
</w:body></w:document>`;
const settings = (val: string) =>
`<?xml version="1.0"?><w:settings ${W}><w:evenAndOddHeaders w:val="${val}"/></w:settings>`;
const build = (settingsXml: string) => zipSync({
'[Content_Types].xml': strToU8(CT),
'_rels/.rels': strToU8(RELS),
'word/_rels/document.xml.rels': strToU8(DOC_RELS),
'word/document.xml': strToU8(DOCUMENT),
'word/header1.xml': strToU8(HEADER),
'word/settings.xml': strToU8(settingsXml),
});
describe('DOCX on/off toggles', () => {
it('reads an explicit false as off', () => {
const r = importDocx(build(settings('false')));
expect(r.header).not.toBeNull();
// Both off, so the one header repeats on every page instead of odd/even variants.
expect(r.differentOddEven).toBe(false);
expect(r.differentFirstPage).toBe(false);
});
it('still reads a bare element as on', () => {
const r = importDocx(build(`<?xml version="1.0"?><w:settings ${W}><w:evenAndOddHeaders/></w:settings>`));
expect(r.differentOddEven).toBe(true);
});
});