import type { Editor } from "@tiptap/react" import type { DocParagraphStyleDefinition } from "@/lib/drive/docs-paragraph-styles" import { inferParagraphStyleIdFromEditorState, normalizeParagraphStyleId, resolveParagraphStyleDefinition, } from "@/lib/drive/docs-paragraph-styles" import { getActiveParagraphStyleCatalog } from "@/lib/drive/docs-paragraph-style-bridge" import { fontFamilyStackForName } from "@/lib/drive/docs-font-family" type BlockNodeType = "paragraph" | "heading" function blockNodeType(def: DocParagraphStyleDefinition): BlockNodeType { return def.blockType === "heading" && def.level ? "heading" : "paragraph" } function applyRunMarksToRange( editor: Editor, from: number, to: number, def: DocParagraphStyleDefinition ) { if (from >= to) return let chain = editor.chain().setTextSelection({ from, to }) const stack = def.fontFamily ? def.fontFamily.includes(",") ? def.fontFamily : fontFamilyStackForName(def.fontFamily) : null if (stack) chain = chain.setFontFamily(stack) else chain = chain.unsetFontFamily() if (def.fontSizePx != null) chain = chain.setFontSize(`${def.fontSizePx}px`) else chain = chain.unsetFontSize() if (def.bold) chain = chain.setMark("bold") else chain = chain.unsetMark("bold") if (def.italic) chain = chain.setMark("italic") else chain = chain.unsetMark("italic") if (def.underline) chain = chain.setMark("underline") else chain = chain.unsetMark("underline") if (def.color) chain = chain.setColor(def.color) else chain = chain.unsetColor() chain.run() } function forEachSelectedBlock( editor: Editor, fn: (from: number, to: number, nodeType: BlockNodeType) => void ) { const { state } = editor const { from, to } = state.selection state.doc.nodesBetween(from, to, (node, pos) => { if (node.type.name !== "paragraph" && node.type.name !== "heading") return const start = pos const end = pos + node.content.size fn(start + 1, end + 1, node.type.name as BlockNodeType) return false }) } export function applyParagraphStyleDefinition( editor: Editor, def: DocParagraphStyleDefinition ) { const nodeType = blockNodeType(def) const chain = editor.chain().focus() as ReturnType & { setParagraph: () => ReturnType setHeading: (attrs: { level: number }) => ReturnType updateAttributes: ( type: string, attrs: Record ) => ReturnType setTextAlign: (align: string) => ReturnType } if (nodeType === "heading" && def.level) { chain.setHeading({ level: def.level }) } else { chain.setParagraph() } chain.updateAttributes(nodeType, { styleId: def.id, }) if (def.textAlign) chain.setTextAlign(def.textAlign) chain.run() forEachSelectedBlock(editor, (blockFrom, blockTo) => { applyRunMarksToRange(editor, blockFrom, blockTo, def) }) if (editor.state.selection.empty) { const { $from } = editor.state.selection if ($from.parent.type.name === "paragraph" || $from.parent.type.name === "heading") { applyRunMarksToRange(editor, $from.start(), $from.end(), def) } } } export function applyDocsParagraphStyleById(editor: Editor, styleId: string) { const catalog = getActiveParagraphStyleCatalog() const def = resolveParagraphStyleDefinition(catalog, styleId) if (!def) return false applyParagraphStyleDefinition(editor, def) return true } export function extractParagraphStyleFromSelection( editor: Editor, styleId: string ): DocParagraphStyleDefinition | null { const catalog = getActiveParagraphStyleCatalog() const existing = resolveParagraphStyleDefinition(catalog, styleId) ?? resolveParagraphStyleDefinition(catalog, "normal") if (!existing) return null const { $from } = editor.state.selection const node = $from.parent if (node.type.name !== "paragraph" && node.type.name !== "heading") return null const attrs = node.attrs as { styleId?: string; textAlign?: string } const marks = editor.state.storedMarks ?? $from.marks() const textStyle = marks.find((m) => m.type.name === "textStyle") const fontFamily = (textStyle?.attrs.fontFamily as string | undefined) ?? existing.fontFamily const fontSizeRaw = textStyle?.attrs.fontSize as string | undefined const fontSizePx = fontSizeRaw ? Number.parseFloat(fontSizeRaw) : existing.fontSizePx return { ...existing, id: normalizeParagraphStyleId(styleId), name: existing.name, scope: existing.scope, blockType: node.type.name === "heading" ? "heading" : "paragraph", level: node.type.name === "heading" ? (node.attrs.level as number) : undefined, fontFamily, fontSizePx: Number.isFinite(fontSizePx) ? fontSizePx : existing.fontSizePx, bold: marks.some((m) => m.type.name === "bold"), italic: marks.some((m) => m.type.name === "italic"), underline: marks.some((m) => m.type.name === "underline"), color: (marks.find((m) => m.type.name === "textStyle")?.attrs.color as string) ?? existing.color, textAlign: (attrs.textAlign as DocParagraphStyleDefinition["textAlign"]) ?? existing.textAlign, } } export function readActiveParagraphStyleId(editor: Editor): string { const headingAttrs = editor.getAttributes("heading") if (headingAttrs.styleId) return normalizeParagraphStyleId(String(headingAttrs.styleId)) const paragraphAttrs = editor.getAttributes("paragraph") if (paragraphAttrs.styleId) return normalizeParagraphStyleId(String(paragraphAttrs.styleId)) return inferParagraphStyleIdFromEditorState({ isHeading: editor.isActive("heading"), headingLevel: headingAttrs.level as number | undefined, }) }