// ============================================================ // SLIDE SPEC // ============================================================ // The half of deck rendering that decides what each slide is. Pandoc had no // such step — every slide became bullets on a reference layout — so this is // where most of the difference in a generated deck now comes from. const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const slideSpec = require('../src/utils/slideSpec'); const read = p => fs.readFileSync(path.join(__dirname, '..', p), 'utf8'); const DECK = [ '% Croup in Children', '% Teaching Resource', '% 2026', '', '# What Croup Is', '', '- A viral illness', '- Peaks at 12-18 months', '', '# Severity', '', '| Feature | Mild | Severe |', '|---|---|---|', '| Stridor | Absent | Present |', '| Retractions | None | Marked |', '', '# Long list', '', '- one', '- two', '- three', '- four', '- five', '- six', '- seven', '- eight', '', '# References', '', '- Nelson, p. 2606' ].join('\n'); test('a title block becomes a title slide, not a bullet', () => { const spec = slideSpec.build(DECK, {}); assert.equal(spec.slides[0].type, 'title'); assert.equal(spec.slides[0].heading, 'Croup in Children'); assert.equal(spec.slides[0].subtitle, 'Teaching Resource'); assert.equal(spec.slides[0].date, '2026'); }); test('each slide gets the layout its content needs', () => { const byHeading = {}; slideSpec.build(DECK, {}).slides.forEach(s => { if (s.heading) byHeading[s.heading] = s; }); assert.equal(byHeading['What Croup Is'].type, 'bullets'); // A pipe table is a table, not eight lines of text with pipes in them. assert.equal(byHeading['Severity'].type, 'table'); assert.deepEqual(byHeading['Severity'].header, ['Feature', 'Mild', 'Severe']); assert.equal(byHeading['Severity'].rows.length, 2, 'the alignment rule is not a row'); // A long list is unreadable at any legible size in one column. assert.equal(byHeading['Long list'].type, 'two'); assert.equal(byHeading['Long list'].left.length + byHeading['Long list'].right.length, 8); }); test('figures are spread through the deck, and References stays last', () => { const spec = slideSpec.build(DECK, { images: ['/tmp/a.png', '/tmp/b.png'] }); const types = spec.slides.map(s => s.type); assert.equal(types.filter(t => t === 'image').length, 2); // Appending them would end the deck with unexplained pictures. assert.notEqual(types[types.length - 1], 'image'); assert.equal(spec.slides[spec.slides.length - 1].heading, 'References'); // And never before the first content slide. assert.ok(types.indexOf('image') > 1); }); test('a slide with no list still becomes a slide', () => { const spec = slideSpec.build('# Just a heading\n', {}); assert.equal(spec.slides[0].type, 'section'); assert.equal(spec.slides[0].heading, 'Just a heading'); }); test('the renderer sizes text to fit rather than trusting autofit', () => { const py = read('scripts/render_pptx.py'); // LibreOffice ignores when converting to PDF, which is how // slides were being cut off mid-sentence. assert.match(py, /def _fit_size\(/); assert.match(py, /BULLET_SIZES = /); // 16:9. Pandoc's reference doc is 4:3. assert.match(py, /SLIDE_W = Emu\(12192000\)/); // An image keeps its own aspect ratio; the old pptxgenjs path stretched every // one of them to the target box. assert.match(py, /ratio = img\.width \/ float\(img\.height\)/); assert.match(py, /width = int\(height \* ratio\)/); // Wrapped lines hang under the text. assert.match(py, /pPr\.set\("indent", str\(-indent\)\)/); }); test('the deck renderer replaces pandoc, and pandoc still catches it if it falls', () => { const exporter = read('src/utils/documentExport.js'); assert.match(exporter, /async function buildDeck\(markdown, workdir, images\)/); assert.match(exporter, /spawn\('python3', \[DECK_RENDERER, out\]/); // A plainer deck beats a failed download. assert.match(exporter, /deck renderer failed, falling back to pandoc/); assert.match(exporter, /runPandoc\(\['doc\.md', '--reference-doc=' \+ REFERENCE_DECK/); // Word is still pandoc's, where its output is good. assert.match(exporter, /runPandoc\(\['doc\.md', '-o', 'doc\.docx'\]/); // And the runtime actually has it. assert.match(read('Dockerfile'), /python-pptx==1\.0\.2/); }); test('a resource remembers its figures, so an export can include them', () => { const route = read('src/routes/myResources.js'); // They were queued and shown on screen, but nothing tied them to the // resource, so an exported deck could never contain them. assert.match(read('migrations/1780500000000_resource-images.js'), /image_ids JSONB/); assert.match(route, /var figureIds = \(ai\.imageJobs \|\| \[\]\)\.map/); // "Add two more diagrams" means more, not instead. assert.match(route, /image_ids = image_ids \|\| \?::jsonb/); assert.match(route, /async function collectFigures\(ids, user, dir\)/); // A figure that cannot be fetched is left out rather than failing a download. assert.match(route, /figure unavailable for export/); assert.match(route, /documentExport\.render\(row\.markdown, row\.kind, format, \{ images: figures \}\)/); });