From 0d4ed9e5916805dce373fbab89d7637821bb27b1 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 7 Aug 2026 17:33:23 +0000 Subject: [PATCH] fix: honour the paragraph mark's font size on every line Its size is the block's line-height floor, not just an empty line's height: 10pt text in an 11pt style kept the taller strut and every line came out 0.4mm high. Both importers now carry it as the blockFontSize attr whenever it differs from what the block renders at, and the ODF export emits the FSZ sentinel for a filled block too. --- src/lib/export/odt.ts | 13 +++++++------ src/lib/import/docx.ts | 20 +++++++++----------- src/lib/import/odt.ts | 11 +++++------ 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 4c3bf90..95a75e2 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -2846,9 +2846,10 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU })); } const content = node.content ?? []; - // An empty line's font size (its paragraph-mark size) rides as an FSZ sentinel - // that applyEmptyLineFontSizes turns into a paragraph-style fo:font-size. - const emptyFs = content.length === 0 && typeof node.attrs?.fontSize === 'string' && node.attrs.fontSize + // The paragraph-mark font size rides as an FSZ sentinel that + // applyEmptyLineFontSizes turns into a paragraph-style fo:font-size — it is the + // block's line-height floor, so it matters on a filled block as much as an empty one. + const fsz = typeof node.attrs?.fontSize === 'string' && node.attrs.fontSize ? `${FSZ}${node.attrs.fontSize}${FSZ}` : ''; // Paragraph background/borders ride as a leading PBX sentinel (odf-kit has no such // options); applyParagraphBoxes mints the style. FSZ stays first so its own pass, @@ -2858,15 +2859,15 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU // Named style, unless it is the one odf-kit puts on this node type anyway. const styleName = styleOf(node); const sty = styleName !== odfDefaultStyleOf(node) ? `${STY}${styleName}${STY}` : ''; - const marks = sty + pbx; + const marks = fsz + sty + pbx; const withPbx = (p: ParagraphBuilder) => { if (marks) p.addText(marks); applyRuns(p, content); }; if (node.type === CUST_P) { - if (content.length === 0) doc.addParagraph(emptyFs + marks, opts); + if (content.length === 0) doc.addParagraph(marks, opts); else doc.addParagraph(withPbx, opts); } else if (node.type === CUST_H) { const level = (node.attrs?.level as number) ?? 1; - if (content.length === 0) doc.addHeading(emptyFs + marks, level, opts); + if (content.length === 0) doc.addHeading(marks, level, opts); else doc.addHeading(withPbx, level, opts); } }, diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index c97728b..bfd96b3 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -702,13 +702,11 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault: } } - // An empty line's height comes from the paragraph mark's own run props (w:pPr/w:rPr), - // which convertInline never sees (there are no runs). Carry its font size as a block - // attr so the empty line renders at the right height (and typed text inherits it). - if (content.length === 0) { - const fs = emptyLineFontSize(ppr, ctx, baseRun, level); - if (fs) attrs.fontSize = fs; - } + // The paragraph mark's own run props (w:pPr/w:rPr) set the line-height floor for + // every line, not just an empty one — a block whose text is smaller than its style + // would otherwise keep the style's taller strut. Carried as a block attr. + const fs = paragraphMarkFontSize(ppr, ctx, baseRun, defaults.fontSizePt); + if (fs) attrs.fontSize = fs; const node: Node = { type: level ? 'heading' : 'paragraph' }; if (level) attrs.level = level; @@ -718,15 +716,15 @@ function convertParagraph(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault: } // The paragraph mark's resolved font size (w:pPr/w:rPr, incl. its rStyle), as a CSS -// pt string, or null when it matches the block's default (suppressed like run sizes). -function emptyLineFontSize(ppr: Element | null, ctx: Ctx, baseRun: RunProps, level: number | null): string | null { +// pt string, or null when it matches what the block renders at anyway (suppressed +// like run sizes, against the same yardstick). +function paragraphMarkFontSize(ppr: Element | null, ctx: Ctx, baseRun: RunProps, defaultPt: number): string | null { const rPr = fc(ppr, 'rPr'); const rStyle = fc(rPr, 'rStyle'); const props = mergeRunProps(mergeRunProps(baseRun, ctx.styles.styleOwn(rStyle ? wVal(rStyle) : null)), parseRunProps(rPr)); if (props.sizeHalfPt == null) return null; const sizePt = props.sizeHalfPt / 2; - const defSize = level != null ? HEADING_SIZES[level - 1] : BODY_FONT_SIZE_PT; - return Math.abs(sizePt - defSize) > 0.05 ? `${Math.round(sizePt * 10) / 10}pt` : null; + return Math.abs(sizePt - defaultPt) > 0.05 ? `${Math.round(sizePt * 10) / 10}pt` : null; } // Heading + clamped level. Detect via the paragraph style id (fast path for our own diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 3573905..30ca297 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -870,12 +870,11 @@ function convertParaLike(el: Element, ctx: Ctx, kind: BlockKind, boldByDefault = if (stops) attrs.tabStops = stops; const content = convertInline(el, ctx, baseTextProps, defaults, false); - // An empty line has no runs, so its height comes from the paragraph style's own - // font size; carry it as a block attr (mirrors the docx paragraph-mark size). - if (content.length === 0) { - const sizePt = lengthToPt(baseTextProps['fo:font-size']); - if (sizePt != null && Math.abs(sizePt - defaults.fontSizePt) > 0.05) attrs.fontSize = formatPt(sizePt); - } + // The paragraph style's own font size is the block's line-height floor on every + // line, not only on an empty one; carry it as a block attr (mirrors the docx + // paragraph-mark size), or text smaller than the style keeps the taller strut. + const markSize = lengthToPt(baseTextProps['fo:font-size']); + if (markSize != null && Math.abs(markSize - defaults.fontSizePt) > 0.05) attrs.fontSize = formatPt(markSize); const node: Node = { type: isHeading ? 'heading' : 'paragraph' }; if (isHeading) attrs.level = level;