import type { Editor } from "@tiptap/react" import type { Mark } from "@tiptap/pm/model" export const DOCS_FONT_FAMILIES = [ { name: "Arial", stack: "Arial, Helvetica, sans-serif" }, { name: "Calibri", stack: "Calibri, Candara, Segoe, sans-serif" }, { name: "Comic Sans MS", stack: '"Comic Sans MS", cursive, sans-serif' }, { name: "Courier New", stack: '"Courier New", Courier, monospace' }, { name: "Georgia", stack: "Georgia, serif" }, { name: "Times New Roman", stack: '"Times New Roman", Times, serif' }, { name: "Trebuchet MS", stack: '"Trebuchet MS", Helvetica, sans-serif' }, { name: "Verdana", stack: "Verdana, Geneva, sans-serif" }, ] as const export type DocsFontFamilyName = (typeof DOCS_FONT_FAMILIES)[number]["name"] export const DOCS_DEFAULT_FONT_FAMILY: DocsFontFamilyName = "Arial" let lastToolbarFontFamily: DocsFontFamilyName = DOCS_DEFAULT_FONT_FAMILY export type FontFamilyToolbarState = | { kind: "single"; name: DocsFontFamilyName } | { kind: "unset" } function textStyleFontFamilyStack( marks: ReadonlyArray<{ type: { name: string }; attrs: Record }> ): string | null { const mark = marks.find((m) => m.type.name === "textStyle") const raw = mark?.attrs.fontFamily as string | undefined return raw?.trim() ? raw : null } export function fontFamilyNameForStack(stack: string | null | undefined): DocsFontFamilyName | null { if (!stack) return null const normalized = stack.trim().toLowerCase() for (const family of DOCS_FONT_FAMILIES) { if ( family.stack.toLowerCase() === normalized || family.name.toLowerCase() === normalized || normalized.startsWith(`${family.name.toLowerCase()},`) || normalized.startsWith(`"${family.name.toLowerCase()}"`) ) { return family.name } } return null } export function fontFamilyStackForName(name: string): string { return DOCS_FONT_FAMILIES.find((f) => f.name === name)?.stack ?? name } function resolveCursorFontFamilyName(editor: Editor): DocsFontFamilyName { const { state } = editor const { $from } = state.selection if (state.storedMarks) { const stored = fontFamilyNameForStack(textStyleFontFamilyStack(state.storedMarks)) if (stored) return stored } const atCursor = fontFamilyNameForStack(textStyleFontFamilyStack($from.marks())) if (atCursor) return atCursor if ($from.parent.content.size === 0) return lastToolbarFontFamily return DOCS_DEFAULT_FONT_FAMILY } function effectiveFontFamilyNameAtPos(marks: readonly Mark[]): DocsFontFamilyName { return fontFamilyNameForStack(textStyleFontFamilyStack(marks)) ?? DOCS_DEFAULT_FONT_FAMILY } function collectEffectiveFontFamiliesInRange( editor: Editor, from: number, to: number ): Set { const names = new Set() const { state } = editor state.doc.nodesBetween(from, to, (node, pos) => { if (!node.isText || !node.text) return const nodeStart = pos const nodeEnd = pos + node.text.length const start = Math.max(from, nodeStart) const end = Math.min(to, nodeEnd) if (start >= end) return names.add(effectiveFontFamilyNameAtPos(node.marks)) }) return names } export function readFontFamilyToolbarState(editor: Editor): FontFamilyToolbarState { const { from, to, empty } = editor.state.selection if (empty) { return { kind: "single", name: resolveCursorFontFamilyName(editor) } } const names = collectEffectiveFontFamiliesInRange(editor, from, to) if (names.size === 0) { return { kind: "single", name: resolveCursorFontFamilyName(editor) } } if (names.size === 1) { return { kind: "single", name: [...names][0]! } } return { kind: "unset" } } export function applyFontFamily(editor: Editor, name: DocsFontFamilyName) { lastToolbarFontFamily = name editor.chain().focus().setFontFamily(fontFamilyStackForName(name)).run() }