From 3a71491b5843cff7f6c8f39ff23ee23ad03180a8 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 7 Aug 2026 22:24:38 +0000 Subject: [PATCH] fix: scale a named style's line spacing by the font's natural line height A proportional line spacing multiplies the font's own single line height in Word and LibreOffice, CSS multiplies the font size. The node attr already scaled by Liberation Serif's 1.15; a style's line-height went out raw, so an imported 1.08 rendered ~20% tighter than the file asks for and the drift accumulated down the page. --- src/lib/editor/extensions/lineHeight.ts | 8 ++++---- src/lib/styles/styleSheet.ts | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/editor/extensions/lineHeight.ts b/src/lib/editor/extensions/lineHeight.ts index 24365d2..e05089a 100644 --- a/src/lib/editor/extensions/lineHeight.ts +++ b/src/lib/editor/extensions/lineHeight.ts @@ -1,9 +1,9 @@ import { Extension } from '@tiptap/core'; +import { DEFAULT_SINGLE_LINE_HEIGHT } from '../../styles/styleSheet'; -// ODF line spacing multiplies the font's natural line height (Liberation Serif -// ≈1.15× em); CSS unitless line-height multiplies the font size. Scale by this -// ratio so the on-screen box matches LibreOffice. Keep in sync with editor.css. -const LINE_HEIGHT_RATIO = 1.15; +// ODF line spacing multiplies the font's natural line height, CSS unitless +// line-height the font size. A node attr knows no family, so it takes the default. +const LINE_HEIGHT_RATIO = DEFAULT_SINGLE_LINE_HEIGHT; declare module '@tiptap/core' { interface Commands { diff --git a/src/lib/styles/styleSheet.ts b/src/lib/styles/styleSheet.ts index 9dece68..f8fe534 100644 --- a/src/lib/styles/styleSheet.ts +++ b/src/lib/styles/styleSheet.ts @@ -198,8 +198,9 @@ export function cssFontFamily(name: string): string { } // Single spacing is the font's *natural* line height, so it differs per family. -// editor.css covers Liberation Serif's 1.15; only the bundled families that deviate -// are listed, measured against LibreOffice at 12pt. +// Liberation Serif's 1.15 is the default (editor.css); only the bundled families that +// deviate are listed, measured against LibreOffice at 12pt. +export const DEFAULT_SINGLE_LINE_HEIGHT = 1.15; const SINGLE_LINE_HEIGHT: Record = { Calibri: 1.2208, Carlito: 1.2208, @@ -207,6 +208,12 @@ const SINGLE_LINE_HEIGHT: Record = { 'Liberation Mono': 1.1333, }; +// A proportional line spacing multiplies the font's natural line height, while CSS +// multiplies the font size — so the stored factor is scaled by the family's own. +export function singleLineHeight(fontFamily?: string): number { + return (fontFamily && SINGLE_LINE_HEIGHT[fontFamily]) || DEFAULT_SINGLE_LINE_HEIGHT; +} + // The text half of a rule, shared with the table-style family (tableStyles.ts). export function textDeclarations(t: TextProps): string[] { const out: string[] = []; @@ -230,7 +237,11 @@ function declarations(r: ResolvedStyle): string[] { const { para: p } = r; const out = textDeclarations(r.text); if (p.textAlign) out.push(`text-align: ${p.textAlign}`); - if (p.lineHeight) out.push(`line-height: ${p.lineHeight}`); + if (p.lineHeight) { + const n = parseFloat(p.lineHeight); + const scaled = Math.round(n * singleLineHeight(r.text.fontFamily) * 1000) / 1000; + out.push(`line-height: ${isNaN(n) ? p.lineHeight : scaled}`); + } if (p.spaceBefore != null) out.push(`margin-top: ${p.spaceBefore}pt`); if (p.spaceAfter != null) out.push(`margin-bottom: ${p.spaceAfter}pt`); if (p.indent != null) out.push(`margin-left: ${p.indent}cm`);