@helping-ai-workflow/md2doc 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@ A single global CLI (`md2doc`) you can call from any directory.
8
8
 
9
9
  Requires Node.js 18 or higher. The first install pulls puppeteer (≈ 170 MB Chromium download); subsequent installs reuse it.
10
10
 
11
+ Chromium is only needed for **PDF export** and the optional `--bake-svg` flag. HTML diagram rendering needs nothing extra — Graphviz runs in-process via WebAssembly, and Mermaid / WaveDrom are bundled and inlined, so diagrams render **offline with no system Graphviz and no CDN**.
12
+
11
13
  ### Recommended: install via nvm
12
14
 
13
15
  If you do not yet have Node.js — or your system Node lives under `/usr/local` and `npm install -g` fails with `EACCES` — install Node through [nvm](https://github.com/nvm-sh/nvm) first. nvm puts Node under `~/.nvm`, so global packages never need `sudo`.
@@ -69,6 +71,7 @@ also pass `--open`.
69
71
  | `--open` | Launch the platform viewer (`xdg-open` / `open` / `start`) after render. Default when `--out` is absent. |
70
72
  | `--no-open` | Skip the viewer launch. |
71
73
  | `--quiet` | Suppress per-file progress messages. |
74
+ | `--bake-svg` | Pre-render Mermaid / WaveDrom to inert SVG at generation time (HTML output only; needs Chromium). The output then contains no diagram JavaScript. |
72
75
  | `--version`, `-v` | Print version. |
73
76
  | `--help`, `-h` | Print help. |
74
77
 
@@ -101,7 +104,9 @@ digraph G { A -> B }
101
104
  ```
102
105
  ````
103
106
 
104
- Diagrams render directly in the output (HTML or PDF).
107
+ All three render directly in the output (HTML or PDF), **offline and with no system dependencies**: Graphviz `dot` runs in-process via WebAssembly (no system `dot` binary required), and Mermaid / WaveDrom are bundled and inlined (no CDN). Each engine's runtime is embedded only when the document actually uses that diagram type.
108
+
109
+ By default, Mermaid and WaveDrom render in the browser when the HTML is opened; pass `--bake-svg` to pre-render them to inert SVG at generation time instead (Graphviz is always pre-rendered to SVG).
105
110
 
106
111
  ## Why a global CLI
107
112
 
package/bin/md2doc.js CHANGED
@@ -32,6 +32,7 @@ function printHelp() {
32
32
  ' --open Launch the platform viewer after render (default when --out is absent).',
33
33
  ' --no-open Skip the viewer launch.',
34
34
  ' --quiet Suppress per-file progress messages.',
35
+ ' --bake-svg Pre-render mermaid/wavedrom to inert SVG (HTML output only; needs Chromium).',
35
36
  ' --version, -v Print version.',
36
37
  ' --help, -h Print this help.',
37
38
  ''
@@ -45,6 +46,7 @@ function parseArgs(argv) {
45
46
  let out = null;
46
47
  let openExplicit = null; // null = unset; true/false = user-specified
47
48
  let quiet = false;
49
+ let bakeSvg = false;
48
50
 
49
51
  for (let i = 0; i < argv.length; i++) {
50
52
  const a = argv[i];
@@ -61,6 +63,7 @@ function parseArgs(argv) {
61
63
  if (a === '--open') { openExplicit = true; continue; }
62
64
  if (a === '--no-open') { openExplicit = false; continue; }
63
65
  if (a === '--quiet') { quiet = true; continue; }
66
+ if (a === '--bake-svg') { bakeSvg = true; continue; }
64
67
  if (a === '--out') {
65
68
  if (out !== null) {
66
69
  process.stderr.write('error: --out specified more than once\n');
@@ -102,7 +105,7 @@ function parseArgs(argv) {
102
105
  open = (out === null);
103
106
  }
104
107
 
105
- return { inputs, formats, formatsExplicit, out, open, quiet };
108
+ return { inputs, formats, formatsExplicit, out, open, quiet, bakeSvg };
106
109
  }
107
110
 
108
111
  function shortHash(absPath) {
@@ -244,7 +247,9 @@ function main() {
244
247
  const outputs = [];
245
248
  for (const { input, output } of pairs) {
246
249
  const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
247
- const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
250
+ const childArgs = [LIB, input, output];
251
+ if (args.bakeSvg) childArgs.push('--bake-svg');
252
+ const r = spawnSync(process.execPath, childArgs, { stdio });
248
253
  if (r.status !== 0) {
249
254
  process.stderr.write('error: render failed for ' + input + ' (exit ' + r.status + ')\n');
250
255
  process.exit(r.status || 1);
package/lib/md2doc.js CHANGED
@@ -20,9 +20,13 @@
20
20
 
21
21
  const fs = require('fs');
22
22
  const path = require('path');
23
- const { spawnSync } = require('child_process');
24
23
 
25
24
  const [,, src, dst] = process.argv;
25
+ const BAKE_SVG = process.argv.slice(4).includes('--bake-svg');
26
+
27
+ // How long to let client-side WaveDrom / Mermaid scripts render before we
28
+ // snapshot the DOM (PDF print, or --bake-svg inert-SVG bake).
29
+ const DIAGRAM_RENDER_WAIT_MS = 2500;
26
30
  if (!src || !dst) {
27
31
  console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
28
32
  process.exit(1);
@@ -55,56 +59,57 @@ function safeResolve(modulePath) {
55
59
  }
56
60
  }
57
61
 
58
- function inlineScriptTag(sourcePath) {
62
+ function inlineScriptTag(sourcePath, engine) {
59
63
  if (!sourcePath) {
60
64
  return null;
61
65
  }
62
- return `<script type="text/javascript">\n${fs.readFileSync(sourcePath, 'utf8')}\n</script>`;
66
+ const marker = engine ? ` data-md2doc-diagram-engine="${engine}"` : '';
67
+ return `<script type="text/javascript"${marker}>\n${fs.readFileSync(sourcePath, 'utf8')}\n</script>`;
63
68
  }
64
69
 
70
+ // wavedrom is pinned to 3.5.0 in package.json: 3.6.1+ adds an `exports` map
71
+ // that no longer exposes the `wavedrom/wavedrom.min.js` / `wavedrom/skins/default.js`
72
+ // subpaths resolved below, so a bump past 3.5.x must switch to an exports-blessed
73
+ // entry (or a vendored copy) or these safeResolve() calls return null and the
74
+ // hard guard further down exits 1.
65
75
  const localWaveDromSkin = firstExistingPath([
66
76
  process.env.WAVEDROM_SKIN_JS,
67
77
  safeResolve('wavedrom/skins/default.js'),
68
- '/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/wavedrom/skins/default.js',
69
78
  ]);
70
79
 
71
80
  const localWaveDromJs = firstExistingPath([
72
81
  process.env.WAVEDROM_JS,
73
82
  safeResolve('wavedrom/wavedrom.min.js'),
74
- '/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/wavedrom/wavedrom.min.js',
75
83
  ]);
76
84
 
77
85
  const localMermaidJs = firstExistingPath([
78
86
  process.env.MERMAID_JS,
79
87
  safeResolve('mermaid/dist/mermaid.min.js'),
80
- '/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/mermaid/mermaid.min.js',
81
88
  ]);
82
89
 
83
- const waveDromSkinTag = inlineScriptTag(localWaveDromSkin)
84
- || '<script src="https://cdn.jsdelivr.net/npm/wavedrom/skins/default.js" type="text/javascript"></script>';
85
-
86
- const waveDromTag = inlineScriptTag(localWaveDromJs)
87
- || '<script src="https://cdn.jsdelivr.net/npm/wavedrom/wavedrom.min.js" type="text/javascript"></script>';
90
+ if (!localMermaidJs || !localWaveDromJs || !localWaveDromSkin) {
91
+ console.error('[ERROR] bundled diagram runtime missing — reinstall dependencies with `npm install`');
92
+ process.exit(1);
93
+ }
88
94
 
89
- const mermaidScriptTag = inlineScriptTag(localMermaidJs)
90
- || `<script type="module">
91
- import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
92
- mermaid.initialize({ startOnLoad: true, theme: 'default' });
93
- </script>`;
95
+ const waveDromSkinTag = inlineScriptTag(localWaveDromSkin, 'wavedrom');
96
+ const waveDromTag = inlineScriptTag(localWaveDromJs, 'wavedrom');
97
+ const mermaidScriptTag = inlineScriptTag(localMermaidJs, 'mermaid');
94
98
 
95
- const mermaidInitTag = localMermaidJs
96
- ? `<script type="text/javascript">
99
+ const mermaidInitTag = `<script type="text/javascript" data-md2doc-diagram-engine="mermaid">
97
100
  if (typeof mermaid !== 'undefined') {
98
101
  mermaid.initialize({ startOnLoad: true, theme: 'default' });
99
102
  }
100
- </script>`
101
- : '';
103
+ </script>`;
102
104
 
103
105
  // ── Markdown → HTML body ─────────────────────────────────────────────────────
104
106
  // Use a custom renderer to intercept fenced code blocks before marked escapes
105
107
  // their content. This is the correct approach — pre-processing the raw markdown
106
108
  // string causes marked to re-parse the injected HTML and mangle indented lines.
107
109
 
110
+ let usesMermaid = false;
111
+ let usesWaveDrom = false;
112
+
108
113
  let bodyHtml;
109
114
  let tocHtml = '';
110
115
  let serializedSections = '[]';
@@ -306,9 +311,11 @@ ${itemsHtml}
306
311
  const code = (typeof token === 'object') ? token.text : token;
307
312
 
308
313
  if (lang === 'wavedrom') {
314
+ usesWaveDrom = true;
309
315
  return `\n<script type="WaveDrom">\n${code}\n</script>\n`;
310
316
  }
311
317
  if (lang === 'mermaid') {
318
+ usesMermaid = true;
312
319
  // Escape so the browser delivers the literal source to mermaid. Raw
313
320
  // injection lets the HTML parser consume entities and tags first —
314
321
  // an author's &lt;IP&gt; became an <IP> element mermaid sanitized
@@ -316,19 +323,12 @@ ${itemsHtml}
316
323
  return `\n<div class="mermaid">\n${escapeHtml(code)}\n</div>\n`;
317
324
  }
318
325
  if (lang === 'dot' || lang === 'graphviz') {
319
- const r = spawnSync('dot', ['-Tsvg'], { input: code, encoding: 'utf8', timeout: 10000 });
320
- if (r.status === 0) {
321
- // Strip XML declaration / DOCTYPE; keep only the <svg> element.
322
- // Keep intrinsic width/height so small diagrams render at their natural
323
- // size; max-width:100% in CSS still shrinks oversized ones to container.
324
- const svg = r.stdout
325
- .replace(/<\?xml[^>]*\?>/g, '')
326
- .replace(/<!DOCTYPE[^>]*>/g, '')
327
- .trim();
328
- return `\n<div class="graphviz">${svg}</div>\n`;
329
- }
330
- console.error('[WARN] dot render failed:', r.stderr);
331
- // Fall through to default code block
326
+ // Defer rendering to the async bakeGraphviz() post-pass so the
327
+ // synchronous marked() pass stays sync. The dot source is carried as
328
+ // base64 in a data attribute safe for arbitrary dot syntax (quotes,
329
+ // angle brackets, newlines) inside an HTML attribute.
330
+ const b64 = Buffer.from(code, 'utf8').toString('base64');
331
+ return `\n<div class="graphviz" data-graphviz-src="${b64}"></div>\n`;
332
332
  }
333
333
  // Default: syntax-highlighted code block
334
334
  const escaped = code.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
@@ -1054,9 +1054,9 @@ ${bodyHtml}
1054
1054
  </div>
1055
1055
 
1056
1056
  <!-- WaveDrom -->
1057
- ${waveDromSkinTag}
1057
+ ${usesWaveDrom ? `${waveDromSkinTag}
1058
1058
  ${waveDromTag}
1059
- <script type="text/javascript">
1059
+ <script type="text/javascript" data-md2doc-diagram-engine="wavedrom">
1060
1060
  function renderWaveDrom() {
1061
1061
  if (typeof WaveDrom !== 'undefined') {
1062
1062
  WaveDrom.ProcessAll();
@@ -1066,11 +1066,11 @@ ${waveDromTag}
1066
1066
  window.addEventListener('load', renderWaveDrom);
1067
1067
  setTimeout(renderWaveDrom, 250);
1068
1068
  setTimeout(renderWaveDrom, 1000);
1069
- </script>
1069
+ </script>` : ''}
1070
1070
 
1071
1071
  <!-- Mermaid -->
1072
- ${mermaidScriptTag}
1073
- ${mermaidInitTag}
1072
+ ${usesMermaid ? `${mermaidScriptTag}
1073
+ ${mermaidInitTag}` : ''}
1074
1074
 
1075
1075
  <!-- Reader runtime -->
1076
1076
  <script id="reader-section-data" type="application/json">${serializedSections}</script>
@@ -1540,13 +1540,90 @@ ${mermaidInitTag}
1540
1540
  </body>
1541
1541
  </html>`;
1542
1542
 
1543
+ // Render every deferred dot/graphviz placeholder to inline SVG using the
1544
+ // in-process WASM engine. Loads the WASM module only when at least one dot
1545
+ // block exists, so text-only docs pay nothing.
1546
+ async function bakeGraphviz(htmlStr) {
1547
+ if (!/data-graphviz-src=/.test(htmlStr)) return htmlStr;
1548
+ let Graphviz;
1549
+ try {
1550
+ ({ Graphviz } = require('@hpcc-js/wasm-graphviz'));
1551
+ } catch (e) {
1552
+ console.error('[ERROR] @hpcc-js/wasm-graphviz not installed — run `npm install`:', e.message);
1553
+ return htmlStr;
1554
+ }
1555
+ const gv = await Graphviz.load();
1556
+ return htmlStr.replace(/<div class="graphviz" data-graphviz-src="([^"]*)"><\/div>/g, (m, b64) => {
1557
+ const dotSrc = Buffer.from(b64, 'base64').toString('utf8');
1558
+ try {
1559
+ const svg = gv.dot(dotSrc)
1560
+ .replace(/<\?xml[^>]*\?>/g, '')
1561
+ .replace(/<!DOCTYPE[\s\S]*?>/g, '')
1562
+ // drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
1563
+ .replace(/<!--[\s\S]*?-->/g, '')
1564
+ .trim();
1565
+ return `<div class="graphviz">${svg}</div>`;
1566
+ } catch (e) {
1567
+ console.error('[WARN] graphviz render failed:', e.message);
1568
+ const escaped = dotSrc.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1569
+ return `<pre><code class="language-dot">${escaped}</code></pre>\n`;
1570
+ }
1571
+ });
1572
+ }
1573
+
1574
+ function launchBrowser(puppeteer) {
1575
+ return puppeteer.launch({
1576
+ headless: 'new',
1577
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-crash-reporter', '--disable-dev-shm-usage'],
1578
+ });
1579
+ }
1580
+
1581
+ // Pre-render mermaid/wavedrom to inert SVG using headless Chromium, then strip
1582
+ // the diagram-engine runtime scripts so the output HTML carries no diagram JS.
1583
+ async function bakeDiagrams(htmlStr) {
1584
+ if (!/data-md2doc-diagram-engine/.test(htmlStr)) return htmlStr;
1585
+ let puppeteer;
1586
+ try {
1587
+ puppeteer = require('puppeteer');
1588
+ } catch (e) {
1589
+ console.error('[ERROR] --bake-svg requires puppeteer/Chromium — install it, or drop --bake-svg:', e.message);
1590
+ process.exit(1);
1591
+ }
1592
+ const tmp = dst.replace(/\.html$/i, '._bake.html');
1593
+ fs.writeFileSync(tmp, htmlStr, 'utf8');
1594
+ let browser;
1595
+ try {
1596
+ browser = await launchBrowser(puppeteer);
1597
+ } catch (e) {
1598
+ fs.unlinkSync(tmp);
1599
+ console.error('[ERROR] --bake-svg requires Chromium but it failed to launch — drop --bake-svg or install Chromium:', e.message);
1600
+ process.exit(1);
1601
+ }
1602
+ try {
1603
+ const page = await browser.newPage();
1604
+ await page.goto('file://' + path.resolve(tmp), { waitUntil: 'load' });
1605
+ await new Promise(r => setTimeout(r, DIAGRAM_RENDER_WAIT_MS));
1606
+ await page.evaluate(() => {
1607
+ document.querySelectorAll('script[data-md2doc-diagram-engine]').forEach((s) => s.remove());
1608
+ });
1609
+ return await page.content();
1610
+ } finally {
1611
+ await browser.close();
1612
+ fs.unlinkSync(tmp);
1613
+ }
1614
+ }
1615
+
1543
1616
  // ── Output ───────────────────────────────────────────────────────────────────
