268 lines
7.3 KiB
TypeScript
268 lines
7.3 KiB
TypeScript
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<string, DocParagraphStyleDefinition>
|
|
}
|
|
|
|
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<BuiltinParagraphStyleId, string> = {
|
|
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<string, DocParagraphStyleDefinition> = {
|
|
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
|
|
}
|