Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/lib/components/shared/WhatsNewModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,46 @@
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
// Wrap consecutive list items in ul
.replace(/(<li[^>]*>.*<\/li>\n?)+/g, '<ul class="my-2 space-y-1">$&</ul>')
// GFM tables: `| h1 | h2 |\n|---|---|\n| a | b |` -> <table>. Run before
// the \n -> <br> conversion below so newlines inside the table survive.
// Supports column alignment via the separator row (`:---`, `---:`, `:---:`).
.replace(
/^(\|[^\n]+\|)\n(\|[-:| ]+\|)\n((?:\|[^\n]+\|\n?)+)/gm,
(_match: string, headerLine: string, separatorLine: string, bodyBlock: string) => {
const parseCells = (line: string): string[] =>
line.slice(1, -1).split('|').map((c) => c.trim());
const headers = parseCells(headerLine);
const aligns = parseCells(separatorLine).map((sep) => {
const left = sep.startsWith(':');
const right = sep.endsWith(':');
if (left && right) return 'center';
if (right) return 'right';
if (left) return 'left';
return '';
});
const rows: string[][] = bodyBlock.trim().split('\n').map(parseCells);
const styleAttr = (i: number) =>
aligns[i] ? ` style="text-align:${aligns[i]}"` : '';
const ths = headers
.map(
(h, i) =>
`<th class="px-2 py-1 border-b border-gray-200 dark:border-gray-700 font-semibold text-left"${styleAttr(i)}>${h}</th>`
)
.join('');
const trs = rows
.map(
(r: string[]) =>
`<tr>${r
.map(
(c: string, i: number) =>
`<td class="px-2 py-1 border-b border-gray-100 dark:border-gray-800"${styleAttr(i)}>${c}</td>`
)
.join('')}</tr>`
)
.join('');
return `<table class="my-2 border-collapse w-full text-sm"><thead><tr>${ths}</tr></thead><tbody>${trs}</tbody></table>`;
}
)
// Line breaks
.replace(/\n\n/g, '</p><p class="my-2">')
.replace(/\n/g, '<br>');
Expand Down
45 changes: 45 additions & 0 deletions src/tests/components/shared.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,51 @@ describe('WhatsNewModal Component', () => {
expect(screen.getByText('No release notes available.')).toBeInTheDocument();
(whatsNew as any).isOpen = false;
});

it('should render GFM tables in release notes (#235)', async () => {
const { whatsNew } = await import('$lib/stores/whatsNew.svelte');
(whatsNew as any).isOpen = true;
(whatsNew as any).isLoading = false;
(whatsNew as any).release = {
version: '3.10.0',
body: '## Download\n\n| Platform | Download |\n|----------|----------|\n| Windows | `.msi` |\n| macOS | `.dmg` |\n',
htmlUrl: 'https://github.com/test',
publishedAt: '2026-05-16'
};
render(WhatsNewModal);
// The raw markdown pipes should NOT be visible as text — they should be
// inside a <table>. This is the regression #235 reported: tables rendered
// as raw `|---|---|` literals.
const table = document.querySelector('table');
expect(table).toBeInTheDocument();
expect(table?.querySelectorAll('thead th')).toHaveLength(2);
expect(table?.querySelectorAll('tbody tr')).toHaveLength(2);
expect(table?.querySelector('thead')?.textContent).toContain('Platform');
expect(table?.querySelector('thead')?.textContent).toContain('Download');
expect(table?.querySelector('tbody')?.textContent).toContain('Windows');
expect(table?.querySelector('tbody')?.textContent).toContain('macOS');
(whatsNew as any).isOpen = false;
(whatsNew as any).release = null;
});

it('should honor table column alignment specifiers', async () => {
const { whatsNew } = await import('$lib/stores/whatsNew.svelte');
(whatsNew as any).isOpen = true;
(whatsNew as any).isLoading = false;
(whatsNew as any).release = {
version: '3.10.0',
body: '| L | C | R |\n|:---|:---:|---:|\n| a | b | c |\n',
htmlUrl: 'https://github.com/test',
publishedAt: '2026-05-16'
};
render(WhatsNewModal);
const ths = document.querySelectorAll('table thead th');
expect(ths[0].getAttribute('style')).toBe('text-align:left');
expect(ths[1].getAttribute('style')).toBe('text-align:center');
expect(ths[2].getAttribute('style')).toBe('text-align:right');
(whatsNew as any).isOpen = false;
(whatsNew as any).release = null;
});
});

describe('Shared index.ts exports', () => {
Expand Down
Loading