diff --git a/src/lib/editor/extensions/blockFontSize.ts b/src/lib/editor/extensions/blockFontSize.ts index 555ec36..c92942a 100644 --- a/src/lib/editor/extensions/blockFontSize.ts +++ b/src/lib/editor/extensions/blockFontSize.ts @@ -47,12 +47,11 @@ export const BlockFontSize = Extension.create({ renderHTML: (attributes: Record) => { if (!attributes.fontFamily) return {}; const family = String(attributes.fontFamily); - // Single spacing is this family's natural line height. An explicit line - // spacing already scales by it (lineHeight.ts), so it wins outright. - const lh = attributes.lineHeight ? '' : `; line-height: ${singleLineHeight(family)}`; + // Single spacing is this family's natural line height; the paragraph's own + // spacing factor multiplies it (editor.css). return { 'data-block-font-family': family, - style: `font-family: ${cssFontFamily(family)}${lh}`, + style: `font-family: ${cssFontFamily(family)}; --natural-line: ${singleLineHeight(family)}`, }; }, }, diff --git a/src/lib/editor/extensions/lineHeight.ts b/src/lib/editor/extensions/lineHeight.ts index 33c01b4..b35d38a 100644 --- a/src/lib/editor/extensions/lineHeight.ts +++ b/src/lib/editor/extensions/lineHeight.ts @@ -1,5 +1,8 @@ import { Extension } from '@tiptap/core'; -import { singleLineHeight } from '../../styles/styleSheet'; + +// A line spacing is proportional to the font's natural line height, not to the font +// size like CSS unitless line-height. The factor rides as a variable editor.css +// multiplies by the block's own natural line height (blockFontSize.ts). declare module '@tiptap/core' { interface Commands { @@ -28,17 +31,14 @@ export const LineHeight = Extension.create({ default: null, parseHTML: (element: HTMLElement) => element.getAttribute('data-line-height') || null, - // A proportional spacing multiplies the font's natural line height, CSS the - // font size — so it is scaled by the paragraph mark's family (blockFontSize). + // The factor alone: editor.css multiplies it by the block's font's natural + // line height, which is what a proportional spacing is proportional to. renderHTML: (attributes: Record) => { if (!attributes.lineHeight) return {}; const raw = String(attributes.lineHeight); - const num = parseFloat(raw); - const family = attributes.fontFamily ? String(attributes.fontFamily) : undefined; - const rendered = isNaN(num) ? raw : `${Math.round(num * singleLineHeight(family) * 1000) / 1000}`; return { 'data-line-height': raw, - style: `line-height: ${rendered}`, + style: `--line-factor: ${raw}`, }; }, }, diff --git a/src/lib/export/docx.ts b/src/lib/export/docx.ts index 8af465e..5b21bcb 100644 --- a/src/lib/export/docx.ts +++ b/src/lib/export/docx.ts @@ -941,6 +941,7 @@ function paragraphStyleOf(style: Style): IParagraphStyleOptions { // The registry holds the on-screen family; the file declares its metric twin. if (t.fontFamily) run.font = twinFontName(t.fontFamily); if (t.fontSizePt != null) run.size = Math.round(t.fontSizePt * 2); + if (t.letterSpacingPt) run.characterSpacing = Math.round(t.letterSpacingPt * 20); if (t.bold != null) run.bold = t.bold; if (t.italic != null) run.italics = t.italic; if (t.underline) run.underline = {}; @@ -971,6 +972,7 @@ function characterStyleOf(style: Style): ICharacterStyleOptions { const run: Writable = {}; if (t.fontFamily) run.font = t.fontFamily === 'Liberation Serif' ? DOC_FONT : t.fontFamily; if (t.fontSizePt != null) run.size = Math.round(t.fontSizePt * 2); + if (t.letterSpacingPt) run.characterSpacing = Math.round(t.letterSpacingPt * 20); if (t.bold != null) run.bold = t.bold; if (t.italic != null) run.italics = t.italic; if (t.underline) run.underline = {}; diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 5d9acc3..6ef2851 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -1153,6 +1153,7 @@ function ownStyleAttrs(style: { para: Record; text: Record= 600; const style = props['fo:font-style']; diff --git a/src/lib/styles/styleSheet.ts b/src/lib/styles/styleSheet.ts index 58431bf..fa081ba 100644 --- a/src/lib/styles/styleSheet.ts +++ b/src/lib/styles/styleSheet.ts @@ -20,6 +20,7 @@ export type ParaProps = { export type TextProps = { fontFamily?: string; fontSizePt?: number; + letterSpacingPt?: number; // character spacing (Word's w:spacing, ODF fo:letter-spacing) bold?: boolean; italic?: boolean; underline?: boolean; @@ -200,7 +201,7 @@ export function cssFontFamily(name: string): string { // Single spacing is the font's *natural* line height, so it differs per family. // 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 DEFAULT_SINGLE_LINE_HEIGHT = 1.15; const SINGLE_LINE_HEIGHT: Record = { Calibri: 1.2208, 'Calibri Light': 1.2208, @@ -215,16 +216,18 @@ 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[] { +// The text half of a rule, shared with the table-style family (tableStyles.ts). A block +// takes the family's natural line height as the variable editor.css multiplies by the +// paragraph's spacing factor; a run box sets its line height outright. +export function textDeclarations(t: TextProps, asBlock = false): string[] { const out: string[] = []; if (t.fontFamily) { out.push(`font-family: ${cssFontFamily(t.fontFamily)}`); const lh = SINGLE_LINE_HEIGHT[t.fontFamily]; - // An explicit line spacing is emitted after this and wins. - if (lh) out.push(`line-height: ${lh}`); + if (lh) out.push(`${asBlock ? '--natural-line' : 'line-height'}: ${lh}`); } if (t.fontSizePt != null) out.push(`font-size: ${t.fontSizePt}pt`); + if (t.letterSpacingPt) out.push(`letter-spacing: ${t.letterSpacingPt}pt`); if (t.bold != null) out.push(`font-weight: ${t.bold ? 700 : 400}`); if (t.italic != null) out.push(`font-style: ${t.italic ? 'italic' : 'normal'}`); if (t.underline || t.strike) { @@ -236,13 +239,9 @@ export function textDeclarations(t: TextProps): string[] { function declarations(r: ResolvedStyle): string[] { const { para: p } = r; - const out = textDeclarations(r.text); + const out = textDeclarations(r.text, true); if (p.textAlign) out.push(`text-align: ${p.textAlign}`); - 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.lineHeight) out.push(`--line-factor: ${p.lineHeight}`); 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`); diff --git a/src/styles/editor.css b/src/styles/editor.css index bdd53ac..ea33ed1 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -49,9 +49,11 @@ font-family: var(--font-serif); /* 12pt matches odf-kit's Standard style and the toolbar's declared default. */ font-size: 12pt; - /* ODF/LibreOffice "single" spacing = the font's natural line height - (Liberation Serif ≈ 1.15× em), NOT 1.0× the font size like CSS unitless 1. - Keep this in sync with LINE_HEIGHT_RATIO in lineHeight.ts. */ + /* "Single" spacing is the font's natural line height (Liberation Serif ≈ 1.15× em), + not CSS unitless 1. A paragraph's line spacing multiplies that, so the two ride as + separate variables and the block rule below is the one place they meet. */ + --natural-line: 1.15; + --line-factor: 1; line-height: 1.15; color: var(--color-page-text); @@ -392,7 +394,13 @@ (styles/styleSheet.ts → the #document-styles element Editor.svelte writes). */ font: inherit; margin: 0; - line-height: 1.15; +} + +/* The line box: the paragraph's spacing factor times its font's natural line height. + Either half may come from the style rule or, nearer, from the block's own attrs + (lineHeight.ts / blockFontSize.ts) — multiplying them here is what keeps both. */ +.paper .tiptap :is(p, h1, h2, h3, h4, h5, li) { + line-height: calc(var(--line-factor) * var(--natural-line)); } .paper .tiptap ul,