ultisuite-client/lib/drive/docs-font-family.ts
2026-06-09 17:06:20 +02:00

116 lines
3.8 KiB
TypeScript

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, unknown> }>
): 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<DocsFontFamilyName> {
const names = new Set<DocsFontFamilyName>()
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()
}