fix(editor): post-process mention shortcodes to markdown link format
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.
This commit is contained in:
parent
e16f1579a9
commit
34ee700295
2 changed files with 64 additions and 5 deletions
|
|
@ -162,10 +162,44 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
|
|||
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<string, string>();
|
||||
// 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<string, string> = {};
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {}
|
||||
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 (
|
||||
<div className={cn('markdown-content break-words', className)}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue