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; +}