Files
autodoc/exporters/markdown.js
T
Iisyourdad ca73db68e3 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>
2026-06-10 16:53:23 -05:00

94 lines
3.2 KiB
JavaScript

'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { guideSlug, writeStepImages, LEVEL_LABEL } = require('./common');
const { htmlToMarkdown } = require('./htmlmd');
/**
* Markdown exporter. Writes <slug>.md plus a steps-<slug>/ image folder.
* azureWiki mode emits resized image syntax (=WxH) Azure DevOps wikis accept.
*/
const DEFAULT_TEMPLATE = {
toc: true,
includeImages: true,
azureWiki: false,
imageMaxWidth: 0, // 0 = natural size
};
function anchorFor(step) {
return `step-${step.number.replace(/\./g, '-')}`;
}
function exportMarkdown(ast, outDir, template = {}) {
const tpl = { ...DEFAULT_TEMPLATE, ...template };
fs.mkdirSync(outDir, { recursive: true });
const images = tpl.includeImages ? writeStepImages(ast, outDir) : new Map();
const lines = [];
lines.push(`# ${ast.guide.title}`, '');
if (ast.guide.descriptionHtml) lines.push(htmlToMarkdown(ast.guide.descriptionHtml), '');
if (tpl.toc && ast.steps.length > 1) {
lines.push('## Contents', '');
for (const step of ast.steps) {
const indent = ' '.repeat(step.depth);
lines.push(`${indent}- [${step.number}. ${step.title || 'Untitled step'}](#${anchorFor(step)})`);
}
lines.push('');
}
for (const step of ast.steps) {
const heading = step.depth > 0 ? '###' : '##';
lines.push(`<a id="${anchorFor(step)}"></a>`, '');
lines.push(`${heading} ${step.number}. ${step.title || 'Untitled step'}`, '');
if (step.skipped) lines.push('*(skipped)*', '');
emitBlocks(lines, step, 'before-description');
if (step.descriptionHtml) lines.push(htmlToMarkdown(step.descriptionHtml), '');
const img = images.get(step.stepId);
if (img) {
if (tpl.azureWiki && tpl.imageMaxWidth > 0) {
lines.push(`![Step ${step.number}](${img.relPath} =${tpl.imageMaxWidth}x)`, '');
} else {
lines.push(`![Step ${step.number}](${img.relPath})`, '');
}
}
for (const cb of step.codeBlocks) {
lines.push(`\`\`\`${cb.language || ''}`, cb.code || '', '```', '');
}
for (const tb of step.tableBlocks || []) {
if (!tb.rows || !tb.rows.length) continue;
const width = Math.max(...tb.rows.map((r) => r.length));
const pad = (r) => { const c = [...r]; while (c.length < width) c.push(''); return c; };
lines.push(`| ${pad(tb.rows[0]).join(' | ')} |`);
lines.push(`|${' --- |'.repeat(width)}`);
for (const row of tb.rows.slice(1)) lines.push(`| ${pad(row).join(' | ')} |`);
lines.push('');
}
emitBlocks(lines, step, 'after-description');
emitBlocks(lines, step, 'after-image');
}
const file = path.join(outDir, `${guideSlug(ast)}.md`);
fs.writeFileSync(file, lines.join('\n').replace(/\n{3,}/g, '\n\n') + '\n');
return { file, imageCount: images.size };
}
function emitBlocks(lines, step, position) {
for (const tb of step.textBlocks.filter((b) => b.position === position)) {
const label = LEVEL_LABEL[tb.level] || 'Note';
lines.push(`> **${label}${tb.title ? `: ${tb.title}` : ''}**`);
const body = htmlToMarkdown(tb.descriptionHtml);
if (body) lines.push(`> ${body.replace(/\n/g, '\n> ')}`);
lines.push('');
}
}
module.exports = { exportMarkdown, DEFAULT_TEMPLATE, anchorFor };