From f79bbfed9f51c857c97324dd5d26fa0a53752474 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Mon, 15 Jun 2026 14:32:14 -0500 Subject: [PATCH] Style markdown callouts with HTML --- exporters/markdown-guide.js | 20 ++++++++++++++++++++ exporters/markdown.js | 2 +- tests/unit/exporters-text.test.js | 10 +++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/exporters/markdown-guide.js b/exporters/markdown-guide.js index a10fcfc..297641e 100644 --- a/exporters/markdown-guide.js +++ b/exporters/markdown-guide.js @@ -2,6 +2,7 @@ const fs = require('node:fs'); const path = require('node:path'); +const { escapeHtml } = require('../core/util'); const { guideSlug, writeStepImages, LEVEL_LABEL, stepContentGroups, codeBlockText } = require('./common'); const { htmlToMarkdown } = require('./htmlmd'); const { tocEntries, guideMetaLines, guideSummary } = require('./document-layout'); @@ -20,6 +21,13 @@ const WIKIJS_CALLOUT_CLASS = { error: 'is-danger', }; +const HTML_CALLOUT_THEME = { + info: { label: 'Note', border: '#2563eb', bg: '#eff6ff', fg: '#1d4ed8', kind: 'note' }, + success: { label: 'Tip', border: '#10b981', bg: '#ecfdf5', fg: '#047857', kind: 'tip' }, + warn: { label: 'Warning', border: '#f59e0b', bg: '#fffbeb', fg: '#b45309', kind: 'warning' }, + error: { label: 'Important', border: '#ef4444', bg: '#fef2f2', fg: '#b91c1c', kind: 'important' }, +}; + function anchorFor(step) { return `step-${step.number.replace(/\./g, '-')}`; } @@ -30,6 +38,18 @@ function quoteBody(text) { function emitBlock(lines, tb, { alertStyle = 'gfm' } = {}) { const body = htmlToMarkdown(tb.descriptionHtml); + if (alertStyle === 'html') { + const theme = HTML_CALLOUT_THEME[tb.level] || HTML_CALLOUT_THEME.info; + const label = theme.label; + const title = tb.title ? `${label}: ${tb.title}` : label; + lines.push( + `
`, + `
${escapeHtml(title)}
`, + ); + if (body) lines.push(`
${tb.descriptionHtml || ''}
`); + lines.push('
', ''); + return; + } if (alertStyle === 'wikijs') { const label = tb.title || LEVEL_LABEL[tb.level] || 'Note'; const className = WIKIJS_CALLOUT_CLASS[tb.level] || 'is-info'; diff --git a/exporters/markdown.js b/exporters/markdown.js index 8eb9aa7..dc71773 100644 --- a/exporters/markdown.js +++ b/exporters/markdown.js @@ -10,7 +10,7 @@ const { DEFAULT_TEMPLATE, anchorFor, renderMarkdownGuide } = require('./markdown function exportMarkdown(ast, outDir, template = {}) { return renderMarkdownGuide(ast, outDir, template, { defaults: DEFAULT_TEMPLATE, - alertStyle: 'gfm', + alertStyle: 'html', tocTitle: 'Contents', fileExt: '.md', }); diff --git a/tests/unit/exporters-text.test.js b/tests/unit/exporters-text.test.js index e919ce5..2a6b978 100644 --- a/tests/unit/exporters-text.test.js +++ b/tests/unit/exporters-text.test.js @@ -137,11 +137,11 @@ test('Markdown export: TOC anchors resolve, images exist, blocks rendered', (t) assert.equal(lines[fenceStart + 1], '0 2 * * * /usr/local/bin/acmesync --backup'); assert.equal(lines[fenceStart + 2], '```'); assert.ok(lines.some((l) => /^\| Day \| Window \|$/.test(l)), 'table header row'); - // Warning text block became a GFM alert blockquote with its content. - const warnIdx = lines.findIndex((l) => l.startsWith('> [!WARNING]')); - assert.ok(warnIdx > 0); - assert.equal(lines[warnIdx + 1], '> **Access**'); - assert.equal(lines[warnIdx + 2], '> Admins only.'); + // Warning text block became a styled HTML callout with its content. + assert.ok(md.includes('
Admins only.

')); }); test('Wiki.js export: TOC is included, wiki callouts render, images exist', (t) => {