From 8e5fcd924392e3e1a8890ceeaa3129952e95c9a8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 20:18:15 +0530 Subject: [PATCH] feat(markdown): add mention rendering support in Markdown components --- frontend/src/components/atoms/Markdown.tsx | 157 ++++++++++++++++++--- 1 file changed, 138 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/atoms/Markdown.tsx b/frontend/src/components/atoms/Markdown.tsx index 0f203d4c..7b8bb4d3 100644 --- a/frontend/src/components/atoms/Markdown.tsx +++ b/frontend/src/components/atoms/Markdown.tsx @@ -1,6 +1,14 @@ "use client"; -import { memo, type HTMLAttributes } from "react"; +import { + Children, + cloneElement, + isValidElement, + memo, + type HTMLAttributes, + type ReactElement, + type ReactNode, +} from "react"; import ReactMarkdown, { type Components } from "react-markdown"; import remarkBreaks from "remark-breaks"; @@ -13,6 +21,86 @@ type MarkdownCodeProps = HTMLAttributes & { inline?: boolean; }; +const MENTION_PATTERN = + /(^|[^A-Za-z0-9_])(@[A-Za-z0-9_](?:[A-Za-z0-9_.-]*[A-Za-z0-9_])?)/g; + +const renderMentionsInText = (text: string, keyPrefix: string): ReactNode => { + let lastIndex = 0; + let mentionCount = 0; + const nodes: ReactNode[] = []; + + for (const match of text.matchAll(MENTION_PATTERN)) { + const matchIndex = match.index ?? 0; + const prefix = match[1] ?? ""; + const mention = match[2] ?? ""; + const mentionStart = matchIndex + prefix.length; + + if (matchIndex > lastIndex) { + nodes.push(text.slice(lastIndex, matchIndex)); + } + + if (prefix) { + nodes.push(prefix); + } + + nodes.push( + + {mention} + , + ); + + lastIndex = mentionStart + mention.length; + mentionCount += 1; + } + + if (nodes.length === 0) { + return text; + } + + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + + return nodes; +}; + +const renderMentions = (content: ReactNode, keyPrefix = "mention"): ReactNode => { + if (typeof content === "string") { + return renderMentionsInText(content, keyPrefix); + } + if ( + content === null || + content === undefined || + typeof content === "boolean" || + typeof content === "number" + ) { + return content; + } + if (Array.isArray(content)) { + return Children.map(content, (child, index) => + renderMentions(child, `${keyPrefix}-${index}`), + ); + } + if (isValidElement(content)) { + if (typeof content.type === "string" && content.type === "code") { + return content; + } + const childProps = content.props as { children?: ReactNode }; + if (childProps.children === undefined) { + return content; + } + return cloneElement( + content as ReactElement<{ children?: ReactNode }>, + undefined, + renderMentions(childProps.children, keyPrefix), + ); + } + return content; +}; + const MARKDOWN_CODE_COMPONENTS: Components = { pre: ({ node: _node, className, ...props }) => (
 (
     
   ),
-  th: ({ node: _node, className, ...props }) => (
+  th: ({ node: _node, className, children, ...props }) => (
     
+    >
+      {renderMentions(children)}
+    
   ),
-  td: ({ node: _node, className, ...props }) => (
+  td: ({ node: _node, className, children, ...props }) => (
     
+    >
+      {renderMentions(children)}
+    
   ),
 };
 
 const MARKDOWN_COMPONENTS_BASIC: Components = {
   ...MARKDOWN_TABLE_COMPONENTS,
   ...MARKDOWN_CODE_COMPONENTS,
-  p: ({ node: _node, className, ...props }) => (
-    

+ a: ({ node: _node, className, children, ...props }) => ( + + {renderMentions(children)} + + ), + p: ({ node: _node, className, children, ...props }) => ( +

+ {renderMentions(children)} +

), ul: ({ node: _node, className, ...props }) => (
    @@ -106,27 +213,39 @@ const MARKDOWN_COMPONENTS_BASIC: Components = { ol: ({ node: _node, className, ...props }) => (
      ), - li: ({ node: _node, className, ...props }) => ( -
    1. + li: ({ node: _node, className, children, ...props }) => ( +
    2. + {renderMentions(children)} +
    3. ), - strong: ({ node: _node, className, ...props }) => ( - + strong: ({ node: _node, className, children, ...props }) => ( + + {renderMentions(children)} + ), }; const MARKDOWN_COMPONENTS_DESCRIPTION: Components = { ...MARKDOWN_COMPONENTS_BASIC, - p: ({ node: _node, className, ...props }) => ( -

      + p: ({ node: _node, className, children, ...props }) => ( +

      + {renderMentions(children)} +

      ), - h1: ({ node: _node, className, ...props }) => ( -

      + h1: ({ node: _node, className, children, ...props }) => ( +

      + {renderMentions(children)} +

      ), - h2: ({ node: _node, className, ...props }) => ( -

      + h2: ({ node: _node, className, children, ...props }) => ( +

      + {renderMentions(children)} +

      ), - h3: ({ node: _node, className, ...props }) => ( -

      + h3: ({ node: _node, className, children, ...props }) => ( +

      + {renderMentions(children)} +

      ), };