diff --git a/lib/rollup.config.js b/lib/rollup.config.js index 96cc6a0d..2dae9225 100644 --- a/lib/rollup.config.js +++ b/lib/rollup.config.js @@ -44,8 +44,6 @@ export default [ commonjs({ include: [ /node_modules\/attr-accept/, - /node_modules\/tiptap-markdown/, - /node_modules\/markdown-it-task-lists/, /node_modules\/classnames/, /node_modules\/react-qr-code/, /node_modules\/use-sync-external-store/, diff --git a/lib/src/Components/TipTap/extensions/Hashtag.tsx b/lib/src/Components/TipTap/extensions/Hashtag.tsx index da0cc2a8..c02feb85 100644 --- a/lib/src/Components/TipTap/extensions/Hashtag.tsx +++ b/lib/src/Components/TipTap/extensions/Hashtag.tsx @@ -40,6 +40,45 @@ export const Hashtag = Node.create({ } }, + // Markdown tokenizer for @tiptap/markdown - recognizes #hashtag syntax + markdownTokenizer: { + name: 'hashtag', + level: 'inline', + // Fast hint for the lexer - where might a hashtag start? + start: (src: string) => { + // Look for # followed by word characters (but not inside links) + const match = /(? { + // Match hashtag: #tagname (not preceded by [) + const match = /^#([a-zA-Z0-9À-ÖØ-öø-ʸ_-]+)/.exec(src) + if (match) { + return { + type: 'hashtag', + raw: match[0], + label: match[1], + } + } + return undefined + }, + }, + + // Parse Markdown token to Tiptap JSON + parseMarkdown(token: { label: string }) { + return { + type: 'hashtag', + attrs: { + label: token.label, + }, + } + }, + + // Serialize Tiptap node to Markdown + renderMarkdown(node: { attrs: { label: string } }) { + return `#${node.attrs.label}` + }, + addAttributes() { return { id: { @@ -89,20 +128,6 @@ export const Hashtag = Node.create({ } }, - addStorage() { - return { - markdown: { - serialize(state: { write: (text: string) => void }, node: { attrs: { label: string } }) { - // Write as plain hashtag - state.write(`#${node.attrs.label}`) - }, - parse: { - // Parsing is handled by preprocessHashtags - }, - }, - } - }, - addNodeView() { return ReactNodeViewRenderer(HashtagComponent) }, diff --git a/lib/src/Components/TipTap/extensions/ItemMention.tsx b/lib/src/Components/TipTap/extensions/ItemMention.tsx index 33abb846..343635b2 100644 --- a/lib/src/Components/TipTap/extensions/ItemMention.tsx +++ b/lib/src/Components/TipTap/extensions/ItemMention.tsx @@ -39,6 +39,46 @@ export const ItemMention = Node.create({ } }, + // Markdown tokenizer for @tiptap/markdown - recognizes [@Label](/item/id) syntax + markdownTokenizer: { + name: 'itemMention', + level: 'inline', + // Fast hint for the lexer - where might an item mention start? + start: (src: string) => src.indexOf('[@'), + tokenize: (src: string) => { + // Match [@Label](/item/id) or [@Label](/item/layer/id) + // UUID pattern: hex characters (case-insensitive) with dashes + // eslint-disable-next-line security/detect-unsafe-regex + const match = /^\[@([^\]]+?)\]\(\/item\/(?:[^/]+\/)?([a-fA-F0-9-]+)\)/.exec(src) + if (match) { + return { + type: 'itemMention', + raw: match[0], + label: match[1], + id: match[2], + } + } + return undefined + }, + }, + + // Parse Markdown token to Tiptap JSON + parseMarkdown(token: { label: string; id: string }) { + return { + type: 'itemMention', + attrs: { + label: token.label, + id: token.id, + }, + } + }, + + // Serialize Tiptap node to Markdown + renderMarkdown(node: { attrs: { label: string; id: string } }) { + const { label, id } = node.attrs + return `[@${label}](/item/${id})` + }, + addAttributes() { return { id: { @@ -88,24 +128,6 @@ export const ItemMention = Node.create({ } }, - addStorage() { - return { - markdown: { - serialize( - state: { write: (text: string) => void }, - node: { attrs: { id: string; label: string } }, - ) { - // Write as markdown link: [@Label](/item/id) - const { id, label } = node.attrs - state.write(`[@${label}](/item/${id})`) - }, - parse: { - // Parsing is handled by preprocessItemMentions - }, - }, - } - }, - addNodeView() { return ReactNodeViewRenderer(ItemMentionComponent) },