Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Docker Build / Root app tests (push) Successful in 59s
Forgejo Android APK / Build signed APK (push) Successful in 2m6s
Forgejo Docker Build / Build Docker image (push) Successful in 25s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Pandoc's pptx writer was the ceiling on how good a generated deck could be, and the model on top made no difference to it. It maps markdown onto a handful of reference layouts with no per-slide layout, no positioning and no control over how large an image is drawn, which is why every deck came out as bullets on a template — and why autofit had to be injected into its emitted OOXML by hand afterwards, because LibreOffice ignores the autofit pandoc leaves off. scripts/render_pptx.py draws the deck and src/utils/slideSpec.js decides what each slide is. Markdown stays the stored artifact, so "change slide 4" is still a text edit and Word export is untouched — pandoc still writes docx, where its output is good. What that buys, all of it visible in a rendered deck rather than argued for: - 16:9, not pandoc's 4:3. - A pipe table becomes a real table with a header band and banded rows, not eight lines of text with pipes in them. - A list longer than seven items becomes two columns instead of a wall of text. - Text is measured and sized to fit before the file is written, so nothing depends on a renderer honouring autofit. - Wrapped lines hang under the text instead of running back to the margin, which is the clearest single tell that a deck was generated. - An image is drawn at its own aspect ratio, centred, with a caption. Figures now reach the deck at all, which they never did. They were queued and shown on the page, but nothing recorded that they belonged to the resource, so an export could not include them: user_resources.image_ids holds them, a modification adds to that list rather than replacing it, and export fetches the finished ones to a scratch directory. They are spread through the deck rather than appended, because ending on three unexplained pictures is worse than showing each near its material, and a References slide stays last. If the renderer fails for any reason, pandoc still produces a deck — a plainer deck beats a failed download. Verified end to end: a seven-slide request with three figures exported as a 13-page deck; the slides were rendered to PDF, rasterised and looked at. All three formats still download. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
106 lines
5.2 KiB
JavaScript
106 lines
5.2 KiB
JavaScript
// ============================================================
|
|
// 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 <a:normAutofit/> 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 \}\)/);
|
|
});
|