fix: honour w:contextualSpacing on import

Word's List Paragraph carries it, so consecutive list points sit line-tight.
Without it every one of them took the document default's 8pt after, and a
document that is mostly bullets drifted more than a page.
This commit is contained in:
Steffen Becker committed 2026-08-08 08:30:04 +00:00
1 parent 821688c2cb
commit 9f6736d5a8
2 files changed
+26

No files matched your search

+13
View File
@@ -685,6 +685,7 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault:
// w:pPr counts as formatting on the block.
const attrs = blockAttrs(ppr, kind, level, directJc ? jcVal : null, {});
const styleId = styleIdOf(ppr, ctx);
applyContextualSpacing(el, ppr, ctx, styleId, attrs);
// 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))) {
@@ -719,6 +720,18 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault:
return node;
}
// w:contextualSpacing drops a paragraph's own spacing towards a neighbour of the same
// style — Word's List Paragraph carries it, which is why list points sit line-tight. The
// editor has no such mode, so the suppressed side becomes an explicit 0.
function applyContextualSpacing(el: Element, ppr: Element | null, ctx: Ctx, styleId: string | null, attrs: Record<string, unknown>): void {
const direct = fc(ppr, 'contextualSpacing');
if (!(direct ? onOff(direct) : ctx.styles.paragraphContextualSpacing(styleId))) return;
const sameStyle = (sib: Element | null) =>
!!sib && sib.namespaceURI === W && sib.localName === 'p' && styleIdOf(fc(sib, 'pPr'), ctx) === styleId;
if (sameStyle(el.previousElementSibling)) attrs.spaceBefore = 0;
if (sameStyle(el.nextElementSibling)) attrs.spaceAfter = 0;
}
// The paragraph mark's resolved font size (w:pPr/w:rPr, incl. its rStyle), as a CSS
// pt string, or null when it matches what the block renders at anyway (suppressed
// like run sizes, against the same yardstick).
+13
View File
@@ -124,6 +124,7 @@ export class DocxStyles {
private defaultsAlign: string | null = null; // docDefaults w:pPrDefault/w:jc
private defaultsSpacing: ParaSpacing = {}; // docDefaults w:pPrDefault/w:spacing
private ownWidow = new Map<string, boolean>(); // style's own w:pPr/w:widowControl
private ownContextual = new Map<string, boolean>(); // style's own w:pPr/w:contextualSpacing
private ownTabs = new Map<string, TabStop[]>(); // style's own w:pPr/w:tabs
private defaultsWidow: boolean | null = null; // docDefaults w:pPrDefault/w:widowControl
private numToAbstract = new Map<string, string>();
@@ -186,6 +187,8 @@ export class DocxStyles {
if (sp) this.ownSpacing.set(id, readSpacing(sp));
const wc = ppr && firstChild(ppr, 'widowControl');
if (wc) this.ownWidow.set(id, toggle(wc));
const cs = ppr && firstChild(ppr, 'contextualSpacing');
if (cs) this.ownContextual.set(id, toggle(cs));
const tabs = ppr && firstChild(ppr, 'tabs');
if (tabs) this.ownTabs.set(id, readTabStops(tabs));
const ind = ppr && firstChild(ppr, 'ind');
@@ -329,6 +332,16 @@ export class DocxStyles {
return this.paragraphTabs(this.basedOn.get(styleId) ?? null, seen);
}
// w:contextualSpacing along the w:basedOn chain: the style drops its own spacing
// between neighbouring paragraphs that share it (Word's List Paragraph does).
paragraphContextualSpacing(styleId: string | null | undefined, seen = new Set<string>()): boolean {
if (!styleId || seen.has(styleId)) return false;
seen.add(styleId);
const own = this.ownContextual.get(styleId);
if (own != null) return own;
return this.paragraphContextualSpacing(this.basedOn.get(styleId) ?? null, seen);
}
private styleWidow(styleId: string | null | undefined, seen = new Set<string>()): boolean | null {
if (!styleId || seen.has(styleId)) return null;
seen.add(styleId);