/** Default line height for Normal text (matches built-in paragraph styles). */ export const DOCS_DEFAULT_LINE_HEIGHT = 1.15 /** Quick spacing added by « Insérer un espacement avant/après » (Google Docs ≈ 12 pt). */ export const DOCS_QUICK_PARAGRAPH_SPACE_PT = 12 export const DOCS_LINE_HEIGHT_PRESETS = [ { id: "1", value: 1, label: "Simple" }, { id: "1.15", value: 1.15, label: "1,15" }, { id: "1.5", value: 1.5, label: "1,5" }, { id: "2", value: 2, label: "Double" }, ] as const export type DocsLineHeightPresetId = (typeof DOCS_LINE_HEIGHT_PRESETS)[number]["id"] export type DocsParagraphSpacingAttrs = { lineHeight: number | null spaceBeforePt: number spaceAfterPt: number keepWithNext: boolean keepLinesTogether: boolean preventWidowOrphan: boolean pageBreakBefore: boolean } export const DOCS_DEFAULT_PARAGRAPH_SPACING: DocsParagraphSpacingAttrs = { lineHeight: null, spaceBeforePt: 0, spaceAfterPt: 0, keepWithNext: false, keepLinesTogether: false, preventWidowOrphan: false, pageBreakBefore: false, } export function lineHeightPresetId(value: number | null | undefined): DocsLineHeightPresetId | "custom" | null { if (value == null) return null for (const preset of DOCS_LINE_HEIGHT_PRESETS) { if (Math.abs(preset.value - value) < 0.001) return preset.id } return "custom" } export function parseLineHeightAttr(raw: unknown): number | null { if (raw == null || raw === "") return null const value = typeof raw === "number" ? raw : Number.parseFloat(String(raw)) return Number.isFinite(value) && value > 0 ? value : null } export function parseSpacingPt(raw: unknown): number { if (raw == null || raw === "") return 0 const value = typeof raw === "number" ? raw : Number.parseFloat(String(raw)) return Number.isFinite(value) && value > 0 ? Math.round(value * 100) / 100 : 0 } export function readParagraphSpacingAttrs(node: ProseMirrorNodeLike): DocsParagraphSpacingAttrs { const attrs = node.attrs as Record return { lineHeight: parseLineHeightAttr(attrs.lineHeight), spaceBeforePt: parseSpacingPt(attrs.spaceBeforePt), spaceAfterPt: parseSpacingPt(attrs.spaceAfterPt), keepWithNext: Boolean(attrs.keepWithNext), keepLinesTogether: Boolean(attrs.keepLinesTogether), preventWidowOrphan: Boolean(attrs.preventWidowOrphan), pageBreakBefore: Boolean(attrs.pageBreakBefore), } } type ProseMirrorNodeLike = { attrs: Record } export function buildParagraphSpacingStyle(attrs: DocsParagraphSpacingAttrs): string { const parts: string[] = [] if (attrs.lineHeight != null) parts.push(`line-height: ${attrs.lineHeight}`) if (attrs.spaceBeforePt > 0) parts.push(`margin-top: ${attrs.spaceBeforePt}pt`) if (attrs.spaceAfterPt > 0) parts.push(`margin-bottom: ${attrs.spaceAfterPt}pt`) if (attrs.keepWithNext) parts.push("break-after: avoid") if (attrs.keepLinesTogether) parts.push("break-inside: avoid") if (attrs.preventWidowOrphan) parts.push("orphans: 2; widows: 2") if (attrs.pageBreakBefore) parts.push("break-before: page") return parts.join("; ") }