fix: import space after paragraph
This commit is contained in:
1 parent
1fb81c389f
commit
0e96eaab97
2 files changed
+62
-20
No files matched your search
+22
-20
@@ -1,5 +1,5 @@
|
||||
import { unzipSync, strFromU8 } from 'fflate';
|
||||
import { DocxStyles, parseRunProps, mergeRunProps, readNumPr, wVal, W, R, WP, A, WPS, MC, VML, PKG_REL, type RunProps } from './docxStyles';
|
||||
import { DocxStyles, parseRunProps, mergeRunProps, readNumPr, wVal, W, R, WP, A, WPS, MC, VML, PKG_REL, type RunProps, type ParaSpacing } from './docxStyles';
|
||||
import { lengthToPt } from './styleResolver';
|
||||
import { HEADING_STYLE_OVERRIDES, normalizeColor } from '../export/odt';
|
||||
import { HEADER_SHADE } from '../editor/extensions/tableHeaderRow';
|
||||
@@ -375,7 +375,11 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault:
|
||||
// paragraph style commonly carries justify), so style-level alignment isn't lost.
|
||||
const directJc = fc(ppr, 'jc');
|
||||
const jcVal = directJc ? wVal(directJc) : ctx.styles.paragraphAlign(pStyle ? wVal(pStyle) : null);
|
||||
const attrs = blockAttrs(ppr, kind, level, jcVal);
|
||||
// Spacing: a paragraph style's own w:spacing (e.g. "No Spacing" after=0) is honored so
|
||||
// it isn't lost to the editor default; direct w:pPr wins per attribute (in blockAttrs).
|
||||
// Headings keep their em-based editor defaults (direct-only), so skip style resolution.
|
||||
const styleSpacing = level == null ? ctx.styles.paragraphSpacing(pStyle ? wVal(pStyle) : null) : {};
|
||||
const attrs = blockAttrs(ppr, kind, level, jcVal, styleSpacing);
|
||||
const baseRun = mergeRunProps(ctx.styles.defaultRun(), ctx.styles.styleOwn(pStyle ? wVal(pStyle) : null));
|
||||
const content = convertInline(el, ctx, baseRun, level, false, boldByDefault);
|
||||
|
||||
@@ -426,32 +430,30 @@ function headingLevelOf(ppr: Element | null, ctx: Ctx): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Spacing/indent come from DIRECT w:pPr only — Word stores per-paragraph formatting
|
||||
// inline (style-level defaults stay in the style and are left as the editor's defaults).
|
||||
// Alignment (jcVal) is resolved through the style chain by the caller.
|
||||
function blockAttrs(ppr: Element | null, kind: BlockKind, headingLevel: number | null, jcVal: string | null): Record<string, unknown> {
|
||||
// Spacing = the paragraph style's own w:spacing (styleSpacing, resolved by the caller)
|
||||
// overridden per-attribute by DIRECT w:pPr; indent comes from direct w:pPr only.
|
||||
// docDefaults stay the editor's defaults. jcVal is resolved through the chain by the caller.
|
||||
function blockAttrs(ppr: Element | null, kind: BlockKind, headingLevel: number | null, jcVal: string | null, styleSpacing: ParaSpacing): Record<string, unknown> {
|
||||
const attrs: Record<string, unknown> = {};
|
||||
|
||||
if (jcVal === 'center') attrs.textAlign = 'center';
|
||||
else if (jcVal === 'both' || jcVal === 'distribute') attrs.textAlign = 'justify';
|
||||
else if (jcVal === 'right' || jcVal === 'end') attrs.textAlign = 'right';
|
||||
|
||||
if (!ppr) return attrs;
|
||||
|
||||
const sp = fc(ppr, 'spacing');
|
||||
if (sp) {
|
||||
const before = intAttr(sp, W, 'before');
|
||||
const after = intAttr(sp, W, 'after');
|
||||
if (before != null) attrs.spaceBefore = snapPt(twipToPt(before));
|
||||
if (after != null) attrs.spaceAfter = snapPt(twipToPt(after));
|
||||
const line = intAttr(sp, W, 'line');
|
||||
const rule = sp.getAttributeNS(W, 'lineRule');
|
||||
if (line != null && (!rule || rule === 'auto')) {
|
||||
const mult = round2(line / 240);
|
||||
if (Math.abs(mult - 1) > 0.01) attrs.lineHeight = String(mult);
|
||||
}
|
||||
const sp = ppr ? fc(ppr, 'spacing') : null;
|
||||
const before = (sp ? intAttr(sp, W, 'before') : null) ?? styleSpacing.before ?? null;
|
||||
const after = (sp ? intAttr(sp, W, 'after') : null) ?? styleSpacing.after ?? null;
|
||||
const line = (sp ? intAttr(sp, W, 'line') : null) ?? styleSpacing.line ?? null;
|
||||
const rule = (sp && sp.getAttributeNS(W, 'lineRule')) || styleSpacing.lineRule || null;
|
||||
if (before != null) attrs.spaceBefore = snapPt(twipToPt(before));
|
||||
if (after != null) attrs.spaceAfter = snapPt(twipToPt(after));
|
||||
if (line != null && (!rule || rule === 'auto')) {
|
||||
const mult = round2(line / 240);
|
||||
if (Math.abs(mult - 1) > 0.01) attrs.lineHeight = String(mult);
|
||||
}
|
||||
|
||||
if (!ppr) return attrs;
|
||||
|
||||
if (kind !== 'list') {
|
||||
const ind = fc(ppr, 'ind');
|
||||
const left = ind ? intAttr(ind, W, 'left') ?? intAttr(ind, W, 'start') : null;
|
||||
|
||||
@@ -31,6 +31,10 @@ export type RunProps = {
|
||||
// w:rPr/w:rFonts (Wingdings/Symbol give the bullet glyph its meaning).
|
||||
export type LevelDef = { numFmt?: string; lvlText?: string; leftTwip?: number; start?: number; bulletFont?: string };
|
||||
|
||||
// Paragraph spacing from a w:pPr/w:spacing (only the attributes actually present, so
|
||||
// omitted ones inherit up the style chain). before/after in twips, line per w:line.
|
||||
export type ParaSpacing = { before?: number; after?: number; line?: number; lineRule?: string };
|
||||
|
||||
export function wVal(el: Element): string | null {
|
||||
return el.getAttributeNS(W, 'val');
|
||||
}
|
||||
@@ -92,6 +96,7 @@ export class DocxStyles {
|
||||
private styleNum = new Map<string, { numId: number; ilvl: number }>();
|
||||
private ownOutline = new Map<string, number>(); // style's own w:outlineLvl (heading marker)
|
||||
private ownAlign = new Map<string, string>(); // style's own w:pPr/w:jc
|
||||
private ownSpacing = new Map<string, ParaSpacing>(); // style's own w:pPr/w:spacing
|
||||
private defaultParaStyle: string | null = null; // the w:default="1" paragraph style
|
||||
private defaultsAlign: string | null = null; // docDefaults w:pPrDefault/w:jc
|
||||
private numToAbstract = new Map<string, string>();
|
||||
@@ -146,6 +151,8 @@ export class DocxStyles {
|
||||
if (ol) { const n = parseInt(wVal(ol) ?? '', 10); if (Number.isFinite(n)) this.ownOutline.set(id, n); }
|
||||
const jc = ppr && firstChild(ppr, 'jc');
|
||||
if (jc) { const v = wVal(jc); if (v) this.ownAlign.set(id, v); }
|
||||
const sp = ppr && firstChild(ppr, 'spacing');
|
||||
if (sp) this.ownSpacing.set(id, readSpacing(sp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +236,23 @@ export class DocxStyles {
|
||||
return this.defaultsAlign;
|
||||
}
|
||||
|
||||
// The style's effective spacing along the w:basedOn chain (root → leaf, leaf wins per
|
||||
// attribute). Excludes docDefaults on purpose: those stay the editor's own defaults.
|
||||
private styleSpacing(styleId: string | null | undefined, seen = new Set<string>()): ParaSpacing {
|
||||
if (!styleId || seen.has(styleId)) return {};
|
||||
seen.add(styleId);
|
||||
const base = this.styleSpacing(this.basedOn.get(styleId) ?? null, seen);
|
||||
return { ...base, ...(this.ownSpacing.get(styleId) ?? {}) };
|
||||
}
|
||||
|
||||
// Effective paragraph spacing from styles only (direct w:pPr/w:spacing wins in the
|
||||
// caller): the pStyle's basedOn chain, else the default paragraph style. Honors an
|
||||
// explicit style like "No Spacing" (after=0) that the editor default would otherwise
|
||||
// override. docDefaults are deliberately left as the editor's defaults.
|
||||
paragraphSpacing(pStyleId: string | null | undefined): ParaSpacing {
|
||||
return this.styleSpacing(pStyleId ?? this.defaultParaStyle);
|
||||
}
|
||||
|
||||
level(numId: number, ilvl: number): LevelDef {
|
||||
const abs = this.numToAbstract.get(String(numId));
|
||||
if (!abs) return {};
|
||||
@@ -241,6 +265,22 @@ function firstChild(el: Element, localName: string): Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A w:spacing element's present attributes only (omitted ones inherit up the chain).
|
||||
export function readSpacing(sp: Element): ParaSpacing {
|
||||
const num = (name: string) => {
|
||||
const v = sp.getAttributeNS(W, name);
|
||||
if (v == null) return undefined;
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
const out: ParaSpacing = {};
|
||||
const before = num('before'); if (before != null) out.before = before;
|
||||
const after = num('after'); if (after != null) out.after = after;
|
||||
const line = num('line'); if (line != null) out.line = line;
|
||||
const rule = sp.getAttributeNS(W, 'lineRule'); if (rule) out.lineRule = rule;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function readNumPr(numPr: Element): { numId: number; ilvl: number } | null {
|
||||
const idEl = firstChild(numPr, 'numId');
|
||||
if (!idEl) return null;
|
||||
|
||||
Reference in new issue
Block a user