SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
5.4 KB · 125 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : test/unit/markdown.test.mjs8 *  Purpose : Unit tests — the flagship README rendering pipeline9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { describe, it, expect, beforeAll } from 'vitest';14import { renderMarkdown, githubSlug, resolveRelativeUrl } from '../../src/render/markdown.mjs';15import { initHighlighter } from '../../src/render/highlight.mjs';1617const CTX = { repo: 'demo', ref: 'main', basePath: '.', publicUrl: 'https://git.spboucher.ai' };1819beforeAll(async () => {20  await initHighlighter();21}, 60000);2223describe('githubSlug', () => {24  it('matches the GitHub algorithm', () => {25    expect(githubSlug('Installation')).toBe('installation');26    expect(githubSlug('Getting Started!')).toBe('getting-started');27    expect(githubSlug('API — v2.0 (beta)')).toBe('api--v20-beta');28  });29});3031describe('resolveRelativeUrl', () => {32  it('rewrites relative images to raw', () => {33    expect(resolveRelativeUrl('./docs/demo.png', CTX, 'raw')).toBe('/raw/demo/main/docs/demo.png');34    expect(resolveRelativeUrl('docs/demo.png', CTX, 'raw')).toBe('/raw/demo/main/docs/demo.png');35  });36  it('rewrites relative links to blob', () => {37    expect(resolveRelativeUrl('CONTRIBUTING.md', CTX, 'blob')).toBe('/demo/blob/main/CONTRIBUTING.md');38  });39  it('resolves against the document directory', () => {40    const nested = { ...CTX, basePath: 'docs' };41    expect(resolveRelativeUrl('img/x.png', nested, 'raw')).toBe('/raw/demo/main/docs/img/x.png');42    expect(resolveRelativeUrl('../top.png', nested, 'raw')).toBe('/raw/demo/main/top.png');43  });44  it('leaves absolute, anchor, and external URLs alone', () => {45    expect(resolveRelativeUrl('https://x.com/a.png', CTX, 'raw')).toBe('https://x.com/a.png');46    expect(resolveRelativeUrl('#section', CTX, 'blob')).toBe('#section');47    expect(resolveRelativeUrl('/already/rooted', CTX, 'blob')).toBe('/already/rooted');48  });49});5051describe('renderMarkdown', () => {52  it('keeps shields.io badges inline', async () => {53    const html = await renderMarkdown('![b](https://img.shields.io/badge/x-y-green) ![c](https://img.shields.io/badge/a-b-blue)', CTX);54    expect(html).toContain('src="https://img.shields.io/badge/x-y-green"');55    expect(html).toContain('loading="lazy"');56  });5758  it('renders GFM tables, strikethrough, task lists', async () => {59    const html = await renderMarkdown('| a |\n|---|\n| 1 |\n\n~~gone~~\n\n- [x] done\n- [ ] todo', CTX);60    expect(html).toContain('<table>');61    expect(html).toContain('<s>gone</s>');62    expect(html).toMatch(/checkbox[^>]*checked/);63    expect(html).toContain('disabled');64  });6566  it('adds GitHub-style heading anchors', async () => {67    const html = await renderMarkdown('## Getting Started', CTX);68    expect(html).toContain('id="getting-started"');69    expect(html).toContain('heading-anchor');70  });7172  it('strips scripts, event handlers, and iframes but keeps details/kbd/align', async () => {73    const source = [74      '<script>alert(1)</script>',75      '<img src="x.png" onerror="alert(1)">',76      '<iframe src="https://evil.com"></iframe>',77      '<details><summary>More</summary>Body</details>',78      '<kbd>Ctrl</kbd>',79      '<h1 align="center">Centered</h1>',80    ].join('\n\n');81    const html = await renderMarkdown(source, CTX);82    expect(html).not.toContain('<script');83    expect(html).not.toContain('onerror');84    expect(html).not.toContain('<iframe');85    expect(html).toContain('<details>');86    expect(html).toContain('<kbd>Ctrl</kbd>');87    expect(html).toContain('align="center"');88  });8990  it('rewrites relative HTML img src to the raw endpoint', async () => {91    const html = await renderMarkdown('<img src="./assets/logo.png" width="120">', CTX);92    expect(html).toContain('src="/raw/demo/main/assets/logo.png"');93    expect(html).toContain('width="120"');94  });9596  it('highlights fenced code with shiki dual themes', async () => {97    const html = await renderMarkdown('```js\nconst x = 1;\n```', CTX);98    expect(html).toContain('shiki');99    expect(html).toContain('--shiki-dark');100  });101102  it('emits mermaid placeholders for client rendering', async () => {103    const html = await renderMarkdown('```mermaid\ngraph TD; A-->B;\n```', CTX);104    expect(html).toContain('mermaid-block');105    expect(html).toContain('A--&gt;B');106  });107108  it('converts emoji shortcodes', async () => {109    const html = await renderMarkdown('ship it :rocket:', CTX);110    expect(html).toContain('🚀');111  });112113  it('supports footnotes', async () => {114    const html = await renderMarkdown('text[^1]\n\n[^1]: the note', CTX);115    expect(html).toContain('footnote');116  });117118  it('never emits javascript: hrefs (markdown or raw HTML)', async () => {119    const md = await renderMarkdown('[click](javascript:alert(1))', CTX);120    expect(md).not.toMatch(/href\s*=\s*["']?javascript:/i);121    const html = await renderMarkdown('<a href="javascript:alert(1)">click</a>', CTX);122    expect(html).not.toMatch(/href\s*=\s*["']?javascript:/i);123  });124});125