@helping-ai-workflow/md2doc 1.0.2 → 1.1.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 +29 -1
- package/lib/md2doc.js +186 -4
- package/package.json +13 -3
- package/scripts/preinstall.js +70 -0
package/README.md
CHANGED
|
@@ -6,11 +6,39 @@ Two global CLIs (`md2html`, `md2pdf`) you can call from any directory.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
Requires Node.js 18 or higher. The first install pulls puppeteer (≈ 170 MB Chromium download); subsequent installs reuse it.
|
|
10
|
+
|
|
11
|
+
### Recommended: install via nvm
|
|
12
|
+
|
|
13
|
+
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`.
|
|
14
|
+
|
|
9
15
|
```bash
|
|
16
|
+
sudo apt install -y curl # Debian / Ubuntu only; skip if curl is already installed
|
|
17
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
|
18
|
+
source ~/.zshrc # or: source ~/.bashrc
|
|
19
|
+
nvm install --lts
|
|
20
|
+
nvm use --lts
|
|
10
21
|
npm install -g @helping-ai-workflow/md2doc
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
### Already have Node.js
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @helping-ai-workflow/md2doc
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Troubleshooting
|
|
31
|
+
|
|
32
|
+
**`EACCES: permission denied, mkdir '/usr/local/lib/node_modules'`**
|
|
33
|
+
Your system Node is owned by root. Do **not** run `sudo npm install -g` — puppeteer's postinstall would download Chromium as root and break later runs. Instead, switch to nvm using the steps above.
|
|
34
|
+
|
|
35
|
+
**`Failed to set up chrome ...! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`**
|
|
36
|
+
An earlier install left a half-finished Chromium download in `~/.cache/puppeteer`. md2doc ≥ 1.0.3 cleans this automatically; on older versions, clear the cache and retry:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
rm -rf ~/.cache/puppeteer
|
|
40
|
+
npm install -g @helping-ai-workflow/md2doc
|
|
41
|
+
```
|
|
14
42
|
|
|
15
43
|
## Usage
|
|
16
44
|
|
package/lib/md2doc.js
CHANGED
|
@@ -214,6 +214,65 @@ ${itemsHtml}
|
|
|
214
214
|
.join('');
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
function unbreakableRun(s) {
|
|
218
|
+
// Treat `_` as a break point — identifiers like `pmac_tx_*` are unbreakable in CSS but breakable for classification.
|
|
219
|
+
const matches = String(s || '').match(/[A-Za-z0-9\-./@:]+/g);
|
|
220
|
+
if (!matches) return 0;
|
|
221
|
+
let max = 0;
|
|
222
|
+
for (const m of matches) if (m.length > max) max = m.length;
|
|
223
|
+
return max;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function cellRawText(cell) {
|
|
227
|
+
if (!cell) return '';
|
|
228
|
+
if (Array.isArray(cell.tokens)) return flattenTokenText(cell.tokens);
|
|
229
|
+
return String(cell.text || '');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function classifyColumns(token) {
|
|
233
|
+
const colCount = (token.header || []).length;
|
|
234
|
+
const classes = [];
|
|
235
|
+
for (let i = 0; i < colCount; i++) {
|
|
236
|
+
const allCells = [];
|
|
237
|
+
if (token.header && token.header[i]) allCells.push(token.header[i]);
|
|
238
|
+
const dataCells = [];
|
|
239
|
+
if (Array.isArray(token.rows)) {
|
|
240
|
+
for (const row of token.rows) {
|
|
241
|
+
if (row && row[i]) {
|
|
242
|
+
allCells.push(row[i]);
|
|
243
|
+
dataCells.push(row[i]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const allTexts = allCells.map(cellRawText);
|
|
248
|
+
const dataTexts = dataCells.map(cellRawText);
|
|
249
|
+
// Header labels (e.g. "Clock Domain") often contain whitespace not representative of cell content; use data rows for the heuristic and fall back to header only when the column has no data.
|
|
250
|
+
const heuristicTexts = dataTexts.length > 0 ? dataTexts : allTexts;
|
|
251
|
+
let maxTokenLen = 0;
|
|
252
|
+
let totalLen = 0;
|
|
253
|
+
let hasWhitespace = false;
|
|
254
|
+
let hasSentence = false;
|
|
255
|
+
for (const t of heuristicTexts) {
|
|
256
|
+
const r = unbreakableRun(t);
|
|
257
|
+
if (r > maxTokenLen) maxTokenLen = r;
|
|
258
|
+
totalLen += t.length;
|
|
259
|
+
if (/\s/.test(t)) hasWhitespace = true;
|
|
260
|
+
if (/。|\. /.test(t)) hasSentence = true;
|
|
261
|
+
}
|
|
262
|
+
const avgCellLen = heuristicTexts.length ? (totalLen / heuristicTexts.length) : 0;
|
|
263
|
+
|
|
264
|
+
// Narrow takes precedence over prose (tie-break: prefer narrow / conservative).
|
|
265
|
+
if (maxTokenLen <= 12 && !hasWhitespace) {
|
|
266
|
+
classes.push('col-narrow');
|
|
267
|
+
} else if (avgCellLen > 40 || hasSentence) {
|
|
268
|
+
classes.push('col-prose');
|
|
269
|
+
} else {
|
|
270
|
+
classes.push('col-default');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return classes;
|
|
274
|
+
}
|
|
275
|
+
|
|
217
276
|
function stripHtmlTags(value) {
|
|
218
277
|
return String(value || '').replace(/<[^>]*>/g, '');
|
|
219
278
|
}
|
|
@@ -308,7 +367,6 @@ ${itemsHtml}
|
|
|
308
367
|
return baseBlockquote(token);
|
|
309
368
|
};
|
|
310
369
|
|
|
311
|
-
const baseTable = renderer.table.bind(renderer);
|
|
312
370
|
renderer.table = function(token) {
|
|
313
371
|
collectCellText(token.header);
|
|
314
372
|
if (Array.isArray(token.rows)) {
|
|
@@ -316,7 +374,41 @@ ${itemsHtml}
|
|
|
316
374
|
collectCellText(row);
|
|
317
375
|
}
|
|
318
376
|
}
|
|
319
|
-
|
|
377
|
+
|
|
378
|
+
const classes = classifyColumns(token);
|
|
379
|
+
const colHtml = classes
|
|
380
|
+
.map((c) => (c === 'col-default' ? '<col>' : `<col class="${c}">`))
|
|
381
|
+
.join('');
|
|
382
|
+
const cellClassAttr = (i) => {
|
|
383
|
+
const c = classes[i];
|
|
384
|
+
if (c === 'col-narrow') return ' class="cell-narrow"';
|
|
385
|
+
if (c === 'col-prose') return ' class="cell-prose"';
|
|
386
|
+
return '';
|
|
387
|
+
};
|
|
388
|
+
const alignStyle = (cell) => (cell && cell.align)
|
|
389
|
+
? ` style="text-align:${cell.align}"`
|
|
390
|
+
: '';
|
|
391
|
+
|
|
392
|
+
const headerCells = (token.header || []).map((cell, i) => {
|
|
393
|
+
const inner = cell && Array.isArray(cell.tokens)
|
|
394
|
+
? this.parser.parseInline(cell.tokens)
|
|
395
|
+
: '';
|
|
396
|
+
return `<th${cellClassAttr(i)}${alignStyle(cell)}>${inner}</th>`;
|
|
397
|
+
}).join('');
|
|
398
|
+
const headerHtml = `<thead><tr>${headerCells}</tr></thead>`;
|
|
399
|
+
|
|
400
|
+
const bodyRows = Array.isArray(token.rows) ? token.rows.map((row) => {
|
|
401
|
+
const cells = (row || []).map((cell, i) => {
|
|
402
|
+
const inner = cell && Array.isArray(cell.tokens)
|
|
403
|
+
? this.parser.parseInline(cell.tokens)
|
|
404
|
+
: '';
|
|
405
|
+
return `<td${cellClassAttr(i)}${alignStyle(cell)}>${inner}</td>`;
|
|
406
|
+
}).join('');
|
|
407
|
+
return `<tr>${cells}</tr>`;
|
|
408
|
+
}).join('') : '';
|
|
409
|
+
const bodyHtmlPart = `<tbody>${bodyRows}</tbody>`;
|
|
410
|
+
|
|
411
|
+
return `<table>\n<colgroup>${colHtml}</colgroup>\n${headerHtml}\n${bodyHtmlPart}\n</table>\n`;
|
|
320
412
|
};
|
|
321
413
|
|
|
322
414
|
marked.setOptions({ gfm: true, breaks: false, renderer });
|
|
@@ -360,6 +452,7 @@ ${itemsHtml}
|
|
|
360
452
|
<span class="toc-title">Contents</span>
|
|
361
453
|
<button id="toc-expand-all" type="button" aria-label="Expand all">⊞</button>
|
|
362
454
|
<button id="toc-collapse-all" type="button" aria-label="Collapse all">⊟</button>
|
|
455
|
+
<button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar">◀</button>
|
|
363
456
|
</div>
|
|
364
457
|
${renderTocNodes(tocTree)}
|
|
365
458
|
</nav>
|
|
@@ -404,16 +497,52 @@ const html = `<!DOCTYPE html>
|
|
|
404
497
|
.reader-sidebar {
|
|
405
498
|
position: sticky;
|
|
406
499
|
top: 24px;
|
|
407
|
-
|
|
500
|
+
flex: 0 1 300px;
|
|
501
|
+
width: clamp(220px, 22vw, 300px);
|
|
502
|
+
min-width: 220px;
|
|
408
503
|
height: calc(100vh - 48px);
|
|
409
504
|
overflow: hidden;
|
|
410
|
-
flex: 0 0 320px;
|
|
411
505
|
padding-right: 8px;
|
|
412
506
|
box-sizing: border-box;
|
|
413
507
|
display: flex;
|
|
414
508
|
flex-direction: column;
|
|
415
509
|
gap: 12px;
|
|
416
510
|
}
|
|
511
|
+
body[data-toc-collapsed] .reader-sidebar {
|
|
512
|
+
flex-basis: 36px;
|
|
513
|
+
width: 36px;
|
|
514
|
+
min-width: 36px;
|
|
515
|
+
padding-right: 0;
|
|
516
|
+
}
|
|
517
|
+
body[data-toc-collapsed] .reader-tools,
|
|
518
|
+
body[data-toc-collapsed] .search-results,
|
|
519
|
+
body[data-toc-collapsed] .toc > .toc-list,
|
|
520
|
+
body[data-toc-collapsed] .toc-title {
|
|
521
|
+
display: none;
|
|
522
|
+
}
|
|
523
|
+
body[data-toc-collapsed] #toc-collapse-toggle {
|
|
524
|
+
transform: rotate(180deg);
|
|
525
|
+
}
|
|
526
|
+
#toc-collapse-toggle {
|
|
527
|
+
margin-left: 0;
|
|
528
|
+
padding: 2px 8px;
|
|
529
|
+
font: inherit;
|
|
530
|
+
font-size: 0.9em;
|
|
531
|
+
line-height: 1;
|
|
532
|
+
border: 1px solid #d0d7de;
|
|
533
|
+
border-radius: 6px;
|
|
534
|
+
background: #ffffff;
|
|
535
|
+
color: #57606a;
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
transition: transform 0.15s ease;
|
|
538
|
+
}
|
|
539
|
+
#toc-collapse-toggle:hover {
|
|
540
|
+
background: #eef2f6;
|
|
541
|
+
color: #24292e;
|
|
542
|
+
}
|
|
543
|
+
@media (max-width: 1080px) {
|
|
544
|
+
#toc-collapse-toggle { display: none; }
|
|
545
|
+
}
|
|
417
546
|
.reader-tools { flex: 0 0 auto; }
|
|
418
547
|
.sidebar-toggle {
|
|
419
548
|
display: none;
|
|
@@ -698,6 +827,10 @@ const html = `<!DOCTYPE html>
|
|
|
698
827
|
.content {
|
|
699
828
|
min-width: 0;
|
|
700
829
|
flex: 1 1 auto;
|
|
830
|
+
}
|
|
831
|
+
.content p,
|
|
832
|
+
.content li,
|
|
833
|
+
.content blockquote {
|
|
701
834
|
overflow-wrap: anywhere;
|
|
702
835
|
word-break: break-word;
|
|
703
836
|
}
|
|
@@ -753,6 +886,36 @@ const html = `<!DOCTYPE html>
|
|
|
753
886
|
th, td { border: 1px solid #dfe2e5; padding: 7px 14px; text-align: left; }
|
|
754
887
|
th { background: #f6f8fa; font-weight: 600; }
|
|
755
888
|
tr:nth-child(even) { background: #fafbfc; }
|
|
889
|
+
.content table th,
|
|
890
|
+
.content table td {
|
|
891
|
+
overflow-wrap: normal;
|
|
892
|
+
word-break: normal;
|
|
893
|
+
}
|
|
894
|
+
.content table th {
|
|
895
|
+
white-space: nowrap;
|
|
896
|
+
}
|
|
897
|
+
.content table td code,
|
|
898
|
+
.content table th code {
|
|
899
|
+
white-space: nowrap;
|
|
900
|
+
}
|
|
901
|
+
.content table { table-layout: auto; }
|
|
902
|
+
.content table col.col-narrow { width: 1%; }
|
|
903
|
+
.content table col.col-prose { width: auto; }
|
|
904
|
+
.content table th.cell-narrow,
|
|
905
|
+
.content table td.cell-narrow { white-space: nowrap; }
|
|
906
|
+
.content table tbody td:first-child,
|
|
907
|
+
.content table thead th:first-child {
|
|
908
|
+
position: sticky;
|
|
909
|
+
left: 0;
|
|
910
|
+
z-index: 1;
|
|
911
|
+
background: #ffffff;
|
|
912
|
+
}
|
|
913
|
+
.content table thead th:first-child {
|
|
914
|
+
background: #f6f8fa;
|
|
915
|
+
}
|
|
916
|
+
.content table tbody tr:nth-child(even) td:first-child {
|
|
917
|
+
background: #fafbfc;
|
|
918
|
+
}
|
|
756
919
|
blockquote {
|
|
757
920
|
border-left: 4px solid #dfe2e5;
|
|
758
921
|
padding: 0 16px;
|
|
@@ -960,6 +1123,25 @@ ${mermaidInitTag}
|
|
|
960
1123
|
});
|
|
961
1124
|
}
|
|
962
1125
|
|
|
1126
|
+
function readTocCollapsed() {
|
|
1127
|
+
try { return localStorage.getItem('md2doc.toc.collapsed') === '1'; }
|
|
1128
|
+
catch (_) { return false; }
|
|
1129
|
+
}
|
|
1130
|
+
function writeTocCollapsed(v) {
|
|
1131
|
+
try { localStorage.setItem('md2doc.toc.collapsed', v ? '1' : '0'); }
|
|
1132
|
+
catch (_) { /* private mode / quota — ignore */ }
|
|
1133
|
+
}
|
|
1134
|
+
if (readTocCollapsed()) {
|
|
1135
|
+
document.body.setAttribute('data-toc-collapsed', '');
|
|
1136
|
+
}
|
|
1137
|
+
const tocCollapseBtn = document.getElementById('toc-collapse-toggle');
|
|
1138
|
+
if (tocCollapseBtn) {
|
|
1139
|
+
tocCollapseBtn.addEventListener('click', () => {
|
|
1140
|
+
const nowCollapsed = document.body.toggleAttribute('data-toc-collapsed');
|
|
1141
|
+
writeTocCollapsed(nowCollapsed);
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
963
1145
|
const SKIP_SELECTOR = 'svg, .mermaid, .graphviz, script, style';
|
|
964
1146
|
|
|
965
1147
|
function buildSnippet(section, query) {
|
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"html",
|
|
8
|
+
"pdf",
|
|
9
|
+
"renderer",
|
|
10
|
+
"wavedrom",
|
|
11
|
+
"mermaid",
|
|
12
|
+
"graphviz"
|
|
13
|
+
],
|
|
6
14
|
"main": "lib/md2doc.js",
|
|
7
15
|
"bin": {
|
|
8
16
|
"md2html": "bin/md2html.js",
|
|
9
|
-
"md2pdf":
|
|
17
|
+
"md2pdf": "bin/md2pdf.js"
|
|
10
18
|
},
|
|
11
19
|
"files": [
|
|
12
20
|
"lib/",
|
|
13
21
|
"bin/",
|
|
22
|
+
"scripts/",
|
|
14
23
|
"README.md",
|
|
15
24
|
"LICENSE"
|
|
16
25
|
],
|
|
@@ -22,6 +31,7 @@
|
|
|
22
31
|
"puppeteer": "^24.15.0"
|
|
23
32
|
},
|
|
24
33
|
"scripts": {
|
|
34
|
+
"preinstall": "node scripts/preinstall.js",
|
|
25
35
|
"test": "node test/md2doc.test.js"
|
|
26
36
|
},
|
|
27
37
|
"repository": {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Remove stale puppeteer browser-cache folders that exist but are missing
|
|
5
|
+
// the actual binary. An interrupted prior download leaves @puppeteer/browsers
|
|
6
|
+
// in a state where it sees the version folder and refuses to redownload,
|
|
7
|
+
// failing puppeteer's postinstall with:
|
|
8
|
+
// The browser folder (.../chrome/<platform>-<ver>) exists but the
|
|
9
|
+
// executable (.../chrome-<platform>/chrome) is missing
|
|
10
|
+
// We run before puppeteer's postinstall and clean those husks so the
|
|
11
|
+
// download proceeds fresh.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
const BINARY_NAMES = new Set([
|
|
18
|
+
'chrome',
|
|
19
|
+
'chrome.exe',
|
|
20
|
+
'chrome-headless-shell',
|
|
21
|
+
'chrome-headless-shell.exe',
|
|
22
|
+
'Google Chrome for Testing',
|
|
23
|
+
]);
|
|
24
|
+
const MIN_BINARY_BYTES = 1_000_000;
|
|
25
|
+
const MAX_DEPTH = 4;
|
|
26
|
+
|
|
27
|
+
function hasBinary(rootDir) {
|
|
28
|
+
const stack = [[rootDir, 0]];
|
|
29
|
+
while (stack.length) {
|
|
30
|
+
const [dir, depth] = stack.pop();
|
|
31
|
+
if (depth > MAX_DEPTH) continue;
|
|
32
|
+
let entries;
|
|
33
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
34
|
+
catch { continue; }
|
|
35
|
+
for (const ent of entries) {
|
|
36
|
+
const full = path.join(dir, ent.name);
|
|
37
|
+
if (ent.isFile() && BINARY_NAMES.has(ent.name)) {
|
|
38
|
+
try {
|
|
39
|
+
if (fs.statSync(full).size >= MIN_BINARY_BYTES) return true;
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
} else if (ent.isDirectory()) {
|
|
42
|
+
stack.push([full, depth + 1]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cleanStaleVersionDirs(parentDir) {
|
|
50
|
+
if (!fs.existsSync(parentDir)) return;
|
|
51
|
+
let entries;
|
|
52
|
+
try { entries = fs.readdirSync(parentDir, { withFileTypes: true }); }
|
|
53
|
+
catch { return; }
|
|
54
|
+
for (const ent of entries) {
|
|
55
|
+
if (!ent.isDirectory()) continue;
|
|
56
|
+
const versionDir = path.join(parentDir, ent.name);
|
|
57
|
+
if (hasBinary(versionDir)) continue;
|
|
58
|
+
console.log(`[md2doc preinstall] removing stale puppeteer cache: ${versionDir}`);
|
|
59
|
+
try { fs.rmSync(versionDir, { recursive: true, force: true }); }
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.log(`[md2doc preinstall] failed to remove ${versionDir}: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cacheRoot = process.env.PUPPETEER_CACHE_DIR
|
|
67
|
+
|| path.join(os.homedir(), '.cache', 'puppeteer');
|
|
68
|
+
|
|
69
|
+
cleanStaleVersionDirs(path.join(cacheRoot, 'chrome'));
|
|
70
|
+
cleanStaleVersionDirs(path.join(cacheRoot, 'chrome-headless-shell'));
|