(text: string, keyPrefix: string, mentionClassName: string = 'text-white')
| 241 | |
| 242 | // Parse inline markdown and HTML (bold, italic, code, br, links, images, plain URLs, @mentions) |
| 243 | function parseInline(text: string, keyPrefix: string, mentionClassName: string = 'text-white'): React.ReactNode[] { |
| 244 | // FIRST PASS: Handle escape sequences (e.g., \* should become just *) |
| 245 | const unescapedText = text.replace(/\\([*_`[\]()#+-.|!\\])/g, '$1'); |
| 246 | |
| 247 | // SECOND PASS: Extract inline math and replace with safe tokens that won't be matched by markdown regex |
| 248 | const mathBlocks: string[] = []; |
| 249 | const mathPlaceholder = '\u0000MATH'; // Unique placeholder that markdown won't match |
| 250 | |
| 251 | const processedText = unescapedText.replace( |
| 252 | /(?<!\\)((?:\\\\)*)\\\((.+?)\\\)/g, |
| 253 | (match, backslashes, latex) => { |
| 254 | if (backslashes && backslashes.length % 2 === 1) { |
| 255 | // Escaped, remove one backslash |
| 256 | return match.slice(1); |
| 257 | } |
| 258 | // Store math and return placeholder |
| 259 | const index = mathBlocks.push(latex) - 1; |
| 260 | return `${mathPlaceholder}${index}\u0000`; |
| 261 | } |
| 262 | ); |
| 263 | |
| 264 | const parts: React.ReactNode[] = []; |
| 265 | let key = 0; |
| 266 | |
| 267 | // THIRD PASS: Process markdown - math placeholders won't be captured by markdown patterns |
| 268 | // Added @mention pattern at the end: @username (alphanumeric, underscore, hyphen) |
| 269 | // Note: (?<!\S) ensures @ is at start of word (not in email like user@example.com) |
| 270 | const regex = /(\*\*(.+?)\*\*|\*([^\s*](?:[^*]*[^\s*])?)\*|_([^_]+?)_|`([^`]+?)`|<br\s*\/?>|<b>(.+?)<\/b>|<strong>(.+?)<\/strong>|<i>(.+?)<\/i>|<em>(.+?)<\/em>|<code>(.+?)<\/code>|<a\s+href=["']([^"']+)["']>(.+?)<\/a>|\[([^\]]+)\]\(((?:[^\s()]|\([^\s)]*\))+)(?:\s+"([^"]+)")?\)|!\[([^\]]*)\]\(((?:[^()]|\([^)]*\))+)\)|(unicity-connect:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"])|(https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"])|((?<!\S)@[\w-]+))/gi; |
| 271 | let lastIndex = 0; |
| 272 | let match; |
| 273 | |
| 274 | while ((match = regex.exec(processedText)) !== null) { |
| 275 | // Process text before match (may contain math placeholders) |
| 276 | if (match.index > lastIndex) { |
| 277 | const textBefore = processedText.slice(lastIndex, match.index); |
| 278 | parts.push(...replaceMathPlaceholders(textBefore, mathBlocks, keyPrefix, key)); |
| 279 | key += mathBlocks.length; |
| 280 | } |
| 281 | |
| 282 | if (match[2]) { |
| 283 | // **bold** - recursively parse content for @mentions, links, etc. |
| 284 | const content = parseInline(match[2], `${keyPrefix}-bold-${key}`, mentionClassName); |
| 285 | parts.push(<strong key={`${keyPrefix}-strong-${key++}`}>{content}</strong>); |
| 286 | } else if (match[3]) { |
| 287 | // *italic* - recursively parse content |
| 288 | const content = parseInline(match[3], `${keyPrefix}-italic-${key}`, mentionClassName); |
| 289 | parts.push(<em key={`${keyPrefix}-em-${key++}`}>{content}</em>); |
| 290 | } else if (match[4]) { |
| 291 | // _italic_ - recursively parse content |
| 292 | const content = parseInline(match[4], `${keyPrefix}-italic2-${key}`, mentionClassName); |
| 293 | parts.push(<em key={`${keyPrefix}-em2-${key++}`}>{content}</em>); |
| 294 | } else if (match[5]) { |
| 295 | // `code` - don't process math inside code blocks |
| 296 | parts.push( |
| 297 | <code key={`${keyPrefix}-code-${key++}`} className="bg-neutral-200 dark:bg-neutral-700/50 text-neutral-900 dark:text-neutral-200 px-1.5 py-0.5 rounded text-sm font-mono"> |
| 298 | {match[5]} |
| 299 | </code> |
| 300 | ); |
no test coverage detected