@helping-ai-workflow/md2doc 3.6.1 → 3.8.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 +1 -0
- package/THIRD-PARTY-NOTICES.md +21 -0
- package/bin/md2doc.js +19 -3
- package/lib/editor/cell-range.js +85 -0
- package/lib/editor/client.js +809 -14
- package/lib/editor/server.js +5 -0
- package/lib/md2doc.js +433 -131
- package/package.json +6 -2
package/lib/md2doc.js
CHANGED
|
@@ -140,9 +140,14 @@ const waveDromSkinTag = inlineScriptTag(localWaveDromSkin, 'wavedrom');
|
|
|
140
140
|
const waveDromTag = inlineScriptTag(localWaveDromJs, 'wavedrom');
|
|
141
141
|
const mermaidScriptTag = inlineScriptTag(localMermaidJs, 'mermaid');
|
|
142
142
|
|
|
143
|
+
// v3.8.0 (design review item 5): mermaid in md2doc's link blue on a light
|
|
144
|
+
// tint, the body font, white edge labels. Shared by the reader init below and
|
|
145
|
+
// the edit-mode init hook so both modes draw the same diagram.
|
|
146
|
+
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' }";
|
|
147
|
+
|
|
143
148
|
const mermaidInitTag = `<script type="text/javascript" data-md2doc-diagram-engine="mermaid">
|
|
144
149
|
if (typeof mermaid !== 'undefined') {
|
|
145
|
-
mermaid.initialize({ startOnLoad: true,
|
|
150
|
+
mermaid.initialize({ startOnLoad: true, ${MERMAID_CONFIG_JS} });
|
|
146
151
|
}
|
|
147
152
|
</script>`;
|
|
148
153
|
|
|
@@ -370,9 +375,76 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
370
375
|
return parts;
|
|
371
376
|
}
|
|
372
377
|
|
|
378
|
+
// <html lang> for a rendered document (v3.8.0, design review item 2).
|
|
379
|
+
// Tagged zh-Hant when Han characters are at least 10% of (CJK characters +
|
|
380
|
+
// Latin words), counted outside code; when kana is at least 20% of
|
|
381
|
+
// (Han + kana) it is ja, and when Hangul is at least 20% of (Han + Hangul)
|
|
382
|
+
// it is ko (both still need CJK to be 10% of the prose). Measured 2026-10-01: English docs
|
|
383
|
+
// (README, CLAUDE.md) <= 0.013, mixed zh/en specs in mac_top/docs >= 0.237,
|
|
384
|
+
// so 0.10 leaves a wide margin on both sides. Traditional vs Simplified is
|
|
385
|
+
// not detected: every Chinese document this tool renders today is zh-Hant.
|
|
386
|
+
const LANG_TAG_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
387
|
+
function detectDocLang(mdText) {
|
|
388
|
+
const prose = String(mdText)
|
|
389
|
+
// Fences are anchored to line starts (up to 3 spaces of indent) and the
|
|
390
|
+
// closing fence must be at least as long as the opening one, so an inline
|
|
391
|
+
// mention of the marker, or a shorter marker line inside a longer fence,
|
|
392
|
+
// cannot swallow the prose around it. Backticks are written as x60 escapes.
|
|
393
|
+
.replace(/^ {0,3}(\x60{3,})[^\n\x60]*\n[\s\S]*?^ {0,3}\1\x60*[ \t\r]*$/gm, '')
|
|
394
|
+
.replace(/^ {0,3}(~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1~*[ \t\r]*$/gm, '')
|
|
395
|
+
.replace(/\x60[^\x60\n]*\x60/g, '');
|
|
396
|
+
const han = (prose.match(/[\u3400-\u9fff]/g) || []).length;
|
|
397
|
+
const kana = (prose.match(/[\u3040-\u30ff]/g) || []).length;
|
|
398
|
+
const hangul = (prose.match(/[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/g) || []).length;
|
|
399
|
+
const words = (prose.match(/[A-Za-z]+/g) || []).length;
|
|
400
|
+
// Kana or Hangul only count as Japanese or Korean when CJK text is the bulk
|
|
401
|
+
// of the document at all (same 10% floor as zh-Hant); one stray の in an
|
|
402
|
+
// English or Chinese document must not flip its language.
|
|
403
|
+
const cjk = han + kana + hangul;
|
|
404
|
+
if (cjk + words === 0) return 'en';
|
|
405
|
+
if (cjk / (cjk + words) >= 0.10) {
|
|
406
|
+
if (kana / (han + kana) >= 0.2) return 'ja';
|
|
407
|
+
if (hangul / (han + hangul) >= 0.2) return 'ko';
|
|
408
|
+
}
|
|
409
|
+
return (han + kana) / (han + kana + words) >= 0.10 ? 'zh-Hant' : 'en';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// v3.8.0 (design review item 12): the paragraph immediately after the
|
|
413
|
+
// document's FIRST </h1>, when EVERY line of it is "<strong>Key:</strong>
|
|
414
|
+
// value", is the spec's metadata block — rendered as a two-column <dl>.
|
|
415
|
+
// Allow-list rule: one non-matching line and the paragraph is left alone.
|
|
416
|
+
function renderDocMetaBlock(bodyHtml) {
|
|
417
|
+
const at = bodyHtml.indexOf('</h1>');
|
|
418
|
+
if (at < 0) return bodyHtml;
|
|
419
|
+
const head = bodyHtml.slice(0, at + 5);
|
|
420
|
+
const tail = bodyHtml.slice(at + 5);
|
|
421
|
+
const m = tail.match(/^(\s*)<p>([\s\S]*?)<\/p>/);
|
|
422
|
+
if (!m) return bodyHtml;
|
|
423
|
+
const lines = m[2].split('\n').filter((l) => l.trim());
|
|
424
|
+
const rows = lines.map((l) => l.match(/^\s*<strong>([^<]+):<\/strong>\s*([\s\S]*)$/));
|
|
425
|
+
if (rows.length < 2 || rows.some((r) => !r)) return bodyHtml;
|
|
426
|
+
const items = rows.map((r) => '<dt>' + r[1] + '</dt><dd>' + r[2] + '</dd>').join('');
|
|
427
|
+
return head + m[1] + '<dl class="doc-meta">' + items + '</dl>' + tail.slice(m[0].length);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// v3.8.0 sidebar icons. Path data from Lucide (ISC licence, see
|
|
431
|
+
// THIRD-PARTY-NOTICES.md), inlined so the single-file output works offline.
|
|
432
|
+
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">';
|
|
433
|
+
const READER_ICONS = {
|
|
434
|
+
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>',
|
|
435
|
+
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>',
|
|
436
|
+
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>',
|
|
437
|
+
up: ICON_SVG_OPEN + '<path d="m18 15-6-6-6 6"/></svg>',
|
|
438
|
+
down: ICON_SVG_OPEN + '<path d="m6 9 6 6 6-6"/></svg>',
|
|
439
|
+
x: ICON_SVG_OPEN + '<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
440
|
+
};
|
|
441
|
+
|
|
373
442
|
async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
374
443
|
const md = mdText;
|
|
375
444
|
const src = srcPath;
|
|
445
|
+
const docLang = (typeof opts.lang === 'string' && LANG_TAG_RE.test(opts.lang))
|
|
446
|
+
? opts.lang
|
|
447
|
+
: detectDocLang(md);
|
|
376
448
|
let usesMermaid = false;
|
|
377
449
|
let usesWaveDrom = false;
|
|
378
450
|
let usesDrawio = false;
|
|
@@ -699,6 +771,16 @@ async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
|
699
771
|
return root;
|
|
700
772
|
}
|
|
701
773
|
|
|
774
|
+
// v3.8.0 (design review item 7+8): when the document's only H1 is its
|
|
775
|
+
// title and it has sections, list those sections at the TOC's top level —
|
|
776
|
+
// the title row indented every entry one level and carried no information.
|
|
777
|
+
// Several H1s are chapters and are kept as they are.
|
|
778
|
+
function flattenSingleRoot(tree) {
|
|
779
|
+
return tree.length === 1 && tree[0].depth === 1 && tree[0].children.length
|
|
780
|
+
? tree[0].children
|
|
781
|
+
: tree;
|
|
782
|
+
}
|
|
783
|
+
|
|
702
784
|
function renderTocNodes(nodes, level = 1) {
|
|
703
785
|
if (!nodes.length) {
|
|
704
786
|
return '';
|
|
@@ -959,7 +1041,14 @@ ${itemsHtml}
|
|
|
959
1041
|
const depth = Number(token.depth || 1);
|
|
960
1042
|
const headingText = stripHtmlTags(flattenTokenText(token.tokens) || token.text || '');
|
|
961
1043
|
const headingId = slugifyHeading(headingText);
|
|
962
|
-
const
|
|
1044
|
+
const inlineHtml = this.parser.parseInline(token.tokens);
|
|
1045
|
+
// v3.8.0 (design review item 4): a leading section number ("4.1 ",
|
|
1046
|
+
// "12. ") gets its own span so CSS can widen the gap after it. The
|
|
1047
|
+
// space stays in the markup, so textContent is unchanged. Read mode
|
|
1048
|
+
// only — the editor turns heading DOM back into Markdown.
|
|
1049
|
+
const headingHtml = opts.editMode
|
|
1050
|
+
? inlineHtml
|
|
1051
|
+
: inlineHtml.replace(/^(\d+(?:\.\d+)*\.?)(\s)/, '<span class="sec">$1</span>$2');
|
|
963
1052
|
|
|
964
1053
|
tocItems.push({
|
|
965
1054
|
depth,
|
|
@@ -1126,6 +1215,7 @@ ${itemsHtml}
|
|
|
1126
1215
|
}
|
|
1127
1216
|
// Both the ```math fence and the $/$$ extension emit class="katex" — a single
|
|
1128
1217
|
// post-parse scan is the source of truth for conditional CSS injection.
|
|
1218
|
+
if (!opts.editMode) bodyHtml = renderDocMetaBlock(bodyHtml);
|
|
1129
1219
|
usesMath = /class="katex/.test(bodyHtml);
|
|
1130
1220
|
serializedSections = JSON.stringify(
|
|
1131
1221
|
sections.map((section) => ({
|
|
@@ -1141,30 +1231,31 @@ ${itemsHtml}
|
|
|
1141
1231
|
<section class="reader-tools">
|
|
1142
1232
|
<label class="reader-search-label" for="doc-search-input">Search</label>
|
|
1143
1233
|
<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"
|
|
1234
|
+
<input type="search" id="doc-search-input" placeholder="Search this document">
|
|
1235
|
+
<button id="doc-search-submit" type="button" tabindex="-1">Search</button>
|
|
1236
|
+
<button id="doc-search-clear" type="button" aria-label="Clear search" title="Clear search">${READER_ICONS.x}</button>
|
|
1147
1237
|
</div>
|
|
1148
1238
|
</section>
|
|
1149
1239
|
<section class="search-results" id="search-results" hidden>
|
|
1150
1240
|
<div class="search-results-header">
|
|
1151
1241
|
<span class="search-results-title">Results</span>
|
|
1152
1242
|
<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"
|
|
1243
|
+
<button id="search-prev" type="button" disabled aria-label="Previous match" title="Previous match">${READER_ICONS.up}</button>
|
|
1244
|
+
<button id="search-next" type="button" disabled aria-label="Next match" title="Next match">${READER_ICONS.down}</button>
|
|
1155
1245
|
</div>
|
|
1156
1246
|
<div id="search-results-list"></div>
|
|
1157
1247
|
</section>
|
|
1158
1248
|
<nav class="toc" aria-label="Table of contents" data-reader-toc>
|
|
1159
1249
|
<div class="toc-header">
|
|
1250
|
+
<span class="toc-title">Contents</span>
|
|
1160
1251
|
<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"
|
|
1252
|
+
<button id="toc-expand-all" type="button" aria-label="Expand all" title="Expand all">${READER_ICONS.unfold}</button>
|
|
1253
|
+
<button id="toc-collapse-all" type="button" aria-label="Collapse all" title="Collapse all">${READER_ICONS.fold}</button>
|
|
1254
|
+
<button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar">${READER_ICONS.panel}</button>
|
|
1164
1255
|
</div>
|
|
1165
1256
|
<div class="toc-breadcrumb" data-toc-breadcrumb aria-label="Current location"></div>
|
|
1166
1257
|
</div>
|
|
1167
|
-
${renderTocNodes(tocTree)}
|
|
1258
|
+
${renderTocNodes(flattenSingleRoot(tocTree))}
|
|
1168
1259
|
</nav>
|
|
1169
1260
|
</aside>
|
|
1170
1261
|
<div class="sidebar-splitter" id="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="Resize sidebar"></div>`;
|
|
@@ -1214,7 +1305,7 @@ ${itemsHtml}
|
|
|
1214
1305
|
window.__md2docInitDiagrams = function (rootEl) {
|
|
1215
1306
|
rootEl = rootEl || document;
|
|
1216
1307
|
if (typeof mermaid !== 'undefined') {
|
|
1217
|
-
mermaid.initialize({ startOnLoad: false,
|
|
1308
|
+
mermaid.initialize({ startOnLoad: false, ${MERMAID_CONFIG_JS} });
|
|
1218
1309
|
var mermaidNodes = rootEl.querySelectorAll ? rootEl.querySelectorAll('.mermaid') : [];
|
|
1219
1310
|
var pending = [];
|
|
1220
1311
|
for (var i = 0; i < mermaidNodes.length; i++) {
|
|
@@ -1423,6 +1514,14 @@ ${itemsHtml}
|
|
|
1423
1514
|
/* min-width: 0 so an unbreakable token inside an item cannot widen the row
|
|
1424
1515
|
past its siblings — that would break the equal-width guarantee above. */
|
|
1425
1516
|
.ed-block[data-block-type="li"] > .ed-li-text { flex: 1 1 auto; min-width: 0; }
|
|
1517
|
+
/* The MD 原始碼 raw editor replaces the item's children with one .ed-editing
|
|
1518
|
+
wrap, which is then this flex row's only child. Without a grow it shrank
|
|
1519
|
+
to the textarea's intrinsic size and .ed-raw's width: 100% resolved
|
|
1520
|
+
against that: 189px at every viewport width (measured), where every other
|
|
1521
|
+
block's raw editor spans the column. The marker, and with it the indent,
|
|
1522
|
+
is gone while the textarea is open, so it spans the full column at every
|
|
1523
|
+
depth; the source line's own leading spaces show the nesting. */
|
|
1524
|
+
.ed-block[data-block-type="li"] > .ed-editing { flex: 1 1 auto; min-width: 0; }
|
|
1426
1525
|
/* column-gap already provides the marker/checkbox separation. */
|
|
1427
1526
|
.ed-block[data-block-type="li"] > .ed-li-check { flex: 0 0 auto; margin-right: 0; }
|
|
1428
1527
|
.ed-block[data-list-type="ul"] > .ed-li-marker::before { content: "\\2022"; }${edOlCounterCss}
|
|
@@ -1601,6 +1700,10 @@ ${itemsHtml}
|
|
|
1601
1700
|
reader-mode output never includes it, so the mobile drawer toggle there
|
|
1602
1701
|
is unaffected. */
|
|
1603
1702
|
.sidebar-toggle { display: none; }
|
|
1703
|
+
.mobile-bar { display: none; }
|
|
1704
|
+
/* The reader bar is gone in edit mode, so its 52px anchor offset goes too
|
|
1705
|
+
(it moved scrollIntoView landings under the edit toolbar). */
|
|
1706
|
+
.content [id] { scroll-margin-top: 0; }
|
|
1604
1707
|
/* T11-2 step 5: enterSourceMode() (lib/editor/client.js) sets
|
|
1605
1708
|
contentEl.hidden = true and mounts a plain textarea in its place, and
|
|
1606
1709
|
setDocMode() writes docMode onto body as data-ed-mode. Measured on that
|
|
@@ -2129,15 +2232,24 @@ ${itemsHtml}
|
|
|
2129
2232
|
overflow-x: auto; font-family: monospace; font-size: 12px;
|
|
2130
2233
|
}`;
|
|
2131
2234
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2235
|
+
// The phone bar only exists when there is a TOC for its button to open: a
|
|
2236
|
+
// document with no headings has no sidebar, and an empty bar would carry a
|
|
2237
|
+
// button that opens an empty drawer.
|
|
2238
|
+
const mobileBarHtml = tocHtml
|
|
2239
|
+
? '<header class="mobile-bar" id="mobile-bar">\n' +
|
|
2240
|
+
'<button class="sidebar-toggle" id="sidebar-toggle" type="button" aria-label="Toggle sidebar" aria-expanded="false">☰</button>\n' +
|
|
2241
|
+
'<span class="mobile-bar-title" id="mobile-bar-title"></span>\n' +
|
|
2242
|
+
'</header>\n'
|
|
2243
|
+
: '';
|
|
2244
|
+
const html = `<!DOCTYPE html>
|
|
2245
|
+
<html lang="${docLang}">
|
|
2134
2246
|
<head>
|
|
2135
2247
|
<meta charset="UTF-8">
|
|
2136
2248
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2137
2249
|
<title>${title}</title>
|
|
2138
2250
|
<style>
|
|
2139
2251
|
body {
|
|
2140
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
2252
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", "Noto Sans TC", sans-serif;
|
|
2141
2253
|
margin: 0;
|
|
2142
2254
|
padding: 0;
|
|
2143
2255
|
line-height: 1.65;
|
|
@@ -2166,7 +2278,9 @@ ${itemsHtml}
|
|
|
2166
2278
|
min-width: 0;
|
|
2167
2279
|
height: calc(100vh - 48px);
|
|
2168
2280
|
overflow: hidden;
|
|
2169
|
-
padding
|
|
2281
|
+
padding: 12px 8px 12px 12px;
|
|
2282
|
+
background: #f3f5f7;
|
|
2283
|
+
border-radius: 10px;
|
|
2170
2284
|
box-sizing: border-box;
|
|
2171
2285
|
display: flex;
|
|
2172
2286
|
flex-direction: column;
|
|
@@ -2176,7 +2290,7 @@ ${itemsHtml}
|
|
|
2176
2290
|
flex-basis: 36px;
|
|
2177
2291
|
width: 36px;
|
|
2178
2292
|
min-width: 36px;
|
|
2179
|
-
padding
|
|
2293
|
+
padding: 12px 4px;
|
|
2180
2294
|
}
|
|
2181
2295
|
body[data-toc-collapsed] .reader-tools,
|
|
2182
2296
|
body[data-toc-collapsed] .search-results,
|
|
@@ -2232,26 +2346,13 @@ ${itemsHtml}
|
|
|
2232
2346
|
display: none;
|
|
2233
2347
|
}
|
|
2234
2348
|
#toc-collapse-toggle {
|
|
2235
|
-
margin-left: 0;
|
|
2236
|
-
padding: 2px 8px;
|
|
2237
|
-
font: inherit;
|
|
2238
|
-
font-size: 0.9em;
|
|
2239
|
-
line-height: 1;
|
|
2240
|
-
border: 1px solid #d0d7de;
|
|
2241
|
-
border-radius: 6px;
|
|
2242
|
-
background: #ffffff;
|
|
2243
|
-
color: #57606a;
|
|
2244
|
-
cursor: pointer;
|
|
2245
2349
|
transition: transform 0.15s ease;
|
|
2246
2350
|
}
|
|
2247
|
-
#toc-collapse-toggle:hover {
|
|
2248
|
-
background: #eef2f6;
|
|
2249
|
-
color: #24292e;
|
|
2250
|
-
}
|
|
2251
2351
|
@media (max-width: 1080px) {
|
|
2252
2352
|
#toc-collapse-toggle { display: none; }
|
|
2253
2353
|
}
|
|
2254
2354
|
.reader-tools { flex: 0 0 auto; }
|
|
2355
|
+
.mobile-bar { display: none; }
|
|
2255
2356
|
.sidebar-toggle {
|
|
2256
2357
|
display: none;
|
|
2257
2358
|
position: fixed;
|
|
@@ -2296,65 +2397,91 @@ ${itemsHtml}
|
|
|
2296
2397
|
.reader-tools,
|
|
2297
2398
|
.search-results,
|
|
2298
2399
|
.toc {
|
|
2299
|
-
border:
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
padding: 12px 14px;
|
|
2400
|
+
border: none;
|
|
2401
|
+
background: transparent;
|
|
2402
|
+
padding: 2px 0;
|
|
2303
2403
|
}
|
|
2304
|
-
.reader-search-label
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2404
|
+
.reader-search-label,
|
|
2405
|
+
#doc-search-submit {
|
|
2406
|
+
position: absolute;
|
|
2407
|
+
width: 1px;
|
|
2408
|
+
height: 1px;
|
|
2409
|
+
padding: 0;
|
|
2410
|
+
margin: -1px;
|
|
2411
|
+
overflow: hidden;
|
|
2412
|
+
clip: rect(0, 0, 0, 0);
|
|
2413
|
+
white-space: nowrap;
|
|
2414
|
+
border: 0;
|
|
2312
2415
|
}
|
|
2313
2416
|
.reader-search-row {
|
|
2417
|
+
position: relative;
|
|
2314
2418
|
display: flex;
|
|
2315
|
-
|
|
2316
|
-
margin-bottom: 8px;
|
|
2419
|
+
margin-bottom: 4px;
|
|
2317
2420
|
}
|
|
2318
2421
|
.reader-search-row input[type="search"] {
|
|
2319
2422
|
flex: 1 1 auto;
|
|
2320
2423
|
min-width: 0;
|
|
2321
|
-
padding:
|
|
2424
|
+
padding: 7px 30px 7px 10px;
|
|
2322
2425
|
border: 1px solid #d0d7de;
|
|
2323
2426
|
border-radius: 6px;
|
|
2427
|
+
background: #ffffff;
|
|
2324
2428
|
font: inherit;
|
|
2325
2429
|
}
|
|
2326
|
-
.reader-search-row button {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2430
|
+
.reader-search-row input[type="search"]::-webkit-search-cancel-button {
|
|
2431
|
+
-webkit-appearance: none;
|
|
2432
|
+
appearance: none;
|
|
2433
|
+
display: none;
|
|
2434
|
+
}
|
|
2435
|
+
#doc-search-clear {
|
|
2436
|
+
position: absolute;
|
|
2437
|
+
right: 4px;
|
|
2438
|
+
top: 50%;
|
|
2439
|
+
transform: translateY(-50%);
|
|
2440
|
+
display: inline-grid;
|
|
2441
|
+
place-items: center;
|
|
2442
|
+
padding: 4px;
|
|
2443
|
+
line-height: 0;
|
|
2444
|
+
border: none;
|
|
2445
|
+
border-radius: 4px;
|
|
2446
|
+
background: transparent;
|
|
2447
|
+
color: #59636e;
|
|
2333
2448
|
cursor: pointer;
|
|
2334
2449
|
}
|
|
2335
|
-
|
|
2336
|
-
background: #
|
|
2450
|
+
#doc-search-clear:hover {
|
|
2451
|
+
background: #e3e8ee;
|
|
2452
|
+
color: #1f2328;
|
|
2453
|
+
}
|
|
2454
|
+
#doc-search-input:placeholder-shown ~ #doc-search-clear {
|
|
2455
|
+
display: none;
|
|
2337
2456
|
}
|
|
2338
2457
|
.search-results-header,
|
|
2339
2458
|
.toc-header {
|
|
2340
2459
|
display: flex;
|
|
2341
|
-
flex-direction:
|
|
2342
|
-
align-items:
|
|
2343
|
-
gap:
|
|
2460
|
+
flex-direction: row;
|
|
2461
|
+
align-items: center;
|
|
2462
|
+
gap: 6px;
|
|
2344
2463
|
margin-bottom: 8px;
|
|
2345
2464
|
}
|
|
2465
|
+
.toc-header {
|
|
2466
|
+
justify-content: space-between;
|
|
2467
|
+
border-bottom: 1px solid #d8dee4;
|
|
2468
|
+
padding-bottom: 6px;
|
|
2469
|
+
}
|
|
2470
|
+
body[data-toc-collapsed] .toc-header {
|
|
2471
|
+
border-bottom: none;
|
|
2472
|
+
}
|
|
2346
2473
|
.toc-header-actions {
|
|
2347
2474
|
flex: 0 0 auto;
|
|
2348
2475
|
display: flex;
|
|
2349
|
-
gap:
|
|
2476
|
+
gap: 2px;
|
|
2350
2477
|
justify-content: flex-end;
|
|
2351
2478
|
}
|
|
2352
|
-
/*
|
|
2353
|
-
|
|
2479
|
+
/* Breadcrumb: kept in the DOM (the runtime and editor-reader-rebind read it)
|
|
2480
|
+
but hidden since v3.8.0 — the TOC highlights the whole active path instead. */
|
|
2354
2481
|
.toc-breadcrumb {
|
|
2355
2482
|
flex: 1 1 auto;
|
|
2356
2483
|
min-width: 0;
|
|
2357
|
-
display:
|
|
2484
|
+
display: none;
|
|
2358
2485
|
flex-direction: column;
|
|
2359
2486
|
gap: 1px;
|
|
2360
2487
|
}
|
|
@@ -2379,13 +2506,14 @@ ${itemsHtml}
|
|
|
2379
2506
|
}
|
|
2380
2507
|
.search-results-title,
|
|
2381
2508
|
.toc-title {
|
|
2382
|
-
font-size: 0.
|
|
2383
|
-
font-weight:
|
|
2384
|
-
|
|
2385
|
-
text-transform: uppercase;
|
|
2386
|
-
color: #57606a;
|
|
2509
|
+
font-size: 0.86rem;
|
|
2510
|
+
font-weight: 600;
|
|
2511
|
+
color: #1f2328;
|
|
2387
2512
|
flex: 0 0 auto;
|
|
2388
2513
|
}
|
|
2514
|
+
body[data-toc-collapsed] .toc-title {
|
|
2515
|
+
display: none;
|
|
2516
|
+
}
|
|
2389
2517
|
.reader-status {
|
|
2390
2518
|
flex: 1 1 auto;
|
|
2391
2519
|
font-size: 0.82em;
|
|
@@ -2393,20 +2521,21 @@ ${itemsHtml}
|
|
|
2393
2521
|
}
|
|
2394
2522
|
.search-results-header button,
|
|
2395
2523
|
.toc-header button {
|
|
2396
|
-
|
|
2524
|
+
display: inline-grid;
|
|
2525
|
+
place-items: center;
|
|
2526
|
+
padding: 4px;
|
|
2397
2527
|
font: inherit;
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
border: 1px solid #d0d7de;
|
|
2528
|
+
line-height: 0;
|
|
2529
|
+
border: none;
|
|
2401
2530
|
border-radius: 6px;
|
|
2402
|
-
background:
|
|
2403
|
-
color: #
|
|
2531
|
+
background: transparent;
|
|
2532
|
+
color: #59636e;
|
|
2404
2533
|
cursor: pointer;
|
|
2405
2534
|
}
|
|
2406
2535
|
.search-results-header button:hover,
|
|
2407
2536
|
.toc-header button:hover {
|
|
2408
|
-
background: #
|
|
2409
|
-
color: #
|
|
2537
|
+
background: #e3e8ee;
|
|
2538
|
+
color: #1f2328;
|
|
2410
2539
|
}
|
|
2411
2540
|
.search-results-header button:disabled {
|
|
2412
2541
|
opacity: 0.45;
|
|
@@ -2452,8 +2581,8 @@ ${itemsHtml}
|
|
|
2452
2581
|
background: #eef2f6;
|
|
2453
2582
|
}
|
|
2454
2583
|
.search-result-item.is-active {
|
|
2455
|
-
background: #
|
|
2456
|
-
box-shadow:
|
|
2584
|
+
background: #dbe6f3;
|
|
2585
|
+
box-shadow: none;
|
|
2457
2586
|
}
|
|
2458
2587
|
.search-result-title {
|
|
2459
2588
|
font-weight: 600;
|
|
@@ -2502,6 +2631,8 @@ ${itemsHtml}
|
|
|
2502
2631
|
the shift+wheel handler; appears only when a title actually overflows. */
|
|
2503
2632
|
overflow-x: auto;
|
|
2504
2633
|
scrollbar-width: thin;
|
|
2634
|
+
-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
|
|
2635
|
+
mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
|
|
2505
2636
|
}
|
|
2506
2637
|
.toc ul {
|
|
2507
2638
|
list-style: none;
|
|
@@ -2526,13 +2657,15 @@ ${itemsHtml}
|
|
|
2526
2657
|
min-width: 100%;
|
|
2527
2658
|
color: #57606a;
|
|
2528
2659
|
text-decoration: none;
|
|
2529
|
-
padding: 2px
|
|
2660
|
+
padding: 2px 8px;
|
|
2530
2661
|
line-height: 1.4;
|
|
2531
2662
|
white-space: nowrap;
|
|
2532
2663
|
box-sizing: border-box;
|
|
2664
|
+
border-radius: 4px;
|
|
2533
2665
|
}
|
|
2534
2666
|
.toc summary > a {
|
|
2535
|
-
flex:
|
|
2667
|
+
flex: 1 0 auto;
|
|
2668
|
+
min-width: 0;
|
|
2536
2669
|
}
|
|
2537
2670
|
.toc summary {
|
|
2538
2671
|
min-width: 0;
|
|
@@ -2542,17 +2675,41 @@ ${itemsHtml}
|
|
|
2542
2675
|
}
|
|
2543
2676
|
.toc a:hover {
|
|
2544
2677
|
color: #0969da;
|
|
2678
|
+
background: #e8edf2;
|
|
2545
2679
|
}
|
|
2546
2680
|
.toc a.is-active {
|
|
2547
|
-
color: #
|
|
2548
|
-
font-weight:
|
|
2681
|
+
color: #0550ae;
|
|
2682
|
+
font-weight: 600;
|
|
2683
|
+
background: #dbe6f3;
|
|
2549
2684
|
}
|
|
2550
2685
|
.toc a.is-match {
|
|
2551
|
-
color:
|
|
2552
|
-
background: #
|
|
2686
|
+
color: inherit;
|
|
2687
|
+
background: #fff5c2;
|
|
2688
|
+
}
|
|
2689
|
+
.toc li:has(a.is-active) > details > summary > a:not(.is-active) {
|
|
2690
|
+
color: #1f2328;
|
|
2691
|
+
font-weight: 600;
|
|
2692
|
+
}
|
|
2693
|
+
.reader-sidebar :is(a, button, input, summary):focus-visible,
|
|
2694
|
+
.mobile-bar button:focus-visible,
|
|
2695
|
+
.content a:focus-visible {
|
|
2696
|
+
outline: 2px solid #0969da;
|
|
2697
|
+
outline-offset: 2px;
|
|
2553
2698
|
border-radius: 4px;
|
|
2554
|
-
|
|
2555
|
-
|
|
2699
|
+
}
|
|
2700
|
+
.toc :is(a, summary):focus-visible {
|
|
2701
|
+
outline-offset: -2px;
|
|
2702
|
+
}
|
|
2703
|
+
/* The list's right-edge fade is a mask, and a mask clips the ring of a
|
|
2704
|
+
focused row along with the text. Lift it while a row holds focus. */
|
|
2705
|
+
.toc > .toc-list:has(:focus-visible) {
|
|
2706
|
+
-webkit-mask-image: none;
|
|
2707
|
+
mask-image: none;
|
|
2708
|
+
}
|
|
2709
|
+
.toc-header button:focus-visible,
|
|
2710
|
+
.search-results-header button:focus-visible,
|
|
2711
|
+
.reader-sidebar .search-result-item:focus-visible {
|
|
2712
|
+
outline-offset: -2px;
|
|
2556
2713
|
}
|
|
2557
2714
|
.toc details {
|
|
2558
2715
|
margin: 0;
|
|
@@ -2574,6 +2731,8 @@ ${itemsHtml}
|
|
|
2574
2731
|
font-size: 0.78em;
|
|
2575
2732
|
line-height: 1.8;
|
|
2576
2733
|
flex: 0 0 auto;
|
|
2734
|
+
width: 12px;
|
|
2735
|
+
text-align: center;
|
|
2577
2736
|
transform: translateY(1px);
|
|
2578
2737
|
}
|
|
2579
2738
|
.toc details[open] > summary::before {
|
|
@@ -2585,15 +2744,15 @@ ${itemsHtml}
|
|
|
2585
2744
|
border-left: 1px solid #d8dee4;
|
|
2586
2745
|
}
|
|
2587
2746
|
.toc-item:not(.toc-parent) > a {
|
|
2588
|
-
padding-left:
|
|
2747
|
+
padding-left: 26px;
|
|
2589
2748
|
}
|
|
2590
2749
|
.toc-list-level-1 > .toc-item > a,
|
|
2591
2750
|
.toc-list-level-1 > .toc-item > details > summary > a {
|
|
2592
|
-
font-
|
|
2751
|
+
font-size: 0.95em;
|
|
2593
2752
|
}
|
|
2594
2753
|
.toc-list-level-2 > .toc-item > a,
|
|
2595
2754
|
.toc-list-level-2 > .toc-item > details > summary > a {
|
|
2596
|
-
font-size: 0.
|
|
2755
|
+
font-size: 0.9em;
|
|
2597
2756
|
}
|
|
2598
2757
|
.toc-list-level-3 > .toc-item > a,
|
|
2599
2758
|
.toc-list-level-3 > .toc-item > details > summary > a,
|
|
@@ -2625,6 +2784,19 @@ ${itemsHtml}
|
|
|
2625
2784
|
}
|
|
2626
2785
|
.content > * { max-width: 100%; }
|
|
2627
2786
|
.content iframe, .content video, .content canvas { max-width: 100%; height: auto; }
|
|
2787
|
+
.doc-meta {
|
|
2788
|
+
display: grid;
|
|
2789
|
+
grid-template-columns: max-content 1fr;
|
|
2790
|
+
gap: 4px 20px;
|
|
2791
|
+
background: #f6f8fa;
|
|
2792
|
+
border-radius: 6px;
|
|
2793
|
+
padding: 12px 16px;
|
|
2794
|
+
margin: 16px 0 28px;
|
|
2795
|
+
font-size: 0.92em;
|
|
2796
|
+
line-height: 1.55;
|
|
2797
|
+
}
|
|
2798
|
+
.doc-meta dt { color: #57606a; }
|
|
2799
|
+
.doc-meta dd { margin: 0; }
|
|
2628
2800
|
.heading-with-anchor {
|
|
2629
2801
|
position: relative;
|
|
2630
2802
|
}
|
|
@@ -2645,14 +2817,15 @@ ${itemsHtml}
|
|
|
2645
2817
|
color: #0969da;
|
|
2646
2818
|
}
|
|
2647
2819
|
h1 { font-size: 2em; border-bottom: 2px solid #e1e4e8; padding-bottom: 10px; margin-top: 1.5em; }
|
|
2648
|
-
h2 { font-size: 1.5em; border-bottom: 1px solid #e1e4e8; padding-bottom: 6px; margin-top:
|
|
2649
|
-
h3 { font-size: 1.
|
|
2650
|
-
h4 { font-size: 1.
|
|
2820
|
+
h2 { font-size: 1.5em; border-bottom: 1px solid #e1e4e8; padding-bottom: 6px; margin-top: 2.2em; margin-bottom: 0.7em; }
|
|
2821
|
+
h3 { font-size: 1.3em; margin-top: 2em; margin-bottom: 0.5em; }
|
|
2822
|
+
h4 { font-size: 1.1em; margin-top: 1.8em; margin-bottom: 0.4em; }
|
|
2823
|
+
.sec { margin-right: 0.4em; font-variant-numeric: tabular-nums; }
|
|
2651
2824
|
code {
|
|
2652
2825
|
background: #f6f8fa;
|
|
2653
2826
|
padding: 2px 5px;
|
|
2654
2827
|
border-radius: 3px;
|
|
2655
|
-
font-family: "
|
|
2828
|
+
font-family: "Cascadia Mono", Consolas, "SFMono-Regular", "Liberation Mono", Menlo, "Microsoft JhengHei", monospace;
|
|
2656
2829
|
font-size: 0.875em;
|
|
2657
2830
|
}
|
|
2658
2831
|
pre {
|
|
@@ -2663,6 +2836,13 @@ ${itemsHtml}
|
|
|
2663
2836
|
line-height: 1.45;
|
|
2664
2837
|
}
|
|
2665
2838
|
pre code { background: none; padding: 0; font-size: 0.875em; }
|
|
2839
|
+
.content :not(pre) > code {
|
|
2840
|
+
background: none;
|
|
2841
|
+
padding: 0;
|
|
2842
|
+
border-radius: 0;
|
|
2843
|
+
color: #b93a0c;
|
|
2844
|
+
font-size: 0.92em;
|
|
2845
|
+
}
|
|
2666
2846
|
table {
|
|
2667
2847
|
display: block;
|
|
2668
2848
|
border-collapse: collapse;
|
|
@@ -2672,9 +2852,8 @@ ${itemsHtml}
|
|
|
2672
2852
|
font-size: 0.9em;
|
|
2673
2853
|
overflow-x: auto;
|
|
2674
2854
|
}
|
|
2675
|
-
th, td { border: 1px solid #
|
|
2676
|
-
th {
|
|
2677
|
-
tr:nth-child(even) { background: #fafbfc; }
|
|
2855
|
+
th, td { border: none; border-bottom: 1px solid #e1e4e8; padding: 7px 14px; text-align: left; vertical-align: top; }
|
|
2856
|
+
th { font-weight: 600; border-bottom: 2px solid #c9ced4; }
|
|
2678
2857
|
.content table th,
|
|
2679
2858
|
.content table td {
|
|
2680
2859
|
overflow-wrap: normal;
|
|
@@ -2687,7 +2866,7 @@ ${itemsHtml}
|
|
|
2687
2866
|
.content table th code {
|
|
2688
2867
|
white-space: nowrap;
|
|
2689
2868
|
}
|
|
2690
|
-
.content table { table-layout: auto; }
|
|
2869
|
+
.content table { table-layout: auto; font-variant-numeric: tabular-nums; }
|
|
2691
2870
|
.content table col.col-narrow { width: 1%; }
|
|
2692
2871
|
.content table col.col-prose { width: auto; }
|
|
2693
2872
|
.content table th.cell-narrow,
|
|
@@ -2700,10 +2879,7 @@ ${itemsHtml}
|
|
|
2700
2879
|
background: #ffffff;
|
|
2701
2880
|
}
|
|
2702
2881
|
.content table thead th:first-child {
|
|
2703
|
-
background: #
|
|
2704
|
-
}
|
|
2705
|
-
.content table tbody tr:nth-child(even) td:first-child {
|
|
2706
|
-
background: #fafbfc;
|
|
2882
|
+
background: #ffffff;
|
|
2707
2883
|
}
|
|
2708
2884
|
blockquote {
|
|
2709
2885
|
border-left: 4px solid #dfe2e5;
|
|
@@ -2917,21 +3093,59 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2917
3093
|
html[data-lightbox-open] { overflow: hidden; }
|
|
2918
3094
|
@media print { .lightbox { display: none !important; } }
|
|
2919
3095
|
@media (max-width: 1080px) {
|
|
3096
|
+
.content table td.cell-prose,
|
|
3097
|
+
.content table th.cell-prose { min-width: 15em; }
|
|
2920
3098
|
.sidebar-toggle { display: inline-flex; align-items: center; }
|
|
3099
|
+
.mobile-bar {
|
|
3100
|
+
display: flex;
|
|
3101
|
+
align-items: center;
|
|
3102
|
+
gap: 10px;
|
|
3103
|
+
position: fixed;
|
|
3104
|
+
top: 0;
|
|
3105
|
+
left: 0;
|
|
3106
|
+
right: 0;
|
|
3107
|
+
height: 44px;
|
|
3108
|
+
padding: 0 12px;
|
|
3109
|
+
box-sizing: border-box;
|
|
3110
|
+
background: #ffffff;
|
|
3111
|
+
border-bottom: 1px solid #d8dee4;
|
|
3112
|
+
z-index: 100;
|
|
3113
|
+
}
|
|
3114
|
+
.mobile-bar .sidebar-toggle {
|
|
3115
|
+
position: static;
|
|
3116
|
+
box-shadow: none;
|
|
3117
|
+
}
|
|
3118
|
+
.mobile-bar-title {
|
|
3119
|
+
min-width: 0;
|
|
3120
|
+
flex: 1 1 auto;
|
|
3121
|
+
font-size: 14px;
|
|
3122
|
+
font-weight: 600;
|
|
3123
|
+
color: #24292e;
|
|
3124
|
+
white-space: nowrap;
|
|
3125
|
+
overflow: hidden;
|
|
3126
|
+
text-overflow: ellipsis;
|
|
3127
|
+
}
|
|
3128
|
+
.content [id] { scroll-margin-top: 52px; }
|
|
3129
|
+
/* The 60px clears the fixed bar, so it only applies when the bar exists
|
|
3130
|
+
(a document with no headings has none). :where() keeps the selector at
|
|
3131
|
+
the specificity of a bare class, so the edit-mode override still wins. */
|
|
3132
|
+
.page-layout:where(.mobile-bar ~ *) {
|
|
3133
|
+
padding-top: 60px;
|
|
3134
|
+
}
|
|
2921
3135
|
.page-layout {
|
|
2922
3136
|
display: block;
|
|
2923
3137
|
max-width: 100%;
|
|
2924
|
-
padding-top: 60px;
|
|
2925
3138
|
}
|
|
2926
3139
|
.reader-sidebar {
|
|
2927
3140
|
position: fixed;
|
|
2928
|
-
top:
|
|
3141
|
+
top: 44px;
|
|
2929
3142
|
left: 0;
|
|
2930
3143
|
bottom: 0;
|
|
2931
3144
|
width: 85%;
|
|
2932
3145
|
max-width: 360px;
|
|
2933
|
-
height: 100vh;
|
|
2934
|
-
background: #
|
|
3146
|
+
height: calc(100vh - 44px);
|
|
3147
|
+
background: #f3f5f7;
|
|
3148
|
+
border-radius: 0;
|
|
2935
3149
|
z-index: 99;
|
|
2936
3150
|
transform: translateX(-100%);
|
|
2937
3151
|
transition: transform 0.2s ease;
|
|
@@ -2941,10 +3155,9 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2941
3155
|
display: flex;
|
|
2942
3156
|
flex-direction: column;
|
|
2943
3157
|
gap: 12px;
|
|
2944
|
-
box-shadow: 2px 0 12px rgba(0,0,0,0.15);
|
|
2945
3158
|
flex: initial;
|
|
2946
3159
|
}
|
|
2947
|
-
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); }
|
|
3160
|
+
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); box-shadow: 2px 0 12px rgba(0,0,0,0.15); }
|
|
2948
3161
|
/* Scroll lock for the drawer, same two-rule shape the lightbox uses a
|
|
2949
3162
|
few lines above (body[data-lightbox-open] / html[data-lightbox-open])
|
|
2950
3163
|
and for the identical reason: 'html, body { overflow-x: clip }' keeps
|
|
@@ -2961,7 +3174,6 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2961
3174
|
body[data-sidebar-open] { overflow: hidden; }
|
|
2962
3175
|
html[data-sidebar-open] { overflow: hidden; }
|
|
2963
3176
|
.sidebar-splitter { display: none; }
|
|
2964
|
-
.heading-anchor { opacity: 1; }
|
|
2965
3177
|
}
|
|
2966
3178
|
@media print {
|
|
2967
3179
|
body { font-size: 11pt; }
|
|
@@ -2986,10 +3198,19 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2986
3198
|
padding: 0 10px;
|
|
2987
3199
|
}
|
|
2988
3200
|
.reader-sidebar,
|
|
3201
|
+
.mobile-bar,
|
|
2989
3202
|
.sidebar-toggle,
|
|
2990
3203
|
.sidebar-splitter,
|
|
2991
3204
|
.sidebar-scrim { display: none !important; }
|
|
2992
3205
|
.content { max-width: 100%; }
|
|
3206
|
+
/* A4 lays out at about 680px, inside the max-width: 1080px block above,
|
|
3207
|
+
so its 15em prose floor applied to print; the table scrolls
|
|
3208
|
+
horizontally on screen and a PDF clips what overflows. Drop the floor.
|
|
3209
|
+
The sticky first column goes static so its bottom border prints. */
|
|
3210
|
+
.content table td.cell-prose,
|
|
3211
|
+
.content table th.cell-prose { min-width: 0; }
|
|
3212
|
+
.content table tbody td:first-child,
|
|
3213
|
+
.content table thead th:first-child { position: static; }
|
|
2993
3214
|
.heading-anchor { display: none; }
|
|
2994
3215
|
pre { font-size: 9pt; }
|
|
2995
3216
|
a[href]:after { content: none; }
|
|
@@ -3007,18 +3228,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3007
3228
|
.ed-te-hl).
|
|
3008
3229
|
|
|
3009
3230
|
The descendant rules and their !important are not decoration, they are
|
|
3010
|
-
the whole problem: <pre> (#f6f8fa)
|
|
3011
|
-
tr:nth-child(even) (#fafbfc) and the sticky first column (#ffffff) all
|
|
3231
|
+
the whole problem: <pre> (#f6f8fa) and the sticky first column (#ffffff)
|
|
3012
3232
|
carry OPAQUE backgrounds, and the sticky column additionally paints at
|
|
3013
|
-
z-index: 1 over anything drawn beneath it.
|
|
3014
|
-
|
|
3015
|
-
.content table tbody tr:nth-child(even) td:first-child
|
|
3016
|
-
against this rule's (0,3,2)
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3233
|
+
z-index: 1 over anything drawn beneath it. Until v3.8.0 the grey <th>
|
|
3234
|
+
(#f6f8fa) and the zebra stripe tr:nth-child(even) (#fafbfc, whose sticky
|
|
3235
|
+
override .content table tbody tr:nth-child(even) td:first-child was
|
|
3236
|
+
(0,3,4) against this rule's (0,3,2)) did too; v3.8.0 removed both, and
|
|
3237
|
+
the th / tr:nth-child(even) selectors below stay so a later restyle
|
|
3238
|
+
cannot quietly reopen the hole. Without !important the tint is invisible
|
|
3239
|
+
on exactly the blocks a user is most likely to select. .ed-te-hl needed
|
|
3240
|
+
the identical treatment for the identical reason. This ADDS rules rather
|
|
3241
|
+
than reordering the sticky-column rules, which win by source order.
|
|
3022
3242
|
(No backticks in this comment: it lives inside a JS template literal.)
|
|
3023
3243
|
|
|
3024
3244
|
:focus outline is suppressed because the roving tabindex below focuses a
|
|
@@ -3267,6 +3487,11 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3267
3487
|
indicator' is the singleton line shown while dragging a row.
|
|
3268
3488
|
'.ed-te-row-dragging' dims the row actually being dragged. */
|
|
3269
3489
|
.ed-te-hl { background: rgba(59, 130, 246, 0.15) !important; }
|
|
3490
|
+
/* Table cell-range selection: the painted rectangle, and the table that
|
|
3491
|
+
holds one (its caret and native selection are hidden while it stands). */
|
|
3492
|
+
.ed-cell-range { background: rgba(59, 130, 246, 0.18) !important; }
|
|
3493
|
+
.ed-cell-range-active { caret-color: transparent; }
|
|
3494
|
+
.ed-cell-range-active ::selection { background: transparent; }
|
|
3270
3495
|
.ed-te-menu {
|
|
3271
3496
|
position: fixed; z-index: 12;
|
|
3272
3497
|
display: flex; align-items: center; gap: 4px;
|
|
@@ -3710,8 +3935,7 @@ ${opts.editMode ? editModeLayoutCss : ''}
|
|
|
3710
3935
|
${usesMath ? buildKatexStyleTag() : ''}
|
|
3711
3936
|
</head>
|
|
3712
3937
|
<body>
|
|
3713
|
-
<
|
|
3714
|
-
<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3938
|
+
${mobileBarHtml}<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3715
3939
|
<div class="page-layout">
|
|
3716
3940
|
${tocHtml}
|
|
3717
3941
|
<main class="content">
|
|
@@ -3799,11 +4023,15 @@ ${mermaidInitTag}`)
|
|
|
3799
4023
|
}
|
|
3800
4024
|
|
|
3801
4025
|
const breadcrumbEl = document.querySelector('[data-toc-breadcrumb]');
|
|
4026
|
+
const mobileBarTitle = document.getElementById('mobile-bar-title');
|
|
4027
|
+
if (mobileBarTitle && sections.length) mobileBarTitle.textContent = sections[0].title;
|
|
3802
4028
|
const sectionIndexById = new Map(sections.map((section, i) => [section.id, i]));
|
|
3803
4029
|
|
|
3804
4030
|
// Sticky breadcrumb: the ancestor chain (shallow→deep) of the active section.
|
|
3805
4031
|
// For each decreasing depth, take the nearest preceding section, then append
|
|
3806
|
-
// the active section itself.
|
|
4032
|
+
// the active section itself. The rows are still written into the breadcrumb
|
|
4033
|
+
// element, but CSS hides it (the TOC highlights the path); the useful output
|
|
4034
|
+
// today is the last link's title, which names the mobile bar.
|
|
3807
4035
|
function renderBreadcrumb(sectionId) {
|
|
3808
4036
|
if (!breadcrumbEl) return;
|
|
3809
4037
|
if (!sectionIndexById.has(sectionId)) {
|
|
@@ -3824,6 +4052,7 @@ ${mermaidInitTag}`)
|
|
|
3824
4052
|
return;
|
|
3825
4053
|
}
|
|
3826
4054
|
const minDepth = chain[0].depth;
|
|
4055
|
+
if (mobileBarTitle) mobileBarTitle.textContent = chain[chain.length - 1].title;
|
|
3827
4056
|
breadcrumbEl.innerHTML = chain
|
|
3828
4057
|
.map((section, i) => {
|
|
3829
4058
|
const indent = (section.depth - minDepth) * 10;
|
|
@@ -4403,18 +4632,30 @@ ${mermaidInitTag}`)
|
|
|
4403
4632
|
var hit = document.elementFromPoint(rect.left + rect.width / 2, ANCHOR_LINE);
|
|
4404
4633
|
var node = (hit && hit.closest) ? hit.closest('main.content > *') : null;
|
|
4405
4634
|
if (!node) {
|
|
4406
|
-
//
|
|
4407
|
-
//
|
|
4408
|
-
//
|
|
4635
|
+
// Over a gap (a heading's top margin, the space between blocks): pin the
|
|
4636
|
+
// first block that starts below the anchor line, which is what the reader
|
|
4637
|
+
// sees next to it. Falling back to the last heading ABOVE the line would
|
|
4638
|
+
// pin something up to a whole section away. Block tops are monotonic in
|
|
4639
|
+
// document order, so this is a binary search over the column's children.
|
|
4640
|
+
var kids = contentEl.children;
|
|
4409
4641
|
var lo = 0;
|
|
4410
|
-
var hi =
|
|
4642
|
+
var hi = kids.length - 1;
|
|
4411
4643
|
while (lo <= hi) {
|
|
4412
4644
|
var mid = (lo + hi) >> 1;
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4645
|
+
// A child with no layout boxes (every WaveDrom fence leaves a script
|
|
4646
|
+
// element) reports an all-zero rect, which would break the
|
|
4647
|
+
// monotonic order the search depends on. Compare the nearest
|
|
4648
|
+
// following child that is rendered; none left before hi means the
|
|
4649
|
+
// answer is in the lower half.
|
|
4650
|
+
var probe = mid;
|
|
4651
|
+
while (probe <= hi && !kids[probe].getClientRects().length) probe++;
|
|
4652
|
+
if (probe > hi) {
|
|
4417
4653
|
hi = mid - 1;
|
|
4654
|
+
} else if (kids[probe].getBoundingClientRect().top > ANCHOR_LINE) {
|
|
4655
|
+
node = kids[probe];
|
|
4656
|
+
hi = mid - 1;
|
|
4657
|
+
} else {
|
|
4658
|
+
lo = probe + 1;
|
|
4418
4659
|
}
|
|
4419
4660
|
}
|
|
4420
4661
|
}
|
|
@@ -5643,6 +5884,42 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
5643
5884
|
}
|
|
5644
5885
|
module.exports = { renderMarkdown };
|
|
5645
5886
|
|
|
5887
|
+
// Index of the first "{" in dot source that opens the graph body: not inside a
|
|
5888
|
+
// double-quoted string, a // or /* */ comment, a line starting with # (dot
|
|
5889
|
+
// treats it as preprocessor output), or an HTML-like <...> id. -1 if none.
|
|
5890
|
+
function firstDotBrace(src) {
|
|
5891
|
+
let i = 0;
|
|
5892
|
+
const n = src.length;
|
|
5893
|
+
while (i < n) {
|
|
5894
|
+
const c = src[i];
|
|
5895
|
+
if (c === '"') {
|
|
5896
|
+
i++;
|
|
5897
|
+
while (i < n && src[i] !== '"') i += src[i] === '\\' ? 2 : 1;
|
|
5898
|
+
i++;
|
|
5899
|
+
} else if (c === '/' && src[i + 1] === '/') {
|
|
5900
|
+
while (i < n && src[i] !== '\n') i++;
|
|
5901
|
+
} else if (c === '/' && src[i + 1] === '*') {
|
|
5902
|
+
const end = src.indexOf('*/', i + 2);
|
|
5903
|
+
i = end < 0 ? n : end + 2;
|
|
5904
|
+
} else if (c === '#' && (i === 0 || src[i - 1] === '\n')) {
|
|
5905
|
+
while (i < n && src[i] !== '\n') i++;
|
|
5906
|
+
} else if (c === '<') {
|
|
5907
|
+
let depth = 0;
|
|
5908
|
+
while (i < n) {
|
|
5909
|
+
if (src[i] === '<') depth++;
|
|
5910
|
+
else if (src[i] === '>' && --depth === 0) break;
|
|
5911
|
+
i++;
|
|
5912
|
+
}
|
|
5913
|
+
i++;
|
|
5914
|
+
} else if (c === '{') {
|
|
5915
|
+
return i;
|
|
5916
|
+
} else {
|
|
5917
|
+
i++;
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
return -1;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5646
5923
|
// Render every deferred dot/graphviz placeholder to inline SVG using the
|
|
5647
5924
|
// in-process WASM engine. Loads the WASM module only when at least one dot
|
|
5648
5925
|
// block exists, so text-only docs pay nothing.
|
|
@@ -5659,11 +5936,35 @@ async function bakeGraphviz(htmlStr) {
|
|
|
5659
5936
|
return htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, (m, b64) => {
|
|
5660
5937
|
const dotSrc = Buffer.from(b64, 'base64').toString('utf8');
|
|
5661
5938
|
try {
|
|
5662
|
-
|
|
5939
|
+
// Graphviz measures label widths with the font it lays out in, and with
|
|
5940
|
+
// no fontname that is Times, whose metrics are narrower than the sans
|
|
5941
|
+
// the SVG is drawn in -- so labels overflowed their boxes. Declare
|
|
5942
|
+
// Helvetica BEFORE layout, right after the first brace; a fontname the
|
|
5943
|
+
// author writes later in the source still wins. Exactly "Helvetica":
|
|
5944
|
+
// a "Helvetica,sans-Serif" family list falls back to Times widths.
|
|
5945
|
+
const at = firstDotBrace(dotSrc);
|
|
5946
|
+
const laidOut = at < 0 ? dotSrc : dotSrc.slice(0, at + 1) +
|
|
5947
|
+
' graph [fontname="Helvetica"]; node [fontname="Helvetica"]; edge [fontname="Helvetica"]; ' + dotSrc.slice(at + 1);
|
|
5948
|
+
let laid;
|
|
5949
|
+
try {
|
|
5950
|
+
laid = gv.dot(laidOut);
|
|
5951
|
+
} catch (injectErr) {
|
|
5952
|
+
// Injection must never turn a valid graph into an error: lay the
|
|
5953
|
+
// original source out as the author wrote it.
|
|
5954
|
+
if (laidOut === dotSrc) throw injectErr;
|
|
5955
|
+
laid = gv.dot(dotSrc);
|
|
5956
|
+
}
|
|
5957
|
+
const svg = laid
|
|
5663
5958
|
.replace(/<\?xml[^>]*\?>/g, '')
|
|
5664
5959
|
.replace(/<!DOCTYPE[\s\S]*?>/g, '')
|
|
5665
5960
|
// drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
|
|
5666
5961
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
5962
|
+
// v3.8.0: graphviz falls back to Times when no fontname is set. The
|
|
5963
|
+
// injection above makes Helvetica the layout font, so this rewrite
|
|
5964
|
+
// now only touches an author's explicit Times-Roman, or a figure
|
|
5965
|
+
// whose injection fell back to the original source; other explicit
|
|
5966
|
+
// fontnames are untouched.
|
|
5967
|
+
.replace(/font-family="Times,serif"/g, 'font-family="Helvetica,sans-Serif"')
|
|
5667
5968
|
.trim();
|
|
5668
5969
|
return `<div class="graphviz">${svg}</div>`;
|
|
5669
5970
|
} catch (e) {
|
|
@@ -5825,6 +6126,7 @@ async function bakeDiagrams(htmlStr, dstPath) {
|
|
|
5825
6126
|
if (require.main === module) {
|
|
5826
6127
|
const [,, src, dst] = process.argv;
|
|
5827
6128
|
const BAKE_SVG = process.argv.slice(4).includes('--bake-svg');
|
|
6129
|
+
const LANG_ARG = (process.argv.slice(4).find((a) => a.startsWith('--lang=')) || '').slice('--lang='.length) || undefined;
|
|
5828
6130
|
if (!src || !dst) {
|
|
5829
6131
|
console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
|
|
5830
6132
|
process.exit(1);
|
|
@@ -5834,7 +6136,7 @@ if (require.main === module) {
|
|
|
5834
6136
|
|
|
5835
6137
|
(async () => {
|
|
5836
6138
|
const mdText = fs.readFileSync(src, 'utf8');
|
|
5837
|
-
const { html } = await renderMarkdown(mdText, path.resolve(src), {});
|
|
6139
|
+
const { html } = await renderMarkdown(mdText, path.resolve(src), { lang: LANG_ARG });
|
|
5838
6140
|
let finalHtml = html;
|
|
5839
6141
|
|
|
5840
6142
|
if (ext === '.html') {
|