import React from 'react';
import { CodeBlock } from '../code-block';
/**
* Props for the MarkdownMessage component.
*/
interface MarkdownMessageProps {
/** Raw markdown content to render */
content: string;
/** Custom CSS classes for the container */
className?: string;
}
/**
* Fenced code blocks (```lang\ncode\n```) are extracted before any other
* regex runs and rendered as real `$1
'
);
html = html.replace(
/^### (.*$)/gim,
'$1
'
);
html = html.replace(
/^## (.*$)/gim,
'$1
'
);
html = html.replace(
/^# (.*$)/gim,
'$1
'
);
// Blockquotes — `>` was already escaped to `>` above, so match that.
// Consecutive `>`-prefixed lines are grouped into a single .
html = html.replace(/(?:^> ?.*$(?:\n|$))+/gim, match => {
const lines = match
.trim()
.split(/\r?\n/)
.map(line => line.replace(/^> ?/, ''));
return `
${lines.join('
`;
});
// Lists run BEFORE bold/italic, not after — GFM allows `*` as a bullet
// marker (alongside `-`/`•`), which is also the italic delimiter. If bold/
// italic ran first, a bullet line's leading `* ` with no closing `*` on
// the same line would sit there waiting, and the *next* bullet line's `*`
// (or a genuine `*italic*` later in the same item) could pair with it,
// corrupting both. Converting bullet/number markers to `
')}${items}
`;
});
// Ordered lists - properly wrap in ol
html = html.replace(/((?:^\d+\. .*$(?:\n|$))+)/gim, match => {
const items = match
.trim()
.split('\n')
.map(
item => `${items}
`;
});
// Bold (must run before italic, or "**x**" is read as two "*...*" matches)
html = html.replace(/\*\*(.*?)\*\*/g, '$1');
// Italic
html = html.replace(/\*(.*?)\*/g, '$1');
// Strikethrough (GFM)
html = html.replace(/~~(.*?)~~/g, '$1');
// Links
html = html.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'$1'
);
// Code inline
html = html.replace(
/`([^`]+)`/g,
'$1'
);
// GFM Tables — must run before line-break transforms
html = html.replace(/((?:^\|.+\|[ \t]*(?:\r?\n|$))+)/gm, match => {
const rows = match
.trim()
.split(/\r?\n/)
.filter(line => line.trim());
if (rows.length < 2) return match;
const isSeparatorRow = /^\|[\s\-:|]+\|$/.test(rows[1].trim());
if (!isSeparatorRow) return match;
const parseCells = (row: string) =>
row
.split('|')
.slice(1, -1)
.map(cell => cell.trim());
const headerCells = parseCells(rows[0])
.map(
cell =>
`${cell} `
)
.join('');
const bodyRows = rows
.slice(2)
.map(
row =>
`${parseCells(row)
.map(cell => ` `
)
.join('');
return `${cell} `)
.join('')}${headerCells} ${bodyRows}
');
html = html.replace(/\n/g, '
');
// Wrap in initial paragraph
html = '
' + html + '
'; // Clean up empty paragraphs html = html.replace(/]*>\s*<\/p>/g, ''); return html; } /** * Lightweight Markdown Parser and Renderer. * * @description * A simplified markdown renderer built for chat messages, where a full-blown * parser (react-markdown + remark/rehype plugins) would be overkill. Supports * headers (h1-h4), bold, italic, strikethrough, links, inline code, fenced * code blocks (rendered via `CodeBlock`, with syntax highlighting), blockquotes, * ordered/unordered lists, and GFM tables. * * @ai-rules * 1. Sanitization: Uses regex for conversion. Be cautious with complex markdown structures. * 2. Styling: Injected HTML uses specific Tailwind classes for consistent typography. * 3. Security: Non-code content uses `dangerouslySetInnerHTML` after escaping `<`/`>`. * Fenced code content is rendered as a React text child (via `CodeBlock`), which * React escapes automatically — never re-escape it manually. */ export function MarkdownMessage({ content, className = '' }: MarkdownMessageProps) { const segments = splitCodeBlocks(content); return (