fix: DOCX cell spacing accretion and nested-list kind flips
Found by the cross-format fuzz leg. importDocx baked the resolved cell chain in as direct formatting even where it equals what an unset attr already renders (0/0 spacing; single line height over a single default) — every re-imported own file grew spacing attrs on every cell paragraph. And a numbering level kept the first list kind it saw, so a nested bullet list beside a nested ordered one came back ordered; a kind conflict now forks its own numbering reference, as the ODT side mints a list style.
This commit is contained in:
1 parent
d78181fdd9
commit
76856f4aff
3 files changed
+26
-2
No files matched your search
+11
-1
@@ -238,6 +238,11 @@ class Numbering {
|
||||
return reference;
|
||||
}
|
||||
|
||||
levelKindAt(reference: string, depth: number): 'ordered' | 'bullet' | null {
|
||||
const l = this.map.get(reference)!.find((lv) => lv.level === depth);
|
||||
return l ? (l.format === LevelFormat.BULLET ? 'bullet' : 'ordered') : null;
|
||||
}
|
||||
|
||||
ensureLevel(reference: string, depth: number, node: TiptapNode, extraIndentCm: number, cycle: OrderedCycle): void {
|
||||
// A multilevel top list turns the whole reference into a "%1.%2." chain; its
|
||||
// attr-less nested lists inherit it (the top registers first — pre-order walk).
|
||||
@@ -1475,7 +1480,12 @@ function listToParagraphs(
|
||||
let numberedFirst = false;
|
||||
for (const child of item.content ?? []) {
|
||||
if (child.type === 'bulletList' || child.type === 'orderedList') {
|
||||
listToParagraphs(child, depth + 1, reference, indentCm, num, out, cChild);
|
||||
// A level keeps the first kind it saw, so a sibling nested list of the other
|
||||
// kind forks its own reference (as the ODT side mints its own list style).
|
||||
const seen = num.levelKindAt(reference, depth + 1);
|
||||
const ref = seen && seen !== (child.type === 'orderedList' ? 'ordered' : 'bullet')
|
||||
? num.newReference() : reference;
|
||||
listToParagraphs(child, depth + 1, ref, indentCm, num, out, cChild);
|
||||
} else if (child.type === 'paragraph' || child.type === 'heading') {
|
||||
if (!numberedFirst) {
|
||||
out.push(paragraphToDocx(child, { numbering: { reference, level: depth } }));
|
||||
|
||||
@@ -940,6 +940,16 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault:
|
||||
const attrs = blockAttrs(ppr, kind, level, directJc ? jcVal : null,
|
||||
kind === 'cell' ? ctx.styles.paragraphSpacing(styleId, ctx.cellSpacing) : {});
|
||||
applyContextualSpacing(el, ppr, ctx, styleId, attrs);
|
||||
// The baked-in cell chain must not accrete no-op direct formatting: in a cell,
|
||||
// unset spacing renders 0/0 (no word processor passes the default style's spacing
|
||||
// into a cell), and single line height is only kept over a non-single default.
|
||||
if (kind === 'cell') {
|
||||
if (attrs.spaceBefore === 0) delete attrs.spaceBefore;
|
||||
if (attrs.spaceAfter === 0) delete attrs.spaceAfter;
|
||||
const def = ctx.styles.paragraphSpacing(null);
|
||||
const defSingle = (def.line ?? 240) === 240 && (!def.lineRule || def.lineRule === 'auto');
|
||||
if (attrs.lineHeight === '1' && defSingle) delete attrs.lineHeight;
|
||||
}
|
||||
// Widow-orphan control has no registry home, so the resolved value rides the block.
|
||||
const directWc = fc(ppr, 'widowControl');
|
||||
if (!(directWc ? onOff(directWc) : ctx.styles.paragraphWidowControl(styleId))) {
|
||||
|
||||
@@ -156,6 +156,10 @@ describe('a table style’s w:pPr ranks under the paragraph style', () => {
|
||||
it('round-trips the baked values', async () => {
|
||||
const doc = { type: 'doc', content: importDocx(spacedTable()).content.content };
|
||||
const bytes = await buildDocx(doc as N, { top: 2, bottom: 2, left: 2, right: 2 });
|
||||
expect(attrsOf(bytes)).toMatchObject({ spaceAfter: 8, lineHeight: '1' });
|
||||
// Our own export's default line height is single again, so the re-import drops
|
||||
// the baked '1' as default-equal; the non-default space after stays.
|
||||
const attrs = attrsOf(bytes);
|
||||
expect(attrs).toMatchObject({ spaceAfter: 8 });
|
||||
expect(attrs.lineHeight).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user