@helping-ai-workflow/md2doc 3.7.0 → 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/md2doc.js +420 -131
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ also pass `--open`.
|
|
|
74
74
|
| `--no-open` | Skip the viewer launch. |
|
|
75
75
|
| `--quiet` | Suppress per-file progress messages. |
|
|
76
76
|
| `--bake-svg` | Pre-render Mermaid / WaveDrom to inert SVG at generation time (HTML output only; needs Chromium). The output then contains no diagram JavaScript. |
|
|
77
|
+
| `--lang <tag>` | Set `<html lang>` (for example `en`, `zh-Hant`). Default: detected — `zh-Hant` when Chinese characters are at least 10% of the prose (code excluded), `ja` / `ko` when kana / Hangul make up at least 20% of the CJK characters, otherwise `en`. |
|
|
77
78
|
| `--version`, `-v` | Print version. |
|
|
78
79
|
| `--help`, `-h` | Print help. |
|
|
79
80
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -42,3 +42,24 @@ turndown, `@hpcc-js/wasm-graphviz`) are ordinary npm packages: they are
|
|
|
42
42
|
resolved into your `node_modules` by your own installer, carry their own
|
|
43
43
|
licence files there, and are not redistributed inside md2doc's tarball.
|
|
44
44
|
They are out of scope for this file.
|
|
45
|
+
|
|
46
|
+
## Lucide (icon path data)
|
|
47
|
+
|
|
48
|
+
The sidebar icons in `lib/md2doc.js` (`READER_ICONS`) use path data from
|
|
49
|
+
Lucide, https://lucide.dev — ISC License.
|
|
50
|
+
|
|
51
|
+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part
|
|
52
|
+
of Feather (MIT). All other copyright (c) for Lucide are held by Lucide
|
|
53
|
+
Contributors 2022.
|
|
54
|
+
|
|
55
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
56
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
57
|
+
copyright notice and this permission notice appear in all copies.
|
|
58
|
+
|
|
59
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
60
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
61
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
62
|
+
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
63
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
64
|
+
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
65
|
+
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/bin/md2doc.js
CHANGED
|
@@ -32,6 +32,7 @@ function printHelp() {
|
|
|
32
32
|
' --open Launch the platform viewer after render (default when --out is absent).',
|
|
33
33
|
' --no-open Skip the viewer launch.',
|
|
34
34
|
' --quiet Suppress per-file progress messages.',
|
|
35
|
+
' --lang <tag> Set <html lang> (default: detected — zh-Hant for Chinese-heavy text, ja / ko for Japanese / Korean, else en).',
|
|
35
36
|
' --bake-svg Pre-render mermaid/wavedrom to inert SVG (HTML output only; needs Chromium).',
|
|
36
37
|
' --edit Serve listed .md files in the browser editor (localhost).',
|
|
37
38
|
' --port <n> Pin the editor server port (only with --edit).',
|
|
@@ -51,6 +52,7 @@ function parseArgs(argv) {
|
|
|
51
52
|
let bakeSvg = false;
|
|
52
53
|
let edit = false;
|
|
53
54
|
let port = null;
|
|
55
|
+
let lang = null;
|
|
54
56
|
|
|
55
57
|
for (let i = 0; i < argv.length; i++) {
|
|
56
58
|
const a = argv[i];
|
|
@@ -96,6 +98,19 @@ function parseArgs(argv) {
|
|
|
96
98
|
out = argv[i];
|
|
97
99
|
continue;
|
|
98
100
|
}
|
|
101
|
+
if (a === '--lang') {
|
|
102
|
+
i++;
|
|
103
|
+
if (i >= argv.length) {
|
|
104
|
+
process.stderr.write('error: --lang requires a value\n');
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
if (!/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(argv[i])) {
|
|
108
|
+
process.stderr.write('error: --lang must be a language tag such as en or zh-Hant\n');
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
lang = argv[i];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
99
114
|
if (a.startsWith('-')) {
|
|
100
115
|
process.stderr.write('error: unknown flag ' + a + '\n');
|
|
101
116
|
process.exit(2);
|
|
@@ -124,7 +139,7 @@ function parseArgs(argv) {
|
|
|
124
139
|
open = (out === null);
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
return { inputs, formats, formatsExplicit, out, open, quiet, bakeSvg, edit, port };
|
|
142
|
+
return { inputs, formats, formatsExplicit, out, open, quiet, bakeSvg, edit, port, lang };
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
function shortHash(absPath) {
|
|
@@ -232,8 +247,8 @@ function main() {
|
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
if (args.edit) {
|
|
235
|
-
if (args.formatsExplicit || args.out !== null || args.bakeSvg) {
|
|
236
|
-
process.stderr.write('error: --edit cannot be combined with --html/--pdf/--out/--bake-svg\n');
|
|
250
|
+
if (args.formatsExplicit || args.out !== null || args.bakeSvg || args.lang !== null) {
|
|
251
|
+
process.stderr.write('error: --edit cannot be combined with --html/--pdf/--out/--bake-svg/--lang\n');
|
|
237
252
|
process.exit(2);
|
|
238
253
|
}
|
|
239
254
|
for (const input of args.inputs) {
|
|
@@ -262,6 +277,7 @@ function main() {
|
|
|
262
277
|
const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
|
|
263
278
|
const childArgs = [LIB, input, output];
|
|
264
279
|
if (args.bakeSvg) childArgs.push('--bake-svg');
|
|
280
|
+
if (args.lang) childArgs.push('--lang=' + args.lang);
|
|
265
281
|
const r = spawnSync(process.execPath, childArgs, { stdio });
|
|
266
282
|
if (r.status !== 0) {
|
|
267
283
|
process.stderr.write('error: render failed for ' + input + ' (exit ' + r.status + ')\n');
|
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++) {
|
|
@@ -1609,6 +1700,10 @@ ${itemsHtml}
|
|
|
1609
1700
|
reader-mode output never includes it, so the mobile drawer toggle there
|
|
1610
1701
|
is unaffected. */
|
|
1611
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; }
|
|
1612
1707
|
/* T11-2 step 5: enterSourceMode() (lib/editor/client.js) sets
|
|
1613
1708
|
contentEl.hidden = true and mounts a plain textarea in its place, and
|
|
1614
1709
|
setDocMode() writes docMode onto body as data-ed-mode. Measured on that
|
|
@@ -2137,15 +2232,24 @@ ${itemsHtml}
|
|
|
2137
2232
|
overflow-x: auto; font-family: monospace; font-size: 12px;
|
|
2138
2233
|
}`;
|
|
2139
2234
|
|
|
2140
|
-
|
|
2141
|
-
|
|
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}">
|
|
2142
2246
|
<head>
|
|
2143
2247
|
<meta charset="UTF-8">
|
|
2144
2248
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2145
2249
|
<title>${title}</title>
|
|
2146
2250
|
<style>
|
|
2147
2251
|
body {
|
|
2148
|
-
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;
|
|
2149
2253
|
margin: 0;
|
|
2150
2254
|
padding: 0;
|
|
2151
2255
|
line-height: 1.65;
|
|
@@ -2174,7 +2278,9 @@ ${itemsHtml}
|
|
|
2174
2278
|
min-width: 0;
|
|
2175
2279
|
height: calc(100vh - 48px);
|
|
2176
2280
|
overflow: hidden;
|
|
2177
|
-
padding
|
|
2281
|
+
padding: 12px 8px 12px 12px;
|
|
2282
|
+
background: #f3f5f7;
|
|
2283
|
+
border-radius: 10px;
|
|
2178
2284
|
box-sizing: border-box;
|
|
2179
2285
|
display: flex;
|
|
2180
2286
|
flex-direction: column;
|
|
@@ -2184,7 +2290,7 @@ ${itemsHtml}
|
|
|
2184
2290
|
flex-basis: 36px;
|
|
2185
2291
|
width: 36px;
|
|
2186
2292
|
min-width: 36px;
|
|
2187
|
-
padding
|
|
2293
|
+
padding: 12px 4px;
|
|
2188
2294
|
}
|
|
2189
2295
|
body[data-toc-collapsed] .reader-tools,
|
|
2190
2296
|
body[data-toc-collapsed] .search-results,
|
|
@@ -2240,26 +2346,13 @@ ${itemsHtml}
|
|
|
2240
2346
|
display: none;
|
|
2241
2347
|
}
|
|
2242
2348
|
#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
2349
|
transition: transform 0.15s ease;
|
|
2254
2350
|
}
|
|
2255
|
-
#toc-collapse-toggle:hover {
|
|
2256
|
-
background: #eef2f6;
|
|
2257
|
-
color: #24292e;
|
|
2258
|
-
}
|
|
2259
2351
|
@media (max-width: 1080px) {
|
|
2260
2352
|
#toc-collapse-toggle { display: none; }
|
|
2261
2353
|
}
|
|
2262
2354
|
.reader-tools { flex: 0 0 auto; }
|
|
2355
|
+
.mobile-bar { display: none; }
|
|
2263
2356
|
.sidebar-toggle {
|
|
2264
2357
|
display: none;
|
|
2265
2358
|
position: fixed;
|
|
@@ -2304,65 +2397,91 @@ ${itemsHtml}
|
|
|
2304
2397
|
.reader-tools,
|
|
2305
2398
|
.search-results,
|
|
2306
2399
|
.toc {
|
|
2307
|
-
border:
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
padding: 12px 14px;
|
|
2400
|
+
border: none;
|
|
2401
|
+
background: transparent;
|
|
2402
|
+
padding: 2px 0;
|
|
2311
2403
|
}
|
|
2312
|
-
.reader-search-label
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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;
|
|
2320
2415
|
}
|
|
2321
2416
|
.reader-search-row {
|
|
2417
|
+
position: relative;
|
|
2322
2418
|
display: flex;
|
|
2323
|
-
|
|
2324
|
-
margin-bottom: 8px;
|
|
2419
|
+
margin-bottom: 4px;
|
|
2325
2420
|
}
|
|
2326
2421
|
.reader-search-row input[type="search"] {
|
|
2327
2422
|
flex: 1 1 auto;
|
|
2328
2423
|
min-width: 0;
|
|
2329
|
-
padding:
|
|
2424
|
+
padding: 7px 30px 7px 10px;
|
|
2330
2425
|
border: 1px solid #d0d7de;
|
|
2331
2426
|
border-radius: 6px;
|
|
2427
|
+
background: #ffffff;
|
|
2332
2428
|
font: inherit;
|
|
2333
2429
|
}
|
|
2334
|
-
.reader-search-row button {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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;
|
|
2341
2448
|
cursor: pointer;
|
|
2342
2449
|
}
|
|
2343
|
-
|
|
2344
|
-
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;
|
|
2345
2456
|
}
|
|
2346
2457
|
.search-results-header,
|
|
2347
2458
|
.toc-header {
|
|
2348
2459
|
display: flex;
|
|
2349
|
-
flex-direction:
|
|
2350
|
-
align-items:
|
|
2351
|
-
gap:
|
|
2460
|
+
flex-direction: row;
|
|
2461
|
+
align-items: center;
|
|
2462
|
+
gap: 6px;
|
|
2352
2463
|
margin-bottom: 8px;
|
|
2353
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
|
+
}
|
|
2354
2473
|
.toc-header-actions {
|
|
2355
2474
|
flex: 0 0 auto;
|
|
2356
2475
|
display: flex;
|
|
2357
|
-
gap:
|
|
2476
|
+
gap: 2px;
|
|
2358
2477
|
justify-content: flex-end;
|
|
2359
2478
|
}
|
|
2360
|
-
/*
|
|
2361
|
-
|
|
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. */
|
|
2362
2481
|
.toc-breadcrumb {
|
|
2363
2482
|
flex: 1 1 auto;
|
|
2364
2483
|
min-width: 0;
|
|
2365
|
-
display:
|
|
2484
|
+
display: none;
|
|
2366
2485
|
flex-direction: column;
|
|
2367
2486
|
gap: 1px;
|
|
2368
2487
|
}
|
|
@@ -2387,13 +2506,14 @@ ${itemsHtml}
|
|
|
2387
2506
|
}
|
|
2388
2507
|
.search-results-title,
|
|
2389
2508
|
.toc-title {
|
|
2390
|
-
font-size: 0.
|
|
2391
|
-
font-weight:
|
|
2392
|
-
|
|
2393
|
-
text-transform: uppercase;
|
|
2394
|
-
color: #57606a;
|
|
2509
|
+
font-size: 0.86rem;
|
|
2510
|
+
font-weight: 600;
|
|
2511
|
+
color: #1f2328;
|
|
2395
2512
|
flex: 0 0 auto;
|
|
2396
2513
|
}
|
|
2514
|
+
body[data-toc-collapsed] .toc-title {
|
|
2515
|
+
display: none;
|
|
2516
|
+
}
|
|
2397
2517
|
.reader-status {
|
|
2398
2518
|
flex: 1 1 auto;
|
|
2399
2519
|
font-size: 0.82em;
|
|
@@ -2401,20 +2521,21 @@ ${itemsHtml}
|
|
|
2401
2521
|
}
|
|
2402
2522
|
.search-results-header button,
|
|
2403
2523
|
.toc-header button {
|
|
2404
|
-
|
|
2524
|
+
display: inline-grid;
|
|
2525
|
+
place-items: center;
|
|
2526
|
+
padding: 4px;
|
|
2405
2527
|
font: inherit;
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
border: 1px solid #d0d7de;
|
|
2528
|
+
line-height: 0;
|
|
2529
|
+
border: none;
|
|
2409
2530
|
border-radius: 6px;
|
|
2410
|
-
background:
|
|
2411
|
-
color: #
|
|
2531
|
+
background: transparent;
|
|
2532
|
+
color: #59636e;
|
|
2412
2533
|
cursor: pointer;
|
|
2413
2534
|
}
|
|
2414
2535
|
.search-results-header button:hover,
|
|
2415
2536
|
.toc-header button:hover {
|
|
2416
|
-
background: #
|
|
2417
|
-
color: #
|
|
2537
|
+
background: #e3e8ee;
|
|
2538
|
+
color: #1f2328;
|
|
2418
2539
|
}
|
|
2419
2540
|
.search-results-header button:disabled {
|
|
2420
2541
|
opacity: 0.45;
|
|
@@ -2460,8 +2581,8 @@ ${itemsHtml}
|
|
|
2460
2581
|
background: #eef2f6;
|
|
2461
2582
|
}
|
|
2462
2583
|
.search-result-item.is-active {
|
|
2463
|
-
background: #
|
|
2464
|
-
box-shadow:
|
|
2584
|
+
background: #dbe6f3;
|
|
2585
|
+
box-shadow: none;
|
|
2465
2586
|
}
|
|
2466
2587
|
.search-result-title {
|
|
2467
2588
|
font-weight: 600;
|
|
@@ -2510,6 +2631,8 @@ ${itemsHtml}
|
|
|
2510
2631
|
the shift+wheel handler; appears only when a title actually overflows. */
|
|
2511
2632
|
overflow-x: auto;
|
|
2512
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);
|
|
2513
2636
|
}
|
|
2514
2637
|
.toc ul {
|
|
2515
2638
|
list-style: none;
|
|
@@ -2534,13 +2657,15 @@ ${itemsHtml}
|
|
|
2534
2657
|
min-width: 100%;
|
|
2535
2658
|
color: #57606a;
|
|
2536
2659
|
text-decoration: none;
|
|
2537
|
-
padding: 2px
|
|
2660
|
+
padding: 2px 8px;
|
|
2538
2661
|
line-height: 1.4;
|
|
2539
2662
|
white-space: nowrap;
|
|
2540
2663
|
box-sizing: border-box;
|
|
2664
|
+
border-radius: 4px;
|
|
2541
2665
|
}
|
|
2542
2666
|
.toc summary > a {
|
|
2543
|
-
flex:
|
|
2667
|
+
flex: 1 0 auto;
|
|
2668
|
+
min-width: 0;
|
|
2544
2669
|
}
|
|
2545
2670
|
.toc summary {
|
|
2546
2671
|
min-width: 0;
|
|
@@ -2550,17 +2675,41 @@ ${itemsHtml}
|
|
|
2550
2675
|
}
|
|
2551
2676
|
.toc a:hover {
|
|
2552
2677
|
color: #0969da;
|
|
2678
|
+
background: #e8edf2;
|
|
2553
2679
|
}
|
|
2554
2680
|
.toc a.is-active {
|
|
2555
|
-
color: #
|
|
2556
|
-
font-weight:
|
|
2681
|
+
color: #0550ae;
|
|
2682
|
+
font-weight: 600;
|
|
2683
|
+
background: #dbe6f3;
|
|
2557
2684
|
}
|
|
2558
2685
|
.toc a.is-match {
|
|
2559
|
-
color:
|
|
2560
|
-
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;
|
|
2561
2698
|
border-radius: 4px;
|
|
2562
|
-
|
|
2563
|
-
|
|
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;
|
|
2564
2713
|
}
|
|
2565
2714
|
.toc details {
|
|
2566
2715
|
margin: 0;
|
|
@@ -2582,6 +2731,8 @@ ${itemsHtml}
|
|
|
2582
2731
|
font-size: 0.78em;
|
|
2583
2732
|
line-height: 1.8;
|
|
2584
2733
|
flex: 0 0 auto;
|
|
2734
|
+
width: 12px;
|
|
2735
|
+
text-align: center;
|
|
2585
2736
|
transform: translateY(1px);
|
|
2586
2737
|
}
|
|
2587
2738
|
.toc details[open] > summary::before {
|
|
@@ -2593,15 +2744,15 @@ ${itemsHtml}
|
|
|
2593
2744
|
border-left: 1px solid #d8dee4;
|
|
2594
2745
|
}
|
|
2595
2746
|
.toc-item:not(.toc-parent) > a {
|
|
2596
|
-
padding-left:
|
|
2747
|
+
padding-left: 26px;
|
|
2597
2748
|
}
|
|
2598
2749
|
.toc-list-level-1 > .toc-item > a,
|
|
2599
2750
|
.toc-list-level-1 > .toc-item > details > summary > a {
|
|
2600
|
-
font-
|
|
2751
|
+
font-size: 0.95em;
|
|
2601
2752
|
}
|
|
2602
2753
|
.toc-list-level-2 > .toc-item > a,
|
|
2603
2754
|
.toc-list-level-2 > .toc-item > details > summary > a {
|
|
2604
|
-
font-size: 0.
|
|
2755
|
+
font-size: 0.9em;
|
|
2605
2756
|
}
|
|
2606
2757
|
.toc-list-level-3 > .toc-item > a,
|
|
2607
2758
|
.toc-list-level-3 > .toc-item > details > summary > a,
|
|
@@ -2633,6 +2784,19 @@ ${itemsHtml}
|
|
|
2633
2784
|
}
|
|
2634
2785
|
.content > * { max-width: 100%; }
|
|
2635
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; }
|
|
2636
2800
|
.heading-with-anchor {
|
|
2637
2801
|
position: relative;
|
|
2638
2802
|
}
|
|
@@ -2653,14 +2817,15 @@ ${itemsHtml}
|
|
|
2653
2817
|
color: #0969da;
|
|
2654
2818
|
}
|
|
2655
2819
|
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.
|
|
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; }
|
|
2659
2824
|
code {
|
|
2660
2825
|
background: #f6f8fa;
|
|
2661
2826
|
padding: 2px 5px;
|
|
2662
2827
|
border-radius: 3px;
|
|
2663
|
-
font-family: "
|
|
2828
|
+
font-family: "Cascadia Mono", Consolas, "SFMono-Regular", "Liberation Mono", Menlo, "Microsoft JhengHei", monospace;
|
|
2664
2829
|
font-size: 0.875em;
|
|
2665
2830
|
}
|
|
2666
2831
|
pre {
|
|
@@ -2671,6 +2836,13 @@ ${itemsHtml}
|
|
|
2671
2836
|
line-height: 1.45;
|
|
2672
2837
|
}
|
|
2673
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
|
+
}
|
|
2674
2846
|
table {
|
|
2675
2847
|
display: block;
|
|
2676
2848
|
border-collapse: collapse;
|
|
@@ -2680,9 +2852,8 @@ ${itemsHtml}
|
|
|
2680
2852
|
font-size: 0.9em;
|
|
2681
2853
|
overflow-x: auto;
|
|
2682
2854
|
}
|
|
2683
|
-
th, td { border: 1px solid #
|
|
2684
|
-
th {
|
|
2685
|
-
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; }
|
|
2686
2857
|
.content table th,
|
|
2687
2858
|
.content table td {
|
|
2688
2859
|
overflow-wrap: normal;
|
|
@@ -2695,7 +2866,7 @@ ${itemsHtml}
|
|
|
2695
2866
|
.content table th code {
|
|
2696
2867
|
white-space: nowrap;
|
|
2697
2868
|
}
|
|
2698
|
-
.content table { table-layout: auto; }
|
|
2869
|
+
.content table { table-layout: auto; font-variant-numeric: tabular-nums; }
|
|
2699
2870
|
.content table col.col-narrow { width: 1%; }
|
|
2700
2871
|
.content table col.col-prose { width: auto; }
|
|
2701
2872
|
.content table th.cell-narrow,
|
|
@@ -2708,10 +2879,7 @@ ${itemsHtml}
|
|
|
2708
2879
|
background: #ffffff;
|
|
2709
2880
|
}
|
|
2710
2881
|
.content table thead th:first-child {
|
|
2711
|
-
background: #
|
|
2712
|
-
}
|
|
2713
|
-
.content table tbody tr:nth-child(even) td:first-child {
|
|
2714
|
-
background: #fafbfc;
|
|
2882
|
+
background: #ffffff;
|
|
2715
2883
|
}
|
|
2716
2884
|
blockquote {
|
|
2717
2885
|
border-left: 4px solid #dfe2e5;
|
|
@@ -2925,21 +3093,59 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2925
3093
|
html[data-lightbox-open] { overflow: hidden; }
|
|
2926
3094
|
@media print { .lightbox { display: none !important; } }
|
|
2927
3095
|
@media (max-width: 1080px) {
|
|
3096
|
+
.content table td.cell-prose,
|
|
3097
|
+
.content table th.cell-prose { min-width: 15em; }
|
|
2928
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
|
+
}
|
|
2929
3135
|
.page-layout {
|
|
2930
3136
|
display: block;
|
|
2931
3137
|
max-width: 100%;
|
|
2932
|
-
padding-top: 60px;
|
|
2933
3138
|
}
|
|
2934
3139
|
.reader-sidebar {
|
|
2935
3140
|
position: fixed;
|
|
2936
|
-
top:
|
|
3141
|
+
top: 44px;
|
|
2937
3142
|
left: 0;
|
|
2938
3143
|
bottom: 0;
|
|
2939
3144
|
width: 85%;
|
|
2940
3145
|
max-width: 360px;
|
|
2941
|
-
height: 100vh;
|
|
2942
|
-
background: #
|
|
3146
|
+
height: calc(100vh - 44px);
|
|
3147
|
+
background: #f3f5f7;
|
|
3148
|
+
border-radius: 0;
|
|
2943
3149
|
z-index: 99;
|
|
2944
3150
|
transform: translateX(-100%);
|
|
2945
3151
|
transition: transform 0.2s ease;
|
|
@@ -2949,10 +3155,9 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2949
3155
|
display: flex;
|
|
2950
3156
|
flex-direction: column;
|
|
2951
3157
|
gap: 12px;
|
|
2952
|
-
box-shadow: 2px 0 12px rgba(0,0,0,0.15);
|
|
2953
3158
|
flex: initial;
|
|
2954
3159
|
}
|
|
2955
|
-
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); }
|
|
2956
3161
|
/* Scroll lock for the drawer, same two-rule shape the lightbox uses a
|
|
2957
3162
|
few lines above (body[data-lightbox-open] / html[data-lightbox-open])
|
|
2958
3163
|
and for the identical reason: 'html, body { overflow-x: clip }' keeps
|
|
@@ -2969,7 +3174,6 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2969
3174
|
body[data-sidebar-open] { overflow: hidden; }
|
|
2970
3175
|
html[data-sidebar-open] { overflow: hidden; }
|
|
2971
3176
|
.sidebar-splitter { display: none; }
|
|
2972
|
-
.heading-anchor { opacity: 1; }
|
|
2973
3177
|
}
|
|
2974
3178
|
@media print {
|
|
2975
3179
|
body { font-size: 11pt; }
|
|
@@ -2994,10 +3198,19 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
2994
3198
|
padding: 0 10px;
|
|
2995
3199
|
}
|
|
2996
3200
|
.reader-sidebar,
|
|
3201
|
+
.mobile-bar,
|
|
2997
3202
|
.sidebar-toggle,
|
|
2998
3203
|
.sidebar-splitter,
|
|
2999
3204
|
.sidebar-scrim { display: none !important; }
|
|
3000
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; }
|
|
3001
3214
|
.heading-anchor { display: none; }
|
|
3002
3215
|
pre { font-size: 9pt; }
|
|
3003
3216
|
a[href]:after { content: none; }
|
|
@@ -3015,18 +3228,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3015
3228
|
.ed-te-hl).
|
|
3016
3229
|
|
|
3017
3230
|
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
|
|
3231
|
+
the whole problem: <pre> (#f6f8fa) and the sticky first column (#ffffff)
|
|
3020
3232
|
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
|
-
|
|
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.
|
|
3030
3242
|
(No backticks in this comment: it lives inside a JS template literal.)
|
|
3031
3243
|
|
|
3032
3244
|
:focus outline is suppressed because the roving tabindex below focuses a
|
|
@@ -3723,8 +3935,7 @@ ${opts.editMode ? editModeLayoutCss : ''}
|
|
|
3723
3935
|
${usesMath ? buildKatexStyleTag() : ''}
|
|
3724
3936
|
</head>
|
|
3725
3937
|
<body>
|
|
3726
|
-
<
|
|
3727
|
-
<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3938
|
+
${mobileBarHtml}<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
3728
3939
|
<div class="page-layout">
|
|
3729
3940
|
${tocHtml}
|
|
3730
3941
|
<main class="content">
|
|
@@ -3812,11 +4023,15 @@ ${mermaidInitTag}`)
|
|
|
3812
4023
|
}
|
|
3813
4024
|
|
|
3814
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;
|
|
3815
4028
|
const sectionIndexById = new Map(sections.map((section, i) => [section.id, i]));
|
|
3816
4029
|
|
|
3817
4030
|
// Sticky breadcrumb: the ancestor chain (shallow→deep) of the active section.
|
|
3818
4031
|
// For each decreasing depth, take the nearest preceding section, then append
|
|
3819
|
-
// 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.
|
|
3820
4035
|
function renderBreadcrumb(sectionId) {
|
|
3821
4036
|
if (!breadcrumbEl) return;
|
|
3822
4037
|
if (!sectionIndexById.has(sectionId)) {
|
|
@@ -3837,6 +4052,7 @@ ${mermaidInitTag}`)
|
|
|
3837
4052
|
return;
|
|
3838
4053
|
}
|
|
3839
4054
|
const minDepth = chain[0].depth;
|
|
4055
|
+
if (mobileBarTitle) mobileBarTitle.textContent = chain[chain.length - 1].title;
|
|
3840
4056
|
breadcrumbEl.innerHTML = chain
|
|
3841
4057
|
.map((section, i) => {
|
|
3842
4058
|
const indent = (section.depth - minDepth) * 10;
|
|
@@ -4416,18 +4632,30 @@ ${mermaidInitTag}`)
|
|
|
4416
4632
|
var hit = document.elementFromPoint(rect.left + rect.width / 2, ANCHOR_LINE);
|
|
4417
4633
|
var node = (hit && hit.closest) ? hit.closest('main.content > *') : null;
|
|
4418
4634
|
if (!node) {
|
|
4419
|
-
//
|
|
4420
|
-
//
|
|
4421
|
-
//
|
|
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;
|
|
4422
4641
|
var lo = 0;
|
|
4423
|
-
var hi =
|
|
4642
|
+
var hi = kids.length - 1;
|
|
4424
4643
|
while (lo <= hi) {
|
|
4425
4644
|
var mid = (lo + hi) >> 1;
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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) {
|
|
4430
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;
|
|
4431
4659
|
}
|
|
4432
4660
|
}
|
|
4433
4661
|
}
|
|
@@ -5656,6 +5884,42 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
5656
5884
|
}
|
|
5657
5885
|
module.exports = { renderMarkdown };
|
|
5658
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
|
+
|
|
5659
5923
|
// Render every deferred dot/graphviz placeholder to inline SVG using the
|
|
5660
5924
|
// in-process WASM engine. Loads the WASM module only when at least one dot
|
|
5661
5925
|
// block exists, so text-only docs pay nothing.
|
|
@@ -5672,11 +5936,35 @@ async function bakeGraphviz(htmlStr) {
|
|
|
5672
5936
|
return htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, (m, b64) => {
|
|
5673
5937
|
const dotSrc = Buffer.from(b64, 'base64').toString('utf8');
|
|
5674
5938
|
try {
|
|
5675
|
-
|
|
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
|
|
5676
5958
|
.replace(/<\?xml[^>]*\?>/g, '')
|
|
5677
5959
|
.replace(/<!DOCTYPE[\s\S]*?>/g, '')
|
|
5678
5960
|
// drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
|
|
5679
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"')
|
|
5680
5968
|
.trim();
|
|
5681
5969
|
return `<div class="graphviz">${svg}</div>`;
|
|
5682
5970
|
} catch (e) {
|
|
@@ -5838,6 +6126,7 @@ async function bakeDiagrams(htmlStr, dstPath) {
|
|
|
5838
6126
|
if (require.main === module) {
|
|
5839
6127
|
const [,, src, dst] = process.argv;
|
|
5840
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;
|
|
5841
6130
|
if (!src || !dst) {
|
|
5842
6131
|
console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
|
|
5843
6132
|
process.exit(1);
|
|
@@ -5847,7 +6136,7 @@ if (require.main === module) {
|
|
|
5847
6136
|
|
|
5848
6137
|
(async () => {
|
|
5849
6138
|
const mdText = fs.readFileSync(src, 'utf8');
|
|
5850
|
-
const { html } = await renderMarkdown(mdText, path.resolve(src), {});
|
|
6139
|
+
const { html } = await renderMarkdown(mdText, path.resolve(src), { lang: LANG_ARG });
|
|
5851
6140
|
let finalHtml = html;
|
|
5852
6141
|
|
|
5853
6142
|
if (ext === '.html') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"preinstall": "node scripts/preinstall.js",
|
|
48
|
-
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/docsource.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/wave-rail-clearance.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js"
|
|
48
|
+
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/docsource.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/wave-rail-clearance.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js && node test/reader-design.test.js",
|
|
49
|
+
"test:browser": "node test/reader-click.test.js"
|
|
49
50
|
},
|
|
50
51
|
"repository": {
|
|
51
52
|
"type": "git",
|
|
@@ -57,5 +58,8 @@
|
|
|
57
58
|
"license": "MIT AND Apache-2.0",
|
|
58
59
|
"publishConfig": {
|
|
59
60
|
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"playwright": "1.63.0"
|
|
60
64
|
}
|
|
61
65
|
}
|