fix: the running head keeps the chapter name its field cached
A STYLEREF field's result runs are the chapter a reader shows before it repaginates. The DOCX import dropped them and set the field's text empty, so a header saved as .docx came back with a blank running head. ODF kept it. Found by comparing the header and footer zones on a round trip, which nothing did — they are documents of their own and hold what only they can. The corpus gains two legs besides: a second save, and Word's dialect on its way out as ODT.
This commit is contained in:
1 parent
a66ee78ba2
commit
1efe981872
5 files changed
+44
-7
No files matched your search
@@ -108,6 +108,10 @@ Body text with no resolved font falls back to the *document's own theme minor fo
|
||||
on the page layout governing the body (`HfSet.pageNumberFormat`), which is how a roman
|
||||
front matter precedes a decimal body; Word's is `w:pgNumType w:fmt` per section, and the
|
||||
document's own comes from the **first** section, as the paper and the margins already do.
|
||||
- **The running head keeps the name the field cached.** A `STYLEREF` field's result runs
|
||||
are the chapter a reader shows before it repaginates, so `chapterField.text` takes them
|
||||
(`emitField`, both the `fldSimple` and `fldChar` forms).
|
||||
|
||||
- **A header row is `w:tblHeader` on the row**, which says what ODF's
|
||||
`<table:table-header-rows>` says: its cells become `tableHeader`, and the export writes
|
||||
the flag for a row whose cells are header cells even where the table asks for no repeat.
|
||||
|
||||
@@ -1579,7 +1579,7 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD
|
||||
else {
|
||||
const mark = !hfFields && indexEntryFromInstr(fieldInstr);
|
||||
if (mark) out.push(mark);
|
||||
else emitField(out, fieldInstr, hfFields, marks);
|
||||
else emitField(out, fieldInstr, hfFields, marks, fieldResultText);
|
||||
}
|
||||
fieldMode = 'none';
|
||||
fieldDateTime = null;
|
||||
@@ -1594,7 +1594,7 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD
|
||||
case 'delText':
|
||||
case 't':
|
||||
if (customMark !== null && (child.textContent ?? '') === customMark) { customMark = null; break; }
|
||||
if ((fieldShown || fieldSeq) && fieldMode === 'result') fieldResultText += child.textContent ?? '';
|
||||
if ((fieldShown || fieldSeq || hfFields) && fieldMode === 'result') fieldResultText += child.textContent ?? '';
|
||||
else if (!skipResult()) pushText(child.textContent ?? '', marks);
|
||||
break;
|
||||
case 'ruby': {
|
||||
@@ -1671,7 +1671,7 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD
|
||||
}
|
||||
case 'fldSimple': {
|
||||
const instr = el.getAttributeNS(W, 'instr') ?? '';
|
||||
if (hfFields) { const first = fcAll(el, 'r')[0]; emitField(out, instr, true, first ? runMarks(first) : []); break; }
|
||||
if (hfFields) { const first = fcAll(el, 'r')[0]; emitField(out, instr, true, first ? runMarks(first) : [], el.textContent ?? ''); break; }
|
||||
const xref = crossRefFromInstr(instr);
|
||||
if (xref) {
|
||||
const first = fcAll(el, 'r')[0];
|
||||
@@ -1726,7 +1726,7 @@ function formulaNode(el: Element, ctx: Ctx): Node | null {
|
||||
return { type: 'formula', attrs: { latex, display } };
|
||||
}
|
||||
|
||||
function emitField(out: Node[], instr: string, hfFields: boolean, marks: Mark[] = []): void {
|
||||
function emitField(out: Node[], instr: string, hfFields: boolean, marks: Mark[] = [], cached = ''): void {
|
||||
if (!hfFields) return;
|
||||
// The atom carries the field run's marks so its digits render in the run's font/size.
|
||||
const push = (type: string) => out.push(marks.length ? { type, marks } : { type });
|
||||
@@ -1736,7 +1736,9 @@ function emitField(out: Node[], instr: string, hfFields: boolean, marks: Mark[]
|
||||
// level rides in the style name ("Heading 2", or a localized "Überschrift 2").
|
||||
else {
|
||||
const m = /\bSTYLEREF\s+"?[^"\d]*(\d)/i.exec(instr);
|
||||
if (m) out.push({ type: 'chapterField', attrs: { level: Number(m[1]), text: '' }, ...(marks.length ? { marks } : {}) });
|
||||
// The field's cached result is the chapter name a reader shows before it
|
||||
// repaginates; this editor's own layer overwrites it on the first pass.
|
||||
if (m) out.push({ type: 'chapterField', attrs: { level: Number(m[1]), text: cached }, ...(marks.length ? { marks } : {}) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,19 @@ describe.skipIf(!files.length)('the authored corpus', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// A second save is the one that drifts: an importer storing as direct formatting what
|
||||
// it read from a style writes that back out, and the file grows a little each pass.
|
||||
for (const f of files) {
|
||||
it(`${f} does not drift on a second save`, async () => {
|
||||
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
||||
const build = f.endsWith('.odt') ? buildOdt : buildDocx;
|
||||
const read = f.endsWith('.odt') ? importOdt : importDocx;
|
||||
const once = read(await build(readAny(f, load(f)).content, margins, 'portrait', undefined, undefined, undefined, readAny(f, load(f)).styles));
|
||||
const twice = read(await build(once.content, margins, 'portrait', undefined, undefined, undefined, once.styles));
|
||||
expect(look(twice.content)).toEqual(look(once.content));
|
||||
});
|
||||
}
|
||||
|
||||
// Word re-saves of the same documents (tests/corpus/word/): Word rewrites the file
|
||||
// in its own dialect — rsids, proofErr, separator notes, theme docDefaults — which
|
||||
// neither the docx lib nor LibreOffice produce, so this is the third author here.
|
||||
@@ -120,6 +133,13 @@ describe.skipIf(!files.length)('the authored corpus', () => {
|
||||
const again = importDocx(await buildDocx(doc, margins, 'portrait'));
|
||||
expect(outline(again.content)).toEqual(outline(doc));
|
||||
});
|
||||
|
||||
it(`word/${f} keeps its look on the way out as ODT`, async () => {
|
||||
const read = importDocx(new Uint8Array(readFileSync(join(WORD, f))));
|
||||
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
||||
const asOdt = await buildOdt(read.content as N, margins, 'portrait', undefined, undefined, undefined, read.styles);
|
||||
expect(look(importOdt(asOdt).content)).toEqual(look(read.content as N));
|
||||
});
|
||||
}
|
||||
|
||||
// The same document in both formats, and each one exported as the other: the four
|
||||
|
||||
@@ -52,5 +52,14 @@ describe.each([['ODT', buildOdt, importOdt], ['DOCX', buildDocx, importDocx]] as
|
||||
expect(res.warnings ?? []).toEqual([]);
|
||||
expect(firstDiff(clean(doc), clean(res.content))).toBeNull();
|
||||
});
|
||||
|
||||
// The zones are documents of their own, and hold what only they can: the running
|
||||
// head, the page number and count.
|
||||
it('brings its header and footer back as they went out', async () => {
|
||||
const res = read(await (build as N)(kitchenSinkDoc() as N, ...args)) as N;
|
||||
for (const zone of ['header', 'footer'] as const) {
|
||||
expect(firstDiff(clean((o.hf as N)[zone]), clean(res[zone])), zone).toBeNull();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -37,11 +37,13 @@ function styledTable(): N {
|
||||
if (paint.fill) attrs.backgroundColor = paint.fill;
|
||||
for (const [k, v] of Object.entries(paint.borders)) if (v !== null) attrs[k] = v;
|
||||
if (paint.regions.length) attrs.region = paint.regions.join(' ');
|
||||
return { type: 'tableCell', attrs,
|
||||
// The first row is the table's heading, the shape a file's <table:table-header-rows>
|
||||
// / w:tblHeader imports as: header cells that repeat over a page break.
|
||||
return { type: ri === 0 ? 'tableHeader' : 'tableCell', attrs,
|
||||
content: [p([t(`Zelle ${ri + 1}.${ci + 1}`)])] };
|
||||
}),
|
||||
}));
|
||||
return { type: 'table', attrs: { tableStyle: name, tableLook: tableLookAttr(look) }, content: rows };
|
||||
return { type: 'table', attrs: { tableStyle: name, tableLook: tableLookAttr(look), repeatHeader: true }, content: rows };
|
||||
}
|
||||
|
||||
export function kitchenSinkDoc(): N {
|
||||
|
||||
Reference in new issue
Block a user