import type { Editor } from '@tiptap/core' import type { Node as PMNode } from '@tiptap/pm/model' const ID_ATTR = 'data-id' const BLOCK_TYPES_WITH_ID = new Set([ 'paragraph', 'heading', 'blockquote', 'bulletList', 'orderedList', 'taskList', 'codeBlock', ]) export function generateBlockId(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } function readNodeId(node: PMNode | null | undefined): string | null { const id = node?.attrs[ID_ATTR] return typeof id === 'string' && id.length > 0 ? id : null } function findDataIdInSubtree(node: PMNode): string | null { const direct = readNodeId(node) if (direct) return direct let found: string | null = null node.descendants((child) => { if (found) return false found = readNodeId(child) return !found }) return found } function findDataIdFromDom(editor: Editor, blockPos: number): string | null { const dom = editor.view.nodeDOM(blockPos) if (!(dom instanceof HTMLElement)) return null const direct = dom.getAttribute(ID_ATTR) if (direct) return direct const nested = dom.querySelector(`[${ID_ATTR}]`) const nestedId = nested?.getAttribute(ID_ATTR) return nestedId || null } function findTextBlockToAssign( editor: Editor, blockPos: number, blockNode: PMNode, ): { pos: number; node: PMNode } | null { if (BLOCK_TYPES_WITH_ID.has(blockNode.type.name)) { return { pos: blockPos, node: blockNode } } let target: { pos: number; node: PMNode } | null = null editor.state.doc.nodesBetween(blockPos, blockPos + blockNode.nodeSize, (node, pos) => { if (target) return false if (node.isTextblock && BLOCK_TYPES_WITH_ID.has(node.type.name)) { target = { pos, node } return false } return true }) return target } /** Cherche un data-id sur le bloc résolu (nœud, descendants, ancêtres, DOM). */ export function findBlockReferenceId( editor: Editor, blockPos: number, blockNode: PMNode | null, ): string | null { const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null) if (!node) return null const fromSubtree = findDataIdInSubtree(node) if (fromSubtree) return fromSubtree const $pos = editor.state.doc.resolve(Math.min(blockPos + 1, editor.state.doc.content.size)) for (let depth = $pos.depth; depth > 0; depth--) { const id = readNodeId($pos.node(depth)) if (id) return id } return findDataIdFromDom(editor, blockPos) } /** Assigne un data-id si absent, retourne l'id utilisable pour la référence. */ export function ensureBlockReferenceId( editor: Editor, blockPos: number, blockNode: PMNode | null, ): string | null { const existing = findBlockReferenceId(editor, blockPos, blockNode) if (existing) return existing const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null) if (!node || blockPos < 0) return null const target = findTextBlockToAssign(editor, blockPos, node) if (!target) return null const newId = generateBlockId() editor.view.dispatch( editor.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, [ID_ATTR]: newId, }), ) return newId }