/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : test/unit/markdown.test.mjs * Purpose : Unit tests — the flagship README rendering pipeline * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { describe, it, expect, beforeAll } from 'vitest'; import { renderMarkdown, githubSlug, resolveRelativeUrl } from '../../src/render/markdown.mjs'; import { initHighlighter } from '../../src/render/highlight.mjs'; const CTX = { repo: 'demo', ref: 'main', basePath: '.', publicUrl: 'https://git.spboucher.ai' }; beforeAll(async () => { await initHighlighter(); }, 60000); describe('githubSlug', () => { it('matches the GitHub algorithm', () => { expect(githubSlug('Installation')).toBe('installation'); expect(githubSlug('Getting Started!')).toBe('getting-started'); expect(githubSlug('API — v2.0 (beta)')).toBe('api--v20-beta'); }); }); describe('resolveRelativeUrl', () => { it('rewrites relative images to raw', () => { expect(resolveRelativeUrl('./docs/demo.png', CTX, 'raw')).toBe('/raw/demo/main/docs/demo.png'); expect(resolveRelativeUrl('docs/demo.png', CTX, 'raw')).toBe('/raw/demo/main/docs/demo.png'); }); it('rewrites relative links to blob', () => { expect(resolveRelativeUrl('CONTRIBUTING.md', CTX, 'blob')).toBe('/demo/blob/main/CONTRIBUTING.md'); }); it('resolves against the document directory', () => { const nested = { ...CTX, basePath: 'docs' }; expect(resolveRelativeUrl('img/x.png', nested, 'raw')).toBe('/raw/demo/main/docs/img/x.png'); expect(resolveRelativeUrl('../top.png', nested, 'raw')).toBe('/raw/demo/main/top.png'); }); it('leaves absolute, anchor, and external URLs alone', () => { expect(resolveRelativeUrl('https://x.com/a.png', CTX, 'raw')).toBe('https://x.com/a.png'); expect(resolveRelativeUrl('#section', CTX, 'blob')).toBe('#section'); expect(resolveRelativeUrl('/already/rooted', CTX, 'blob')).toBe('/already/rooted'); }); }); describe('renderMarkdown', () => { it('keeps shields.io badges inline', async () => { const html = await renderMarkdown('![b](https://img.shields.io/badge/x-y-green) ![c](https://img.shields.io/badge/a-b-blue)', CTX); expect(html).toContain('src="https://img.shields.io/badge/x-y-green"'); expect(html).toContain('loading="lazy"'); }); it('renders GFM tables, strikethrough, task lists', async () => { const html = await renderMarkdown('| a |\n|---|\n| 1 |\n\n~~gone~~\n\n- [x] done\n- [ ] todo', CTX); expect(html).toContain(''); expect(html).toContain('gone'); expect(html).toMatch(/checkbox[^>]*checked/); expect(html).toContain('disabled'); }); it('adds GitHub-style heading anchors', async () => { const html = await renderMarkdown('## Getting Started', CTX); expect(html).toContain('id="getting-started"'); expect(html).toContain('heading-anchor'); }); it('strips scripts, event handlers, and iframes but keeps details/kbd/align', async () => { const source = [ '', '', '', '
MoreBody
', 'Ctrl', '

Centered

', ].join('\n\n'); const html = await renderMarkdown(source, CTX); expect(html).not.toContain(''); expect(html).toContain('Ctrl'); expect(html).toContain('align="center"'); }); it('rewrites relative HTML img src to the raw endpoint', async () => { const html = await renderMarkdown('', CTX); expect(html).toContain('src="/raw/demo/main/assets/logo.png"'); expect(html).toContain('width="120"'); }); it('highlights fenced code with shiki dual themes', async () => { const html = await renderMarkdown('```js\nconst x = 1;\n```', CTX); expect(html).toContain('shiki'); expect(html).toContain('--shiki-dark'); }); it('emits mermaid placeholders for client rendering', async () => { const html = await renderMarkdown('```mermaid\ngraph TD; A-->B;\n```', CTX); expect(html).toContain('mermaid-block'); expect(html).toContain('A-->B'); }); it('converts emoji shortcodes', async () => { const html = await renderMarkdown('ship it :rocket:', CTX); expect(html).toContain('🚀'); }); it('supports footnotes', async () => { const html = await renderMarkdown('text[^1]\n\n[^1]: the note', CTX); expect(html).toContain('footnote'); }); it('never emits javascript: hrefs (markdown or raw HTML)', async () => { const md = await renderMarkdown('[click](javascript:alert(1))', CTX); expect(md).not.toMatch(/href\s*=\s*["']?javascript:/i); const html = await renderMarkdown('click', CTX); expect(html).not.toMatch(/href\s*=\s*["']?javascript:/i); }); });