/** * The documentation renderer. * * The five player-facing documents are written in Markdown — that is the one copy, and the whole * reason TODO #15a exists — and rendered to pages at build time. This covers the subset those * documents actually use, and the two properties that matter most: a table comes out as a TABLE * (the entire point of rendering rather than serving text), and nothing in the prose can become * markup by accident. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { renderMarkdown } from '../scripts/markdown.ts'; const html = (src: string): string => renderMarkdown(src).html; describe('the documentation renderer', () => { it('turns a pipe table into a real table, with its alignment', () => { // This is what rendering is FOR. A card reference is mostly tables, and as plain text a table // is rows of pipes — which is exactly how the guide read when it was served as text/plain. const out = html( ['| Card | Regions | Passes |', '| --- | ---: | :---: |', '| Plains | 1 | no |', '| Tunnel | 2 | yes |'].join('\n'), ); assert.match(out, //, 'the table is not a table'); assert.match(out, //g) ?? []).length, 3, 'wrong number of rows'); // Wrapped, so a wide table scrolls inside the page instead of widening it on a phone. assert.match(out, /
/, 'the table can widen the page on a narrow screen'); }); it('gives every heading an id and an anchor, numbered prefixes stripped', () => { const { html: out, headings } = renderMarkdown('## 4.2 Local Operations\n\ntext\n'); assert.deepEqual(headings, [{ level: 2, text: '4.2 Local Operations', id: 'local-operations' }]); assert.match(out, /

/, 'the heading has no id to link to'); assert.match(out, / { const { headings } = renderMarkdown('## Trains\n\na\n\n## Trains\n\nb\n'); assert.deepEqual(headings.map((h) => h.id), ['trains', 'trains-2']); }); it('renders lists, quotes, rules and fenced code', () => { assert.match(html('- one\n- two\n'), /
  • one<\/li>
  • two<\/li><\/ul>/); assert.match(html('1. first\n2. second\n'), /
    1. first<\/li>
    2. second<\/li><\/ol>/); assert.match(html('> a note\n> continued\n'), /

      a note continued<\/p><\/blockquote>/); assert.match(html('---\n'), /


      /); assert.match(html('```\nconst x = 1;\n```\n'), /
      const x = 1;<\/code><\/pre>/);
        });
      
        it('renders a table inside a block quote', () => {
          // The rules reference puts one there, so this is not hypothetical.
          const out = html('> | A | B |\n> | --- | --- |\n> | 1 | 2 |\n');
          assert.match(out, /

Card<\/th>/, 'the header row is not a header'); assert.match(out, /Regions<\/th>/, 'a right-aligned column lost its alignment'); assert.match(out, /Passes<\/th>/, 'a centred column lost its alignment'); assert.match(out, /Plains<\/td>1<\/td>/, 'a body row lost its cells'); assert.equal((out.match(/
/, 'a quoted table did not render'); }); it('handles bold, italic and code spans, and leaves markup inside code alone', () => { assert.match(html('**loud** and *quiet*\n'), /loud<\/strong> and quiet<\/em>/); // `**` inside backticks is a literal, which matters: the docs quote field names that way. assert.match(html('`**not bold**`\n'), /\*\*not bold\*\*<\/code>/); assert.ok(!//.test(html('`**not bold**`\n')), 'markup inside a code span was rendered'); }); it('escapes everything, so prose can never become markup', () => { const out = html('A < B & C > D, and "quoted".\n'); assert.match(out, /A < B & C > D/, 'angle brackets or ampersands reached the page raw'); assert.ok(!/\n')), 'raw HTML passed through'); assert.match(html('\n'), /<script>/, 'raw HTML was not escaped'); }); it('rewrites links between documents, and opens external ones in a new tab', () => { const out = renderMarkdown( '[Rules](rules.md) and [anchor](rules.md#draw) and [site](https://example.com)\n', (href) => (/^https?:/.test(href) ? href : href.replace(/\.md(#|$)/, '.html$1')), ).html; assert.match(out, /Rules<\/a>/, 'a link between documents still points at the Markdown'); assert.match(out, //, 'an anchored link lost its fragment'); assert.match(out, //, 'an external link is not safe'); }); it('drops HTML comments, so the generated-card markers never show', () => { // `build-card-reference.ts` writes its tables between `` markers. const out = html('before\n\n\n| A |\n| --- |\n| 1 |\n\n\nafter\n'); assert.ok(!/BEGIN CARDS/.test(out), 'a build marker is visible on the page'); assert.match(out, /
/, 'the generated table inside the markers was lost with them'); assert.match(out, /before/, 'content before the markers was lost'); assert.match(out, /after/, 'content after the markers was lost'); }); it('renders each real document without losing its tables or headings', () => { // The documents themselves, not a fixture: what has to render is what is actually written. for (const name of ['quickstart', 'rules', 'home-deck', 'mainline-deck', 'components']) { const src = readFileSync(join(import.meta.dirname, '..', 'docs', `${name}.md`), 'utf8'); const { html: out, headings } = renderMarkdown(src); assert.ok(headings.length > 2, `${name}.md rendered only ${headings.length} headings`); assert.ok(out.length > 1000, `${name}.md rendered almost nothing`); // No pipe table survives as text — that would mean a table failed to parse. const stripped = out.replace(/<[^>]+>/g, ''); assert.ok( !/^\s*\|\s*---/m.test(stripped), `${name}.md has a table the renderer did not recognise`, ); assert.ok(!/BEGIN CARDS/.test(out), `${name}.md leaked a build marker onto the page`); } }); it('nests a more-indented bullet as a list inside its item (v0.8.4)', () => { /** * The code said "nesting is rendered by recursion" and appended the nested bullet to the parent * as text, so the published home-deck page read "…knows. - ABS Signals is the exception…" with * a literal dash mid-sentence. */ const out = html(['- parent', ' continues here.', ' - child one', ' wraps too', ' - child two', '- second'].join('\n')); assert.equal( out.trim(), '
  • parent continues here.
    • child one wraps too
    • child two
  • second
', ); assert.doesNotMatch(html(readFileSync(join(import.meta.dirname, '..', 'docs', 'home-deck.md'), 'utf8')), /\. - /); }); });