Files
Momento/memento-note/lib/editor/block-reference-id.ts
Antigravity f46654f574 feat: editor improvements and architectural grid prototype
Multiple feature additions and improvements across the application:

- NextGen Editor: drag handles, smart paste, block actions
- Structured views: Kanban and table layouts for notes
- Architectural Grid: new brainstorming/agent interface prototype
- Flashcards: SM-2 revision algorithm with AI generation
- MCP server: robustness improvements
- Graph/PDF chat: fix click propagation and copy behavior
- Various UI/UX enhancements and bug fixes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:45:15 +00:00

123 lines
3.4 KiB
TypeScript

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
}