diff --git a/package.json b/package.json
index cb8b3051..7dddce96 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
},
"devDependencies": {
"@eslint/js": "9.29.0",
- "@storybook/react-vite": "9.0.11",
+ "@storybook/react-vite": "9.0.12",
"@testing-library/react": "16.3.0",
"@types/node": "24.0.3",
"@types/react": "19.1.8",
@@ -73,12 +73,12 @@
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
- "eslint-plugin-storybook": "9.0.11",
+ "eslint-plugin-storybook": "9.0.12",
"globals": "16.2.0",
"jsdom": "26.1.0",
"nodemon": "3.1.10",
"npm-run-all": "4.1.5",
- "storybook": "9.0.11",
+ "storybook": "9.0.12",
"typescript": "5.8.3",
"typescript-eslint": "8.34.1",
"vite": "6.3.5",
diff --git a/src/components/Markdown/Markdown.test.tsx b/src/components/Markdown/Markdown.test.tsx
index 67c8689d..fae194c2 100644
--- a/src/components/Markdown/Markdown.test.tsx
+++ b/src/components/Markdown/Markdown.test.tsx
@@ -449,14 +449,14 @@ describe('Markdown with tables', () => {
})
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()
+ const text = 'alpha | beta\n---\ngamma'
+ const { getByText, queryByRole } = render()
+ expect(getByText('alpha | beta').tagName).toBe('P')
+ expect(getByText('gamma').tagName).toBe('P')
+ expect(queryByRole('table')).toBeNull()
})
- it('ignores pipe‑separated text that lacks a separator line', () => {
+ 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
@@ -478,4 +478,30 @@ describe('Markdown with tables', () => {
expect(getByText('Header1')).toBeDefined()
expect(getByText('Data2')).toBeDefined()
})
+
+ it('keeps a pipe that is inside inline code in a table', () => {
+ const text = 'Header1 | Header2\n------- | -------\n| Here is some `inline | code` with a pipe. |'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('inline | code')).toBeDefined()
+ })
+
+ it('does not treat --- as a table separator', () => {
+ const text = 'Column 1 | Column 2\n---\nData 1 | Data 2'
+ const { queryByRole, getByRole, getByText } = render()
+ expect(queryByRole('table')).toBeNull() // no table
+ expect(getByText('Column 1 | Column 2')).toBeDefined()
+ expect(getByRole('separator')).toBeDefined() // horizontal rule
+ expect(getByText('Data 1 | Data 2')).toBeDefined()
+ })
+
+ it('handles escaped pipes in table cells', () => {
+ const text = '| Header \\| 1 | Header 2 |\n|----------|----------|\n| Cell with \\| escaped pipe | Normal cell |'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('Header | 1')).toBeDefined()
+ expect(getByText('Header 2')).toBeDefined()
+ expect(getByText('Cell with | escaped pipe')).toBeDefined()
+ expect(getByText('Normal cell')).toBeDefined()
+ })
})
diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx
index ac12311b..ffc7ea9b 100644
--- a/src/components/Markdown/Markdown.tsx
+++ b/src/components/Markdown/Markdown.tsx
@@ -113,7 +113,7 @@ function parseMarkdown(text: string): Token[] {
// Check if the next line is a valid table separator
// Extended markdown alignment syntax: |:--|, |:--:|, |--:|
const tableSepRegex = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/
- if (tableSepRegex.test(sepLine)) {
+ if (sepLine.includes('|') && tableSepRegex.test(sepLine)) {
// collect header cells
const headerCells = splitTableRow(line)
i += 2
@@ -446,12 +446,40 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
/** Split a table row, trimming outer pipes and whitespace */
function splitTableRow(row: string): string[] {
- return row
- .trim()
- .replace(/^\|/, '')
- .replace(/\|$/, '')
- .split('|')
- .map(cell => cell.trim())
+ const cells: string[] = []
+ let current = ''
+ let inCode = false
+
+ // strip a single leading / trailing pipe so we don't create empty edge‑cells
+ const trimmed = row.trim()
+ const start = trimmed.startsWith('|') ? 1 : 0
+ const end = trimmed.endsWith('|') ? trimmed.length - 1 : trimmed.length
+
+ for (let i = start; i < end; i++) {
+ const ch = trimmed[i] ?? ''
+
+ if (ch === '`') {
+ inCode = !inCode
+ current += ch
+ continue
+ }
+
+ if (ch === '|' && !inCode) {
+ // escaped pipe \| becomes |
+ if (i > start && trimmed[i - 1] === '\\') {
+ current = current.slice(0, -1) + '|'
+ continue
+ }
+ cells.push(current.trim())
+ current = ''
+ continue
+ }
+
+ current += ch
+ }
+
+ cells.push(current.trim())
+ return cells
}
function isOpeningUnderscore(text: string, pos: number): boolean {
@@ -591,6 +619,10 @@ function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
})
}
+/**
+ * Markdown component. Converts markdown text to React elements.
+ * Designed to handle streaming markdown input gracefully.
+ */
export default function Markdown({ text, className }: MarkdownProps) {
const tokens = parseMarkdown(text)
return createElement('div', { className }, renderTokens(tokens))
diff --git a/src/components/MarkdownView/MarkdownView.module.css b/src/components/MarkdownView/MarkdownView.module.css
index be129583..a1284e2e 100644
--- a/src/components/MarkdownView/MarkdownView.module.css
+++ b/src/components/MarkdownView/MarkdownView.module.css
@@ -37,14 +37,16 @@
}
.markdownView th,
.markdownView td {
- border-bottom: 1px solid #333;
padding: 8px;
text-align: left;
}
.markdownView th {
- border-bottom: 2px solid #333;
+ border-bottom: 2px solid #888;
font-weight: 500;
}
+.markdownView td {
+ border-bottom: 1px solid #666;
+}
.markdownView tr:last-child td {
border-bottom: none;
}