Add Render AST and text exporters: JSON, Markdown, HTML simple/rich

- Render AST: placeholder expansion, hierarchical numbering (1, 1.1),
  hidden/skipped filtering, preview step limit, annotated image rendering
- JSON exporter with sidecar annotated PNGs
- Markdown exporter: TOC with resolving anchors, text blocks as
  blockquotes, fenced code, tables, Azure-wiki image sizing option
- Self-contained HTML exporters (data-URI images, zero external refs);
  rich variant adds floating TOC, checkboxes, localStorage progress
- HTML->Markdown converter for the sanitizer-allowed tag set
- 7 exporter workflow tests (42 total)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Iisyourdad
2026-06-10 16:53:23 -05:00
parent b7e64c79b4
commit ca73db68e3
8 changed files with 886 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { slugify } = require('../core/util');
const { encodePng } = require('../core/png');
const { renderStepImage } = require('../core/renderast');
/**
* Shared exporter helpers: every image-bearing exporter renders annotated
* step images through the same pipeline so output is consistent.
*/
function guideSlug(ast) {
return slugify(ast.guide.title, 'guide');
}
function imagesDirName(ast) {
return `steps-${guideSlug(ast)}`;
}
/**
* Render every image step to an annotated PNG inside outDir/<steps-slug>/.
* Returns Map stepId -> { relPath, width, height }.
*/
function writeStepImages(ast, outDir) {
const dirName = imagesDirName(ast);
const dir = path.join(outDir, dirName);
const result = new Map();
let n = 0;
for (const step of ast.steps) {
n += 1;
const img = renderStepImage(step);
if (!img) continue;
fs.mkdirSync(dir, { recursive: true });
const fileName = `${String(n).padStart(3, '0')}-${slugify(step.title || step.stepId, step.stepId)}.png`;
fs.writeFileSync(path.join(dir, fileName), encodePng(img));
result.set(step.stepId, { relPath: `${dirName}/${fileName}`, width: img.width, height: img.height });
}
return result;
}
/** Render step images in-memory (for self-contained HTML, PDF, GIF...). */
function renderAllImages(ast) {
const result = new Map();
for (const step of ast.steps) {
const img = renderStepImage(step);
if (img) result.set(step.stepId, img);
}
return result;
}
const LEVEL_LABEL = { info: 'Note', warn: 'Warning', error: 'Important', success: 'Tip' };
module.exports = { guideSlug, imagesDirName, writeStepImages, renderAllImages, LEVEL_LABEL };