export type DocParagraphStyleScope = "document" | "user" export type DocParagraphStyleBlockType = "paragraph" | "heading" export type DocParagraphStyleDefinition = { id: string name: string scope: DocParagraphStyleScope basedOn?: string blockType: DocParagraphStyleBlockType level?: number fontFamily?: string fontSizePx?: number bold?: boolean italic?: boolean underline?: boolean color?: string textAlign?: "left" | "center" | "right" | "justify" lineHeight?: number spaceBeforePt?: number spaceAfterPt?: number } export type DocParagraphStylesCatalog = { definitions: Record } export const BUILTIN_PARAGRAPH_STYLE_IDS = [ "normal", "title", "subtitle", "heading1", "heading2", "heading3", "heading4", "heading5", "heading6", ] as const export type BuiltinParagraphStyleId = (typeof BUILTIN_PARAGRAPH_STYLE_IDS)[number] const BUILTIN_LABELS: Record = { normal: "Normal", title: "Titre", subtitle: "Sous-titre", heading1: "Titre 1", heading2: "Titre 2", heading3: "Titre 3", heading4: "Titre 4", heading5: "Titre 5", heading6: "Titre 6", } export function normalizeParagraphStyleId(styleId: string): string { if (styleId === "paragraph") return "normal" return styleId } export function defaultDocumentParagraphStyles(): DocParagraphStylesCatalog { const defs: Record = { normal: { id: "normal", name: BUILTIN_LABELS.normal, scope: "document", blockType: "paragraph", fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 11, lineHeight: 1.15, }, title: { id: "title", name: BUILTIN_LABELS.title, scope: "document", blockType: "paragraph", fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 26, lineHeight: 1.15, }, subtitle: { id: "subtitle", name: BUILTIN_LABELS.subtitle, scope: "document", blockType: "paragraph", fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 15, color: "#666666", lineHeight: 1.15, }, heading1: { id: "heading1", name: BUILTIN_LABELS.heading1, scope: "document", blockType: "heading", level: 1, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 20, lineHeight: 1.15, }, heading2: { id: "heading2", name: BUILTIN_LABELS.heading2, scope: "document", blockType: "heading", level: 2, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 16, lineHeight: 1.15, }, heading3: { id: "heading3", name: BUILTIN_LABELS.heading3, scope: "document", blockType: "heading", level: 3, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 14, lineHeight: 1.15, }, heading4: { id: "heading4", name: BUILTIN_LABELS.heading4, scope: "document", blockType: "heading", level: 4, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 12, bold: true, lineHeight: 1.15, }, heading5: { id: "heading5", name: BUILTIN_LABELS.heading5, scope: "document", blockType: "heading", level: 5, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 11, bold: true, lineHeight: 1.15, }, heading6: { id: "heading6", name: BUILTIN_LABELS.heading6, scope: "document", blockType: "heading", level: 6, fontFamily: "Arial, Helvetica, sans-serif", fontSizePx: 11, italic: true, lineHeight: 1.15, }, } return { definitions: defs } } export function emptyUserParagraphStyles(): DocParagraphStylesCatalog { return { definitions: {} } } function styleRunKey(def: DocParagraphStyleDefinition): string { return JSON.stringify({ fontFamily: def.fontFamily ?? "", fontSizePx: def.fontSizePx ?? null, bold: def.bold ?? false, italic: def.italic ?? false, underline: def.underline ?? false, color: def.color ?? "", textAlign: def.textAlign ?? "", lineHeight: def.lineHeight ?? null, blockType: def.blockType, level: def.level ?? null, }) } export function paragraphStylesDiffer( a: DocParagraphStyleDefinition | undefined, b: DocParagraphStyleDefinition | undefined ): boolean { if (!a || !b) return Boolean(a || b) return styleRunKey(a) !== styleRunKey(b) } export function mergeParagraphStyleCatalogs( documentStyles: DocParagraphStylesCatalog | null | undefined, userStyles: DocParagraphStylesCatalog | null | undefined ): DocParagraphStylesCatalog { const base = defaultDocumentParagraphStyles() const docDefs = { ...base.definitions, ...(documentStyles?.definitions ?? {}) } const merged = { ...docDefs, ...(userStyles?.definitions ?? {}) } return { definitions: merged } } export type ParagraphStyleMenuEntry = { definition: DocParagraphStyleDefinition section: "document" | "user" } export function buildParagraphStyleMenuEntries( documentStyles: DocParagraphStylesCatalog, userStyles: DocParagraphStylesCatalog ): ParagraphStyleMenuEntry[] { const docBase = defaultDocumentParagraphStyles() const docDefs = { ...docBase.definitions, ...documentStyles.definitions } const entries: ParagraphStyleMenuEntry[] = [] for (const id of BUILTIN_PARAGRAPH_STYLE_IDS) { const def = docDefs[id] if (def) entries.push({ definition: def, section: "document" }) } for (const id of BUILTIN_PARAGRAPH_STYLE_IDS) { const userDef = userStyles.definitions[id] const docDef = docDefs[id] if (userDef && paragraphStylesDiffer(docDef, userDef)) { entries.push({ definition: { ...userDef, scope: "user" }, section: "user" }) } } for (const def of Object.values(userStyles.definitions)) { if (def.scope !== "user") continue if ((BUILTIN_PARAGRAPH_STYLE_IDS as readonly string[]).includes(def.id)) continue entries.push({ definition: def, section: "user" }) } return entries } export function resolveParagraphStyleDefinition( catalog: DocParagraphStylesCatalog, styleId: string ): DocParagraphStyleDefinition | null { const id = normalizeParagraphStyleId(styleId) return catalog.definitions[id] ?? defaultDocumentParagraphStyles().definitions[id] ?? null } export function inferParagraphStyleIdFromEditorState(input: { styleId?: string | null isHeading?: boolean headingLevel?: number }): string { if (input.styleId) return normalizeParagraphStyleId(input.styleId) if (input.isHeading && input.headingLevel) return `heading${input.headingLevel}` return "normal" } export function createCustomParagraphStyle(input: { name: string basedOn?: string template?: DocParagraphStyleDefinition }): DocParagraphStyleDefinition { const id = `custom-${typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10)}` const base = input.template ?? resolveParagraphStyleDefinition(defaultDocumentParagraphStyles(), input.basedOn ?? "normal")! return { ...base, id, name: input.name.trim() || "Style personnalisé", scope: "user", basedOn: input.basedOn ?? base.id, } } export function builtinParagraphStyleLabel(id: string): string { const normalized = normalizeParagraphStyleId(id) as BuiltinParagraphStyleId return BUILTIN_LABELS[normalized] ?? id }