@helping-ai-workflow/md2doc 2.4.2 → 2.6.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 +19 -0
- package/lib/md2doc.js +458 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -98,6 +98,25 @@ A reference with no file on disk keeps its original `src` and prints
|
|
|
98
98
|
Each reference carries its own copy of the payload, so re-using one large diagram
|
|
99
99
|
in several places grows the HTML accordingly. PDF output is unaffected.
|
|
100
100
|
|
|
101
|
+
### Viewing diagrams
|
|
102
|
+
|
|
103
|
+
Click any image, Mermaid, Graphviz or WaveDrom graphic in the rendered HTML to open it
|
|
104
|
+
full-screen.
|
|
105
|
+
|
|
106
|
+
| Input | Action |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Wheel / shift+wheel | Scroll vertically / horizontally |
|
|
109
|
+
| Ctrl (or ⌘) + wheel | Zoom around the pointer |
|
|
110
|
+
| Drag | Pan |
|
|
111
|
+
| `+` `-` | Zoom in / out |
|
|
112
|
+
| `0` `1` | Fit to window / actual size |
|
|
113
|
+
| Double-click | Toggle fit ↔ 100% |
|
|
114
|
+
| `Esc`, ✕, backdrop click | Close |
|
|
115
|
+
|
|
116
|
+
Vector diagrams open scaled to fill the window; raster images open at actual size. An
|
|
117
|
+
image wrapped in a link stays a link. The overlay is hidden in print and never reaches
|
|
118
|
+
the PDF output.
|
|
119
|
+
|
|
101
120
|
### Migration from md2html / md2pdf (v1.x → v2.0.0)
|
|
102
121
|
|
|
103
122
|
| Old | New |
|
package/lib/md2doc.js
CHANGED
|
@@ -706,7 +706,7 @@ ${itemsHtml}
|
|
|
706
706
|
const tocTree = buildTocTree(tocItems);
|
|
707
707
|
tocHtml = `<aside class="reader-sidebar" data-reader-sidebar>
|
|
708
708
|
<section class="reader-tools">
|
|
709
|
-
<label class="reader-search-label" for="doc-search-input">Search
|
|
709
|
+
<label class="reader-search-label" for="doc-search-input">Search</label>
|
|
710
710
|
<div class="reader-search-row">
|
|
711
711
|
<input type="search" id="doc-search-input" placeholder="Enter keyword and press Enter">
|
|
712
712
|
<button id="doc-search-submit" type="button">Search</button>
|
|
@@ -733,7 +733,8 @@ ${itemsHtml}
|
|
|
733
733
|
</div>
|
|
734
734
|
${renderTocNodes(tocTree)}
|
|
735
735
|
</nav>
|
|
736
|
-
</aside
|
|
736
|
+
</aside>
|
|
737
|
+
<div class="sidebar-splitter" id="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="Resize sidebar"></div>`;
|
|
737
738
|
}
|
|
738
739
|
} catch (e) {
|
|
739
740
|
console.error('[ERROR] marked not found — install with: npm install marked');
|
|
@@ -765,7 +766,9 @@ const html = `<!DOCTYPE html>
|
|
|
765
766
|
.page-layout {
|
|
766
767
|
display: flex;
|
|
767
768
|
align-items: flex-start;
|
|
768
|
-
|
|
769
|
+
/* Sidebar/content spacing is carried by .sidebar-splitter (32px), not gap,
|
|
770
|
+
so the drag handle can live inside the visual gutter. */
|
|
771
|
+
gap: 0;
|
|
769
772
|
margin: 0;
|
|
770
773
|
padding: 24px 24px 48px;
|
|
771
774
|
max-width: 100%;
|
|
@@ -774,9 +777,9 @@ const html = `<!DOCTYPE html>
|
|
|
774
777
|
.reader-sidebar {
|
|
775
778
|
position: sticky;
|
|
776
779
|
top: 24px;
|
|
777
|
-
flex: 0
|
|
778
|
-
width: clamp(220px, 22vw, 300px);
|
|
779
|
-
min-width:
|
|
780
|
+
flex: 0 0 auto;
|
|
781
|
+
width: var(--md2doc-sidebar-w, clamp(220px, 22vw, 300px));
|
|
782
|
+
min-width: 0;
|
|
780
783
|
height: calc(100vh - 48px);
|
|
781
784
|
overflow: hidden;
|
|
782
785
|
padding-right: 8px;
|
|
@@ -817,6 +820,32 @@ const html = `<!DOCTYPE html>
|
|
|
817
820
|
body[data-toc-collapsed] .toc-header-actions {
|
|
818
821
|
justify-content: center;
|
|
819
822
|
}
|
|
823
|
+
.sidebar-splitter {
|
|
824
|
+
flex: 0 0 32px;
|
|
825
|
+
align-self: stretch;
|
|
826
|
+
cursor: col-resize;
|
|
827
|
+
display: flex;
|
|
828
|
+
justify-content: center;
|
|
829
|
+
touch-action: none;
|
|
830
|
+
user-select: none;
|
|
831
|
+
}
|
|
832
|
+
.sidebar-splitter::before {
|
|
833
|
+
content: '';
|
|
834
|
+
width: 3px;
|
|
835
|
+
border-radius: 2px;
|
|
836
|
+
background: transparent;
|
|
837
|
+
transition: background 0.15s ease;
|
|
838
|
+
}
|
|
839
|
+
.sidebar-splitter:hover::before,
|
|
840
|
+
.sidebar-splitter.is-dragging::before {
|
|
841
|
+
background: #93c5fd;
|
|
842
|
+
}
|
|
843
|
+
body[data-toc-collapsed] .sidebar-splitter {
|
|
844
|
+
cursor: default;
|
|
845
|
+
}
|
|
846
|
+
body[data-toc-collapsed] .sidebar-splitter::before {
|
|
847
|
+
display: none;
|
|
848
|
+
}
|
|
820
849
|
#toc-collapse-toggle {
|
|
821
850
|
margin-left: 0;
|
|
822
851
|
padding: 2px 8px;
|
|
@@ -1032,6 +1061,16 @@ const html = `<!DOCTYPE html>
|
|
|
1032
1061
|
font-size: 0.82em;
|
|
1033
1062
|
color: #57606a;
|
|
1034
1063
|
line-height: 1.35;
|
|
1064
|
+
display: -webkit-box;
|
|
1065
|
+
-webkit-line-clamp: 2;
|
|
1066
|
+
-webkit-box-orient: vertical;
|
|
1067
|
+
overflow: hidden;
|
|
1068
|
+
}
|
|
1069
|
+
.search-result-snippet mark {
|
|
1070
|
+
background: #fde68a;
|
|
1071
|
+
color: inherit;
|
|
1072
|
+
padding: 0 1px;
|
|
1073
|
+
border-radius: 2px;
|
|
1035
1074
|
}
|
|
1036
1075
|
.search-empty {
|
|
1037
1076
|
margin: 0;
|
|
@@ -1056,6 +1095,8 @@ const html = `<!DOCTYPE html>
|
|
|
1056
1095
|
flex: 1 1 auto;
|
|
1057
1096
|
min-height: 0;
|
|
1058
1097
|
overflow-y: auto;
|
|
1098
|
+
/* Horizontal peek: no scrollbar, position driven by shift+wheel only. */
|
|
1099
|
+
overflow-x: hidden;
|
|
1059
1100
|
}
|
|
1060
1101
|
.toc ul {
|
|
1061
1102
|
list-style: none;
|
|
@@ -1074,21 +1115,19 @@ const html = `<!DOCTYPE html>
|
|
|
1074
1115
|
}
|
|
1075
1116
|
.toc a {
|
|
1076
1117
|
display: block;
|
|
1077
|
-
|
|
1118
|
+
/* Full natural width (no ellipsis) so shift+wheel can peek clipped tails;
|
|
1119
|
+
min-width keeps hover/match backgrounds covering the whole row. */
|
|
1120
|
+
width: max-content;
|
|
1121
|
+
min-width: 100%;
|
|
1078
1122
|
color: #57606a;
|
|
1079
1123
|
text-decoration: none;
|
|
1080
1124
|
padding: 2px 0;
|
|
1081
1125
|
line-height: 1.4;
|
|
1082
1126
|
white-space: nowrap;
|
|
1083
|
-
overflow: hidden;
|
|
1084
|
-
text-overflow: ellipsis;
|
|
1085
1127
|
box-sizing: border-box;
|
|
1086
1128
|
}
|
|
1087
|
-
/* Parent rows put the anchor inside a flex <summary>; default flex min-width
|
|
1088
|
-
is auto, which would block ellipsis. Allow the anchor to shrink + clip. */
|
|
1089
1129
|
.toc summary > a {
|
|
1090
|
-
|
|
1091
|
-
flex: 0 1 auto;
|
|
1130
|
+
flex: 0 0 auto;
|
|
1092
1131
|
}
|
|
1093
1132
|
.toc summary {
|
|
1094
1133
|
min-width: 0;
|
|
@@ -1282,6 +1321,82 @@ const html = `<!DOCTYPE html>
|
|
|
1282
1321
|
height: auto;
|
|
1283
1322
|
margin: 0 auto;
|
|
1284
1323
|
}
|
|
1324
|
+
/* Anything the reader can pop out. The click handler uses the same
|
|
1325
|
+
selectors, so this cursor and that behaviour cannot drift apart. */
|
|
1326
|
+
.content img,
|
|
1327
|
+
.content .mermaid,
|
|
1328
|
+
.content .graphviz,
|
|
1329
|
+
.content [id^="WaveDrom_Display_"] { cursor: zoom-in; }
|
|
1330
|
+
.content a img { cursor: pointer; }
|
|
1331
|
+
|
|
1332
|
+
.lightbox {
|
|
1333
|
+
position: fixed;
|
|
1334
|
+
inset: 0;
|
|
1335
|
+
z-index: 200;
|
|
1336
|
+
display: flex;
|
|
1337
|
+
flex-direction: column;
|
|
1338
|
+
background: rgba(16, 18, 21, 0.92);
|
|
1339
|
+
}
|
|
1340
|
+
.lightbox[hidden] { display: none; }
|
|
1341
|
+
.lightbox-bar {
|
|
1342
|
+
flex: 0 0 auto;
|
|
1343
|
+
display: flex;
|
|
1344
|
+
align-items: center;
|
|
1345
|
+
justify-content: flex-end;
|
|
1346
|
+
gap: 6px;
|
|
1347
|
+
padding: 8px 12px;
|
|
1348
|
+
background: rgba(0, 0, 0, 0.35);
|
|
1349
|
+
color: #e6edf3;
|
|
1350
|
+
font-size: 13px;
|
|
1351
|
+
}
|
|
1352
|
+
.lightbox-bar button {
|
|
1353
|
+
min-width: 32px;
|
|
1354
|
+
height: 28px;
|
|
1355
|
+
padding: 0 10px;
|
|
1356
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
1357
|
+
border-radius: 6px;
|
|
1358
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1359
|
+
color: inherit;
|
|
1360
|
+
font: inherit;
|
|
1361
|
+
line-height: 1;
|
|
1362
|
+
cursor: pointer;
|
|
1363
|
+
}
|
|
1364
|
+
.lightbox-bar button:hover { background: rgba(255, 255, 255, 0.18); }
|
|
1365
|
+
.lightbox-zoom-value {
|
|
1366
|
+
min-width: 56px;
|
|
1367
|
+
text-align: center;
|
|
1368
|
+
font-variant-numeric: tabular-nums;
|
|
1369
|
+
}
|
|
1370
|
+
.lightbox-hint {
|
|
1371
|
+
margin-right: auto;
|
|
1372
|
+
opacity: 0.7;
|
|
1373
|
+
font-size: 12px;
|
|
1374
|
+
}
|
|
1375
|
+
/* position: relative makes the stage the canvas's offsetParent, which the
|
|
1376
|
+
cursor-anchored zoom maths depends on. */
|
|
1377
|
+
.lightbox-stage {
|
|
1378
|
+
position: relative;
|
|
1379
|
+
flex: 1 1 auto;
|
|
1380
|
+
overflow: auto;
|
|
1381
|
+
cursor: grab;
|
|
1382
|
+
overscroll-behavior: contain;
|
|
1383
|
+
}
|
|
1384
|
+
.lightbox-stage[data-panning] { cursor: grabbing; }
|
|
1385
|
+
.lightbox-canvas { margin: 0 auto; }
|
|
1386
|
+
/* Sized in px by the runtime; the child follows, so the scroll extent grows
|
|
1387
|
+
with the zoom. A CSS transform would scale the pixels and leave the scroll
|
|
1388
|
+
area at the original size, stranding the edges out of reach. */
|
|
1389
|
+
.lightbox-canvas > * {
|
|
1390
|
+
display: block;
|
|
1391
|
+
width: 100%;
|
|
1392
|
+
height: auto;
|
|
1393
|
+
max-width: none;
|
|
1394
|
+
max-height: none;
|
|
1395
|
+
margin: 0;
|
|
1396
|
+
background: #ffffff;
|
|
1397
|
+
}
|
|
1398
|
+
body[data-lightbox-open] { overflow: hidden; }
|
|
1399
|
+
@media print { .lightbox { display: none !important; } }
|
|
1285
1400
|
@media (max-width: 1080px) {
|
|
1286
1401
|
.sidebar-toggle { display: inline-flex; align-items: center; }
|
|
1287
1402
|
.page-layout {
|
|
@@ -1311,6 +1426,7 @@ const html = `<!DOCTYPE html>
|
|
|
1311
1426
|
flex: initial;
|
|
1312
1427
|
}
|
|
1313
1428
|
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); }
|
|
1429
|
+
.sidebar-splitter { display: none; }
|
|
1314
1430
|
.heading-anchor { opacity: 1; }
|
|
1315
1431
|
}
|
|
1316
1432
|
@media print {
|
|
@@ -1323,6 +1439,7 @@ const html = `<!DOCTYPE html>
|
|
|
1323
1439
|
}
|
|
1324
1440
|
.reader-sidebar,
|
|
1325
1441
|
.sidebar-toggle,
|
|
1442
|
+
.sidebar-splitter,
|
|
1326
1443
|
.sidebar-scrim { display: none !important; }
|
|
1327
1444
|
.content { max-width: 100%; }
|
|
1328
1445
|
.heading-anchor { display: none; }
|
|
@@ -1539,14 +1656,24 @@ ${mermaidInitTag}` : ''}
|
|
|
1539
1656
|
const lower = haystack.toLowerCase();
|
|
1540
1657
|
const index = lower.indexOf(query);
|
|
1541
1658
|
if (index === -1) {
|
|
1542
|
-
return haystack.slice(0,
|
|
1659
|
+
return { text: haystack.slice(0, 80), matchStart: -1, matchLength: 0 };
|
|
1543
1660
|
}
|
|
1544
|
-
const start = Math.max(0, index -
|
|
1545
|
-
const end = Math.min(haystack.length, index + query.length +
|
|
1546
|
-
let
|
|
1547
|
-
|
|
1548
|
-
if (
|
|
1549
|
-
|
|
1661
|
+
const start = Math.max(0, index - 25);
|
|
1662
|
+
const end = Math.min(haystack.length, index + query.length + 45);
|
|
1663
|
+
let text = haystack.slice(start, end);
|
|
1664
|
+
let matchStart = index - start;
|
|
1665
|
+
if (start > 0) { text = '…' + text; matchStart += 1; }
|
|
1666
|
+
if (end < haystack.length) text += '…';
|
|
1667
|
+
return { text, matchStart, matchLength: query.length };
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function renderSnippetHtml(snippet) {
|
|
1671
|
+
if (!snippet) return '';
|
|
1672
|
+
if (snippet.matchStart < 0) return escapeHtml(snippet.text);
|
|
1673
|
+
const before = snippet.text.slice(0, snippet.matchStart);
|
|
1674
|
+
const hit = snippet.text.slice(snippet.matchStart, snippet.matchStart + snippet.matchLength);
|
|
1675
|
+
const after = snippet.text.slice(snippet.matchStart + snippet.matchLength);
|
|
1676
|
+
return escapeHtml(before) + '<mark>' + escapeHtml(hit) + '</mark>' + escapeHtml(after);
|
|
1550
1677
|
}
|
|
1551
1678
|
|
|
1552
1679
|
function clearMatchedTocState() {
|
|
@@ -1608,7 +1735,7 @@ ${mermaidInitTag}` : ''}
|
|
|
1608
1735
|
list.innerHTML = readerState.results.map((result, index) => (
|
|
1609
1736
|
'<button class="search-result-item' + (index === readerState.selectedResultIndex ? ' is-active' : '') + '" data-result-index="' + index + '" type="button">'
|
|
1610
1737
|
+ '<span class="search-result-title">' + escapeHtml(result.title) + '</span>'
|
|
1611
|
-
+ '<span class="search-result-snippet">' +
|
|
1738
|
+
+ '<span class="search-result-snippet">' + renderSnippetHtml(result.snippet) + '</span>'
|
|
1612
1739
|
+ '</button>'
|
|
1613
1740
|
)).join('');
|
|
1614
1741
|
}
|
|
@@ -1857,6 +1984,7 @@ ${mermaidInitTag}` : ''}
|
|
|
1857
1984
|
sidebarScrim.addEventListener('click', () => setSidebarOpen(false));
|
|
1858
1985
|
}
|
|
1859
1986
|
document.addEventListener('keydown', (event) => {
|
|
1987
|
+
if (lightboxIsOpen()) return;
|
|
1860
1988
|
if (event.key === 'Escape' && document.body.hasAttribute('data-sidebar-open')) {
|
|
1861
1989
|
setSidebarOpen(false);
|
|
1862
1990
|
}
|
|
@@ -1869,6 +1997,70 @@ ${mermaidInitTag}` : ''}
|
|
|
1869
1997
|
}
|
|
1870
1998
|
});
|
|
1871
1999
|
|
|
2000
|
+
// ── Sidebar splitter: drag to resize, persisted; double-click resets ─────
|
|
2001
|
+
const sidebarEl = document.querySelector('.reader-sidebar');
|
|
2002
|
+
const splitterEl = document.getElementById('sidebar-splitter');
|
|
2003
|
+
const SIDEBAR_WIDTH_KEY = 'md2doc.sidebar.width';
|
|
2004
|
+
function clampSidebarWidth(px) {
|
|
2005
|
+
return Math.min(Math.max(px, 180), Math.round(window.innerWidth * 0.5));
|
|
2006
|
+
}
|
|
2007
|
+
function applySidebarWidth(px) {
|
|
2008
|
+
document.documentElement.style.setProperty('--md2doc-sidebar-w', px + 'px');
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
const storedWidth = parseInt(localStorage.getItem(SIDEBAR_WIDTH_KEY), 10);
|
|
2012
|
+
if (Number.isFinite(storedWidth)) applySidebarWidth(clampSidebarWidth(storedWidth));
|
|
2013
|
+
} catch (e) { /* storage unavailable */ }
|
|
2014
|
+
if (splitterEl && sidebarEl) {
|
|
2015
|
+
let dragging = false;
|
|
2016
|
+
let dragMoved = false;
|
|
2017
|
+
let dragStartX = 0;
|
|
2018
|
+
let dragStartWidth = 0;
|
|
2019
|
+
splitterEl.addEventListener('pointerdown', (event) => {
|
|
2020
|
+
if (document.body.hasAttribute('data-toc-collapsed')) return;
|
|
2021
|
+
dragging = true;
|
|
2022
|
+
dragMoved = false;
|
|
2023
|
+
dragStartX = event.clientX;
|
|
2024
|
+
dragStartWidth = sidebarEl.offsetWidth;
|
|
2025
|
+
splitterEl.classList.add('is-dragging');
|
|
2026
|
+
if (splitterEl.setPointerCapture) splitterEl.setPointerCapture(event.pointerId);
|
|
2027
|
+
event.preventDefault();
|
|
2028
|
+
});
|
|
2029
|
+
splitterEl.addEventListener('pointermove', (event) => {
|
|
2030
|
+
if (!dragging) return;
|
|
2031
|
+
const delta = event.clientX - dragStartX;
|
|
2032
|
+
if (Math.abs(delta) >= 3) dragMoved = true;
|
|
2033
|
+
if (dragMoved) applySidebarWidth(clampSidebarWidth(dragStartWidth + delta));
|
|
2034
|
+
});
|
|
2035
|
+
function endSidebarDrag() {
|
|
2036
|
+
if (!dragging) return;
|
|
2037
|
+
dragging = false;
|
|
2038
|
+
splitterEl.classList.remove('is-dragging');
|
|
2039
|
+
if (!dragMoved) return;
|
|
2040
|
+
try { localStorage.setItem(SIDEBAR_WIDTH_KEY, String(sidebarEl.offsetWidth)); }
|
|
2041
|
+
catch (e) { /* storage unavailable */ }
|
|
2042
|
+
}
|
|
2043
|
+
splitterEl.addEventListener('pointerup', endSidebarDrag);
|
|
2044
|
+
splitterEl.addEventListener('pointercancel', endSidebarDrag);
|
|
2045
|
+
splitterEl.addEventListener('dblclick', () => {
|
|
2046
|
+
document.documentElement.style.removeProperty('--md2doc-sidebar-w');
|
|
2047
|
+
try { localStorage.removeItem(SIDEBAR_WIDTH_KEY); }
|
|
2048
|
+
catch (e) { /* storage unavailable */ }
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// ── TOC horizontal peek: shift+wheel scrolls a hidden-overflow x-axis ─────
|
|
2053
|
+
const tocScrollEl = document.querySelector('.toc > .toc-list');
|
|
2054
|
+
if (tocScrollEl) {
|
|
2055
|
+
tocScrollEl.addEventListener('wheel', (event) => {
|
|
2056
|
+
if (!event.shiftKey) return;
|
|
2057
|
+
const delta = event.deltaX !== 0 ? event.deltaX : event.deltaY;
|
|
2058
|
+
if (!delta) return;
|
|
2059
|
+
event.preventDefault();
|
|
2060
|
+
tocScrollEl.scrollLeft += delta;
|
|
2061
|
+
}, { passive: false });
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1872
2064
|
// ── Zoom / resize scroll anchoring ────────────────────────────────────────
|
|
1873
2065
|
// Browser zoom (and any window resize) reflows the column but leaves the
|
|
1874
2066
|
// pixel scroll offset untouched, so the passage being read slides out of
|
|
@@ -1945,6 +2137,251 @@ ${mermaidInitTag}` : ''}
|
|
|
1945
2137
|
});
|
|
1946
2138
|
|
|
1947
2139
|
captureScrollAnchor();
|
|
2140
|
+
|
|
2141
|
+
// ── Diagram / image lightbox ──────────────────────────────────────────────
|
|
2142
|
+
// Specs are read at 100% but their block diagrams and waveforms are drawn far
|
|
2143
|
+
// wider than the column, so the inline copy is unreadably small. Clicking one
|
|
2144
|
+
// pops it into a modal stage that zooms and scrolls.
|
|
2145
|
+
var LIGHTBOX_TARGETS = 'img, .mermaid, .graphviz, [id^="WaveDrom_Display_"]';
|
|
2146
|
+
var LIGHTBOX_MIN_ZOOM = 0.05;
|
|
2147
|
+
var LIGHTBOX_MAX_ZOOM = 8;
|
|
2148
|
+
var LIGHTBOX_STEP = 1.25;
|
|
2149
|
+
var lightboxEl = null;
|
|
2150
|
+
var lightboxStage = null;
|
|
2151
|
+
var lightboxCanvas = null;
|
|
2152
|
+
var lightboxZoomValue = null;
|
|
2153
|
+
var lightboxZoom = 1;
|
|
2154
|
+
var lightboxFitZoom = 1;
|
|
2155
|
+
var lightboxNaturalW = 1;
|
|
2156
|
+
var lightboxNaturalH = 1;
|
|
2157
|
+
var lightboxReturnScroll = 0;
|
|
2158
|
+
|
|
2159
|
+
function lightboxIsOpen() {
|
|
2160
|
+
return !!lightboxEl && !lightboxEl.hidden;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function lightboxButton(label, title, onClick) {
|
|
2164
|
+
var button = document.createElement('button');
|
|
2165
|
+
button.type = 'button';
|
|
2166
|
+
button.textContent = label;
|
|
2167
|
+
button.title = title;
|
|
2168
|
+
button.setAttribute('aria-label', title);
|
|
2169
|
+
button.addEventListener('click', onClick);
|
|
2170
|
+
return button;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function computeLightboxFit(isVector) {
|
|
2174
|
+
var pad = 24;
|
|
2175
|
+
var byWidth = (lightboxStage.clientWidth - pad) / lightboxNaturalW;
|
|
2176
|
+
var byHeight = (lightboxStage.clientHeight - pad) / lightboxNaturalH;
|
|
2177
|
+
var fit = Math.min(byWidth, byHeight);
|
|
2178
|
+
if (!isFinite(fit) || fit <= 0) return 1;
|
|
2179
|
+
// Vector art fills the window — a waveform authored at 480px wide is the
|
|
2180
|
+
// whole reason for popping it out. A raster image stops at actual size,
|
|
2181
|
+
// where enlarging past 100% only buys blur.
|
|
2182
|
+
return isVector ? fit : Math.min(fit, 1);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function setLightboxZoom(next, anchorX, anchorY) {
|
|
2186
|
+
if (!lightboxCanvas) return;
|
|
2187
|
+
var clamped = Math.min(LIGHTBOX_MAX_ZOOM, Math.max(LIGHTBOX_MIN_ZOOM, next));
|
|
2188
|
+
var stage = lightboxStage;
|
|
2189
|
+
var ax = (typeof anchorX === 'number') ? anchorX : stage.clientWidth / 2;
|
|
2190
|
+
var ay = (typeof anchorY === 'number') ? anchorY : stage.clientHeight / 2;
|
|
2191
|
+
// Keep whatever sits under the anchor point pinned across the zoom.
|
|
2192
|
+
var contentX = stage.scrollLeft + ax - lightboxCanvas.offsetLeft;
|
|
2193
|
+
var contentY = stage.scrollTop + ay - lightboxCanvas.offsetTop;
|
|
2194
|
+
var ratio = clamped / lightboxZoom;
|
|
2195
|
+
lightboxZoom = clamped;
|
|
2196
|
+
// Width in px, not a transform: the scroll extent has to grow with the
|
|
2197
|
+
// zoom, or the enlarged edges can never be scrolled into view.
|
|
2198
|
+
lightboxCanvas.style.width = Math.max(1, Math.round(lightboxNaturalW * clamped)) + 'px';
|
|
2199
|
+
if (lightboxZoomValue) {
|
|
2200
|
+
lightboxZoomValue.textContent = Math.round(clamped * 100) + '%';
|
|
2201
|
+
}
|
|
2202
|
+
stage.scrollLeft = contentX * ratio - ax + lightboxCanvas.offsetLeft;
|
|
2203
|
+
stage.scrollTop = contentY * ratio - ay + lightboxCanvas.offsetTop;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function zoomLightboxBy(factor, anchorX, anchorY) {
|
|
2207
|
+
setLightboxZoom(lightboxZoom * factor, anchorX, anchorY);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function buildLightbox() {
|
|
2211
|
+
if (lightboxEl) return;
|
|
2212
|
+
lightboxEl = document.createElement('div');
|
|
2213
|
+
lightboxEl.className = 'lightbox';
|
|
2214
|
+
lightboxEl.hidden = true;
|
|
2215
|
+
lightboxEl.setAttribute('role', 'dialog');
|
|
2216
|
+
lightboxEl.setAttribute('aria-modal', 'true');
|
|
2217
|
+
|
|
2218
|
+
var bar = document.createElement('div');
|
|
2219
|
+
bar.className = 'lightbox-bar';
|
|
2220
|
+
var hint = document.createElement('span');
|
|
2221
|
+
hint.className = 'lightbox-hint';
|
|
2222
|
+
hint.textContent = 'scroll to pan · shift+scroll sideways · ctrl+scroll to zoom · drag to pan · esc to close';
|
|
2223
|
+
lightboxZoomValue = document.createElement('span');
|
|
2224
|
+
lightboxZoomValue.className = 'lightbox-zoom-value';
|
|
2225
|
+
lightboxZoomValue.setAttribute('data-lightbox-zoom-value', '');
|
|
2226
|
+
bar.appendChild(hint);
|
|
2227
|
+
bar.appendChild(lightboxButton('−', 'Zoom out', function () { zoomLightboxBy(1 / LIGHTBOX_STEP); }));
|
|
2228
|
+
bar.appendChild(lightboxZoomValue);
|
|
2229
|
+
bar.appendChild(lightboxButton('+', 'Zoom in', function () { zoomLightboxBy(LIGHTBOX_STEP); }));
|
|
2230
|
+
bar.appendChild(lightboxButton('Fit', 'Fit to window', function () { setLightboxZoom(lightboxFitZoom); }));
|
|
2231
|
+
bar.appendChild(lightboxButton('1:1', 'Actual size', function () { setLightboxZoom(1); }));
|
|
2232
|
+
bar.appendChild(lightboxButton('✕', 'Close', closeLightbox));
|
|
2233
|
+
|
|
2234
|
+
lightboxStage = document.createElement('div');
|
|
2235
|
+
lightboxStage.className = 'lightbox-stage';
|
|
2236
|
+
lightboxCanvas = document.createElement('div');
|
|
2237
|
+
lightboxCanvas.className = 'lightbox-canvas';
|
|
2238
|
+
lightboxStage.appendChild(lightboxCanvas);
|
|
2239
|
+
lightboxEl.appendChild(bar);
|
|
2240
|
+
lightboxEl.appendChild(lightboxStage);
|
|
2241
|
+
document.body.appendChild(lightboxEl);
|
|
2242
|
+
|
|
2243
|
+
var panning = false;
|
|
2244
|
+
var panX = 0;
|
|
2245
|
+
var panY = 0;
|
|
2246
|
+
var panDistance = 0;
|
|
2247
|
+
|
|
2248
|
+
lightboxStage.addEventListener('wheel', function (event) {
|
|
2249
|
+
// Plain and shift+wheel stay native scrolling — that is the pan.
|
|
2250
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
2251
|
+
event.preventDefault();
|
|
2252
|
+
var rect = lightboxStage.getBoundingClientRect();
|
|
2253
|
+
zoomLightboxBy(
|
|
2254
|
+
event.deltaY < 0 ? LIGHTBOX_STEP : 1 / LIGHTBOX_STEP,
|
|
2255
|
+
event.clientX - rect.left,
|
|
2256
|
+
event.clientY - rect.top
|
|
2257
|
+
);
|
|
2258
|
+
}, { passive: false });
|
|
2259
|
+
|
|
2260
|
+
lightboxStage.addEventListener('mousedown', function (event) {
|
|
2261
|
+
if (event.button !== 0) return;
|
|
2262
|
+
panning = true;
|
|
2263
|
+
panDistance = 0;
|
|
2264
|
+
panX = event.clientX;
|
|
2265
|
+
panY = event.clientY;
|
|
2266
|
+
lightboxStage.setAttribute('data-panning', '');
|
|
2267
|
+
event.preventDefault();
|
|
2268
|
+
});
|
|
2269
|
+
document.addEventListener('mousemove', function (event) {
|
|
2270
|
+
if (!panning) return;
|
|
2271
|
+
var dx = event.clientX - panX;
|
|
2272
|
+
var dy = event.clientY - panY;
|
|
2273
|
+
panX = event.clientX;
|
|
2274
|
+
panY = event.clientY;
|
|
2275
|
+
panDistance += Math.abs(dx) + Math.abs(dy);
|
|
2276
|
+
lightboxStage.scrollLeft -= dx;
|
|
2277
|
+
lightboxStage.scrollTop -= dy;
|
|
2278
|
+
});
|
|
2279
|
+
document.addEventListener('mouseup', function () {
|
|
2280
|
+
if (!panning) return;
|
|
2281
|
+
panning = false;
|
|
2282
|
+
lightboxStage.removeAttribute('data-panning');
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
lightboxStage.addEventListener('click', function (event) {
|
|
2286
|
+
if (panDistance > 4) return;
|
|
2287
|
+
// Only the empty backdrop closes; a click on the artwork must not.
|
|
2288
|
+
if (event.target === lightboxStage) closeLightbox();
|
|
2289
|
+
});
|
|
2290
|
+
lightboxStage.addEventListener('dblclick', function (event) {
|
|
2291
|
+
event.preventDefault();
|
|
2292
|
+
setLightboxZoom(Math.abs(lightboxZoom - 1) < 0.001 ? lightboxFitZoom : 1);
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function lightboxSourceOf(node) {
|
|
2297
|
+
if (node.tagName === 'IMG') return node;
|
|
2298
|
+
return node.querySelector('svg') || node;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function lightboxNaturalSize(source) {
|
|
2302
|
+
if (source.tagName === 'IMG') {
|
|
2303
|
+
return { w: source.naturalWidth || source.clientWidth || 1, h: source.naturalHeight || source.clientHeight || 1 };
|
|
2304
|
+
}
|
|
2305
|
+
var box = source.viewBox ? source.viewBox.baseVal : null;
|
|
2306
|
+
if (box && box.width) return { w: box.width, h: box.height };
|
|
2307
|
+
var rect = source.getBoundingClientRect();
|
|
2308
|
+
return { w: rect.width || 1, h: rect.height || 1 };
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function openLightbox(node) {
|
|
2312
|
+
buildLightbox();
|
|
2313
|
+
var source = lightboxSourceOf(node);
|
|
2314
|
+
var size = lightboxNaturalSize(source);
|
|
2315
|
+
lightboxNaturalW = Math.max(1, Math.round(size.w));
|
|
2316
|
+
lightboxNaturalH = Math.max(1, Math.round(size.h));
|
|
2317
|
+
|
|
2318
|
+
var isVector = !!(source.tagName && source.tagName.toLowerCase() === 'svg');
|
|
2319
|
+
var clone = source.cloneNode(true);
|
|
2320
|
+
clone.removeAttribute('id');
|
|
2321
|
+
clone.removeAttribute('class');
|
|
2322
|
+
clone.removeAttribute('style');
|
|
2323
|
+
clone.removeAttribute('width');
|
|
2324
|
+
clone.removeAttribute('height');
|
|
2325
|
+
if (isVector && !clone.getAttribute('viewBox')) {
|
|
2326
|
+
clone.setAttribute('viewBox', '0 0 ' + lightboxNaturalW + ' ' + lightboxNaturalH);
|
|
2327
|
+
}
|
|
2328
|
+
while (lightboxCanvas.firstChild) {
|
|
2329
|
+
lightboxCanvas.removeChild(lightboxCanvas.firstChild);
|
|
2330
|
+
}
|
|
2331
|
+
lightboxCanvas.appendChild(clone);
|
|
2332
|
+
|
|
2333
|
+
lightboxReturnScroll = window.scrollY;
|
|
2334
|
+
suppressAnchorCapture = true;
|
|
2335
|
+
document.body.setAttribute('data-lightbox-open', '');
|
|
2336
|
+
lightboxEl.hidden = false;
|
|
2337
|
+
lightboxStage.scrollTop = 0;
|
|
2338
|
+
lightboxStage.scrollLeft = 0;
|
|
2339
|
+
lightboxZoom = 1;
|
|
2340
|
+
lightboxFitZoom = computeLightboxFit(isVector);
|
|
2341
|
+
setLightboxZoom(lightboxFitZoom);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function closeLightbox() {
|
|
2345
|
+
if (!lightboxIsOpen()) return;
|
|
2346
|
+
lightboxEl.hidden = true;
|
|
2347
|
+
document.body.removeAttribute('data-lightbox-open');
|
|
2348
|
+
while (lightboxCanvas.firstChild) {
|
|
2349
|
+
lightboxCanvas.removeChild(lightboxCanvas.firstChild);
|
|
2350
|
+
}
|
|
2351
|
+
window.scrollTo(window.scrollX, lightboxReturnScroll);
|
|
2352
|
+
window.requestAnimationFrame(function () {
|
|
2353
|
+
suppressAnchorCapture = false;
|
|
2354
|
+
captureScrollAnchor();
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
if (contentEl) {
|
|
2359
|
+
contentEl.addEventListener('click', function (event) {
|
|
2360
|
+
if (event.defaultPrevented || event.button !== 0) return;
|
|
2361
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
|
2362
|
+
var target = (event.target && event.target.closest) ? event.target.closest(LIGHTBOX_TARGETS) : null;
|
|
2363
|
+
if (!target) return;
|
|
2364
|
+
// A linked image is a link first.
|
|
2365
|
+
if (target.closest('a[href]')) return;
|
|
2366
|
+
event.preventDefault();
|
|
2367
|
+
openLightbox(target);
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
document.addEventListener('keydown', function (event) {
|
|
2372
|
+
if (!lightboxIsOpen()) return;
|
|
2373
|
+
if (event.key === 'Escape') { event.preventDefault(); closeLightbox(); return; }
|
|
2374
|
+
if (event.key === '+' || event.key === '=') { event.preventDefault(); zoomLightboxBy(LIGHTBOX_STEP); return; }
|
|
2375
|
+
if (event.key === '-' || event.key === '_') { event.preventDefault(); zoomLightboxBy(1 / LIGHTBOX_STEP); return; }
|
|
2376
|
+
if (event.key === '0') { event.preventDefault(); setLightboxZoom(lightboxFitZoom); return; }
|
|
2377
|
+
if (event.key === '1') { event.preventDefault(); setLightboxZoom(1); }
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
window.addEventListener('resize', function () {
|
|
2381
|
+
if (!lightboxIsOpen()) return;
|
|
2382
|
+
var shown = lightboxCanvas.firstElementChild;
|
|
2383
|
+
lightboxFitZoom = computeLightboxFit(!!(shown && shown.tagName.toLowerCase() === 'svg'));
|
|
2384
|
+
});
|
|
1948
2385
|
})();
|
|
1949
2386
|
</script>
|
|
1950
2387
|
</body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"preinstall": "node scripts/preinstall.js",
|
|
39
|
-
"test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/cli.test.js && node test/code-operator.test.js"
|
|
39
|
+
"test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|