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;