ultisuite-client/lib/drive/docs-paragraph-styles.ts
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

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
}