diff --git a/src/components/Markdown/Markdown.test.tsx b/src/components/Markdown/Markdown.test.tsx
index 10d5e5bf..6950aad3 100644
--- a/src/components/Markdown/Markdown.test.tsx
+++ b/src/components/Markdown/Markdown.test.tsx
@@ -400,3 +400,47 @@ describe('Markdown with nested elements', () => {
expect(getByText('Second').tagName).toBe('LI')
})
})
+
+describe('Markdown with tables', () => {
+ it('renders a simple table', () => {
+ const text = '| Header 1 | Header 2 |\n|----------|----------|\n| Row 1 | Data 1 |\n| Row 2 | Data 2 |'
+ const { getByText } = render()
+ expect(getByText('Header 1')).toBeDefined()
+ expect(getByText('Header 2')).toBeDefined()
+ expect(getByText('Row 1')).toBeDefined()
+ expect(getByText('Data 1')).toBeDefined()
+ expect(getByText('Row 2')).toBeDefined()
+ expect(getByText('Data 2')).toBeDefined()
+ })
+
+ it('renders a table that omits the outer pipes', () => {
+ const text = 'Header A | Header B\n---|---\nCell 1 | Cell 2'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('Header A')).toBeDefined()
+ expect(getByText('Cell 2')).toBeDefined()
+ })
+
+ it('renders inline formatting inside cells', () => {
+ const text = '| **Bold** | Link |\n|-----------|------|\n| `code` | [x](#) |'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('Bold').tagName).toBe('STRONG')
+ expect(getByRole('link', { name: 'x' })).toBeDefined()
+ })
+
+ it('keeps surrounding paragraphs intact', () => {
+ const text = 'Above\n\n| H1 | H2 |\n|----|----|\n| a | b |\n\nBelow'
+ const { getByText, getByRole } = render()
+ expect(getByText('Above').tagName).toBe('P')
+ expect(getByText('Below').tagName).toBe('P')
+ expect(getByRole('table')).toBeDefined()
+ })
+
+ it('ignores pipe‑separated text that lacks a separator line', () => {
+ const bogus = 'not | a | table'
+ const { queryByRole, getByText } = render()
+ expect(queryByRole('table')).toBeNull() // no table
+ expect(getByText('not | a | table')).toBeDefined()
+ })
+})
diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx
index 81a27d91..4cd5b0d8 100644
--- a/src/components/Markdown/Markdown.tsx
+++ b/src/components/Markdown/Markdown.tsx
@@ -18,6 +18,7 @@ type Token =
| { type: 'blockquote', children: Token[] }
| { type: 'codeblock', language?: string, content: string }
| { type: 'hr' }
+ | { type: 'table', header: Token[][], rows: Token[][][] }
function parseMarkdown(text: string): Token[] {
const tokens: Token[] = []
@@ -106,6 +107,31 @@ function parseMarkdown(text: string): Token[] {
continue
}
+ // Table (requires header line | separator line)
+ if (line.includes('|') && i + 1 < lines.length) {
+ const sepLine = lines[i + 1] ?? ''
+ // Check if the next line is a valid table separator
+ // Extended markdown alignment syntax: |:--|, |:--:|, |--:|
+ if (/^\s*\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$/.test(sepLine)) {
+ // collect header cells
+ const headerCells = splitTableRow(line)
+ i += 2
+
+ const rows: Token[][][] = []
+ while (i < lines.length && lines[i]?.includes('|')) {
+ rows.push(splitTableRow(lines[i] ?? '').map(c => parseInline(c)))
+ i++
+ }
+
+ tokens.push({
+ type: 'table',
+ header: headerCells.map(c => parseInline(c)),
+ rows,
+ })
+ continue
+ }
+ }
+
// Paragraph
const paraLines: string[] = []
while (i < lines.length) {
@@ -416,6 +442,16 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
return [tokens, i]
}
+/** Split a table row, trimming outer pipes and whitespace */
+function splitTableRow(row: string): string[] {
+ return row
+ .trim()
+ .replace(/^\|/, '')
+ .replace(/\|$/, '')
+ .split('|')
+ .map(cell => cell.trim())
+}
+
function isOpeningUnderscore(text: string, pos: number): boolean {
const prev = text[pos - 1] ?? '\n'
const next = text[pos + 1] ?? '\n'
@@ -510,6 +546,43 @@ function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
)
case 'hr':
return createElement('hr', { key })
+ case 'table': {
+ const thead = createElement(
+ 'thead',
+ null,
+ createElement(
+ 'tr',
+ null,
+ token.header.map((cell, c) =>
+ createElement(
+ 'th',
+ { key: `${key}-h${c}` },
+ renderTokens(cell, `${key}-h${c}-`)
+ )
+ )
+ )
+ )
+
+ const tbody = createElement(
+ 'tbody',
+ null,
+ token.rows.map((row, r) =>
+ createElement(
+ 'tr',
+ { key: `${key}-r${r}` },
+ row.map((cell, c) =>
+ createElement(
+ 'td',
+ { key: `${key}-r${r}c${c}` },
+ renderTokens(cell, `${key}-r${r}c${c}-`)
+ )
+ )
+ )
+ )
+ )
+
+ return createElement('table', { key }, [thead, tbody])
+ }
default:
return null
}