@helping-ai-workflow/md2doc 2.3.1 → 2.4.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/lib/md2doc.js +147 -14
- package/package.json +3 -1
package/lib/md2doc.js
CHANGED
|
@@ -109,12 +109,32 @@ const mermaidInitTag = `<script type="text/javascript" data-md2doc-diagram-engin
|
|
|
109
109
|
|
|
110
110
|
let usesMermaid = false;
|
|
111
111
|
let usesWaveDrom = false;
|
|
112
|
+
let usesMath = false;
|
|
113
|
+
|
|
114
|
+
// Self-contained KaTeX stylesheet: inline katex.min.css with each woff2 @font-face
|
|
115
|
+
// rewritten to a base64 data: URI and the woff/ttf alternates stripped, so a
|
|
116
|
+
// math-bearing HTML prints offline (and in puppeteer PDF) with no font fetch.
|
|
117
|
+
// Built lazily — only when a document actually contains math.
|
|
118
|
+
function buildKatexStyleTag() {
|
|
119
|
+
const cssPath = require.resolve('katex/dist/katex.min.css');
|
|
120
|
+
const fontDir = path.join(path.dirname(cssPath), 'fonts');
|
|
121
|
+
let css = fs.readFileSync(cssPath, 'utf8');
|
|
122
|
+
css = css.replace(/url\(fonts\/(KaTeX_[\w-]+)\.woff2\)/g, (_, name) => {
|
|
123
|
+
const b64 = fs.readFileSync(path.join(fontDir, `${name}.woff2`)).toString('base64');
|
|
124
|
+
return `url(data:font/woff2;base64,${b64})`;
|
|
125
|
+
});
|
|
126
|
+
// Drop the now-redundant woff/ttf src alternates (woff2 is universal in modern
|
|
127
|
+
// browsers + puppeteer Chromium), so nothing references the on-disk font files.
|
|
128
|
+
css = css.replace(/,url\(fonts\/[\w-]+\.(?:woff|ttf)\) format\("(?:woff|truetype)"\)/g, '');
|
|
129
|
+
return `<style data-md2doc-math>${css}</style>`;
|
|
130
|
+
}
|
|
112
131
|
|
|
113
132
|
let bodyHtml;
|
|
114
133
|
let tocHtml = '';
|
|
115
134
|
let serializedSections = '[]';
|
|
116
135
|
try {
|
|
117
136
|
const { marked, Renderer } = require('marked');
|
|
137
|
+
const katex = require('katex');
|
|
118
138
|
|
|
119
139
|
const renderer = new Renderer();
|
|
120
140
|
const tocItems = [];
|
|
@@ -178,7 +198,7 @@ try {
|
|
|
178
198
|
|
|
179
199
|
const itemsHtml = nodes
|
|
180
200
|
.map((node) => {
|
|
181
|
-
const linkHtml = `<a href="#${node.id}">${escapeHtml(node.text)}</a>`;
|
|
201
|
+
const linkHtml = `<a href="#${node.id}" title="${escapeHtml(node.text)}">${escapeHtml(node.text)}</a>`;
|
|
182
202
|
const hasChildren = node.children && node.children.length > 0;
|
|
183
203
|
|
|
184
204
|
if (!hasChildren) {
|
|
@@ -322,6 +342,17 @@ ${itemsHtml}
|
|
|
322
342
|
// away — diverging from GitHub's escaped-code-block semantics.
|
|
323
343
|
return `\n<div class="mermaid">\n${escapeHtml(code)}\n</div>\n`;
|
|
324
344
|
}
|
|
345
|
+
if (lang === 'math') {
|
|
346
|
+
// KaTeX renders synchronously to static HTML (class="katex"); no client
|
|
347
|
+
// runtime, no async post-pass. throwOnError:false degrades a bad formula
|
|
348
|
+
// to red error text instead of crashing the whole render.
|
|
349
|
+
try {
|
|
350
|
+
return `\n${katex.renderToString(code, { displayMode: true, throwOnError: false })}\n`;
|
|
351
|
+
} catch (e) {
|
|
352
|
+
const esc = code.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
353
|
+
return `<pre><code class="language-math">${esc}</code></pre>\n`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
325
356
|
if (lang === 'dot' || lang === 'graphviz') {
|
|
326
357
|
// Defer rendering to the async bakeGraphviz() post-pass so the
|
|
327
358
|
// synchronous marked() pass stays sync. The dot source is carried as
|
|
@@ -454,6 +485,12 @@ ${itemsHtml}
|
|
|
454
485
|
],
|
|
455
486
|
});
|
|
456
487
|
|
|
488
|
+
// $$…$$ (display) and $…$ (inline) math via KaTeX. marked tokenizes code
|
|
489
|
+
// spans/fences first, so $ inside code stays literal; the extension's
|
|
490
|
+
// default no-space-adjacency rules keep prose currency ($5 to $10) unrendered.
|
|
491
|
+
const markedKatex = require('marked-katex-extension');
|
|
492
|
+
marked.use(markedKatex({ throwOnError: false }));
|
|
493
|
+
|
|
457
494
|
// Pre-process the remaining non-standard inline syntax before marked parses
|
|
458
495
|
const escAttr = (s) => String(s)
|
|
459
496
|
.replace(/&/g, '&')
|
|
@@ -468,6 +505,9 @@ ${itemsHtml}
|
|
|
468
505
|
});
|
|
469
506
|
|
|
470
507
|
bodyHtml = marked.parse(mdPre);
|
|
508
|
+
// Both the ```math fence and the $/$$ extension emit class="katex" — a single
|
|
509
|
+
// post-parse scan is the source of truth for conditional CSS injection.
|
|
510
|
+
usesMath = /class="katex/.test(bodyHtml);
|
|
471
511
|
serializedSections = JSON.stringify(
|
|
472
512
|
sections.map((section) => ({
|
|
473
513
|
id: section.id,
|
|
@@ -498,10 +538,12 @@ ${itemsHtml}
|
|
|
498
538
|
</section>
|
|
499
539
|
<nav class="toc" aria-label="Table of contents" data-reader-toc>
|
|
500
540
|
<div class="toc-header">
|
|
501
|
-
<
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
541
|
+
<div class="toc-header-actions">
|
|
542
|
+
<button id="toc-expand-all" type="button" aria-label="Expand all">⊞</button>
|
|
543
|
+
<button id="toc-collapse-all" type="button" aria-label="Collapse all">⊟</button>
|
|
544
|
+
<button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar">◀</button>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="toc-breadcrumb" data-toc-breadcrumb aria-label="Current location"></div>
|
|
505
547
|
</div>
|
|
506
548
|
${renderTocNodes(tocTree)}
|
|
507
549
|
</nav>
|
|
@@ -566,7 +608,7 @@ const html = `<!DOCTYPE html>
|
|
|
566
608
|
body[data-toc-collapsed] .reader-tools,
|
|
567
609
|
body[data-toc-collapsed] .search-results,
|
|
568
610
|
body[data-toc-collapsed] .toc > .toc-list,
|
|
569
|
-
body[data-toc-collapsed] .toc-
|
|
611
|
+
body[data-toc-collapsed] .toc-breadcrumb {
|
|
570
612
|
display: none;
|
|
571
613
|
}
|
|
572
614
|
body[data-toc-collapsed] #toc-collapse-toggle {
|
|
@@ -586,6 +628,9 @@ const html = `<!DOCTYPE html>
|
|
|
586
628
|
margin-bottom: 0;
|
|
587
629
|
justify-content: center;
|
|
588
630
|
}
|
|
631
|
+
body[data-toc-collapsed] .toc-header-actions {
|
|
632
|
+
justify-content: center;
|
|
633
|
+
}
|
|
589
634
|
#toc-collapse-toggle {
|
|
590
635
|
margin-left: 0;
|
|
591
636
|
padding: 2px 8px;
|
|
@@ -675,10 +720,45 @@ const html = `<!DOCTYPE html>
|
|
|
675
720
|
.search-results-header,
|
|
676
721
|
.toc-header {
|
|
677
722
|
display: flex;
|
|
678
|
-
|
|
679
|
-
|
|
723
|
+
flex-direction: column;
|
|
724
|
+
align-items: stretch;
|
|
725
|
+
gap: 4px;
|
|
680
726
|
margin-bottom: 8px;
|
|
681
727
|
}
|
|
728
|
+
.toc-header-actions {
|
|
729
|
+
flex: 0 0 auto;
|
|
730
|
+
display: flex;
|
|
731
|
+
gap: 6px;
|
|
732
|
+
justify-content: flex-end;
|
|
733
|
+
}
|
|
734
|
+
/* Sticky breadcrumb: stacked ancestor chain of the current scroll position.
|
|
735
|
+
Each row is single-line + ellipsis; full text on hover via title=. */
|
|
736
|
+
.toc-breadcrumb {
|
|
737
|
+
flex: 1 1 auto;
|
|
738
|
+
min-width: 0;
|
|
739
|
+
display: flex;
|
|
740
|
+
flex-direction: column;
|
|
741
|
+
gap: 1px;
|
|
742
|
+
}
|
|
743
|
+
.toc-breadcrumb a {
|
|
744
|
+
display: block;
|
|
745
|
+
max-width: 100%;
|
|
746
|
+
white-space: nowrap;
|
|
747
|
+
overflow: hidden;
|
|
748
|
+
text-overflow: ellipsis;
|
|
749
|
+
color: #57606a;
|
|
750
|
+
text-decoration: none;
|
|
751
|
+
font-size: 0.82em;
|
|
752
|
+
line-height: 1.55;
|
|
753
|
+
box-sizing: border-box;
|
|
754
|
+
}
|
|
755
|
+
.toc-breadcrumb a:hover {
|
|
756
|
+
color: #0969da;
|
|
757
|
+
}
|
|
758
|
+
.toc-breadcrumb a.breadcrumb-current {
|
|
759
|
+
color: #0b57d0;
|
|
760
|
+
font-weight: 700;
|
|
761
|
+
}
|
|
682
762
|
.search-results-title,
|
|
683
763
|
.toc-title {
|
|
684
764
|
font-size: 0.78rem;
|
|
@@ -804,18 +884,26 @@ const html = `<!DOCTYPE html>
|
|
|
804
884
|
margin-top: 2px;
|
|
805
885
|
}
|
|
806
886
|
.toc-item {
|
|
807
|
-
margin:
|
|
887
|
+
margin: 0;
|
|
808
888
|
}
|
|
809
889
|
.toc a {
|
|
810
|
-
display:
|
|
890
|
+
display: block;
|
|
811
891
|
max-width: 100%;
|
|
812
892
|
color: #57606a;
|
|
813
893
|
text-decoration: none;
|
|
814
|
-
padding:
|
|
815
|
-
|
|
816
|
-
|
|
894
|
+
padding: 2px 0;
|
|
895
|
+
line-height: 1.4;
|
|
896
|
+
white-space: nowrap;
|
|
897
|
+
overflow: hidden;
|
|
898
|
+
text-overflow: ellipsis;
|
|
817
899
|
box-sizing: border-box;
|
|
818
900
|
}
|
|
901
|
+
/* Parent rows put the anchor inside a flex <summary>; default flex min-width
|
|
902
|
+
is auto, which would block ellipsis. Allow the anchor to shrink + clip. */
|
|
903
|
+
.toc summary > a {
|
|
904
|
+
min-width: 0;
|
|
905
|
+
flex: 0 1 auto;
|
|
906
|
+
}
|
|
819
907
|
.toc summary {
|
|
820
908
|
min-width: 0;
|
|
821
909
|
}
|
|
@@ -845,7 +933,7 @@ const html = `<!DOCTYPE html>
|
|
|
845
933
|
display: flex;
|
|
846
934
|
align-items: flex-start;
|
|
847
935
|
gap: 6px;
|
|
848
|
-
padding:
|
|
936
|
+
padding: 0;
|
|
849
937
|
}
|
|
850
938
|
.toc summary::-webkit-details-marker {
|
|
851
939
|
display: none;
|
|
@@ -1056,6 +1144,7 @@ const html = `<!DOCTYPE html>
|
|
|
1056
1144
|
a[href]:after { content: none; }
|
|
1057
1145
|
}
|
|
1058
1146
|
</style>
|
|
1147
|
+
${usesMath ? buildKatexStyleTag() : ''}
|
|
1059
1148
|
</head>
|
|
1060
1149
|
<body>
|
|
1061
1150
|
<button class="sidebar-toggle" id="sidebar-toggle" type="button" aria-label="Toggle sidebar" aria-expanded="false">☰</button>
|
|
@@ -1120,6 +1209,43 @@ ${mermaidInitTag}` : ''}
|
|
|
1120
1209
|
return String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
1121
1210
|
}
|
|
1122
1211
|
|
|
1212
|
+
const breadcrumbEl = document.querySelector('[data-toc-breadcrumb]');
|
|
1213
|
+
const sectionIndexById = new Map(sections.map((section, i) => [section.id, i]));
|
|
1214
|
+
|
|
1215
|
+
// Sticky breadcrumb: the ancestor chain (shallow→deep) of the active section.
|
|
1216
|
+
// For each decreasing depth, take the nearest preceding section, then append
|
|
1217
|
+
// the active section itself. Rendered as stacked, indented, clickable rows.
|
|
1218
|
+
function renderBreadcrumb(sectionId) {
|
|
1219
|
+
if (!breadcrumbEl) return;
|
|
1220
|
+
if (!sectionIndexById.has(sectionId)) {
|
|
1221
|
+
breadcrumbEl.innerHTML = '';
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const idx = sectionIndexById.get(sectionId);
|
|
1225
|
+
const chain = [];
|
|
1226
|
+
let need = sections[idx].depth;
|
|
1227
|
+
for (let i = idx; i >= 0 && need >= 1; i--) {
|
|
1228
|
+
if (sections[i].depth <= need) {
|
|
1229
|
+
chain.unshift(sections[i]);
|
|
1230
|
+
need = sections[i].depth - 1;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (!chain.length) {
|
|
1234
|
+
breadcrumbEl.innerHTML = '';
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const minDepth = chain[0].depth;
|
|
1238
|
+
breadcrumbEl.innerHTML = chain
|
|
1239
|
+
.map((section, i) => {
|
|
1240
|
+
const indent = (section.depth - minDepth) * 10;
|
|
1241
|
+
const cls = i === chain.length - 1 ? ' class="breadcrumb-current"' : '';
|
|
1242
|
+
const title = escapeHtml(section.title);
|
|
1243
|
+
return '<a href="#' + section.id + '"' + cls + ' title="' + title +
|
|
1244
|
+
'" style="padding-left:' + indent + 'px">' + title + '</a>';
|
|
1245
|
+
})
|
|
1246
|
+
.join('');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1123
1249
|
function expandTocAncestors(link) {
|
|
1124
1250
|
let node = link && link.closest('details');
|
|
1125
1251
|
while (node) {
|
|
@@ -1153,6 +1279,7 @@ ${mermaidInitTag}` : ''}
|
|
|
1153
1279
|
previous.classList.remove('is-active');
|
|
1154
1280
|
}
|
|
1155
1281
|
readerState.activeSectionId = sectionId;
|
|
1282
|
+
renderBreadcrumb(sectionId);
|
|
1156
1283
|
const next = tocLinks.get(sectionId);
|
|
1157
1284
|
if (next) {
|
|
1158
1285
|
next.classList.add('is-active');
|
|
@@ -1178,6 +1305,12 @@ ${mermaidInitTag}` : ''}
|
|
|
1178
1305
|
headingNodes.forEach((node) => observer.observe(node));
|
|
1179
1306
|
}
|
|
1180
1307
|
|
|
1308
|
+
// Paint the breadcrumb immediately so the header is populated before the
|
|
1309
|
+
// first IntersectionObserver callback fires.
|
|
1310
|
+
if (sections[0]) {
|
|
1311
|
+
renderBreadcrumb(sections[0].id);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1181
1314
|
const allTocDetails = () => Array.from(document.querySelectorAll('.toc details'));
|
|
1182
1315
|
const expandAllBtn = document.getElementById('toc-expand-all');
|
|
1183
1316
|
if (expandAllBtn) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@hpcc-js/wasm-graphviz": "1.22.0",
|
|
30
|
+
"katex": "^0.16.47",
|
|
30
31
|
"marked": "^14.1.0",
|
|
32
|
+
"marked-katex-extension": "^5.1.10",
|
|
31
33
|
"mermaid": "11.15.0",
|
|
32
34
|
"puppeteer": "^24.15.0",
|
|
33
35
|
"wavedrom": "3.5.0"
|