From 34ee70029544070a6282c1ffef361b30a06d98b3 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Tue, 31 Mar 2026 16:23:11 +0800 Subject: [PATCH] fix(editor): post-process mention shortcodes to markdown link format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tiptap Mention extension's createInlineMarkdownSpec serializes mentions as shortcodes [@ id="..." label="..."] — the .extend() renderMarkdown override may not reliably take effect. Added a robust fallback: post-process the editor's markdown output by replacing shortcodes with [@Label](mention://type/id) using the Tiptap JSON document for type info. Also preprocess stored shortcodes in the Markdown renderer for backward compatibility. --- .../components/common/rich-text-editor.tsx | 40 +++++++++++++++++-- apps/web/components/markdown/Markdown.tsx | 29 +++++++++++++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/apps/web/components/common/rich-text-editor.tsx b/apps/web/components/common/rich-text-editor.tsx index 0cd67ec9..a2df7749 100644 --- a/apps/web/components/common/rich-text-editor.tsx +++ b/apps/web/components/common/rich-text-editor.tsx @@ -162,10 +162,44 @@ const RichTextEditor = forwardRef( const onUpdateRef = useRef(onUpdate); const onSubmitRef = useRef(onSubmit); - // Helper to get markdown from @tiptap/markdown extension + // Helper to get markdown from @tiptap/markdown extension. + // Post-processes mention shortcodes [@ id="..." label="..."] → markdown + // links, using the Tiptap JSON doc for type info, in case the + // renderMarkdown override doesn't take effect. // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getEditorMarkdown = (ed: any): string => - ed?.getMarkdown?.() ?? ""; + const getEditorMarkdown = (ed: any): string => { + const md: string = ed?.getMarkdown?.() ?? ""; + if (!md || !md.includes("[@ ")) return md; + + // Build type map from editor JSON (which always has the type attr) + const json = ed?.getJSON?.(); + const typeMap = new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function walk(node: any) { + if (node?.type === "mention" && node.attrs?.id) { + typeMap.set(node.attrs.id, node.attrs.type || "member"); + } + if (node?.content) node.content.forEach(walk); + } + if (json) walk(json); + + return md.replace( + /\[@\s+([^\]]*)\]/g, + (match: string, attrString: string) => { + const attrs: Record = {}; + const re = /(\w+)="([^"]*)"/g; + let m; + while ((m = re.exec(attrString)) !== null) { + if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2]; + } + const { id, label } = attrs; + if (!id || !label) return match; + const type = typeMap.get(id) || "member"; + const display = type === "issue" ? label : `@${label}`; + return `[${display}](mention://${type}/${id})`; + }, + ); + }; // Keep refs in sync without recreating editor onUpdateRef.current = onUpdate; diff --git a/apps/web/components/markdown/Markdown.tsx b/apps/web/components/markdown/Markdown.tsx index 6ddff630..178f91b7 100644 --- a/apps/web/components/markdown/Markdown.tsx +++ b/apps/web/components/markdown/Markdown.tsx @@ -53,6 +53,28 @@ function urlTransform(url: string): string { return defaultUrlTransform(url) } +/** + * Convert legacy mention shortcodes [@ id="UUID" label="LABEL"] to markdown + * link format [@LABEL](mention://member/UUID) so they render as styled mentions. + */ +function preprocessMentionShortcodes(text: string): string { + if (!text.includes('[@ ')) return text + return text.replace( + /\[@\s+([^\]]*)\]/g, + (match, attrString: string) => { + const attrs: Record = {} + const re = /(\w+)="([^"]*)"/g + let m + while ((m = re.exec(attrString)) !== null) { + if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2] + } + const { id, label } = attrs + if (!id || !label) return match + return `[@${label}](mention://member/${id})` + } + ) +} + // File path detection regex - matches paths starting with /, ~/, or ./ const FILE_PATH_REGEX = /^(?:\/|~\/|\.\/)[\w\-./@]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yaml|yml|py|go|rs|css|scss|less|html|htm|txt|log|sh|bash|zsh|swift|kt|java|c|cpp|h|hpp|rb|php|xml|toml|ini|cfg|conf|env|sql|graphql|vue|svelte|astro|prisma)$/i @@ -291,8 +313,11 @@ export function Markdown({ [mode, onUrlClick, onFileClick] ) - // Preprocess to convert raw URLs and file paths to markdown links - const processedContent = React.useMemo(() => preprocessLinks(children), [children]) + // Preprocess: convert mention shortcodes and raw URLs/file paths to markdown links + const processedContent = React.useMemo( + () => preprocessLinks(preprocessMentionShortcodes(children)), + [children] + ) return (