@helping-ai-workflow/md2doc 2.3.0 → 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 +161 -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,12 +608,29 @@ 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 {
|
|
573
615
|
transform: rotate(180deg);
|
|
574
616
|
}
|
|
617
|
+
/* In the 36px collapsed rail only the restore toggle remains: hide the
|
|
618
|
+
TOC-list buttons (no list to act on) and shrink padding so the toggle
|
|
619
|
+
fits inside the rail instead of being clipped by overflow:hidden. */
|
|
620
|
+
body[data-toc-collapsed] #toc-expand-all,
|
|
621
|
+
body[data-toc-collapsed] #toc-collapse-all {
|
|
622
|
+
display: none;
|
|
623
|
+
}
|
|
624
|
+
body[data-toc-collapsed] .toc {
|
|
625
|
+
padding: 12px 3px;
|
|
626
|
+
}
|
|
627
|
+
body[data-toc-collapsed] .toc-header {
|
|
628
|
+
margin-bottom: 0;
|
|
629
|
+
justify-content: center;
|
|
630
|
+
}
|
|
631
|
+
body[data-toc-collapsed] .toc-header-actions {
|
|
632
|
+
justify-content: center;
|
|
633
|
+
}
|
|
575
634
|
#toc-collapse-toggle {
|
|
576
635
|
margin-left: 0;
|
|
577
636
|
padding: 2px 8px;
|
|
@@ -661,10 +720,45 @@ const html = `<!DOCTYPE html>
|
|
|
661
720
|
.search-results-header,
|
|
662
721
|
.toc-header {
|
|
663
722
|
display: flex;
|
|
664
|
-
|
|
665
|
-
|
|
723
|
+
flex-direction: column;
|
|
724
|
+
align-items: stretch;
|
|
725
|
+
gap: 4px;
|
|
666
726
|
margin-bottom: 8px;
|
|
667
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
|
+
}
|
|
668
762
|
.search-results-title,
|
|
669
763
|
.toc-title {
|
|
670
764
|
font-size: 0.78rem;
|
|
@@ -790,18 +884,26 @@ const html = `<!DOCTYPE html>
|
|
|
790
884
|
margin-top: 2px;
|
|
791
885
|
}
|
|
792
886
|
.toc-item {
|
|
793
|
-
margin:
|
|
887
|
+
margin: 0;
|
|
794
888
|
}
|
|
795
889
|
.toc a {
|
|
796
|
-
display:
|
|
890
|
+
display: block;
|
|
797
891
|
max-width: 100%;
|
|
798
892
|
color: #57606a;
|
|
799
893
|
text-decoration: none;
|
|
800
|
-
padding:
|
|
801
|
-
|
|
802
|
-
|
|
894
|
+
padding: 2px 0;
|
|
895
|
+
line-height: 1.4;
|
|
896
|
+
white-space: nowrap;
|
|
897
|
+
overflow: hidden;
|
|
898
|
+
text-overflow: ellipsis;
|
|
803
899
|
box-sizing: border-box;
|
|
804
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
|
+
}
|
|
805
907
|
.toc summary {
|
|
806
908
|
min-width: 0;
|
|
807
909
|
}
|
|
@@ -831,7 +933,7 @@ const html = `<!DOCTYPE html>
|
|
|
831
933
|
display: flex;
|
|
832
934
|
align-items: flex-start;
|
|
833
935
|
gap: 6px;
|
|
834
|
-
padding:
|
|
936
|
+
padding: 0;
|
|
835
937
|
}
|
|
836
938
|
.toc summary::-webkit-details-marker {
|
|
837
939
|
display: none;
|
|
@@ -1042,6 +1144,7 @@ const html = `<!DOCTYPE html>
|
|
|
1042
1144
|
a[href]:after { content: none; }
|
|
1043
1145
|
}
|
|
1044
1146
|
</style>
|
|
1147
|
+
${usesMath ? buildKatexStyleTag() : ''}
|
|
1045
1148
|
</head>
|
|
1046
1149
|
<body>
|
|
1047
1150
|
<button class="sidebar-toggle" id="sidebar-toggle" type="button" aria-label="Toggle sidebar" aria-expanded="false">☰</button>
|
|
@@ -1106,6 +1209,43 @@ ${mermaidInitTag}` : ''}
|
|
|
1106
1209
|
return String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
1107
1210
|
}
|
|
1108
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
|
+
|
|
1109
1249
|
function expandTocAncestors(link) {
|
|
1110
1250
|
let node = link && link.closest('details');
|
|
1111
1251
|
while (node) {
|
|
@@ -1139,6 +1279,7 @@ ${mermaidInitTag}` : ''}
|
|
|
1139
1279
|
previous.classList.remove('is-active');
|
|
1140
1280
|
}
|
|
1141
1281
|
readerState.activeSectionId = sectionId;
|
|
1282
|
+
renderBreadcrumb(sectionId);
|
|
1142
1283
|
const next = tocLinks.get(sectionId);
|
|
1143
1284
|
if (next) {
|
|
1144
1285
|
next.classList.add('is-active');
|
|
@@ -1164,6 +1305,12 @@ ${mermaidInitTag}` : ''}
|
|
|
1164
1305
|
headingNodes.forEach((node) => observer.observe(node));
|
|
1165
1306
|
}
|
|
1166
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
|
+
|
|
1167
1314
|
const allTocDetails = () => Array.from(document.querySelectorAll('.toc details'));
|
|
1168
1315
|
const expandAllBtn = document.getElementById('toc-expand-all');
|
|
1169
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"
|