@helping-ai-workflow/md2doc 3.7.0 → 3.9.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 +5 -0
- package/THIRD-PARTY-NOTICES.md +21 -0
- package/bin/md2doc.js +19 -3
- package/lib/editor/client.js +3 -2
- package/lib/md2doc.js +531 -194
- package/lib/theme/runtime.js +268 -0
- package/lib/theme/tokens.js +140 -0
- package/package.json +6 -2
package/lib/md2doc.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const drawio = require('./drawio.js');
|
|
24
|
+
const { applyReaderTheme } = require('./theme/tokens');
|
|
24
25
|
|
|
25
26
|
// How long to let client-side WaveDrom / Mermaid scripts render before we
|
|
26
27
|
// snapshot the DOM (PDF print, or --bake-svg inert-SVG bake).
|
|
@@ -140,9 +141,14 @@ const waveDromSkinTag = inlineScriptTag(localWaveDromSkin, 'wavedrom');
|
|
|
140
141
|
const waveDromTag = inlineScriptTag(localWaveDromJs, 'wavedrom');
|
|
141
142
|
const mermaidScriptTag = inlineScriptTag(localMermaidJs, 'mermaid');
|
|
142
143
|
|
|
144
|
+
// v3.8.0 (design review item 5): mermaid in md2doc's link blue on a light
|
|
145
|
+
// tint, the body font, white edge labels. Shared by the reader init below and
|
|
146
|
+
// the edit-mode init hook so both modes draw the same diagram.
|
|
147
|
+
const MERMAID_CONFIG_JS = "theme: 'base', themeVariables: { fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Arial, \"Microsoft JhengHei\", sans-serif', primaryColor: '#eaf2fd', primaryBorderColor: '#0969da', primaryTextColor: '#1f2328', lineColor: '#57606a', secondaryColor: '#f6f8fa', tertiaryColor: '#ffffff', edgeLabelBackground: '#ffffff' }";
|
|
148
|
+
|
|
143
149
|
const mermaidInitTag = `<script type="text/javascript" data-md2doc-diagram-engine="mermaid">
|
|
144
150
|
if (typeof mermaid !== 'undefined') {
|
|
145
|
-
mermaid.initialize({ startOnLoad: true,
|
|
151
|
+
mermaid.initialize({ startOnLoad: true, ${MERMAID_CONFIG_JS} });
|
|
146
152
|
}
|
|
147
153
|
</script>`;
|
|
148
154
|
|
|
@@ -370,9 +376,76 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
370
376
|
return parts;
|
|
371
377
|
}
|
|
372
378
|
|
|
379
|
+
// <html lang> for a rendered document (v3.8.0, design review item 2).
|
|
380
|
+
// Tagged zh-Hant when Han characters are at least 10% of (CJK characters +
|
|
381
|
+
// Latin words), counted outside code; when kana is at least 20% of
|
|
382
|
+
// (Han + kana) it is ja, and when Hangul is at least 20% of (Han + Hangul)
|
|
383
|
+
// it is ko (both still need CJK to be 10% of the prose). Measured 2026-10-01: English docs
|
|
384
|
+
// (README, CLAUDE.md) <= 0.013, mixed zh/en specs in mac_top/docs >= 0.237,
|
|
385
|
+
// so 0.10 leaves a wide margin on both sides. Traditional vs Simplified is
|
|
386
|
+
// not detected: every Chinese document this tool renders today is zh-Hant.
|
|
387
|
+
const LANG_TAG_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
388
|
+
function detectDocLang(mdText) {
|
|
389
|
+
const prose = String(mdText)
|
|
390
|
+
// Fences are anchored to line starts (up to 3 spaces of indent) and the
|
|
391
|
+
// closing fence must be at least as long as the opening one, so an inline
|
|
392
|
+
// mention of the marker, or a shorter marker line inside a longer fence,
|
|
393
|
+
// cannot swallow the prose around it. Backticks are written as x60 escapes.
|
|
394
|
+
.replace(/^ {0,3}(\x60{3,})[^\n\x60]*\n[\s\S]*?^ {0,3}\1\x60*[ \t\r]*$/gm, '')
|
|
395
|
+
.replace(/^ {0,3}(~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1~*[ \t\r]*$/gm, '')
|
|
396
|
+
.replace(/\x60[^\x60\n]*\x60/g, '');
|
|
397
|
+
const han = (prose.match(/[\u3400-\u9fff]/g) || []).length;
|
|
398
|
+
const kana = (prose.match(/[\u3040-\u30ff]/g) || []).length;
|
|
399
|
+
const hangul = (prose.match(/[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/g) || []).length;
|
|
400
|
+
const words = (prose.match(/[A-Za-z]+/g) || []).length;
|
|
401
|
+
// Kana or Hangul only count as Japanese or Korean when CJK text is the bulk
|
|
402
|
+
// of the document at all (same 10% floor as zh-Hant); one stray の in an
|
|
403
|
+
// English or Chinese document must not flip its language.
|
|
404
|
+
const cjk = han + kana + hangul;
|
|
405
|
+
if (cjk + words === 0) return 'en';
|
|
406
|
+
if (cjk / (cjk + words) >= 0.10) {
|
|
407
|
+
if (kana / (han + kana) >= 0.2) return 'ja';
|
|
408
|
+
if (hangul / (han + hangul) >= 0.2) return 'ko';
|
|
409
|
+
}
|
|
410
|
+
return (han + kana) / (han + kana + words) >= 0.10 ? 'zh-Hant' : 'en';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// v3.8.0 (design review item 12): the paragraph immediately after the
|
|
414
|
+
// document's FIRST </h1>, when EVERY line of it is "<strong>Key:</strong>
|
|
415
|
+
// value", is the spec's metadata block — rendered as a two-column <dl>.
|
|
416
|
+
// Allow-list rule: one non-matching line and the paragraph is left alone.
|
|
417
|
+
function renderDocMetaBlock(bodyHtml) {
|
|
418
|
+
const at = bodyHtml.indexOf('</h1>');
|
|
419
|
+
if (at < 0) return bodyHtml;
|
|
420
|
+
const head = bodyHtml.slice(0, at + 5);
|
|
421
|
+
const tail = bodyHtml.slice(at + 5);
|
|
422
|
+
const m = tail.match(/^(\s*)<p>([\s\S]*?)<\/p>/);
|
|
423
|
+
if (!m) return bodyHtml;
|
|
424
|
+
const lines = m[2].split('\n').filter((l) => l.trim());
|
|
425
|
+
const rows = lines.map((l) => l.match(/^\s*<strong>([^<]+):<\/strong>\s*([\s\S]*)$/));
|
|
426
|
+
if (rows.length < 2 || rows.some((r) => !r)) return bodyHtml;
|
|
427
|
+
const items = rows.map((r) => '<dt>' + r[1] + '</dt><dd>' + r[2] + '</dd>').join('');
|
|
428
|
+
return head + m[1] + '<dl class="doc-meta">' + items + '</dl>' + tail.slice(m[0].length);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// v3.8.0 sidebar icons. Path data from Lucide (ISC licence, see
|
|
432
|
+
// THIRD-PARTY-NOTICES.md), inlined so the single-file output works offline.
|
|
433
|
+
const ICON_SVG_OPEN = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">';
|
|
434
|
+
const READER_ICONS = {
|
|
435
|
+
unfold: ICON_SVG_OPEN + '<path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3 3-3-3"/><path d="m15 5-3-3-3 3"/></svg>',
|
|
436
|
+
fold: ICON_SVG_OPEN + '<path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3-3-3 3"/><path d="m15 5-3 3-3-3"/></svg>',
|
|
437
|
+
panel: ICON_SVG_OPEN + '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>',
|
|
438
|
+
up: ICON_SVG_OPEN + '<path d="m18 15-6-6-6 6"/></svg>',
|
|
439
|
+
down: ICON_SVG_OPEN + '<path d="m6 9 6 6 6-6"/></svg>',
|
|
440
|
+
x: ICON_SVG_OPEN + '<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
441
|
+
};
|
|
442
|
+
|
|
373
443
|
async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
374
444
|
const md = mdText;
|
|
375
445
|
const src = srcPath;
|
|
446
|
+
const docLang = (typeof opts.lang === 'string' && LANG_TAG_RE.test(opts.lang))
|
|
447
|
+
? opts.lang
|
|
448
|
+
: detectDocLang(md);
|
|
376
449
|
let usesMermaid = false;
|
|
377
450
|
let usesWaveDrom = false;
|
|
378
451
|
let usesDrawio = false;
|
|
@@ -699,6 +772,16 @@ async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
|
699
772
|
return root;
|
|
700
773
|
}
|
|
701
774
|
|
|
775
|
+
// v3.8.0 (design review item 7+8): when the document's only H1 is its
|
|
776
|
+
// title and it has sections, list those sections at the TOC's top level —
|
|
777
|
+
// the title row indented every entry one level and carried no information.
|
|
778
|
+
// Several H1s are chapters and are kept as they are.
|
|
779
|
+
function flattenSingleRoot(tree) {
|
|
780
|
+
return tree.length === 1 && tree[0].depth === 1 && tree[0].children.length
|
|
781
|
+
? tree[0].children
|
|
782
|
+
: tree;
|
|
783
|
+
}
|
|
784
|
+
|
|
702
785
|
function renderTocNodes(nodes, level = 1) {
|
|
703
786
|
if (!nodes.length) {
|
|
704
787
|
return '';
|
|
@@ -959,7 +1042,14 @@ ${itemsHtml}
|
|
|
959
1042
|
const depth = Number(token.depth || 1);
|
|
960
1043
|
const headingText = stripHtmlTags(flattenTokenText(token.tokens) || token.text || '');
|
|
961
1044
|
const headingId = slugifyHeading(headingText);
|
|
962
|
-
const
|
|
1045
|
+
const inlineHtml = this.parser.parseInline(token.tokens);
|
|
1046
|
+
// v3.8.0 (design review item 4): a leading section number ("4.1 ",
|
|
1047
|
+
// "12. ") gets its own span so CSS can widen the gap after it. The
|
|
1048
|
+
// space stays in the markup, so textContent is unchanged. Read mode
|
|
1049
|
+
// only — the editor turns heading DOM back into Markdown.
|
|
1050
|
+
const headingHtml = opts.editMode
|
|
1051
|
+
? inlineHtml
|
|
1052
|
+
: inlineHtml.replace(/^(\d+(?:\.\d+)*\.?)(\s)/, '<span class="sec">$1</span>$2');
|
|
963
1053
|
|
|
964
1054
|
tocItems.push({
|
|
965
1055
|
depth,
|
|
@@ -1126,6 +1216,7 @@ ${itemsHtml}
|
|
|
1126
1216
|
}
|
|
1127
1217
|
// Both the ```math fence and the $/$$ extension emit class="katex" — a single
|
|
1128
1218
|
// post-parse scan is the source of truth for conditional CSS injection.
|
|
1219
|
+
if (!opts.editMode) bodyHtml = renderDocMetaBlock(bodyHtml);
|
|
1129
1220
|
usesMath = /class="katex/.test(bodyHtml);
|
|
1130
1221
|
serializedSections = JSON.stringify(
|
|
1131
1222
|
sections.map((section) => ({
|
|
@@ -1141,30 +1232,31 @@ ${itemsHtml}
|
|
|
1141
1232
|
<section class="reader-tools">
|
|
1142
1233
|
<label class="reader-search-label" for="doc-search-input">Search</label>
|
|
1143
1234
|
<div class="reader-search-row">
|
|
1144
|
-
<input type="search" id="doc-search-input" placeholder="
|
|
1145
|
-
<button id="doc-search-submit" type="button">Search</button>
|
|
1146
|
-
<button id="doc-search-clear" type="button"
|
|
1235
|
+
<input type="search" id="doc-search-input" placeholder="Search this document">
|
|
1236
|
+
<button id="doc-search-submit" type="button" tabindex="-1">Search</button>
|
|
1237
|
+
<button id="doc-search-clear" type="button" aria-label="Clear search" title="Clear search">${READER_ICONS.x}</button>
|
|
1147
1238
|
</div>
|
|
1148
1239
|
</section>
|
|
1149
1240
|
<section class="search-results" id="search-results" hidden>
|
|
1150
1241
|
<div class="search-results-header">
|
|
1151
1242
|
<span class="search-results-title">Results</span>
|
|
1152
1243
|
<span id="search-result-count" class="reader-status">0</span>
|
|
1153
|
-
<button id="search-prev" type="button" disabled aria-label="Previous match"
|
|
1154
|
-
<button id="search-next" type="button" disabled aria-label="Next match"
|
|
1244
|
+
<button id="search-prev" type="button" disabled aria-label="Previous match" title="Previous match">${READER_ICONS.up}</button>
|
|
1245
|
+
<button id="search-next" type="button" disabled aria-label="Next match" title="Next match">${READER_ICONS.down}</button>
|
|
1155
1246
|
</div>
|
|
1156
1247
|
<div id="search-results-list"></div>
|
|
1157
1248
|
</section>
|
|
1158
1249
|
<nav class="toc" aria-label="Table of contents" data-reader-toc>
|
|
1159
1250
|
<div class="toc-header">
|
|
1251
|
+
<span class="toc-title">Contents</span>
|
|
1160
1252
|
<div class="toc-header-actions">
|
|
1161
|
-
<button id="toc-expand-all" type="button" aria-label="Expand all"
|
|
1162
|
-
<button id="toc-collapse-all" type="button" aria-label="Collapse all"
|
|
1163
|
-
<button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar"
|
|
1253
|
+
<button id="toc-expand-all" type="button" aria-label="Expand all" title="Expand all">${READER_ICONS.unfold}</button>
|
|
1254
|
+
<button id="toc-collapse-all" type="button" aria-label="Collapse all" title="Collapse all">${READER_ICONS.fold}</button>
|
|
1255
|
+
<button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar">${READER_ICONS.panel}</button>
|
|
1164
1256
|
</div>
|
|
1165
1257
|
<div class="toc-breadcrumb" data-toc-breadcrumb aria-label="Current location"></div>
|
|
1166
1258
|
</div>
|
|
1167
|
-
${renderTocNodes(tocTree)}
|
|
1259
|
+
${renderTocNodes(flattenSingleRoot(tocTree))}
|
|
1168
1260
|
</nav>
|
|
1169
1261
|
</aside>
|
|
1170
1262
|
<div class="sidebar-splitter" id="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="Resize sidebar"></div>`;
|
|
@@ -1214,7 +1306,7 @@ ${itemsHtml}
|
|
|
1214
1306
|
window.__md2docInitDiagrams = function (rootEl) {
|
|
1215
1307
|
rootEl = rootEl || document;
|
|
1216
1308
|
if (typeof mermaid !== 'undefined') {
|
|
1217
|
-
mermaid.initialize({ startOnLoad: false,
|
|
1309
|
+
mermaid.initialize({ startOnLoad: false, ${MERMAID_CONFIG_JS} });
|
|
1218
1310
|
var mermaidNodes = rootEl.querySelectorAll ? rootEl.querySelectorAll('.mermaid') : [];
|
|
1219
1311
|
var pending = [];
|
|
1220
1312
|
for (var i = 0; i < mermaidNodes.length; i++) {
|
|
@@ -1255,7 +1347,7 @@ ${itemsHtml}
|
|
|
1255
1347
|
// (on a later call) — the CSS cursor rule and the lightbox click
|
|
1256
1348
|
// target selector both key off this class (in addition to the
|
|
1257
1349
|
// WaveDrom_Display_ id prefix, which non-edit pages rely on and
|
|
1258
|
-
// never rename), so a diagram stays clickable and
|
|
1350
|
+
// never rename), so a diagram stays clickable and shows its click cursor
|
|
1259
1351
|
// for its whole lifetime, independent of id reclaiming.
|
|
1260
1352
|
var displayDiv = wavePending[j].previousElementSibling;
|
|
1261
1353
|
if (displayDiv) {
|
|
@@ -1609,6 +1701,10 @@ ${itemsHtml}
|
|
|
1609
1701
|
reader-mode output never includes it, so the mobile drawer toggle there
|
|
1610
1702
|
is unaffected. */
|
|
1611
1703
|
.sidebar-toggle { display: none; }
|
|
1704
|
+
.mobile-bar { display: none; }
|
|
1705
|
+
/* The reader bar is gone in edit mode, so its 52px anchor offset goes too
|
|
1706
|
+
(it moved scrollIntoView landings under the edit toolbar). */
|
|
1707
|
+
.content [id] { scroll-margin-top: 0; }
|
|
1612
1708
|
/* T11-2 step 5: enterSourceMode() (lib/editor/client.js) sets
|
|
1613
1709
|
contentEl.hidden = true and mounts a plain textarea in its place, and
|
|
1614
1710
|
setDocMode() writes docMode onto body as data-ed-mode. Measured on that
|
|
@@ -1813,9 +1909,9 @@ ${itemsHtml}
|
|
|
1813
1909
|
}
|
|
1814
1910
|
.ed-wave-lane-row.is-selected { background: #eaf2fe; }
|
|
1815
1911
|
.ed-wave-lane-handle {
|
|
1816
|
-
flex: 0 0 auto; cursor:
|
|
1912
|
+
flex: 0 0 auto; cursor: move; color: #aaa; user-select: none; font-size: 13px;
|
|
1817
1913
|
}
|
|
1818
|
-
.ed-wave-lane-handle:active { cursor:
|
|
1914
|
+
.ed-wave-lane-handle:active { cursor: move; }
|
|
1819
1915
|
/* min-width, not just flex-grow: min-width is what a flex item will not
|
|
1820
1916
|
shrink past, and shrinking past it is the vertical-text failure above.
|
|
1821
1917
|
MEASURED after this change (real page, 800x600, the journey suite's own
|
|
@@ -2137,15 +2233,24 @@ ${itemsHtml}
|
|
|
2137
2233
|
overflow-x: auto; font-family: monospace; font-size: 12px;
|
|
2138
2234
|
}`;
|
|
2139
2235
|
|
|
2140
|
-
|
|
2141
|
-
|
|
2236
|
+
// The phone bar only exists when there is a TOC for its button to open: a
|
|
2237
|
+
// document with no headings has no sidebar, and an empty bar would carry a
|
|
2238
|
+
// button that opens an empty drawer.
|
|
2239
|
+
const mobileBarHtml = tocHtml
|
|
2240
|
+
? '<header class="mobile-bar" id="mobile-bar">\n' +
|
|
2241
|
+
'<button class="sidebar-toggle" id="sidebar-toggle" type="button" aria-label="Toggle sidebar" aria-expanded="false">☰</button>\n' +
|
|
2242
|
+
'<span class="mobile-bar-title" id="mobile-bar-title"></span>\n' +
|
|
2243
|
+
'</header>\n'
|
|
2244
|
+
: '';
|
|
2245
|
+
const html = `<!DOCTYPE html>
|
|
2246
|
+
<html lang="${docLang}">
|
|
2142
2247
|
<head>
|
|
2143
2248
|
<meta charset="UTF-8">
|
|
2144
2249
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2145
2250
|
<title>${title}</title>
|
|
2146
2251
|
<style>
|
|
2147
2252
|
body {
|
|
2148
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
2253
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", "Noto Sans TC", sans-serif;
|
|
2149
2254
|
margin: 0;
|
|
2150
2255
|
padding: 0;
|
|
2151
2256
|
line-height: 1.65;
|
|
@@ -2174,7 +2279,9 @@ ${itemsHtml}
|
|
|
2174
2279
|
min-width: 0;
|
|
2175
2280
|
height: calc(100vh - 48px);
|
|
2176
2281
|
overflow: hidden;
|
|
2177
|
-
padding
|
|
2282
|
+
padding: 12px 8px 12px 12px;
|
|
2283
|
+
background: #f3f5f7;
|
|
2284
|
+
border-radius: 10px;
|
|
2178
2285
|
box-sizing: border-box;
|
|
2179
2286
|
display: flex;
|
|
2180
2287
|
flex-direction: column;
|
|
@@ -2184,7 +2291,7 @@ ${itemsHtml}
|
|
|
2184
2291
|
flex-basis: 36px;
|
|
2185
2292
|
width: 36px;
|
|
2186
2293
|
min-width: 36px;
|
|
2187
|
-
padding
|
|
2294
|
+
padding: 12px 4px;
|
|
2188
2295
|
}
|
|
2189
2296
|
body[data-toc-collapsed] .reader-tools,
|
|
2190
2297
|
body[data-toc-collapsed] .search-results,
|
|
@@ -2215,7 +2322,7 @@ ${itemsHtml}
|
|
|
2215
2322
|
.sidebar-splitter {
|
|
2216
2323
|
flex: 0 0 32px;
|
|
2217
2324
|
align-self: stretch;
|
|
2218
|
-
cursor:
|
|
2325
|
+
cursor: ew-resize;
|
|
2219
2326
|
display: flex;
|
|
2220
2327
|
justify-content: flex-start;
|
|
2221
2328
|
touch-action: none;
|
|
@@ -2240,26 +2347,13 @@ ${itemsHtml}
|
|
|
2240
2347
|
display: none;
|
|
2241
2348
|
}
|
|
2242
2349
|
#toc-collapse-toggle {
|
|
2243
|
-
margin-left: 0;
|
|
2244
|
-
padding: 2px 8px;
|
|
2245
|
-
font: inherit;
|
|
2246
|
-
font-size: 0.9em;
|
|
2247
|
-
line-height: 1;
|
|
2248
|
-
border: 1px solid #d0d7de;
|
|
2249
|
-
border-radius: 6px;
|
|
2250
|
-
background: #ffffff;
|
|
2251
|
-
color: #57606a;
|
|
2252
|
-
cursor: pointer;
|
|
2253
2350
|
transition: transform 0.15s ease;
|
|
2254
2351
|
}
|
|
2255
|
-
#toc-collapse-toggle:hover {
|
|
2256
|
-
background: #eef2f6;
|
|
2257
|
-
color: #24292e;
|
|
2258
|
-
}
|
|
2259
2352
|
@media (max-width: 1080px) {
|
|
2260
2353
|
#toc-collapse-toggle { display: none; }
|
|
2261
2354
|
}
|
|
2262
2355
|
.reader-tools { flex: 0 0 auto; }
|
|
2356
|
+
.mobile-bar { display: none; }
|
|
2263
2357
|
.sidebar-toggle {
|
|
2264
2358
|
display: none;
|
|
2265
2359
|
position: fixed;
|
|
@@ -2304,65 +2398,91 @@ ${itemsHtml}
|
|
|
2304
2398
|
.reader-tools,
|
|
2305
2399
|
.search-results,
|
|
2306
2400
|
.toc {
|
|
2307
|
-
border:
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
padding: 12px 14px;
|
|
2401
|
+
border: none;
|
|
2402
|
+
background: transparent;
|
|
2403
|
+
padding: 2px 0;
|
|
2311
2404
|
}
|
|
2312
|
-
.reader-search-label
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2405
|
+
.reader-search-label,
|
|
2406
|
+
#doc-search-submit {
|
|
2407
|
+
position: absolute;
|
|
2408
|
+
width: 1px;
|
|
2409
|
+
height: 1px;
|
|
2410
|
+
padding: 0;
|
|
2411
|
+
margin: -1px;
|
|
2412
|
+
overflow: hidden;
|
|
2413
|
+
clip: rect(0, 0, 0, 0);
|
|
2414
|
+
white-space: nowrap;
|
|
2415
|
+
border: 0;
|
|
2320
2416
|
}
|
|
2321
2417
|
.reader-search-row {
|
|
2418
|
+
position: relative;
|
|
2322
2419
|
display: flex;
|
|
2323
|
-
|
|
2324
|
-
margin-bottom: 8px;
|
|
2420
|
+
margin-bottom: 4px;
|
|
2325
2421
|
}
|
|
2326
2422
|
.reader-search-row input[type="search"] {
|
|
2327
2423
|
flex: 1 1 auto;
|
|
2328
2424
|
min-width: 0;
|
|
2329
|
-
padding:
|
|
2425
|
+
padding: 7px 30px 7px 10px;
|
|
2330
2426
|
border: 1px solid #d0d7de;
|
|
2331
2427
|
border-radius: 6px;
|
|
2428
|
+
background: #ffffff;
|
|
2332
2429
|
font: inherit;
|
|
2333
2430
|
}
|
|
2334
|
-
.reader-search-row button {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2431
|
+
.reader-search-row input[type="search"]::-webkit-search-cancel-button {
|
|
2432
|
+
-webkit-appearance: none;
|
|
2433
|
+
appearance: none;
|
|
2434
|
+
display: none;
|
|
2435
|
+
}
|
|
2436
|
+
#doc-search-clear {
|
|
2437
|
+
position: absolute;
|
|
2438
|
+
right: 4px;
|
|
2439
|
+
top: 50%;
|
|
2440
|
+
transform: translateY(-50%);
|
|
2441
|
+
display: inline-grid;
|
|
2442
|
+
place-items: center;
|
|
2443
|
+
padding: 4px;
|
|
2444
|
+
line-height: 0;
|
|
2445
|
+
border: none;
|
|
2446
|
+
border-radius: 4px;
|
|
2447
|
+
background: transparent;
|
|
2448
|
+
color: #59636e;
|
|
2341
2449
|
cursor: pointer;
|
|
2342
2450
|
}
|
|
2343
|
-
|
|
2344
|
-
background: #
|
|
2451
|
+
#doc-search-clear:hover {
|
|
2452
|
+
background: #e3e8ee;
|
|
2453
|
+
color: #1f2328;
|
|
2454
|
+
}
|
|
2455
|
+
#doc-search-input:placeholder-shown ~ #doc-search-clear {
|
|
2456
|
+
display: none;
|
|
2345
2457
|
}
|
|
2346
2458
|
.search-results-header,
|
|
2347
2459
|
.toc-header {
|
|
2348
2460
|
display: flex;
|
|
2349
|
-
flex-direction:
|
|
2350
|
-
align-items:
|
|
2351
|
-
gap:
|
|
2461
|
+
flex-direction: row;
|
|
2462
|
+
align-items: center;
|
|
2463
|
+
gap: 6px;
|
|
2352
2464
|
margin-bottom: 8px;
|
|
2353
2465
|
}
|
|
2466
|
+
.toc-header {
|
|
2467
|
+
justify-content: space-between;
|
|
2468
|
+
border-bottom: 1px solid #d8dee4;
|
|
2469
|
+
padding-bottom: 6px;
|
|
2470
|
+
}
|
|
2471
|
+
body[data-toc-collapsed] .toc-header {
|
|
2472
|
+
border-bottom: none;
|
|
2473
|
+
}
|
|
2354
2474
|
.toc-header-actions {
|
|
2355
2475
|
flex: 0 0 auto;
|
|
2356
2476
|
display: flex;
|
|
2357
|
-
gap:
|
|
2477
|
+
gap: 2px;
|
|
2358
2478
|
justify-content: flex-end;
|
|
2359
2479
|
}
|
|
2360
|
-
/*
|
|
2361
|
-
|
|
2480
|
+
/* Breadcrumb: kept in the DOM (the runtime and editor-reader-rebind read it)
|
|
2481
|
+
but hidden since v3.8.0 — the TOC highlights the whole active path instead. */
|
|
2362
2482
|
.toc-breadcrumb {
|
|
2363
2483
|
flex: 1 1 auto;
|
|
2364
2484
|
min-width: 0;
|
|
2365
|
-
display:
|
|
2485
|
+
display: none;
|
|
2366
2486
|
flex-direction: column;
|
|
2367
2487
|
gap: 1px;
|
|
2368
2488
|
}
|
|
@@ -2387,13 +2507,14 @@ ${itemsHtml}
|
|
|
2387
2507
|
}
|
|
2388
2508
|
.search-results-title,
|
|
2389
2509
|
.toc-title {
|
|
2390
|
-
font-size: 0.
|
|
2391
|
-
font-weight:
|
|
2392
|
-
|
|
2393
|
-
text-transform: uppercase;
|
|
2394
|
-
color: #57606a;
|
|
2510
|
+
font-size: 0.86rem;
|
|
2511
|
+
font-weight: 600;
|
|
2512
|
+
color: #1f2328;
|
|
2395
2513
|
flex: 0 0 auto;
|
|
2396
2514
|
}
|
|
2515
|
+
body[data-toc-collapsed] .toc-title {
|
|
2516
|
+
display: none;
|
|
2517
|
+
}
|
|
2397
2518
|
.reader-status {
|
|
2398
2519
|
flex: 1 1 auto;
|
|
2399
2520
|
font-size: 0.82em;
|
|
@@ -2401,20 +2522,21 @@ ${itemsHtml}
|
|
|
2401
2522
|
}
|
|
2402
2523
|
.search-results-header button,
|
|
2403
2524
|
.toc-header button {
|
|
2404
|
-
|
|
2525
|
+
display: inline-grid;
|
|
2526
|
+
place-items: center;
|
|
2527
|
+
padding: 4px;
|
|
2405
2528
|
font: inherit;
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
border: 1px solid #d0d7de;
|
|
2529
|
+
line-height: 0;
|
|
2530
|
+
border: none;
|
|
2409
2531
|
border-radius: 6px;
|
|
2410
|
-
background:
|
|
2411
|
-
color: #
|
|
2532
|
+
background: transparent;
|
|
2533
|
+
color: #59636e;
|
|
2412
2534
|
cursor: pointer;
|
|
2413
2535
|
}
|
|
2414
2536
|
.search-results-header button:hover,
|
|
2415
2537
|
.toc-header button:hover {
|
|
2416
|
-
background: #
|
|
2417
|
-
color: #
|
|
2538
|
+
background: #e3e8ee;
|
|
2539
|
+
color: #1f2328;
|
|
2418
2540
|
}
|
|
2419
2541
|
.search-results-header button:disabled {
|
|
2420
2542
|
opacity: 0.45;
|
|
@@ -2460,8 +2582,8 @@ ${itemsHtml}
|
|
|
2460
2582
|
background: #eef2f6;
|
|
2461
2583
|
}
|
|
2462
2584
|
.search-result-item.is-active {
|
|
2463
|
-
background: #
|
|
2464
|
-
box-shadow:
|
|
2585
|
+
background: #dbe6f3;
|
|
2586
|
+
box-shadow: none;
|
|
2465
2587
|
}
|
|
2466
2588
|
.search-result-title {
|
|
2467
2589
|
font-weight: 600;
|
|
@@ -2510,6 +2632,8 @@ ${itemsHtml}
|
|
|
2510
2632
|
the shift+wheel handler; appears only when a title actually overflows. */
|
|
2511
2633
|
overflow-x: auto;
|
|
2512
2634
|
scrollbar-width: thin;
|
|
2635
|
+
-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
|
|
2636
|
+
mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
|
|
2513
2637
|
}
|
|
2514
2638
|
.toc ul {
|
|
2515
2639
|
list-style: none;
|
|
@@ -2534,13 +2658,15 @@ ${itemsHtml}
|
|
|
2534
2658
|
min-width: 100%;
|
|
2535
2659
|
color: #57606a;
|
|
2536
2660
|
text-decoration: none;
|
|
2537
|
-
padding: 2px
|
|
2661
|
+
padding: 2px 8px;
|
|
2538
2662
|
line-height: 1.4;
|
|
2539
2663
|
white-space: nowrap;
|
|
2540
2664
|
box-sizing: border-box;
|
|
2665
|
+
border-radius: 4px;
|
|
2541
2666
|
}
|
|
2542
2667
|
.toc summary > a {
|
|
2543
|
-
flex:
|
|
2668
|
+
flex: 1 0 auto;
|
|
2669
|
+
min-width: 0;
|
|
2544
2670
|
}
|
|
2545
2671
|
.toc summary {
|
|
2546
2672
|
min-width: 0;
|
|
@@ -2550,17 +2676,41 @@ ${itemsHtml}
|
|
|
2550
2676
|
}
|
|
2551
2677
|
.toc a:hover {
|
|
2552
2678
|
color: #0969da;
|
|
2679
|
+
background: #e8edf2;
|
|
2553
2680
|
}
|
|
2554
2681
|
.toc a.is-active {
|
|
2555
|
-
color: #
|
|
2556
|
-
font-weight:
|
|
2682
|
+
color: #0550ae;
|
|
2683
|
+
font-weight: 600;
|
|
2684
|
+
background: #dbe6f3;
|
|
2557
2685
|
}
|
|
2558
2686
|
.toc a.is-match {
|
|
2559
|
-
color:
|
|
2560
|
-
background: #
|
|
2687
|
+
color: inherit;
|
|
2688
|
+
background: #fff5c2;
|
|
2689
|
+
}
|
|
2690
|
+
.toc li:has(a.is-active) > details > summary > a:not(.is-active) {
|
|
2691
|
+
color: #1f2328;
|
|
2692
|
+
font-weight: 600;
|
|
2693
|
+
}
|
|
2694
|
+
.reader-sidebar :is(a, button, input, summary):focus-visible,
|
|
2695
|
+
.mobile-bar button:focus-visible,
|
|
2696
|
+
.content a:focus-visible {
|
|
2697
|
+
outline: 2px solid #0969da;
|
|
2698
|
+
outline-offset: 2px;
|
|
2561
2699
|
border-radius: 4px;
|
|
2562
|
-
|
|
2563
|
-
|
|
2700
|
+
}
|
|
2701
|
+
.toc :is(a, summary):focus-visible {
|
|
2702
|
+
outline-offset: -2px;
|
|
2703
|
+
}
|
|
2704
|
+
/* The list's right-edge fade is a mask, and a mask clips the ring of a
|
|
2705
|
+
focused row along with the text. Lift it while a row holds focus. */
|
|
2706
|
+
.toc > .toc-list:has(:focus-visible) {
|
|
2707
|
+
-webkit-mask-image: none;
|
|
2708
|
+
mask-image: none;
|
|
2709
|
+
}
|
|
2710
|
+
.toc-header button:focus-visible,
|
|
2711
|
+
.search-results-header button:focus-visible,
|
|
2712
|
+
.reader-sidebar .search-result-item:focus-visible {
|
|
2713
|
+
outline-offset: -2px;
|
|
2564
2714
|
}
|
|
2565
2715
|
.toc details {
|
|
2566
2716
|
margin: 0;
|
|
@@ -2582,6 +2732,8 @@ ${itemsHtml}
|
|
|
2582
2732
|
font-size: 0.78em;
|
|
2583
2733
|
line-height: 1.8;
|
|
2584
2734
|
flex: 0 0 auto;
|
|
2735
|
+
width: 12px;
|
|
2736
|
+
text-align: center;
|
|
2585
2737
|
transform: translateY(1px);
|
|
2586
2738
|
}
|
|
2587
2739
|
.toc details[open] > summary::before {
|
|
@@ -2593,15 +2745,15 @@ ${itemsHtml}
|
|
|
2593
2745
|
border-left: 1px solid #d8dee4;
|
|
2594
2746
|
}
|
|
2595
2747
|
.toc-item:not(.toc-parent) > a {
|
|
2596
|
-
padding-left:
|
|
2748
|
+
padding-left: 26px;
|
|
2597
2749
|
}
|
|
2598
2750
|
.toc-list-level-1 > .toc-item > a,
|
|
2599
2751
|
.toc-list-level-1 > .toc-item > details > summary > a {
|
|
2600
|
-
font-
|
|
2752
|
+
font-size: 0.95em;
|
|
2601
2753
|
}
|
|
2602
2754
|
.toc-list-level-2 > .toc-item > a,
|
|
2603
2755
|
.toc-list-level-2 > .toc-item > details > summary > a {
|
|
2604
|
-
font-size: 0.
|
|
2756
|
+
font-size: 0.9em;
|
|
2605
2757
|
}
|
|
2606
2758
|
.toc-list-level-3 > .toc-item > a,
|
|
2607
2759
|
.toc-list-level-3 > .toc-item > details > summary > a,
|
|
@@ -2633,6 +2785,19 @@ ${itemsHtml}
|
|
|
2633
2785
|
}
|
|
2634
2786
|
.content > * { max-width: 100%; }
|
|
2635
2787
|
.content iframe, .content video, .content canvas { max-width: 100%; height: auto; }
|
|
2788
|
+
.doc-meta {
|
|
2789
|
+
display: grid;
|
|
2790
|
+
grid-template-columns: max-content 1fr;
|
|
2791
|
+
gap: 4px 20px;
|
|
2792
|
+
background: #f6f8fa;
|
|
2793
|
+
border-radius: 6px;
|
|
2794
|
+
padding: 12px 16px;
|
|
2795
|
+
margin: 16px 0 28px;
|
|
2796
|
+
font-size: 0.92em;
|
|
2797
|
+
line-height: 1.55;
|
|
2798
|
+
}
|
|
2799
|
+
.doc-meta dt { color: #57606a; }
|
|
2800
|
+
.doc-meta dd { margin: 0; }
|
|
2636
2801
|
.heading-with-anchor {
|
|
2637
2802
|
position: relative;
|
|
2638
2803
|
}
|
|
@@ -2653,14 +2818,15 @@ ${itemsHtml}
|
|
|
2653
2818
|
color: #0969da;
|
|
2654
2819
|
}
|
|
2655
2820
|
h1 { font-size: 2em; border-bottom: 2px solid #e1e4e8; padding-bottom: 10px; margin-top: 1.5em; }
|
|
2656
|
-
h2 { font-size: 1.5em; border-bottom: 1px solid #e1e4e8; padding-bottom: 6px; margin-top:
|
|
2657
|
-
h3 { font-size: 1.
|
|
2658
|
-
h4 { font-size: 1.
|
|
2821
|
+
h2 { font-size: 1.5em; border-bottom: 1px solid #e1e4e8; padding-bottom: 6px; margin-top: 2.2em; margin-bottom: 0.7em; }
|
|
2822
|
+
h3 { font-size: 1.3em; margin-top: 2em; margin-bottom: 0.5em; }
|
|
2823
|
+
h4 { font-size: 1.1em; margin-top: 1.8em; margin-bottom: 0.4em; }
|
|
2824
|
+
.sec { margin-right: 0.4em; font-variant-numeric: tabular-nums; }
|
|
2659
2825
|
code {
|
|
2660
2826
|
background: #f6f8fa;
|
|
2661
2827
|
padding: 2px 5px;
|
|
2662
2828
|
border-radius: 3px;
|
|
2663
|
-
font-family: "
|
|
2829
|
+
font-family: "Cascadia Mono", Consolas, "SFMono-Regular", "Liberation Mono", Menlo, "Microsoft JhengHei", monospace;
|
|
2664
2830
|
font-size: 0.875em;
|
|
2665
2831
|
}
|
|
2666
2832
|
pre {
|
|
@@ -2671,6 +2837,13 @@ ${itemsHtml}
|
|
|
2671
2837
|
line-height: 1.45;
|
|
2672
2838
|
}
|
|
2673
2839
|
pre code { background: none; padding: 0; font-size: 0.875em; }
|
|
2840
|
+
.content :not(pre) > code {
|
|
2841
|
+
background: none;
|
|
2842
|
+
padding: 0;
|
|
2843
|
+
border-radius: 0;
|
|
2844
|
+
color: #b93a0c;
|
|
2845
|
+
font-size: 0.92em;
|
|
2846
|
+
}
|
|
2674
2847
|
table {
|
|
2675
2848
|
display: block;
|
|
2676
2849
|
border-collapse: collapse;
|
|
@@ -2680,9 +2853,8 @@ ${itemsHtml}
|
|
|
2680
2853
|
font-size: 0.9em;
|
|
2681
2854
|
overflow-x: auto;
|
|
2682
2855
|
}
|
|
2683
|
-
th, td { border: 1px solid #
|
|
2684
|
-
th {
|
|
2685
|
-
tr:nth-child(even) { background: #fafbfc; }
|
|
2856
|
+
th, td { border: none; border-bottom: 1px solid #e1e4e8; padding: 7px 14px; text-align: left; vertical-align: top; }
|
|
2857
|
+
th { font-weight: 600; border-bottom: 2px solid #c9ced4; }
|
|
2686
2858
|
.content table th,
|
|
2687
2859
|
.content table td {
|
|
2688
2860
|
overflow-wrap: normal;
|
|
@@ -2695,7 +2867,7 @@ ${itemsHtml}
|
|
|
2695
2867
|
.content table th code {
|
|
2696
2868
|
white-space: nowrap;
|
|
2697
2869
|
}
|
|
2698
|
-
.content table { table-layout: auto; }
|
|
2870
|
+
.content table { table-layout: auto; font-variant-numeric: tabular-nums; }
|
|
2699
2871
|
.content table col.col-narrow { width: 1%; }
|
|
2700
2872
|
.content table col.col-prose { width: auto; }
|
|
2701
2873
|
.content table th.cell-narrow,
|
|
@@ -2708,10 +2880,7 @@ ${itemsHtml}
|
|
|
2708
2880
|
background: #ffffff;
|
|
2709
2881
|
}
|
|
2710
2882
|
.content table thead th:first-child {
|
|
2711
|
-
background: #
|
|
2712
|
-
}
|
|
2713
|
-
.content table tbody tr:nth-child(even) td:first-child {
|
|
2714
|
-
background: #fafbfc;
|
|
2883
|
+
background: #ffffff;
|
|
2715
2884
|
}
|
|
2716
2885
|
blockquote {
|
|
2717
2886
|
border-left: 4px solid #dfe2e5;
|
|
@@ -2789,7 +2958,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2789
2958
|
.content .graphviz,
|
|
2790
2959
|
.content .drawio,
|
|
2791
2960
|
.content [id^="WaveDrom_Display_"],
|
|
2792
|
-
.content .wavedrom-diagram { cursor:
|
|
2961
|
+
.content .wavedrom-diagram { cursor: pointer; }
|
|
2793
2962
|
.content a img { cursor: pointer; }
|
|
2794
2963
|
|
|
2795
2964
|
.lightbox {
|
|
@@ -2894,10 +3063,10 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2894
3063
|
position: relative;
|
|
2895
3064
|
flex: 1 1 auto;
|
|
2896
3065
|
overflow: auto;
|
|
2897
|
-
cursor:
|
|
3066
|
+
cursor: move;
|
|
2898
3067
|
overscroll-behavior: contain;
|
|
2899
3068
|
}
|
|
2900
|
-
.lightbox-stage[data-panning] { cursor:
|
|
3069
|
+
.lightbox-stage[data-panning] { cursor: move; }
|
|
2901
3070
|
.lightbox-canvas { margin: 0 auto; position: relative; }
|
|
2902
3071
|
/* Sized in px by the runtime; the child follows, so the scroll extent grows
|
|
2903
3072
|
with the zoom. A CSS transform would scale the pixels and leave the scroll
|
|
@@ -2925,21 +3094,59 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2925
3094
|
html[data-lightbox-open] { overflow: hidden; }
|
|
2926
3095
|
@media print { .lightbox { display: none !important; } }
|
|
2927
3096
|
@media (max-width: 1080px) {
|
|
3097
|
+
.content table td.cell-prose,
|
|
3098
|
+
.content table th.cell-prose { min-width: 15em; }
|
|
2928
3099
|
.sidebar-toggle { display: inline-flex; align-items: center; }
|
|
3100
|
+
.mobile-bar {
|
|
3101
|
+
display: flex;
|
|
3102
|
+
align-items: center;
|
|
3103
|
+
gap: 10px;
|
|
3104
|
+
position: fixed;
|
|
3105
|
+
top: 0;
|
|
3106
|
+
left: 0;
|
|
3107
|
+
right: 0;
|
|
3108
|
+
height: 44px;
|
|
3109
|
+
padding: 0 12px;
|
|
3110
|
+
box-sizing: border-box;
|
|
3111
|
+
background: #ffffff;
|
|
3112
|
+
border-bottom: 1px solid #d8dee4;
|
|
3113
|
+
z-index: 100;
|
|
3114
|
+
}
|
|
3115
|
+
.mobile-bar .sidebar-toggle {
|
|
3116
|
+
position: static;
|
|
3117
|
+
box-shadow: none;
|
|
3118
|
+
}
|
|
3119
|
+
.mobile-bar-title {
|
|
3120
|
+
min-width: 0;
|
|
3121
|
+
flex: 1 1 auto;
|
|
3122
|
+
font-size: 14px;
|
|
3123
|
+
font-weight: 600;
|
|
3124
|
+
color: #24292e;
|
|
3125
|
+
white-space: nowrap;
|
|
3126
|
+
overflow: hidden;
|
|
3127
|
+
text-overflow: ellipsis;
|
|
3128
|
+
}
|
|
3129
|
+
.content [id] { scroll-margin-top: 52px; }
|
|
3130
|
+
/* The 60px clears the fixed bar, so it only applies when the bar exists
|
|
3131
|
+
(a document with no headings has none). :where() keeps the selector at
|
|
3132
|
+
the specificity of a bare class, so the edit-mode override still wins. */
|
|
3133
|
+
.page-layout:where(.mobile-bar ~ *) {
|
|
3134
|
+
padding-top: 60px;
|
|
3135
|
+
}
|
|
2929
3136
|
.page-layout {
|
|
2930
3137
|
display: block;
|
|
2931
3138
|
max-width: 100%;
|
|
2932
|
-
padding-top: 60px;
|
|
2933
3139
|
}
|
|
2934
3140
|
.reader-sidebar {
|
|
2935
3141
|
position: fixed;
|
|
2936
|
-
top:
|
|
3142
|
+
top: 44px;
|
|
2937
3143
|
left: 0;
|
|
2938
3144
|
bottom: 0;
|
|
2939
3145
|
width: 85%;
|
|
2940
3146
|
max-width: 360px;
|
|
2941
|
-
height: 100vh;
|
|
2942
|
-
background: #
|
|
3147
|
+
height: calc(100vh - 44px);
|
|
3148
|
+
background: #f3f5f7;
|
|
3149
|
+
border-radius: 0;
|
|
2943
3150
|
z-index: 99;
|
|
2944
3151
|
transform: translateX(-100%);
|
|
2945
3152
|
transition: transform 0.2s ease;
|
|
@@ -2949,10 +3156,9 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2949
3156
|
display: flex;
|
|
2950
3157
|
flex-direction: column;
|
|
2951
3158
|
gap: 12px;
|
|
2952
|
-
box-shadow: 2px 0 12px rgba(0,0,0,0.15);
|
|
2953
3159
|
flex: initial;
|
|
2954
3160
|
}
|
|
2955
|
-
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); }
|
|
3161
|
+
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); box-shadow: 2px 0 12px rgba(0,0,0,0.15); }
|
|
2956
3162
|
/* Scroll lock for the drawer, same two-rule shape the lightbox uses a
|
|
2957
3163
|
few lines above (body[data-lightbox-open] / html[data-lightbox-open])
|
|
2958
3164
|
and for the identical reason: 'html, body { overflow-x: clip }' keeps
|
|
@@ -2969,7 +3175,6 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2969
3175
|
body[data-sidebar-open] { overflow: hidden; }
|
|
2970
3176
|
html[data-sidebar-open] { overflow: hidden; }
|
|
2971
3177
|
.sidebar-splitter { display: none; }
|
|
2972
|
-
.heading-anchor { opacity: 1; }
|
|
2973
3178
|
}
|
|
2974
3179
|
@media print {
|
|
2975
3180
|
body { font-size: 11pt; }
|
|
@@ -2994,10 +3199,19 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2994
3199
|
padding: 0 10px;
|
|
2995
3200
|
}
|
|
2996
3201
|
.reader-sidebar,
|
|
3202
|
+
.mobile-bar,
|
|
2997
3203
|
.sidebar-toggle,
|
|
2998
3204
|
.sidebar-splitter,
|
|
2999
3205
|
.sidebar-scrim { display: none !important; }
|
|
3000
3206
|
.content { max-width: 100%; }
|
|
3207
|
+
/* A4 lays out at about 680px, inside the max-width: 1080px block above,
|
|
3208
|
+
so its 15em prose floor applied to print; the table scrolls
|
|
3209
|
+
horizontally on screen and a PDF clips what overflows. Drop the floor.
|
|
3210
|
+
The sticky first column goes static so its bottom border prints. */
|
|
3211
|
+
.content table td.cell-prose,
|
|
3212
|
+
.content table th.cell-prose { min-width: 0; }
|
|
3213
|
+
.content table tbody td:first-child,
|
|
3214
|
+
.content table thead th:first-child { position: static; }
|
|
3001
3215
|
.heading-anchor { display: none; }
|
|
3002
3216
|
pre { font-size: 9pt; }
|
|
3003
3217
|
a[href]:after { content: none; }
|
|
@@ -3015,18 +3229,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3015
3229
|
.ed-te-hl).
|
|
3016
3230
|
|
|
3017
3231
|
The descendant rules and their !important are not decoration, they are
|
|
3018
|
-
the whole problem: <pre> (#f6f8fa)
|
|
3019
|
-
tr:nth-child(even) (#fafbfc) and the sticky first column (#ffffff) all
|
|
3232
|
+
the whole problem: <pre> (#f6f8fa) and the sticky first column (#ffffff)
|
|
3020
3233
|
carry OPAQUE backgrounds, and the sticky column additionally paints at
|
|
3021
|
-
z-index: 1 over anything drawn beneath it.
|
|
3022
|
-
|
|
3023
|
-
.content table tbody tr:nth-child(even) td:first-child
|
|
3024
|
-
against this rule's (0,3,2)
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3234
|
+
z-index: 1 over anything drawn beneath it. Until v3.8.0 the grey <th>
|
|
3235
|
+
(#f6f8fa) and the zebra stripe tr:nth-child(even) (#fafbfc, whose sticky
|
|
3236
|
+
override .content table tbody tr:nth-child(even) td:first-child was
|
|
3237
|
+
(0,3,4) against this rule's (0,3,2)) did too; v3.8.0 removed both, and
|
|
3238
|
+
the th / tr:nth-child(even) selectors below stay so a later restyle
|
|
3239
|
+
cannot quietly reopen the hole. Without !important the tint is invisible
|
|
3240
|
+
on exactly the blocks a user is most likely to select. .ed-te-hl needed
|
|
3241
|
+
the identical treatment for the identical reason. This ADDS rules rather
|
|
3242
|
+
than reordering the sticky-column rules, which win by source order.
|
|
3030
3243
|
(No backticks in this comment: it lives inside a JS template literal.)
|
|
3031
3244
|
|
|
3032
3245
|
:focus outline is suppressed because the roving tabindex below focuses a
|
|
@@ -3328,7 +3541,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3328
3541
|
having: '.ed-handle' (the button the gesture starts on), '.ed-handle-menu-btn'
|
|
3329
3542
|
and every table grip carry their own 'cursor: pointer', and a plain
|
|
3330
3543
|
inherited cursor loses to all of them. */
|
|
3331
|
-
html.ed-block-dragging, html.ed-block-dragging * { cursor:
|
|
3544
|
+
html.ed-block-dragging, html.ed-block-dragging * { cursor: move !important; }
|
|
3332
3545
|
/* Notion-style grip handles: replace the original invisible TE_EDGE_PX=8
|
|
3333
3546
|
proximity zone (user-acceptance feedback: unusably small, no visible
|
|
3334
3547
|
affordance) with two real, adequately-sized (>=18x24px) click/drag
|
|
@@ -3355,7 +3568,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3355
3568
|
place-content: center, so the dot cluster stays compact/centered
|
|
3356
3569
|
regardless of the button's own (larger, hit-target-sized) box — no
|
|
3357
3570
|
images, no background gradients. '.ed-te-grip-dragging' is EITHER
|
|
3358
|
-
grip's own "active drag handle" visual (
|
|
3571
|
+
grip's own "active drag handle" visual (move cursor) while a row OR
|
|
3359
3572
|
column drag is in flight (Task 8: columns became draggable too, same
|
|
3360
3573
|
as rows) — see cancelTeDrag()/the pointermove listener in the client
|
|
3361
3574
|
runtime. */
|
|
@@ -3377,17 +3590,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3377
3590
|
the position was a hard-coded literal 20. */
|
|
3378
3591
|
:root { --ed-te-grip-row-w: 20px; }
|
|
3379
3592
|
.ed-te-grip-row {
|
|
3380
|
-
width: var(--ed-te-grip-row-w); height: 28px; cursor:
|
|
3593
|
+
width: var(--ed-te-grip-row-w); height: 28px; cursor: move; z-index: 7;
|
|
3381
3594
|
grid-template-columns: repeat(2, 3px); grid-template-rows: repeat(3, 3px);
|
|
3382
3595
|
gap: 3px 4px;
|
|
3383
3596
|
}
|
|
3384
|
-
.ed-te-grip-row.ed-te-grip-dragging { cursor:
|
|
3597
|
+
.ed-te-grip-row.ed-te-grip-dragging { cursor: move; }
|
|
3385
3598
|
.ed-te-grip-col {
|
|
3386
|
-
width: 28px; height: 24px; cursor:
|
|
3599
|
+
width: 28px; height: 24px; cursor: move; z-index: 7;
|
|
3387
3600
|
grid-template-columns: repeat(3, 3px); grid-template-rows: repeat(2, 3px);
|
|
3388
3601
|
gap: 4px 3px;
|
|
3389
3602
|
}
|
|
3390
|
-
.ed-te-grip-col.ed-te-grip-dragging { cursor:
|
|
3603
|
+
.ed-te-grip-col.ed-te-grip-dragging { cursor: move; }
|
|
3391
3604
|
/* Task 4: floating selection toolbar (bold/italic/strikethrough/underline/
|
|
3392
3605
|
code/link), shown over a
|
|
3393
3606
|
non-collapsed selection inside an active WYSIWYG session. position:
|
|
@@ -3723,8 +3936,7 @@ ${opts.editMode ? editModeLayoutCss : ''}
|
|
|
3723
3936
|
${usesMath ? buildKatexStyleTag() : ''}
|
|
3724
3937
|
</head>
|
|
3725
3938
|
<body>
|
|
3726
|
-
<
|
|
3727
|
-
<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3939
|
+
${mobileBarHtml}<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3728
3940
|
<div class="page-layout">
|
|
3729
3941
|
${tocHtml}
|
|
3730
3942
|
<main class="content">
|
|
@@ -3790,9 +4002,10 @@ ${mermaidInitTag}`)
|
|
|
3790
4002
|
// wholesale after a commit (see lib/editor/client.js), which detaches every
|
|
3791
4003
|
// node this was captured from. window.__md2docRebindReader() below
|
|
3792
4004
|
// re-queries and reassigns this same binding so every closure in this IIFE
|
|
3793
|
-
// that reads headingNodes (
|
|
3794
|
-
//
|
|
3795
|
-
//
|
|
4005
|
+
// that reads headingNodes (readingLineHeading, …) sees the live nodes without
|
|
4006
|
+
// needing its own re-init call — they all close over this one variable, not a
|
|
4007
|
+
// copy of it. (The zoom/resize anchoring, captureScrollAnchor, does not read
|
|
4008
|
+
// it: it walks contentEl.children.)
|
|
3796
4009
|
let headingNodes = Array.from(document.querySelectorAll('[data-reader-heading]'));
|
|
3797
4010
|
const tocLinks = new Map(
|
|
3798
4011
|
Array.from(document.querySelectorAll('.toc a[href^="#"]')).map((link) => [link.getAttribute('href').slice(1), link])
|
|
@@ -3812,12 +4025,18 @@ ${mermaidInitTag}`)
|
|
|
3812
4025
|
}
|
|
3813
4026
|
|
|
3814
4027
|
const breadcrumbEl = document.querySelector('[data-toc-breadcrumb]');
|
|
4028
|
+
const mobileBarTitle = document.getElementById('mobile-bar-title');
|
|
4029
|
+
if (mobileBarTitle && sections.length) mobileBarTitle.textContent = sections[0].title;
|
|
3815
4030
|
const sectionIndexById = new Map(sections.map((section, i) => [section.id, i]));
|
|
3816
4031
|
|
|
3817
4032
|
// Sticky breadcrumb: the ancestor chain (shallow→deep) of the active section.
|
|
3818
4033
|
// For each decreasing depth, take the nearest preceding section, then append
|
|
3819
|
-
// the active section itself.
|
|
4034
|
+
// the active section itself. The rows are still written into the breadcrumb
|
|
4035
|
+
// element, but CSS hides it (the TOC highlights the path); the useful output
|
|
4036
|
+
// today is the last link's title, which names the mobile bar.
|
|
4037
|
+
let breadcrumbId = null;
|
|
3820
4038
|
function renderBreadcrumb(sectionId) {
|
|
4039
|
+
breadcrumbId = sectionId;
|
|
3821
4040
|
if (!breadcrumbEl) return;
|
|
3822
4041
|
if (!sectionIndexById.has(sectionId)) {
|
|
3823
4042
|
breadcrumbEl.innerHTML = '';
|
|
@@ -3837,6 +4056,7 @@ ${mermaidInitTag}`)
|
|
|
3837
4056
|
return;
|
|
3838
4057
|
}
|
|
3839
4058
|
const minDepth = chain[0].depth;
|
|
4059
|
+
if (mobileBarTitle) mobileBarTitle.textContent = chain[chain.length - 1].title;
|
|
3840
4060
|
breadcrumbEl.innerHTML = chain
|
|
3841
4061
|
.map((section, i) => {
|
|
3842
4062
|
const indent = (section.depth - minDepth) * 10;
|
|
@@ -3862,11 +4082,24 @@ ${mermaidInitTag}`)
|
|
|
3862
4082
|
if (!scroller) return;
|
|
3863
4083
|
const scrollerRect = scroller.getBoundingClientRect();
|
|
3864
4084
|
const linkRect = link.getBoundingClientRect();
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
4085
|
+
// Keep the active row at about 40% of the TOC box instead of nudging it
|
|
4086
|
+
// back to an edge: the highlight stays mid-box, never at 0% or 95%.
|
|
4087
|
+
const target = scroller.scrollTop + (linkRect.top - scrollerRect.top) - scroller.clientHeight * 0.4;
|
|
4088
|
+
if (Math.abs(target - scroller.scrollTop) <= 2) return;
|
|
4089
|
+
const out = linkRect.bottom < scrollerRect.top || linkRect.top > scrollerRect.bottom;
|
|
4090
|
+
const calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
4091
|
+
scroller.scrollTo({ top: target, behavior: out || calm ? 'auto' : 'smooth' });
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
// Accordion: only the active row's ancestor path stays open.
|
|
4095
|
+
function collapseOtherTocDetails(link) {
|
|
4096
|
+
const keep = new Set();
|
|
4097
|
+
for (let node = link && link.closest('details'); node; node = node.parentElement && node.parentElement.closest('details')) {
|
|
4098
|
+
keep.add(node);
|
|
3869
4099
|
}
|
|
4100
|
+
Array.from(document.querySelectorAll('.toc-list details[open]')).forEach((node) => {
|
|
4101
|
+
if (!keep.has(node)) node.open = false;
|
|
4102
|
+
});
|
|
3870
4103
|
}
|
|
3871
4104
|
|
|
3872
4105
|
let observerFrozen = false;
|
|
@@ -3887,52 +4120,92 @@ ${mermaidInitTag}`)
|
|
|
3887
4120
|
next.classList.add('is-active');
|
|
3888
4121
|
if (!freezeSidebar) {
|
|
3889
4122
|
expandTocAncestors(next);
|
|
4123
|
+
collapseOtherTocDetails(next);
|
|
3890
4124
|
ensureTocLinkVisible(next);
|
|
3891
4125
|
}
|
|
3892
4126
|
}
|
|
3893
4127
|
}
|
|
3894
4128
|
|
|
3895
|
-
//
|
|
3896
|
-
//
|
|
3897
|
-
//
|
|
3898
|
-
//
|
|
3899
|
-
//
|
|
3900
|
-
//
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
4129
|
+
// The reading line: the last heading that has a TOC row and sits at or
|
|
4130
|
+
// above 35% of the viewport height (the first such heading when none has
|
|
4131
|
+
// reached the line yet). Headings without a TOC row (a lone H1 is flattened
|
|
4132
|
+
// away) are skipped, so the highlight never lands on a row that is not there.
|
|
4133
|
+
// A TOC click lands the clicked heading at the top of the page; when its
|
|
4134
|
+
// section is shorter than the gap down to the line, the next heading is
|
|
4135
|
+
// already above the line and would steal the highlight from the row that was
|
|
4136
|
+
// just clicked. The clicked heading stays the answer for as long as it is
|
|
4137
|
+
// still between the page top and the line, and the first scroll that moves
|
|
4138
|
+
// it out of that band hands tracking back to the line.
|
|
4139
|
+
let pinnedHeadingId = null;
|
|
4140
|
+
function readingLineHeading() {
|
|
4141
|
+
const line = window.innerHeight * 0.35;
|
|
4142
|
+
if (pinnedHeadingId) {
|
|
4143
|
+
const pinned = document.getElementById(pinnedHeadingId);
|
|
4144
|
+
const top = pinned ? pinned.getBoundingClientRect().top : Infinity;
|
|
4145
|
+
if (pinned && tocLinks.has(pinned.id) && top >= -40 && top <= line) return pinned;
|
|
4146
|
+
pinnedHeadingId = null;
|
|
3909
4147
|
}
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4148
|
+
let first = null;
|
|
4149
|
+
let candidate = null;
|
|
4150
|
+
for (const node of headingNodes) {
|
|
4151
|
+
if (!tocLinks.has(node.id)) continue;
|
|
4152
|
+
if (!first) first = node;
|
|
4153
|
+
if (node.getBoundingClientRect().top <= line) {
|
|
4154
|
+
candidate = node;
|
|
4155
|
+
} else {
|
|
4156
|
+
break;
|
|
3919
4157
|
}
|
|
3920
|
-
}
|
|
3921
|
-
|
|
4158
|
+
}
|
|
4159
|
+
return candidate || first;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
// One scroll listener, installed once, throttled to one pass per animation
|
|
4163
|
+
// frame. bindHeadingObserver keeps its name because
|
|
4164
|
+
// window.__md2docRebindReader calls it after an edit-mode re-render; it only
|
|
4165
|
+
// runs a pass now, since headingNodes is read live on every pass.
|
|
4166
|
+
// A lone H1 has no TOC row, so the highlight skips it; the bar still names
|
|
4167
|
+
// the document while that H1 is on screen, as it always has, and names the
|
|
4168
|
+
// active section once the H1 has scrolled off.
|
|
4169
|
+
function syncReadingTitle(node) {
|
|
4170
|
+
const first = sections[0] ? document.getElementById(sections[0].id) : null;
|
|
4171
|
+
const onScreen = first && first !== node && !tocLinks.has(first.id) && first.getBoundingClientRect().top >= 0;
|
|
4172
|
+
const titleId = onScreen ? first.id : node.id;
|
|
4173
|
+
if (titleId !== breadcrumbId) renderBreadcrumb(titleId);
|
|
4174
|
+
}
|
|
4175
|
+
let readingLinePending = false;
|
|
4176
|
+
function readingLinePass() {
|
|
4177
|
+
readingLinePending = false;
|
|
4178
|
+
if (observerFrozen) return;
|
|
4179
|
+
const node = readingLineHeading();
|
|
4180
|
+
if (!node) return;
|
|
4181
|
+
syncActiveHeading(node.id);
|
|
4182
|
+
syncReadingTitle(node);
|
|
4183
|
+
}
|
|
4184
|
+
window.addEventListener('scroll', () => {
|
|
4185
|
+
if (readingLinePending) return;
|
|
4186
|
+
readingLinePending = true;
|
|
4187
|
+
requestAnimationFrame(readingLinePass);
|
|
4188
|
+
}, { passive: true });
|
|
4189
|
+
document.addEventListener('click', (event) => {
|
|
4190
|
+
const link = event.target && event.target.closest && event.target.closest('.toc a[href^="#"]');
|
|
4191
|
+
if (link) pinnedHeadingId = link.getAttribute('href').slice(1);
|
|
4192
|
+
});
|
|
4193
|
+
function bindHeadingObserver() {
|
|
4194
|
+
readingLinePass();
|
|
3922
4195
|
}
|
|
3923
|
-
bindHeadingObserver();
|
|
3924
4196
|
|
|
3925
4197
|
// Paint the breadcrumb immediately so the header is populated before the
|
|
3926
|
-
// first
|
|
4198
|
+
// first reading-line pass runs, then run that pass once on load.
|
|
3927
4199
|
if (sections[0]) {
|
|
3928
4200
|
renderBreadcrumb(sections[0].id);
|
|
3929
4201
|
}
|
|
4202
|
+
bindHeadingObserver();
|
|
3930
4203
|
|
|
3931
4204
|
// Edit mode only: re-init hook, sibling to window.__md2docInitDiagrams
|
|
3932
4205
|
// (defined in diagramInitHookScript above for edit-mode pages), called
|
|
3933
4206
|
// from lib/editor/client.js's rerenderAll() right after it swaps
|
|
3934
4207
|
// .content's innerHTML on a commit. Re-queries the heading nodes that
|
|
3935
|
-
// just got replaced and
|
|
4208
|
+
// just got replaced and runs one reading-line pass over them, so
|
|
3936
4209
|
// TOC highlighting / breadcrumb tracking / the zoom-resize scroll anchor
|
|
3937
4210
|
// (all of which read the headingNodes binding above) keep working
|
|
3938
4211
|
// against live nodes instead of silently going dead after the first edit.
|
|
@@ -4164,22 +4437,8 @@ ${mermaidInitTag}`)
|
|
|
4164
4437
|
}
|
|
4165
4438
|
}
|
|
4166
4439
|
|
|
4167
|
-
function detectActiveHeading() {
|
|
4168
|
-
const threshold = 120;
|
|
4169
|
-
let candidate = null;
|
|
4170
|
-
for (const node of headingNodes) {
|
|
4171
|
-
const rect = node.getBoundingClientRect();
|
|
4172
|
-
if (rect.top <= threshold) {
|
|
4173
|
-
candidate = node;
|
|
4174
|
-
} else {
|
|
4175
|
-
break;
|
|
4176
|
-
}
|
|
4177
|
-
}
|
|
4178
|
-
return candidate || headingNodes[0] || null;
|
|
4179
|
-
}
|
|
4180
|
-
|
|
4181
4440
|
function resyncTocToScroll() {
|
|
4182
|
-
const node =
|
|
4441
|
+
const node = readingLineHeading();
|
|
4183
4442
|
if (!node) return;
|
|
4184
4443
|
if (readerState.activeSectionId === node.id) {
|
|
4185
4444
|
const link = tocLinks.get(node.id);
|
|
@@ -4187,9 +4446,11 @@ ${mermaidInitTag}`)
|
|
|
4187
4446
|
expandTocAncestors(link);
|
|
4188
4447
|
ensureTocLinkVisible(link);
|
|
4189
4448
|
}
|
|
4449
|
+
syncReadingTitle(node);
|
|
4190
4450
|
return;
|
|
4191
4451
|
}
|
|
4192
4452
|
syncActiveHeading(node.id);
|
|
4453
|
+
syncReadingTitle(node);
|
|
4193
4454
|
}
|
|
4194
4455
|
|
|
4195
4456
|
function ensureActiveResultVisible() {
|
|
@@ -4416,18 +4677,30 @@ ${mermaidInitTag}`)
|
|
|
4416
4677
|
var hit = document.elementFromPoint(rect.left + rect.width / 2, ANCHOR_LINE);
|
|
4417
4678
|
var node = (hit && hit.closest) ? hit.closest('main.content > *') : null;
|
|
4418
4679
|
if (!node) {
|
|
4419
|
-
//
|
|
4420
|
-
//
|
|
4421
|
-
//
|
|
4680
|
+
// Over a gap (a heading's top margin, the space between blocks): pin the
|
|
4681
|
+
// first block that starts below the anchor line, which is what the reader
|
|
4682
|
+
// sees next to it. Falling back to the last heading ABOVE the line would
|
|
4683
|
+
// pin something up to a whole section away. Block tops are monotonic in
|
|
4684
|
+
// document order, so this is a binary search over the column's children.
|
|
4685
|
+
var kids = contentEl.children;
|
|
4422
4686
|
var lo = 0;
|
|
4423
|
-
var hi =
|
|
4687
|
+
var hi = kids.length - 1;
|
|
4424
4688
|
while (lo <= hi) {
|
|
4425
4689
|
var mid = (lo + hi) >> 1;
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4690
|
+
// A child with no layout boxes (every WaveDrom fence leaves a script
|
|
4691
|
+
// element) reports an all-zero rect, which would break the
|
|
4692
|
+
// monotonic order the search depends on. Compare the nearest
|
|
4693
|
+
// following child that is rendered; none left before hi means the
|
|
4694
|
+
// answer is in the lower half.
|
|
4695
|
+
var probe = mid;
|
|
4696
|
+
while (probe <= hi && !kids[probe].getClientRects().length) probe++;
|
|
4697
|
+
if (probe > hi) {
|
|
4430
4698
|
hi = mid - 1;
|
|
4699
|
+
} else if (kids[probe].getBoundingClientRect().top > ANCHOR_LINE) {
|
|
4700
|
+
node = kids[probe];
|
|
4701
|
+
hi = mid - 1;
|
|
4702
|
+
} else {
|
|
4703
|
+
lo = probe + 1;
|
|
4431
4704
|
}
|
|
4432
4705
|
}
|
|
4433
4706
|
}
|
|
@@ -5645,6 +5918,9 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
5645
5918
|
} finally {
|
|
5646
5919
|
await drawioBrowser.closeIfOpen();
|
|
5647
5920
|
}
|
|
5921
|
+
// v3.9.0 dark mode: a post-pass over the finished reader HTML (lib/theme/).
|
|
5922
|
+
// Edit mode is light-only; noTheme is an internal switch for the light-lock test.
|
|
5923
|
+
if (!opts.editMode && !opts.noTheme) bakedHtml = applyReaderTheme(bakedHtml);
|
|
5648
5924
|
return {
|
|
5649
5925
|
html: bakedHtml, bodyHtml: bakedBody, parts, blocks,
|
|
5650
5926
|
// v3.4.0 batch2 Task 6: an added field, not a shape change to the four
|
|
@@ -5656,6 +5932,42 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
5656
5932
|
}
|
|
5657
5933
|
module.exports = { renderMarkdown };
|
|
5658
5934
|
|
|
5935
|
+
// Index of the first "{" in dot source that opens the graph body: not inside a
|
|
5936
|
+
// double-quoted string, a // or /* */ comment, a line starting with # (dot
|
|
5937
|
+
// treats it as preprocessor output), or an HTML-like <...> id. -1 if none.
|
|
5938
|
+
function firstDotBrace(src) {
|
|
5939
|
+
let i = 0;
|
|
5940
|
+
const n = src.length;
|
|
5941
|
+
while (i < n) {
|
|
5942
|
+
const c = src[i];
|
|
5943
|
+
if (c === '"') {
|
|
5944
|
+
i++;
|
|
5945
|
+
while (i < n && src[i] !== '"') i += src[i] === '\\' ? 2 : 1;
|
|
5946
|
+
i++;
|
|
5947
|
+
} else if (c === '/' && src[i + 1] === '/') {
|
|
5948
|
+
while (i < n && src[i] !== '\n') i++;
|
|
5949
|
+
} else if (c === '/' && src[i + 1] === '*') {
|
|
5950
|
+
const end = src.indexOf('*/', i + 2);
|
|
5951
|
+
i = end < 0 ? n : end + 2;
|
|
5952
|
+
} else if (c === '#' && (i === 0 || src[i - 1] === '\n')) {
|
|
5953
|
+
while (i < n && src[i] !== '\n') i++;
|
|
5954
|
+
} else if (c === '<') {
|
|
5955
|
+
let depth = 0;
|
|
5956
|
+
while (i < n) {
|
|
5957
|
+
if (src[i] === '<') depth++;
|
|
5958
|
+
else if (src[i] === '>' && --depth === 0) break;
|
|
5959
|
+
i++;
|
|
5960
|
+
}
|
|
5961
|
+
i++;
|
|
5962
|
+
} else if (c === '{') {
|
|
5963
|
+
return i;
|
|
5964
|
+
} else {
|
|
5965
|
+
i++;
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
return -1;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5659
5971
|
// Render every deferred dot/graphviz placeholder to inline SVG using the
|
|
5660
5972
|
// in-process WASM engine. Loads the WASM module only when at least one dot
|
|
5661
5973
|
// block exists, so text-only docs pay nothing.
|
|
@@ -5672,11 +5984,35 @@ async function bakeGraphviz(htmlStr) {
|
|
|
5672
5984
|
return htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, (m, b64) => {
|
|
5673
5985
|
const dotSrc = Buffer.from(b64, 'base64').toString('utf8');
|
|
5674
5986
|
try {
|
|
5675
|
-
|
|
5987
|
+
// Graphviz measures label widths with the font it lays out in, and with
|
|
5988
|
+
// no fontname that is Times, whose metrics are narrower than the sans
|
|
5989
|
+
// the SVG is drawn in -- so labels overflowed their boxes. Declare
|
|
5990
|
+
// Helvetica BEFORE layout, right after the first brace; a fontname the
|
|
5991
|
+
// author writes later in the source still wins. Exactly "Helvetica":
|
|
5992
|
+
// a "Helvetica,sans-Serif" family list falls back to Times widths.
|
|
5993
|
+
const at = firstDotBrace(dotSrc);
|
|
5994
|
+
const laidOut = at < 0 ? dotSrc : dotSrc.slice(0, at + 1) +
|
|
5995
|
+
' graph [fontname="Helvetica"]; node [fontname="Helvetica"]; edge [fontname="Helvetica"]; ' + dotSrc.slice(at + 1);
|
|
5996
|
+
let laid;
|
|
5997
|
+
try {
|
|
5998
|
+
laid = gv.dot(laidOut);
|
|
5999
|
+
} catch (injectErr) {
|
|
6000
|
+
// Injection must never turn a valid graph into an error: lay the
|
|
6001
|
+
// original source out as the author wrote it.
|
|
6002
|
+
if (laidOut === dotSrc) throw injectErr;
|
|
6003
|
+
laid = gv.dot(dotSrc);
|
|
6004
|
+
}
|
|
6005
|
+
const svg = laid
|
|
5676
6006
|
.replace(/<\?xml[^>]*\?>/g, '')
|
|
5677
6007
|
.replace(/<!DOCTYPE[\s\S]*?>/g, '')
|
|
5678
6008
|
// drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
|
|
5679
6009
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
6010
|
+
// v3.8.0: graphviz falls back to Times when no fontname is set. The
|
|
6011
|
+
// injection above makes Helvetica the layout font, so this rewrite
|
|
6012
|
+
// now only touches an author's explicit Times-Roman, or a figure
|
|
6013
|
+
// whose injection fell back to the original source; other explicit
|
|
6014
|
+
// fontnames are untouched.
|
|
6015
|
+
.replace(/font-family="Times,serif"/g, 'font-family="Helvetica,sans-Serif"')
|
|
5680
6016
|
.trim();
|
|
5681
6017
|
return `<div class="graphviz">${svg}</div>`;
|
|
5682
6018
|
} catch (e) {
|
|
@@ -5838,6 +6174,7 @@ async function bakeDiagrams(htmlStr, dstPath) {
|
|
|
5838
6174
|
if (require.main === module) {
|
|
5839
6175
|
const [,, src, dst] = process.argv;
|
|
5840
6176
|
const BAKE_SVG = process.argv.slice(4).includes('--bake-svg');
|
|
6177
|
+
const LANG_ARG = (process.argv.slice(4).find((a) => a.startsWith('--lang=')) || '').slice('--lang='.length) || undefined;
|
|
5841
6178
|
if (!src || !dst) {
|
|
5842
6179
|
console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
|
|
5843
6180
|
process.exit(1);
|
|
@@ -5847,7 +6184,7 @@ if (require.main === module) {
|
|
|
5847
6184
|
|
|
5848
6185
|
(async () => {
|
|
5849
6186
|
const mdText = fs.readFileSync(src, 'utf8');
|
|
5850
|
-
const { html } = await renderMarkdown(mdText, path.resolve(src), {});
|
|
6187
|
+
const { html } = await renderMarkdown(mdText, path.resolve(src), { lang: LANG_ARG });
|
|
5851
6188
|
let finalHtml = html;
|
|
5852
6189
|
|
|
5853
6190
|
if (ext === '.html') {
|