95 lines
3.4 KiB
JavaScript
95 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { guideSlug, writeStepImages, LEVEL_LABEL, stepBlocks, codeBlockText } = 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(``, '');
|
|
} else {
|
|
lines.push(``, '');
|
|
}
|
|
}
|
|
|
|
for (const block of stepBlocks(step).filter((item) => item.kind !== 'text')) {
|
|
if (block.kind === 'code') {
|
|
lines.push(`\`\`\`${block.language || ''}`, codeBlockText(block), '```', '');
|
|
} else if (block.kind === 'table') {
|
|
if (!block.rows || !block.rows.length) continue;
|
|
const width = Math.max(...block.rows.map((r) => r.length));
|
|
const pad = (r) => { const c = [...r]; while (c.length < width) c.push(''); return c; };
|
|
lines.push(`| ${pad(block.rows[0]).join(' | ')} |`);
|
|
lines.push(`|${' --- |'.repeat(width)}`);
|
|
for (const row of block.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 stepBlocks(step).filter((b) => b.kind === 'text' && 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 };
|