diff --git a/src/components/Markdown/Markdown.test.tsx b/src/components/Markdown/Markdown.test.tsx
index 85f2b54f..67c8689d 100644
--- a/src/components/Markdown/Markdown.test.tsx
+++ b/src/components/Markdown/Markdown.test.tsx
@@ -88,7 +88,7 @@ describe('Markdown', () => {
expect(getByText('Hyp').tagName).toBe('A')
})
- it('renders multiple links in one line', () => {
+ it('multiple links in one line', () => {
const text = 'Check out [Hyp](https://hyperparam.app) on [GitHub](https://github.com/hyparam).'
const { getAllByRole, getByText } = render()
expect(getByText('Hyp')).toBeDefined()
@@ -103,18 +103,27 @@ describe('Markdown', () => {
const { getByText } = render()
expect(getByText('This is a blockquote.')).toBeDefined()
})
+})
+describe('Markdown horizontal rules', () => {
it('renders a horizontal rule', () => {
const text = 'First paragraph\n---\nSecond paragraph'
- const { container } = render()
+ const { container, getByText, queryByRole } = render()
- const paragraphs = container.querySelectorAll('p')
- expect(paragraphs.length).toBe(2)
- expect(paragraphs[0]?.textContent).toBe('First paragraph')
- expect(paragraphs[1]?.textContent).toBe('Second paragraph')
+ expect(container.querySelector('hr')).toBeDefined()
+ expect(queryByRole('separator')).toBeDefined()
+ expect(getByText('First paragraph')).toBeDefined()
+ expect(getByText('Second paragraph')).toBeDefined()
+ })
- const hr = container.querySelector('hr')
- expect(hr).toBeDefined()
+ it('horizontal rule must be entire line', () => {
+ const text = 'First paragraph\n\n--- dashes\n\nSecond paragraph'
+ const { container, getByText, queryByRole } = render()
+ expect(container.querySelector('hr')).toBeNull()
+ expect(queryByRole('separator')).toBeNull()
+ expect(getByText('First paragraph')).toBeDefined()
+ expect(getByText('--- dashes')).toBeDefined()
+ expect(getByText('Second paragraph')).toBeDefined()
})
})
@@ -453,4 +462,20 @@ describe('Markdown with tables', () => {
expect(queryByRole('table')).toBeNull() // no table
expect(getByText('not | a | table')).toBeDefined()
})
+
+ it('single column table', () => {
+ const text = '| Only Header |\n|-------------|\n| Single cell |'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('Only Header')).toBeDefined()
+ expect(getByText('Single cell')).toBeDefined()
+ })
+
+ it('table with no leading or trailing pipes', () => {
+ const text = 'Header1 | Header2\n------- | -------\nData1 | Data2'
+ const { getByText, getByRole } = render()
+ expect(getByRole('table')).toBeDefined()
+ expect(getByText('Header1')).toBeDefined()
+ expect(getByText('Data2')).toBeDefined()
+ })
})
diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx
index 418fc864..ac12311b 100644
--- a/src/components/Markdown/Markdown.tsx
+++ b/src/components/Markdown/Markdown.tsx
@@ -112,7 +112,8 @@ function parseMarkdown(text: string): Token[] {
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)) {
+ const tableSepRegex = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/
+ if (tableSepRegex.test(sepLine)) {
// collect header cells
const headerCells = splitTableRow(line)
i += 2
diff --git a/src/components/MarkdownView/MarkdownView.module.css b/src/components/MarkdownView/MarkdownView.module.css
index c2cc1553..be129583 100644
--- a/src/components/MarkdownView/MarkdownView.module.css
+++ b/src/components/MarkdownView/MarkdownView.module.css
@@ -30,3 +30,21 @@
margin-bottom: 8px;
margin-top: 16px;
}
+
+.markdownView table {
+ border-collapse: collapse;
+ width: 100%;
+}
+.markdownView th,
+.markdownView td {
+ border-bottom: 1px solid #333;
+ padding: 8px;
+ text-align: left;
+}
+.markdownView th {
+ border-bottom: 2px solid #333;
+ font-weight: 500;
+}
+.markdownView tr:last-child td {
+ border-bottom: none;
+}