tiptap のメンションを markdown で永続化するためのショートコード経由ラウンドトリップ
リッチテキストエディタ(tiptap)でメンション(@ユーザー)を扱うとき、保存先が markdown だと素直に往復できない。markdown には mention ノードに相当する記法がないので、エディタ → 保存 → 再表示で id とラベルが落ちる。
専用ショートコード [@ id="..." label="..."] をストレージ上のキャリアに使い、エディタ↔markdown 変換で詰め替える方式が安定した。
const MENTION_LINK = /\[@([^\]\n]+)\]\(mention:([^)]+)\)/g;
const MENTION_SHORTCODE = /\[@\s+([^\]]*)\]/g;
export function toEditorShortcodes(md: string) {
return md.replace(MENTION_LINK, (_, label, id) =>
`[@ id="${escape(id)}" label="${escape(label)}"]`
);
}
export function fromEditorShortcodes(md: string) {
return md.replace(MENTION_SHORTCODE, (full, attrs) => {
const { id, label } = parseAttrs(attrs);
return id ? `[@${escapeMd(label ?? id)}](mention:${id})` : full;
});
}
tiptap 側は Mention 拡張の renderText で @${label} を出すだけにし、独自の Suggestion ポップアップは無効化(suggestion.allow: () => false)して既存の候補UIをそのまま使う:
import { Mention } from "@tiptap/extension-mention";
export const AppMention = Mention.configure({
renderText: ({ node }) => `@${node.attrs.label ?? node.attrs.id}`,
suggestion: { char: "@", allow: () => false },
});
ポイント:
- markdown を一次ストレージにするなら、ノード固有の属性は「人間に読める識別可能なショートコード」で運ぶのが扱いやすい
- 表示・検索ヒット・他クライアントの解釈を考えて、id とラベルの両方を残しておく(id 単独だと表示崩れ、label 単独だと参照ができない)
- tiptap の Suggestion を切るかどうかは、候補UIを別実装で持っているかで決める