From a70df06fc1c4d4e5408b8e3d66acf150de8db1f2 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sun, 9 Aug 2026 20:43:33 +0000 Subject: [PATCH] fix: a paragraph margin the style half-declares follows LibreOffice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibreOffice keeps a paragraph's space above and below in one item, so a style that declares only one of them does not inherit the other from its parent chain — the missing half falls back to the default paragraph style. Probed against soffice with the OPG fixture's own styles, where it is the difference between a heading sitting flush and 4mm too low. --- src/lib/import/styleResolver.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib/import/styleResolver.ts b/src/lib/import/styleResolver.ts index 64bd629..143238d 100644 --- a/src/lib/import/styleResolver.ts +++ b/src/lib/import/styleResolver.ts @@ -81,6 +81,20 @@ export function layerTextProps(base: PropMap, over: PropMap): PropMap { return Object.assign(out, over); } +// LibreOffice holds a paragraph's two vertical margins in one item, so a style that +// declares just one of them does not inherit the other — the missing one falls back to +// the default paragraph style's value (probed: heading margins, and the space after a +// paragraph that only sets fo:margin-top). +const VERTICAL_MARGINS = ['fo:margin-top', 'fo:margin-bottom'] as const; + +export function layerParaProps(base: PropMap, over: PropMap, poolDefaults: PropMap): PropMap { + const out = Object.assign({}, base, over); + if (VERTICAL_MARGINS.some((k) => k in over)) { + for (const k of VERTICAL_MARGINS) if (!(k in over)) out[k] = poolDefaults[k] ?? '0cm'; + } + return out; +} + // The size a percentage font-size is relative to when nothing is inherited yet // (ODF's implied default, matching the editor's body size). const BASE_FONT_SIZE_PT = 12; @@ -277,12 +291,13 @@ export class StyleResolver { if (def) chain.push(def); const result = { text: {} as PropMap, para: {} as PropMap, misc: {} as PropMap }; + const poolDefaults = def?.para ?? {}; // Apply root-first so nearer definitions overwrite. for (const entry of chain.reverse()) { const inherited = result.text['fo:font-size']; result.text = layerTextProps(result.text, entry.text); resolvePercentSize(result.text, inherited); - Object.assign(result.para, entry.para); + result.para = entry === def ? { ...entry.para } : layerParaProps(result.para, entry.para, poolDefaults); Object.assign(result.misc, entry.misc); } this.mergedCache.set(cacheKey, result);