1544
- if (ext === '.html') {
1545
- fs.writeFileSync(dst, html, 'utf8');
1546
- console.log(`[HTML] ${src} → ${dst}`);
1617
+ (async () => {
1618
+ let finalHtml = await bakeGraphviz(html);
1547
1619
 
1548
- } else if (ext === '.pdf') {
1549
- (async () => {
1620
+ if (ext === '.html') {
1621
+ if (BAKE_SVG) finalHtml = await bakeDiagrams(finalHtml);
1622
+ fs.writeFileSync(dst, finalHtml, 'utf8');
1623
+ console.log(`[HTML] ${src} → ${dst}`);
1624
+
1625
+ } else if (ext === '.pdf') {
1626
+ if (BAKE_SVG) console.log('[INFO] --bake-svg is redundant for PDF output (already static); ignoring');
1550
1627
  let puppeteer;
1551
1628
  try {
1552
1629
  puppeteer = require('puppeteer');
@@ -1555,26 +1632,25 @@ if (ext === '.html') {
1555
1632
  process.exit(1);
1556
1633
  }
1557
1634
 
1558
- // Write temporary HTML, launch headless Chromium, export PDF
1635
+ // Write temporary HTML, launch headless Chromium, export PDF.
1559
1636
  // Case-insensitive: an uppercase .PDF dst must not make tmp === dst, or the
1560
1637
  // unlinkSync below deletes the freshly written PDF.
1561
1638
  const tmp = dst.replace(/\.pdf$/i, '._tmp.html');
1562
- fs.writeFileSync(tmp, html, 'utf8');
1639
+ fs.writeFileSync(tmp, finalHtml, 'utf8');
1563
1640
 
1564
- const browser = await puppeteer.launch({
1565
- headless: 'new',
1566
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-crash-reporter', '--disable-dev-shm-usage'],
1567
- });
1568
- const page = await browser.newPage();
1641
+ const browser = await launchBrowser(puppeteer);
1642
+ const page = await browser.newPage();
1569
1643
 
1570
1644
  await page.goto('file://' + path.resolve(tmp), { waitUntil: 'load' });
1571
1645
 
1572
- // Allow WaveDrom / Mermaid scripts time to render diagrams
1573
- await new Promise(r => setTimeout(r, 2500));
1646
+ // Allow WaveDrom / Mermaid scripts time to render diagrams.
1647
+ // NOTE: this sleep is load-bearing for the DEFAULT (view-time) render path.
1648
+ // It is only safe to drop under --bake-svg, where the DOM is already final SVG.
1649
+ await new Promise(r => setTimeout(r, DIAGRAM_RENDER_WAIT_MS));
1574
1650
 
1575
1651
  await page.pdf({
1576
- path: dst,
1577
- format: 'A4',
1652
+ path: dst,
1653
+ format: 'A4',
1578
1654
  printBackground: true,
1579
1655
  outline: true,
1580
1656
  tagged: true,
@@ -1584,9 +1660,12 @@ if (ext === '.html') {
1584
1660
  await browser.close();
1585
1661
  fs.unlinkSync(tmp);
1586
1662
  console.log(`[PDF] ${src} → ${dst}`);
1587
- })();
1588
1663
 
1589
- } else {
1590
- console.error('[ERROR] Output extension must be .html or .pdf');
1664
+ } else {
1665
+ console.error('[ERROR] Output extension must be .html or .pdf');
1666
+ process.exit(1);
1667
+ }
1668
+ })().catch((e) => {
1669
+ console.error('[ERROR]', (e && e.stack) || e);
1591
1670
  process.exit(1);
1592
- }
1671
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -26,8 +26,11 @@
26
26
  "node": ">=18"
27
27
  },
28
28
  "dependencies": {
29
+ "@hpcc-js/wasm-graphviz": "1.22.0",
29
30
  "marked": "^14.1.0",
30
- "puppeteer": "^24.15.0"
31
+ "mermaid": "11.15.0",
32
+ "puppeteer": "^24.15.0",
33
+ "wavedrom": "3.5.0"
31
34
  },
32
35
  "scripts": {
33
36
  "preinstall": "node scripts/preinstall.js",