// ============================================================ // DOCUMENT SPEC // ============================================================ // What Word is built from. Going deck → markdown → pandoc lost the structure // twice; this is the typed source both a deck and an article reduce to. const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const docSpec = require('../src/utils/docSpec'); const read = p => fs.readFileSync(path.join(__dirname, '..', p), 'utf8'); const DECK = { title: 'Croup', subtitle: 'Teaching', date: '2026', slides: [ { type: 'title', heading: 'Croup' }, { type: 'bullets', heading: 'What it is', bullets: [{ text: 'Viral', level: 0 }, { text: 'Peaks 12-18m', level: 1 }] }, { type: 'compare', heading: 'Versus', columns: [ { label: 'CROUP', bullets: [{ text: 'Barking cough' }, { text: 'Hoarse' }] }, { label: 'EPIGLOTTITIS', bullets: [{ text: 'Drooling' }] }] }, { type: 'table', heading: 'Features', header: ['Feature', 'Mild'], rows: [['Stridor', 'Absent']] }, { type: 'callout', heading: 'Red flag', text: 'Do not examine the throat.' }, { type: 'figure', heading: 'Anatomy', bullets: [{ text: 'Subglottis' }], image_job: 'job-a', caption: 'Airway' }, ]}; test('a deck keeps the shape it was designed with', () => { const spec = docSpec.build({ deck: DECK, images: { 'job-a': '/tmp/fig.png' } }); const types = spec.blocks.map(b => b.type); // The title slide is not a section of the document; the document has a title. assert.equal(types.filter(t => t === 'heading').length, 5); assert.equal(spec.title, 'Croup'); // A comparison stays a comparison. Flattened to two headings and two lists it // stopped being one, which was the whole point of the layout. const compare = spec.blocks[3]; assert.equal(compare.type, 'table'); assert.deepEqual(compare.header, ['CROUP', 'EPIGLOTTITIS']); assert.deepEqual(compare.rows, [['Barking cough', 'Drooling'], ['Hoarse', '']]); assert.ok(types.includes('callout'), 'a callout is not bold text'); const image = spec.blocks.find(b => b.type === 'image'); assert.equal(image.path, '/tmp/fig.png'); assert.equal(image.caption, 'Airway'); }); test('a figure with no file is simply absent, not an empty frame', () => { const spec = docSpec.build({ deck: DECK, images: {} }); assert.equal(spec.blocks.filter(b => b.type === 'image').length, 0); // Its heading and bullets still render — the slide had more than a picture. assert.ok(spec.blocks.some(b => b.type === 'bullets' && b.items[0].text === 'Subglottis')); }); test('an article comes from its markdown', () => { const spec = docSpec.build({ markdown: [ '% An article', '', '# Title', '', 'Some prose here.', '', '- one', '- two', '', '| A | B |', '|---|---|', '| 1 | 2 |', '', '> A quote.', ].join('\n') }); assert.equal(spec.title, 'An article', 'the pandoc title block names the document'); assert.deepEqual(spec.blocks.map(b => b.type), ['heading', 'para', 'bullets', 'table', 'callout']); assert.deepEqual(spec.blocks[3].rows, [['1', '2']], 'the alignment rule is not a row'); }); test('the deck wins when there is one', () => { // The markdown beside a deck is a serialisation of it, so reading the // serialisation instead would be reading a lossy copy of what is right there. const spec = docSpec.build({ deck: DECK, markdown: '# Something else\n\n- ignored', images: {} }); assert.ok(spec.blocks.some(b => b.type === 'callout')); assert.ok(!spec.blocks.some(b => b.type === 'bullets' && b.items[0].text === 'ignored')); }); test('Word is rendered by python-docx, with pandoc still catching it', () => { const exporter = read('src/utils/documentExport.js'); assert.match(exporter, /async function buildDoc\(markdown, workdir, options\)/); assert.match(exporter, /runRenderer\(DOC_RENDERER, out, spec, workdir\)/); // A plainer document beats a failed download. assert.match(exporter, /document renderer failed, falling back to pandoc/); assert.match(exporter, /runPandoc\(\['doc\.md', '-o', 'doc\.docx'\]/); // Every format carries the figures now, Word included. assert.doesNotMatch(read('src/routes/myResources.js'), /if \(format !== 'docx'\) \{/); assert.match(read('Dockerfile'), /python-docx==1\.1\.2/); const py = read('scripts/render_docx.py'); // python-docx exposes no API for shading, so it is written onto the XML. assert.match(py, /def shade\(element, hex_fill\)/); // A tall figure at full width runs off the page. assert.match(py, /\(img\.height \/ img\.width\) > 1\.1/); });