@helping-ai-workflow/md2doc 1.0.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/LICENSE +21 -0
- package/README.md +64 -0
- package/bin/md2html.js +134 -0
- package/bin/md2pdf.js +133 -0
- package/lib/md2doc.js +1351 -0
- package/package.json +38 -0
package/lib/md2doc.js
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* md2doc.js — Markdown → HTML / PDF
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - WaveDrom timing diagrams (```wavedrom blocks)
|
|
7
|
+
* - Mermaid diagrams (```mermaid blocks)
|
|
8
|
+
* - GFM tables, code blocks, blockquotes
|
|
9
|
+
*
|
|
10
|
+
* Dependencies:
|
|
11
|
+
* npm install marked # markdown parser
|
|
12
|
+
* npm install puppeteer # PDF only — downloads Chromium (~170MB)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node md2doc.js <input.md> <output.html>
|
|
16
|
+
* node md2doc.js <input.md> <output.pdf>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const [,, src, dst] = process.argv;
|
|
26
|
+
if (!src || !dst) {
|
|
27
|
+
console.error('Usage: node md2doc.js <input.md> <output.html|pdf>');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ext = path.extname(dst).toLowerCase();
|
|
32
|
+
const md = fs.readFileSync(src, 'utf8');
|
|
33
|
+
|
|
34
|
+
function firstExistingPath(candidates) {
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (!candidate) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(candidate)) {
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Ignore invalid candidates and continue probing fallbacks.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeResolve(modulePath) {
|
|
51
|
+
try {
|
|
52
|
+
return require.resolve(modulePath);
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function inlineScriptTag(sourcePath) {
|
|
59
|
+
if (!sourcePath) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return `<script type="text/javascript">\n${fs.readFileSync(sourcePath, 'utf8')}\n</script>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const localWaveDromSkin = firstExistingPath([
|
|
66
|
+
process.env.WAVEDROM_SKIN_JS,
|
|
67
|
+
safeResolve('wavedrom/skins/default.js'),
|
|
68
|
+
'/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/wavedrom/skins/default.js',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const localWaveDromJs = firstExistingPath([
|
|
72
|
+
process.env.WAVEDROM_JS,
|
|
73
|
+
safeResolve('wavedrom/wavedrom.min.js'),
|
|
74
|
+
'/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/wavedrom/wavedrom.min.js',
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const localMermaidJs = firstExistingPath([
|
|
78
|
+
process.env.MERMAID_JS,
|
|
79
|
+
safeResolve('mermaid/dist/mermaid.min.js'),
|
|
80
|
+
'/home/user/.vscode-server/extensions/shd101wyy.markdown-preview-enhanced-0.8.22/crossnote/dependencies/mermaid/mermaid.min.js',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const waveDromSkinTag = inlineScriptTag(localWaveDromSkin)
|
|
84
|
+
|| '<script src="https://cdn.jsdelivr.net/npm/wavedrom/skins/default.js" type="text/javascript"></script>';
|
|
85
|
+
|
|
86
|
+
const waveDromTag = inlineScriptTag(localWaveDromJs)
|
|
87
|
+
|| '<script src="https://cdn.jsdelivr.net/npm/wavedrom/wavedrom.min.js" type="text/javascript"></script>';
|
|
88
|
+
|
|
89
|
+
const mermaidScriptTag = inlineScriptTag(localMermaidJs)
|
|
90
|
+
|| `<script type="module">
|
|
91
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
92
|
+
mermaid.initialize({ startOnLoad: true, theme: 'default' });
|
|
93
|
+
</script>`;
|
|
94
|
+
|
|
95
|
+
const mermaidInitTag = localMermaidJs
|
|
96
|
+
? `<script type="text/javascript">
|
|
97
|
+
if (typeof mermaid !== 'undefined') {
|
|
98
|
+
mermaid.initialize({ startOnLoad: true, theme: 'default' });
|
|
99
|
+
}
|
|
100
|
+
</script>`
|
|
101
|
+
: '';
|
|
102
|
+
|
|
103
|
+
// ── Markdown → HTML body ─────────────────────────────────────────────────────
|
|
104
|
+
// Use a custom renderer to intercept fenced code blocks before marked escapes
|
|
105
|
+
// their content. This is the correct approach — pre-processing the raw markdown
|
|
106
|
+
// string causes marked to re-parse the injected HTML and mangle indented lines.
|
|
107
|
+
|
|
108
|
+
let bodyHtml;
|
|
109
|
+
let tocHtml = '';
|
|
110
|
+
let serializedSections = '[]';
|
|
111
|
+
try {
|
|
112
|
+
const { marked, Renderer } = require('marked');
|
|
113
|
+
|
|
114
|
+
const renderer = new Renderer();
|
|
115
|
+
const tocItems = [];
|
|
116
|
+
const slugCounts = new Map();
|
|
117
|
+
const sections = [];
|
|
118
|
+
let currentSection = null;
|
|
119
|
+
|
|
120
|
+
function startSection({ id, depth, text }) {
|
|
121
|
+
currentSection = {
|
|
122
|
+
id,
|
|
123
|
+
depth,
|
|
124
|
+
title: text,
|
|
125
|
+
searchTextParts: [text],
|
|
126
|
+
};
|
|
127
|
+
sections.push(currentSection);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendSectionText(value) {
|
|
131
|
+
if (!currentSection || !value) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const clean = stripHtmlTags(value).replace(/\s+/g, ' ').trim();
|
|
135
|
+
if (clean) {
|
|
136
|
+
currentSection.searchTextParts.push(clean);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function collectCellText(cells) {
|
|
141
|
+
if (!Array.isArray(cells)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const cell of cells) {
|
|
145
|
+
if (cell && Array.isArray(cell.tokens)) {
|
|
146
|
+
appendSectionText(flattenTokenText(cell.tokens));
|
|
147
|
+
} else if (cell && typeof cell.text === 'string') {
|
|
148
|
+
appendSectionText(cell.text);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildTocTree(items) {
|
|
154
|
+
const root = [];
|
|
155
|
+
const stack = [{ depth: 0, children: root }];
|
|
156
|
+
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
const node = { ...item, children: [] };
|
|
159
|
+
while (stack.length > 1 && item.depth <= stack[stack.length - 1].depth) {
|
|
160
|
+
stack.pop();
|
|
161
|
+
}
|
|
162
|
+
stack[stack.length - 1].children.push(node);
|
|
163
|
+
stack.push(node);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return root;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderTocNodes(nodes, level = 1) {
|
|
170
|
+
if (!nodes.length) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const itemsHtml = nodes
|
|
175
|
+
.map((node) => {
|
|
176
|
+
const linkHtml = `<a href="#${node.id}">${escapeHtml(node.text)}</a>`;
|
|
177
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
178
|
+
|
|
179
|
+
if (!hasChildren) {
|
|
180
|
+
return `<li class="toc-item toc-level-${level}">${linkHtml}</li>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return `<li class="toc-item toc-level-${level} toc-parent">
|
|
184
|
+
<details>
|
|
185
|
+
<summary>${linkHtml}</summary>
|
|
186
|
+
${renderTocNodes(node.children, level + 1)}
|
|
187
|
+
</details>
|
|
188
|
+
</li>`;
|
|
189
|
+
})
|
|
190
|
+
.join('\n');
|
|
191
|
+
|
|
192
|
+
return `<ul class="toc-list toc-list-level-${level}">
|
|
193
|
+
${itemsHtml}
|
|
194
|
+
</ul>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function flattenTokenText(tokens) {
|
|
198
|
+
if (!Array.isArray(tokens)) {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
return tokens
|
|
202
|
+
.map((item) => {
|
|
203
|
+
if (item.type === 'link' || item.type === 'em' || item.type === 'strong' || item.type === 'del') {
|
|
204
|
+
return flattenTokenText(item.tokens);
|
|
205
|
+
}
|
|
206
|
+
if (item.type === 'codespan') {
|
|
207
|
+
return item.text || '';
|
|
208
|
+
}
|
|
209
|
+
if (item.tokens) {
|
|
210
|
+
return flattenTokenText(item.tokens);
|
|
211
|
+
}
|
|
212
|
+
return item.text || '';
|
|
213
|
+
})
|
|
214
|
+
.join('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function stripHtmlTags(value) {
|
|
218
|
+
return String(value || '').replace(/<[^>]*>/g, '');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function slugifyHeading(value) {
|
|
222
|
+
const base = stripHtmlTags(value)
|
|
223
|
+
.normalize('NFKD')
|
|
224
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
225
|
+
.trim()
|
|
226
|
+
.toLowerCase()
|
|
227
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
|
228
|
+
.replace(/^-+|-+$/g, '')
|
|
229
|
+
|| 'section';
|
|
230
|
+
const count = slugCounts.get(base) || 0;
|
|
231
|
+
slugCounts.set(base, count + 1);
|
|
232
|
+
return count === 0 ? base : `${base}-${count + 1}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function escapeHtml(value) {
|
|
236
|
+
return String(value || '')
|
|
237
|
+
.replace(/&/g, '&')
|
|
238
|
+
.replace(/</g, '<')
|
|
239
|
+
.replace(/>/g, '>')
|
|
240
|
+
.replace(/"/g, '"')
|
|
241
|
+
.replace(/'/g, ''');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
renderer.code = function(token) {
|
|
245
|
+
// token is either a string (old API) or {text, lang} object (new API)
|
|
246
|
+
const lang = (typeof token === 'object') ? (token.lang || '') : (arguments[1] || '');
|
|
247
|
+
const code = (typeof token === 'object') ? token.text : token;
|
|
248
|
+
|
|
249
|
+
if (lang === 'wavedrom') {
|
|
250
|
+
return `\n<script type="WaveDrom">\n${code}\n</script>\n`;
|
|
251
|
+
}
|
|
252
|
+
if (lang === 'mermaid') {
|
|
253
|
+
return `\n<div class="mermaid">\n${code}\n</div>\n`;
|
|
254
|
+
}
|
|
255
|
+
if (lang === 'dot' || lang === 'graphviz') {
|
|
256
|
+
const r = spawnSync('dot', ['-Tsvg'], { input: code, encoding: 'utf8', timeout: 10000 });
|
|
257
|
+
if (r.status === 0) {
|
|
258
|
+
// Strip XML declaration / DOCTYPE; keep only the <svg> element.
|
|
259
|
+
// Remove fixed width/height attrs so CSS max-width:100% + height:auto
|
|
260
|
+
// can scale the diagram to content width; viewBox preserves aspect ratio.
|
|
261
|
+
const svg = r.stdout
|
|
262
|
+
.replace(/<\?xml[^>]*\?>/g, '')
|
|
263
|
+
.replace(/<!DOCTYPE[^>]*>/g, '')
|
|
264
|
+
.replace(/(<svg\b[^>]*?)\s+width="[^"]*"/i, '$1')
|
|
265
|
+
.replace(/(<svg\b[^>]*?)\s+height="[^"]*"/i, '$1')
|
|
266
|
+
.trim();
|
|
267
|
+
return `\n<div class="graphviz">${svg}</div>\n`;
|
|
268
|
+
}
|
|
269
|
+
console.error('[WARN] dot render failed:', r.stderr);
|
|
270
|
+
// Fall through to default code block
|
|
271
|
+
}
|
|
272
|
+
// Default: syntax-highlighted code block
|
|
273
|
+
const escaped = code.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
274
|
+
return `<pre><code class="language-${lang}">${escaped}</code></pre>\n`;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
renderer.heading = function(token) {
|
|
278
|
+
const depth = Number(token.depth || 1);
|
|
279
|
+
const headingText = stripHtmlTags(flattenTokenText(token.tokens) || token.text || '');
|
|
280
|
+
const headingId = slugifyHeading(headingText);
|
|
281
|
+
const headingHtml = this.parser.parseInline(token.tokens);
|
|
282
|
+
|
|
283
|
+
tocItems.push({
|
|
284
|
+
depth,
|
|
285
|
+
id: headingId,
|
|
286
|
+
text: headingText,
|
|
287
|
+
});
|
|
288
|
+
startSection({ depth, id: headingId, text: headingText });
|
|
289
|
+
|
|
290
|
+
return `<h${depth} id="${headingId}" class="heading-with-anchor" data-reader-heading data-reader-depth="${depth}">${headingHtml}<a class="heading-anchor" href="#${headingId}" aria-label="Link to this section">#</a></h${depth}>\n`;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const baseParagraph = renderer.paragraph.bind(renderer);
|
|
294
|
+
renderer.paragraph = function(token) {
|
|
295
|
+
appendSectionText(flattenTokenText(token.tokens));
|
|
296
|
+
return baseParagraph(token);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const baseListitem = renderer.listitem.bind(renderer);
|
|
300
|
+
renderer.listitem = function(token) {
|
|
301
|
+
appendSectionText(flattenTokenText(token.tokens));
|
|
302
|
+
return baseListitem(token);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const baseBlockquote = renderer.blockquote.bind(renderer);
|
|
306
|
+
renderer.blockquote = function(token) {
|
|
307
|
+
appendSectionText(flattenTokenText(token.tokens));
|
|
308
|
+
return baseBlockquote(token);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const baseTable = renderer.table.bind(renderer);
|
|
312
|
+
renderer.table = function(token) {
|
|
313
|
+
collectCellText(token.header);
|
|
314
|
+
if (Array.isArray(token.rows)) {
|
|
315
|
+
for (const row of token.rows) {
|
|
316
|
+
collectCellText(row);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return baseTable(token);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
marked.setOptions({ gfm: true, breaks: false, renderer });
|
|
323
|
+
|
|
324
|
+
// Pre-process non-standard inline syntax before marked parses
|
|
325
|
+
const mdPre = md
|
|
326
|
+
.replace(/\^([^^]+)\^/g, '<sup>$1</sup>') // ^a^ → <sup>a</sup>
|
|
327
|
+
.replace(/~([^~]+)~/g, '<sub>$1</sub>'); // ~a~ → <sub>a</sub>
|
|
328
|
+
|
|
329
|
+
bodyHtml = marked.parse(mdPre);
|
|
330
|
+
serializedSections = JSON.stringify(
|
|
331
|
+
sections.map((section) => ({
|
|
332
|
+
id: section.id,
|
|
333
|
+
depth: section.depth,
|
|
334
|
+
title: section.title,
|
|
335
|
+
searchText: section.searchTextParts.join(' '),
|
|
336
|
+
}))
|
|
337
|
+
).replace(/</g, '\\u003c');
|
|
338
|
+
if (tocItems.length > 0) {
|
|
339
|
+
const tocTree = buildTocTree(tocItems);
|
|
340
|
+
tocHtml = `<aside class="reader-sidebar" data-reader-sidebar>
|
|
341
|
+
<section class="reader-tools">
|
|
342
|
+
<label class="reader-search-label" for="doc-search-input">Search this spec</label>
|
|
343
|
+
<div class="reader-search-row">
|
|
344
|
+
<input type="search" id="doc-search-input" placeholder="Enter keyword and press Enter">
|
|
345
|
+
<button id="doc-search-submit" type="button">Search</button>
|
|
346
|
+
<button id="doc-search-clear" type="button">Clear</button>
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
<section class="search-results" id="search-results" hidden>
|
|
350
|
+
<div class="search-results-header">
|
|
351
|
+
<span class="search-results-title">Results</span>
|
|
352
|
+
<span id="search-result-count" class="reader-status">0</span>
|
|
353
|
+
<button id="search-prev" type="button" disabled aria-label="Previous match">◀</button>
|
|
354
|
+
<button id="search-next" type="button" disabled aria-label="Next match">▶</button>
|
|
355
|
+
</div>
|
|
356
|
+
<div id="search-results-list"></div>
|
|
357
|
+
</section>
|
|
358
|
+
<nav class="toc" aria-label="Table of contents" data-reader-toc>
|
|
359
|
+
<div class="toc-header">
|
|
360
|
+
<span class="toc-title">Contents</span>
|
|
361
|
+
<button id="toc-expand-all" type="button" aria-label="Expand all">⊞</button>
|
|
362
|
+
<button id="toc-collapse-all" type="button" aria-label="Collapse all">⊟</button>
|
|
363
|
+
</div>
|
|
364
|
+
${renderTocNodes(tocTree)}
|
|
365
|
+
</nav>
|
|
366
|
+
</aside>`;
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.error('[ERROR] marked not found — install with: npm install marked');
|
|
370
|
+
console.error(e.message);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── HTML template ────────────────────────────────────────────────────────────
|
|
375
|
+
const title = path.basename(src, '.md');
|
|
376
|
+
|
|
377
|
+
const html = `<!DOCTYPE html>
|
|
378
|
+
<html lang="en">
|
|
379
|
+
<head>
|
|
380
|
+
<meta charset="UTF-8">
|
|
381
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
382
|
+
<title>${title}</title>
|
|
383
|
+
<style>
|
|
384
|
+
body {
|
|
385
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
386
|
+
margin: 0;
|
|
387
|
+
padding: 0;
|
|
388
|
+
line-height: 1.65;
|
|
389
|
+
color: #24292e;
|
|
390
|
+
font-size: 15px;
|
|
391
|
+
background: #ffffff;
|
|
392
|
+
}
|
|
393
|
+
html { scroll-behavior: auto; }
|
|
394
|
+
html, body { overflow-x: clip; }
|
|
395
|
+
.page-layout {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: flex-start;
|
|
398
|
+
gap: 32px;
|
|
399
|
+
margin: 0;
|
|
400
|
+
padding: 24px 24px 48px;
|
|
401
|
+
max-width: 100%;
|
|
402
|
+
box-sizing: border-box;
|
|
403
|
+
}
|
|
404
|
+
.reader-sidebar {
|
|
405
|
+
position: sticky;
|
|
406
|
+
top: 24px;
|
|
407
|
+
width: 320px;
|
|
408
|
+
height: calc(100vh - 48px);
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
flex: 0 0 320px;
|
|
411
|
+
padding-right: 8px;
|
|
412
|
+
box-sizing: border-box;
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
gap: 12px;
|
|
416
|
+
}
|
|
417
|
+
.reader-tools { flex: 0 0 auto; }
|
|
418
|
+
.sidebar-toggle {
|
|
419
|
+
display: none;
|
|
420
|
+
position: fixed;
|
|
421
|
+
top: 12px;
|
|
422
|
+
left: 12px;
|
|
423
|
+
z-index: 100;
|
|
424
|
+
background: #ffffff;
|
|
425
|
+
border: 1px solid #d0d7de;
|
|
426
|
+
border-radius: 8px;
|
|
427
|
+
padding: 6px 10px;
|
|
428
|
+
font-size: 1.1em;
|
|
429
|
+
line-height: 1;
|
|
430
|
+
cursor: pointer;
|
|
431
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
|
432
|
+
}
|
|
433
|
+
.sidebar-scrim {
|
|
434
|
+
display: none;
|
|
435
|
+
position: fixed;
|
|
436
|
+
inset: 0;
|
|
437
|
+
background: rgba(0,0,0,0.35);
|
|
438
|
+
z-index: 98;
|
|
439
|
+
}
|
|
440
|
+
body[data-sidebar-open] .sidebar-scrim { display: block; }
|
|
441
|
+
.reader-tools,
|
|
442
|
+
.search-results,
|
|
443
|
+
.toc {
|
|
444
|
+
border: 1px solid #d0d7de;
|
|
445
|
+
border-radius: 10px;
|
|
446
|
+
background: #f8fafc;
|
|
447
|
+
padding: 12px 14px;
|
|
448
|
+
}
|
|
449
|
+
.reader-search-label {
|
|
450
|
+
display: block;
|
|
451
|
+
font-size: 0.78rem;
|
|
452
|
+
font-weight: 700;
|
|
453
|
+
letter-spacing: 0.08em;
|
|
454
|
+
text-transform: uppercase;
|
|
455
|
+
color: #57606a;
|
|
456
|
+
margin-bottom: 6px;
|
|
457
|
+
}
|
|
458
|
+
.reader-search-row {
|
|
459
|
+
display: flex;
|
|
460
|
+
gap: 6px;
|
|
461
|
+
margin-bottom: 8px;
|
|
462
|
+
}
|
|
463
|
+
.reader-search-row input[type="search"] {
|
|
464
|
+
flex: 1 1 auto;
|
|
465
|
+
min-width: 0;
|
|
466
|
+
padding: 6px 8px;
|
|
467
|
+
border: 1px solid #d0d7de;
|
|
468
|
+
border-radius: 6px;
|
|
469
|
+
font: inherit;
|
|
470
|
+
}
|
|
471
|
+
.reader-search-row button {
|
|
472
|
+
padding: 4px 10px;
|
|
473
|
+
font: inherit;
|
|
474
|
+
font-size: 0.85em;
|
|
475
|
+
border: 1px solid #d0d7de;
|
|
476
|
+
border-radius: 6px;
|
|
477
|
+
background: #ffffff;
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
.reader-search-row button:hover {
|
|
481
|
+
background: #eef2f6;
|
|
482
|
+
}
|
|
483
|
+
.search-results-header,
|
|
484
|
+
.toc-header {
|
|
485
|
+
display: flex;
|
|
486
|
+
align-items: center;
|
|
487
|
+
gap: 6px;
|
|
488
|
+
margin-bottom: 8px;
|
|
489
|
+
}
|
|
490
|
+
.search-results-title,
|
|
491
|
+
.toc-title {
|
|
492
|
+
font-size: 0.78rem;
|
|
493
|
+
font-weight: 700;
|
|
494
|
+
letter-spacing: 0.08em;
|
|
495
|
+
text-transform: uppercase;
|
|
496
|
+
color: #57606a;
|
|
497
|
+
flex: 0 0 auto;
|
|
498
|
+
}
|
|
499
|
+
.reader-status {
|
|
500
|
+
flex: 1 1 auto;
|
|
501
|
+
font-size: 0.82em;
|
|
502
|
+
color: #57606a;
|
|
503
|
+
}
|
|
504
|
+
.search-results-header button,
|
|
505
|
+
.toc-header button {
|
|
506
|
+
padding: 2px 8px;
|
|
507
|
+
font: inherit;
|
|
508
|
+
font-size: 0.9em;
|
|
509
|
+
line-height: 1;
|
|
510
|
+
border: 1px solid #d0d7de;
|
|
511
|
+
border-radius: 6px;
|
|
512
|
+
background: #ffffff;
|
|
513
|
+
color: #57606a;
|
|
514
|
+
cursor: pointer;
|
|
515
|
+
}
|
|
516
|
+
.search-results-header button:hover,
|
|
517
|
+
.toc-header button:hover {
|
|
518
|
+
background: #eef2f6;
|
|
519
|
+
color: #24292e;
|
|
520
|
+
}
|
|
521
|
+
.search-results-header button:disabled {
|
|
522
|
+
opacity: 0.45;
|
|
523
|
+
cursor: default;
|
|
524
|
+
}
|
|
525
|
+
.toc-header .toc-title,
|
|
526
|
+
.search-results-header .search-results-title {
|
|
527
|
+
margin-right: auto;
|
|
528
|
+
}
|
|
529
|
+
.search-results[hidden] {
|
|
530
|
+
display: none;
|
|
531
|
+
}
|
|
532
|
+
.search-results:not([hidden]) {
|
|
533
|
+
display: flex;
|
|
534
|
+
flex-direction: column;
|
|
535
|
+
flex: 0 0 auto;
|
|
536
|
+
max-height: 50%;
|
|
537
|
+
min-height: 0;
|
|
538
|
+
}
|
|
539
|
+
.search-results-header { flex: 0 0 auto; }
|
|
540
|
+
#search-results-list {
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
gap: 6px;
|
|
544
|
+
flex: 1 1 auto;
|
|
545
|
+
min-height: 0;
|
|
546
|
+
overflow-y: auto;
|
|
547
|
+
}
|
|
548
|
+
.search-result-item {
|
|
549
|
+
width: 100%;
|
|
550
|
+
text-align: left;
|
|
551
|
+
border: 1px solid transparent;
|
|
552
|
+
border-radius: 8px;
|
|
553
|
+
background: #ffffff;
|
|
554
|
+
padding: 8px 10px;
|
|
555
|
+
cursor: pointer;
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
gap: 2px;
|
|
559
|
+
font: inherit;
|
|
560
|
+
}
|
|
561
|
+
.search-result-item:hover {
|
|
562
|
+
background: #eef2f6;
|
|
563
|
+
}
|
|
564
|
+
.search-result-item.is-active {
|
|
565
|
+
background: #dbeafe;
|
|
566
|
+
box-shadow: inset 0 0 0 1px #93c5fd;
|
|
567
|
+
}
|
|
568
|
+
.search-result-title {
|
|
569
|
+
font-weight: 600;
|
|
570
|
+
font-size: 0.92em;
|
|
571
|
+
color: #24292e;
|
|
572
|
+
}
|
|
573
|
+
.search-result-snippet {
|
|
574
|
+
font-size: 0.82em;
|
|
575
|
+
color: #57606a;
|
|
576
|
+
line-height: 1.35;
|
|
577
|
+
}
|
|
578
|
+
.search-empty {
|
|
579
|
+
margin: 0;
|
|
580
|
+
font-size: 0.85em;
|
|
581
|
+
color: #6a737d;
|
|
582
|
+
}
|
|
583
|
+
mark.search-hit.is-selected {
|
|
584
|
+
background: #fde68a;
|
|
585
|
+
color: inherit;
|
|
586
|
+
padding: 0 2px;
|
|
587
|
+
border-radius: 3px;
|
|
588
|
+
}
|
|
589
|
+
.toc {
|
|
590
|
+
display: flex;
|
|
591
|
+
flex-direction: column;
|
|
592
|
+
flex: 1 1 0;
|
|
593
|
+
min-height: 0;
|
|
594
|
+
overflow: hidden;
|
|
595
|
+
}
|
|
596
|
+
.toc-header { flex: 0 0 auto; }
|
|
597
|
+
.toc > .toc-list {
|
|
598
|
+
flex: 1 1 auto;
|
|
599
|
+
min-height: 0;
|
|
600
|
+
overflow-y: auto;
|
|
601
|
+
}
|
|
602
|
+
.toc ul {
|
|
603
|
+
list-style: none;
|
|
604
|
+
margin: 0;
|
|
605
|
+
padding: 0;
|
|
606
|
+
}
|
|
607
|
+
.toc li {
|
|
608
|
+
margin: 0;
|
|
609
|
+
padding: 0;
|
|
610
|
+
}
|
|
611
|
+
.toc-list + .toc-list {
|
|
612
|
+
margin-top: 2px;
|
|
613
|
+
}
|
|
614
|
+
.toc-item {
|
|
615
|
+
margin: 1px 0;
|
|
616
|
+
}
|
|
617
|
+
.toc a {
|
|
618
|
+
display: inline-block;
|
|
619
|
+
max-width: 100%;
|
|
620
|
+
color: #57606a;
|
|
621
|
+
text-decoration: none;
|
|
622
|
+
padding: 4px 0 4px 0;
|
|
623
|
+
overflow-wrap: anywhere;
|
|
624
|
+
word-break: break-word;
|
|
625
|
+
box-sizing: border-box;
|
|
626
|
+
}
|
|
627
|
+
.toc summary {
|
|
628
|
+
min-width: 0;
|
|
629
|
+
}
|
|
630
|
+
.toc li {
|
|
631
|
+
min-width: 0;
|
|
632
|
+
}
|
|
633
|
+
.toc a:hover {
|
|
634
|
+
color: #0969da;
|
|
635
|
+
}
|
|
636
|
+
.toc a.is-active {
|
|
637
|
+
color: #0b57d0;
|
|
638
|
+
font-weight: 700;
|
|
639
|
+
}
|
|
640
|
+
.toc a.is-match {
|
|
641
|
+
color: #355070;
|
|
642
|
+
background: #eaf2ff;
|
|
643
|
+
border-radius: 4px;
|
|
644
|
+
padding-left: 4px;
|
|
645
|
+
padding-right: 4px;
|
|
646
|
+
}
|
|
647
|
+
.toc details {
|
|
648
|
+
margin: 0;
|
|
649
|
+
}
|
|
650
|
+
.toc summary {
|
|
651
|
+
list-style: none;
|
|
652
|
+
cursor: pointer;
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: flex-start;
|
|
655
|
+
gap: 6px;
|
|
656
|
+
padding: 2px 0;
|
|
657
|
+
}
|
|
658
|
+
.toc summary::-webkit-details-marker {
|
|
659
|
+
display: none;
|
|
660
|
+
}
|
|
661
|
+
.toc summary::before {
|
|
662
|
+
content: '▸';
|
|
663
|
+
color: #57606a;
|
|
664
|
+
font-size: 0.78em;
|
|
665
|
+
line-height: 1.8;
|
|
666
|
+
flex: 0 0 auto;
|
|
667
|
+
transform: translateY(1px);
|
|
668
|
+
}
|
|
669
|
+
.toc details[open] > summary::before {
|
|
670
|
+
content: '▾';
|
|
671
|
+
}
|
|
672
|
+
.toc details > .toc-list {
|
|
673
|
+
margin-left: 14px;
|
|
674
|
+
padding-left: 10px;
|
|
675
|
+
border-left: 1px solid #d8dee4;
|
|
676
|
+
}
|
|
677
|
+
.toc-item:not(.toc-parent) > a {
|
|
678
|
+
padding-left: 18px;
|
|
679
|
+
}
|
|
680
|
+
.toc-list-level-1 > .toc-item > a,
|
|
681
|
+
.toc-list-level-1 > .toc-item > details > summary > a {
|
|
682
|
+
font-weight: 600;
|
|
683
|
+
}
|
|
684
|
+
.toc-list-level-2 > .toc-item > a,
|
|
685
|
+
.toc-list-level-2 > .toc-item > details > summary > a {
|
|
686
|
+
font-size: 0.95em;
|
|
687
|
+
}
|
|
688
|
+
.toc-list-level-3 > .toc-item > a,
|
|
689
|
+
.toc-list-level-3 > .toc-item > details > summary > a,
|
|
690
|
+
.toc-list-level-4 > .toc-item > a,
|
|
691
|
+
.toc-list-level-4 > .toc-item > details > summary > a,
|
|
692
|
+
.toc-list-level-5 > .toc-item > a,
|
|
693
|
+
.toc-list-level-5 > .toc-item > details > summary > a,
|
|
694
|
+
.toc-list-level-6 > .toc-item > a,
|
|
695
|
+
.toc-list-level-6 > .toc-item > details > summary > a {
|
|
696
|
+
font-size: 0.9em;
|
|
697
|
+
}
|
|
698
|
+
.content {
|
|
699
|
+
min-width: 0;
|
|
700
|
+
flex: 1 1 auto;
|
|
701
|
+
overflow-wrap: anywhere;
|
|
702
|
+
word-break: break-word;
|
|
703
|
+
}
|
|
704
|
+
.content > * { max-width: 100%; }
|
|
705
|
+
.content iframe, .content video, .content canvas { max-width: 100%; height: auto; }
|
|
706
|
+
.heading-with-anchor {
|
|
707
|
+
position: relative;
|
|
708
|
+
}
|
|
709
|
+
.heading-anchor {
|
|
710
|
+
margin-left: 0.45em;
|
|
711
|
+
color: #57606a;
|
|
712
|
+
text-decoration: none;
|
|
713
|
+
opacity: 0;
|
|
714
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
715
|
+
font-weight: 500;
|
|
716
|
+
}
|
|
717
|
+
.heading-with-anchor:hover .heading-anchor,
|
|
718
|
+
.heading-with-anchor:focus-within .heading-anchor {
|
|
719
|
+
opacity: 1;
|
|
720
|
+
}
|
|
721
|
+
.heading-anchor:hover,
|
|
722
|
+
.heading-anchor:focus {
|
|
723
|
+
color: #0969da;
|
|
724
|
+
}
|
|
725
|
+
h1 { font-size: 2em; border-bottom: 2px solid #e1e4e8; padding-bottom: 10px; margin-top: 1.5em; }
|
|
726
|
+
h2 { font-size: 1.5em; border-bottom: 1px solid #e1e4e8; padding-bottom: 6px; margin-top: 1.4em; }
|
|
727
|
+
h3 { font-size: 1.2em; margin-top: 1.3em; }
|
|
728
|
+
h4 { font-size: 1.05em; margin-top: 1.2em; }
|
|
729
|
+
code {
|
|
730
|
+
background: #f6f8fa;
|
|
731
|
+
padding: 2px 5px;
|
|
732
|
+
border-radius: 3px;
|
|
733
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
734
|
+
font-size: 0.875em;
|
|
735
|
+
}
|
|
736
|
+
pre {
|
|
737
|
+
background: #f6f8fa;
|
|
738
|
+
padding: 16px;
|
|
739
|
+
border-radius: 6px;
|
|
740
|
+
overflow-x: auto;
|
|
741
|
+
line-height: 1.45;
|
|
742
|
+
}
|
|
743
|
+
pre code { background: none; padding: 0; font-size: 0.875em; }
|
|
744
|
+
table {
|
|
745
|
+
display: block;
|
|
746
|
+
border-collapse: collapse;
|
|
747
|
+
width: 100%;
|
|
748
|
+
max-width: 100%;
|
|
749
|
+
margin: 16px 0;
|
|
750
|
+
font-size: 0.9em;
|
|
751
|
+
overflow-x: auto;
|
|
752
|
+
}
|
|
753
|
+
th, td { border: 1px solid #dfe2e5; padding: 7px 14px; text-align: left; }
|
|
754
|
+
th { background: #f6f8fa; font-weight: 600; }
|
|
755
|
+
tr:nth-child(even) { background: #fafbfc; }
|
|
756
|
+
blockquote {
|
|
757
|
+
border-left: 4px solid #dfe2e5;
|
|
758
|
+
padding: 0 16px;
|
|
759
|
+
color: #6a737d;
|
|
760
|
+
margin: 0 0 16px 0;
|
|
761
|
+
}
|
|
762
|
+
hr { border: none; border-top: 1px solid #e1e4e8; margin: 24px 0; }
|
|
763
|
+
.mermaid { text-align: center; margin: 20px 0; }
|
|
764
|
+
.graphviz { text-align: center; margin: 20px 0; }
|
|
765
|
+
.content img {
|
|
766
|
+
max-width: 100%;
|
|
767
|
+
height: auto;
|
|
768
|
+
}
|
|
769
|
+
.content svg,
|
|
770
|
+
.mermaid svg,
|
|
771
|
+
.graphviz svg {
|
|
772
|
+
display: block;
|
|
773
|
+
max-width: 100%;
|
|
774
|
+
height: auto;
|
|
775
|
+
margin: 0 auto;
|
|
776
|
+
}
|
|
777
|
+
@media (max-width: 1080px) {
|
|
778
|
+
.sidebar-toggle { display: inline-flex; align-items: center; }
|
|
779
|
+
.page-layout {
|
|
780
|
+
display: block;
|
|
781
|
+
max-width: 100%;
|
|
782
|
+
padding-top: 60px;
|
|
783
|
+
}
|
|
784
|
+
.reader-sidebar {
|
|
785
|
+
position: fixed;
|
|
786
|
+
top: 0;
|
|
787
|
+
left: 0;
|
|
788
|
+
bottom: 0;
|
|
789
|
+
width: 85%;
|
|
790
|
+
max-width: 360px;
|
|
791
|
+
height: 100vh;
|
|
792
|
+
background: #ffffff;
|
|
793
|
+
z-index: 99;
|
|
794
|
+
transform: translateX(-100%);
|
|
795
|
+
transition: transform 0.2s ease;
|
|
796
|
+
margin: 0;
|
|
797
|
+
padding: 16px;
|
|
798
|
+
overflow: hidden;
|
|
799
|
+
display: flex;
|
|
800
|
+
flex-direction: column;
|
|
801
|
+
gap: 12px;
|
|
802
|
+
box-shadow: 2px 0 12px rgba(0,0,0,0.15);
|
|
803
|
+
flex: initial;
|
|
804
|
+
}
|
|
805
|
+
body[data-sidebar-open] .reader-sidebar { transform: translateX(0); }
|
|
806
|
+
.heading-anchor { opacity: 1; }
|
|
807
|
+
}
|
|
808
|
+
@media print {
|
|
809
|
+
body { font-size: 11pt; }
|
|
810
|
+
.page-layout {
|
|
811
|
+
display: block;
|
|
812
|
+
max-width: 100%;
|
|
813
|
+
margin: 0;
|
|
814
|
+
padding: 0 10px;
|
|
815
|
+
}
|
|
816
|
+
.reader-sidebar,
|
|
817
|
+
.sidebar-toggle,
|
|
818
|
+
.sidebar-scrim { display: none !important; }
|
|
819
|
+
.content { max-width: 100%; }
|
|
820
|
+
.heading-anchor { display: none; }
|
|
821
|
+
pre { font-size: 9pt; }
|
|
822
|
+
a[href]:after { content: none; }
|
|
823
|
+
}
|
|
824
|
+
</style>
|
|
825
|
+
</head>
|
|
826
|
+
<body>
|
|
827
|
+
<button class="sidebar-toggle" id="sidebar-toggle" type="button" aria-label="Toggle sidebar" aria-expanded="false">☰</button>
|
|
828
|
+
<div class="sidebar-scrim" id="sidebar-scrim"></div>
|
|
829
|
+
<div class="page-layout">
|
|
830
|
+
${tocHtml}
|
|
831
|
+
<main class="content">
|
|
832
|
+
${bodyHtml}
|
|
833
|
+
</main>
|
|
834
|
+
</div>
|
|
835
|
+
|
|
836
|
+
<!-- WaveDrom -->
|
|
837
|
+
${waveDromSkinTag}
|
|
838
|
+
${waveDromTag}
|
|
839
|
+
<script type="text/javascript">
|
|
840
|
+
function renderWaveDrom() {
|
|
841
|
+
if (typeof WaveDrom !== 'undefined') {
|
|
842
|
+
WaveDrom.ProcessAll();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
window.addEventListener('DOMContentLoaded', renderWaveDrom);
|
|
846
|
+
window.addEventListener('load', renderWaveDrom);
|
|
847
|
+
setTimeout(renderWaveDrom, 250);
|
|
848
|
+
setTimeout(renderWaveDrom, 1000);
|
|
849
|
+
</script>
|
|
850
|
+
|
|
851
|
+
<!-- Mermaid -->
|
|
852
|
+
${mermaidScriptTag}
|
|
853
|
+
${mermaidInitTag}
|
|
854
|
+
|
|
855
|
+
<!-- Reader runtime -->
|
|
856
|
+
<script id="reader-section-data" type="application/json">${serializedSections}</script>
|
|
857
|
+
<script type="text/javascript">
|
|
858
|
+
(function () {
|
|
859
|
+
'use strict';
|
|
860
|
+
const readerState = {
|
|
861
|
+
activeSectionId: null,
|
|
862
|
+
query: '',
|
|
863
|
+
results: [],
|
|
864
|
+
selectedResultIndex: -1,
|
|
865
|
+
activeHighlight: null,
|
|
866
|
+
};
|
|
867
|
+
window.__readerState = readerState;
|
|
868
|
+
|
|
869
|
+
const rawData = document.getElementById('reader-section-data');
|
|
870
|
+
const sections = rawData ? JSON.parse(rawData.textContent || '[]') : [];
|
|
871
|
+
const headingNodes = Array.from(document.querySelectorAll('[data-reader-heading]'));
|
|
872
|
+
const tocLinks = new Map(
|
|
873
|
+
Array.from(document.querySelectorAll('.toc a[href^="#"]')).map((link) => [link.getAttribute('href').slice(1), link])
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
function escapeHtml(value) {
|
|
877
|
+
return String(value == null ? '' : value)
|
|
878
|
+
.replace(/&/g, '&')
|
|
879
|
+
.replace(/</g, '<')
|
|
880
|
+
.replace(/>/g, '>')
|
|
881
|
+
.replace(/"/g, '"')
|
|
882
|
+
.replace(/'/g, ''');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function normalizeText(value) {
|
|
886
|
+
return String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function expandTocAncestors(link) {
|
|
890
|
+
let node = link && link.closest('details');
|
|
891
|
+
while (node) {
|
|
892
|
+
node.open = true;
|
|
893
|
+
node = node.parentElement && node.parentElement.closest('details');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function ensureTocLinkVisible(link) {
|
|
898
|
+
if (!link) return;
|
|
899
|
+
const scroller = document.querySelector('.toc > .toc-list');
|
|
900
|
+
if (!scroller) return;
|
|
901
|
+
const scrollerRect = scroller.getBoundingClientRect();
|
|
902
|
+
const linkRect = link.getBoundingClientRect();
|
|
903
|
+
if (linkRect.top < scrollerRect.top) {
|
|
904
|
+
scroller.scrollTop += linkRect.top - scrollerRect.top;
|
|
905
|
+
} else if (linkRect.bottom > scrollerRect.bottom) {
|
|
906
|
+
scroller.scrollTop += linkRect.bottom - scrollerRect.bottom;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
let observerFrozen = false;
|
|
911
|
+
|
|
912
|
+
function syncActiveHeading(sectionId, options) {
|
|
913
|
+
if (!sectionId || readerState.activeSectionId === sectionId) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const freezeSidebar = options && options.freezeSidebar === true;
|
|
917
|
+
const previous = tocLinks.get(readerState.activeSectionId);
|
|
918
|
+
if (previous) {
|
|
919
|
+
previous.classList.remove('is-active');
|
|
920
|
+
}
|
|
921
|
+
readerState.activeSectionId = sectionId;
|
|
922
|
+
const next = tocLinks.get(sectionId);
|
|
923
|
+
if (next) {
|
|
924
|
+
next.classList.add('is-active');
|
|
925
|
+
if (!freezeSidebar) {
|
|
926
|
+
expandTocAncestors(next);
|
|
927
|
+
ensureTocLinkVisible(next);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (typeof IntersectionObserver !== 'undefined' && headingNodes.length) {
|
|
933
|
+
const observer = new IntersectionObserver((entries) => {
|
|
934
|
+
if (observerFrozen) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const visible = entries
|
|
938
|
+
.filter((entry) => entry.isIntersecting)
|
|
939
|
+
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
|
940
|
+
if (visible[0]) {
|
|
941
|
+
syncActiveHeading(visible[0].target.id);
|
|
942
|
+
}
|
|
943
|
+
}, { rootMargin: '0px 0px -65% 0px', threshold: [0, 1] });
|
|
944
|
+
headingNodes.forEach((node) => observer.observe(node));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const allTocDetails = () => Array.from(document.querySelectorAll('.toc details'));
|
|
948
|
+
const expandAllBtn = document.getElementById('toc-expand-all');
|
|
949
|
+
if (expandAllBtn) {
|
|
950
|
+
expandAllBtn.addEventListener('click', () => {
|
|
951
|
+
allTocDetails().forEach((node) => { node.open = true; });
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const collapseAllBtn = document.getElementById('toc-collapse-all');
|
|
955
|
+
if (collapseAllBtn) {
|
|
956
|
+
collapseAllBtn.addEventListener('click', () => {
|
|
957
|
+
allTocDetails().forEach((node) => { node.open = false; });
|
|
958
|
+
const activeLink = tocLinks.get(readerState.activeSectionId);
|
|
959
|
+
expandTocAncestors(activeLink);
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const SKIP_SELECTOR = 'svg, .mermaid, .graphviz, script, style';
|
|
964
|
+
|
|
965
|
+
function buildSnippet(section, query) {
|
|
966
|
+
const haystack = section.searchText || section.title || '';
|
|
967
|
+
const lower = haystack.toLowerCase();
|
|
968
|
+
const index = lower.indexOf(query);
|
|
969
|
+
if (index === -1) {
|
|
970
|
+
return haystack.slice(0, 140);
|
|
971
|
+
}
|
|
972
|
+
const start = Math.max(0, index - 50);
|
|
973
|
+
const end = Math.min(haystack.length, index + query.length + 70);
|
|
974
|
+
let snippet = haystack.slice(start, end).trim();
|
|
975
|
+
if (start > 0) snippet = '…' + snippet;
|
|
976
|
+
if (end < haystack.length) snippet += '…';
|
|
977
|
+
return snippet;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function clearMatchedTocState() {
|
|
981
|
+
tocLinks.forEach((link) => link.classList.remove('is-match'));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function applyMatchedTocState() {
|
|
985
|
+
const matched = new Set(readerState.results.map((result) => result.id));
|
|
986
|
+
tocLinks.forEach((link, id) => {
|
|
987
|
+
link.classList.toggle('is-match', matched.has(id));
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function clearSelectedHighlight() {
|
|
992
|
+
const mark = readerState.activeHighlight;
|
|
993
|
+
if (mark && mark.parentNode) {
|
|
994
|
+
const text = document.createTextNode(mark.textContent || '');
|
|
995
|
+
mark.parentNode.replaceChild(text, mark);
|
|
996
|
+
text.parentNode.normalize();
|
|
997
|
+
}
|
|
998
|
+
readerState.activeHighlight = null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function updateSearchStatus() {
|
|
1002
|
+
const status = document.getElementById('search-result-count');
|
|
1003
|
+
const prevBtn = document.getElementById('search-prev');
|
|
1004
|
+
const nextBtn = document.getElementById('search-next');
|
|
1005
|
+
if (status) {
|
|
1006
|
+
if (!readerState.query) {
|
|
1007
|
+
status.textContent = '';
|
|
1008
|
+
} else if (!readerState.results.length) {
|
|
1009
|
+
status.textContent = '0';
|
|
1010
|
+
} else {
|
|
1011
|
+
status.textContent = readerState.selectedResultIndex >= 0
|
|
1012
|
+
? (readerState.selectedResultIndex + 1) + '/' + readerState.results.length
|
|
1013
|
+
: String(readerState.results.length);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const hasResults = readerState.results.length > 0;
|
|
1017
|
+
if (prevBtn) prevBtn.disabled = !hasResults;
|
|
1018
|
+
if (nextBtn) nextBtn.disabled = !hasResults;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function renderSearchResults() {
|
|
1022
|
+
const panel = document.getElementById('search-results');
|
|
1023
|
+
const list = document.getElementById('search-results-list');
|
|
1024
|
+
if (!panel || !list) return;
|
|
1025
|
+
if (!readerState.query) {
|
|
1026
|
+
panel.hidden = true;
|
|
1027
|
+
list.innerHTML = '';
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (!readerState.results.length) {
|
|
1031
|
+
panel.hidden = false;
|
|
1032
|
+
list.innerHTML = '<p class="search-empty">No matching sections.</p>';
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
panel.hidden = false;
|
|
1036
|
+
list.innerHTML = readerState.results.map((result, index) => (
|
|
1037
|
+
'<button class="search-result-item' + (index === readerState.selectedResultIndex ? ' is-active' : '') + '" data-result-index="' + index + '" type="button">'
|
|
1038
|
+
+ '<span class="search-result-title">' + escapeHtml(result.title) + '</span>'
|
|
1039
|
+
+ '<span class="search-result-snippet">' + escapeHtml(result.snippet) + '</span>'
|
|
1040
|
+
+ '</button>'
|
|
1041
|
+
)).join('');
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function sectionBoundary(sectionId) {
|
|
1045
|
+
const start = document.getElementById(sectionId);
|
|
1046
|
+
if (!start) return null;
|
|
1047
|
+
const startDepth = Number(start.getAttribute('data-reader-depth') || '1');
|
|
1048
|
+
let end = null;
|
|
1049
|
+
let node = start.nextElementSibling;
|
|
1050
|
+
while (node) {
|
|
1051
|
+
if (node.matches && node.matches('[data-reader-heading]')) {
|
|
1052
|
+
const depth = Number(node.getAttribute('data-reader-depth') || '1');
|
|
1053
|
+
if (depth <= startDepth) {
|
|
1054
|
+
end = node;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
node = node.nextElementSibling;
|
|
1059
|
+
}
|
|
1060
|
+
return { start, end };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function highlightFirstOccurrence(sectionId, query) {
|
|
1064
|
+
const bounds = sectionBoundary(sectionId);
|
|
1065
|
+
if (!bounds) return null;
|
|
1066
|
+
const { start, end } = bounds;
|
|
1067
|
+
const container = start.parentNode;
|
|
1068
|
+
if (!container) return null;
|
|
1069
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
|
1070
|
+
acceptNode(node) {
|
|
1071
|
+
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
|
|
1072
|
+
if (node.parentElement && node.parentElement.closest(SKIP_SELECTOR)) return NodeFilter.FILTER_REJECT;
|
|
1073
|
+
if (node.parentElement && node.parentElement.closest('.reader-sidebar')) return NodeFilter.FILTER_REJECT;
|
|
1074
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
let inRange = false;
|
|
1078
|
+
const queryLower = query.toLowerCase();
|
|
1079
|
+
while (walker.nextNode()) {
|
|
1080
|
+
const node = walker.currentNode;
|
|
1081
|
+
if (!inRange) {
|
|
1082
|
+
if (start.contains(node)) {
|
|
1083
|
+
inRange = true;
|
|
1084
|
+
} else if (start.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
1085
|
+
inRange = true;
|
|
1086
|
+
} else {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (node.parentElement && node.parentElement.closest('.heading-anchor')) continue;
|
|
1091
|
+
if (end) {
|
|
1092
|
+
const pos = end.compareDocumentPosition(node);
|
|
1093
|
+
if (end === node || end.contains(node) || (pos & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const text = node.nodeValue;
|
|
1098
|
+
const idx = text.toLowerCase().indexOf(queryLower);
|
|
1099
|
+
if (idx === -1) continue;
|
|
1100
|
+
const before = text.slice(0, idx);
|
|
1101
|
+
const match = text.slice(idx, idx + query.length);
|
|
1102
|
+
const after = text.slice(idx + query.length);
|
|
1103
|
+
const mark = document.createElement('mark');
|
|
1104
|
+
mark.className = 'search-hit is-selected';
|
|
1105
|
+
mark.textContent = match;
|
|
1106
|
+
const parent = node.parentNode;
|
|
1107
|
+
if (!parent) return null;
|
|
1108
|
+
if (before) parent.insertBefore(document.createTextNode(before), node);
|
|
1109
|
+
parent.insertBefore(mark, node);
|
|
1110
|
+
if (after) {
|
|
1111
|
+
node.nodeValue = after;
|
|
1112
|
+
} else {
|
|
1113
|
+
parent.removeChild(node);
|
|
1114
|
+
}
|
|
1115
|
+
return mark;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function jumpToAndHighlight(result) {
|
|
1121
|
+
clearSelectedHighlight();
|
|
1122
|
+
const query = normalizeText(readerState.query);
|
|
1123
|
+
if (!query) return;
|
|
1124
|
+
const mark = highlightFirstOccurrence(result.id, query);
|
|
1125
|
+
if (mark) {
|
|
1126
|
+
readerState.activeHighlight = mark;
|
|
1127
|
+
mark.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
1128
|
+
} else {
|
|
1129
|
+
const heading = document.getElementById(result.id);
|
|
1130
|
+
if (heading) heading.scrollIntoView({ behavior: 'instant', block: 'start' });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function detectActiveHeading() {
|
|
1135
|
+
const threshold = 120;
|
|
1136
|
+
let candidate = null;
|
|
1137
|
+
for (const node of headingNodes) {
|
|
1138
|
+
const rect = node.getBoundingClientRect();
|
|
1139
|
+
if (rect.top <= threshold) {
|
|
1140
|
+
candidate = node;
|
|
1141
|
+
} else {
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return candidate || headingNodes[0] || null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function resyncTocToScroll() {
|
|
1149
|
+
const node = detectActiveHeading();
|
|
1150
|
+
if (!node) return;
|
|
1151
|
+
if (readerState.activeSectionId === node.id) {
|
|
1152
|
+
const link = tocLinks.get(node.id);
|
|
1153
|
+
if (link) {
|
|
1154
|
+
expandTocAncestors(link);
|
|
1155
|
+
ensureTocLinkVisible(link);
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
syncActiveHeading(node.id);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function ensureActiveResultVisible() {
|
|
1163
|
+
const list = document.getElementById('search-results-list');
|
|
1164
|
+
if (!list) return;
|
|
1165
|
+
const active = list.querySelector('.search-result-item.is-active');
|
|
1166
|
+
if (active) {
|
|
1167
|
+
active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function selectResult(index) {
|
|
1172
|
+
if (!readerState.results.length) return;
|
|
1173
|
+
const size = readerState.results.length;
|
|
1174
|
+
const wrapped = ((index % size) + size) % size;
|
|
1175
|
+
const result = readerState.results[wrapped];
|
|
1176
|
+
if (!result) return;
|
|
1177
|
+
readerState.selectedResultIndex = wrapped;
|
|
1178
|
+
observerFrozen = true;
|
|
1179
|
+
renderSearchResults();
|
|
1180
|
+
ensureActiveResultVisible();
|
|
1181
|
+
syncActiveHeading(result.id, { freezeSidebar: true });
|
|
1182
|
+
jumpToAndHighlight(result);
|
|
1183
|
+
updateSearchStatus();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function clearSearchState() {
|
|
1187
|
+
const input = document.getElementById('doc-search-input');
|
|
1188
|
+
if (input) input.value = '';
|
|
1189
|
+
readerState.query = '';
|
|
1190
|
+
readerState.results = [];
|
|
1191
|
+
readerState.selectedResultIndex = -1;
|
|
1192
|
+
observerFrozen = false;
|
|
1193
|
+
clearSelectedHighlight();
|
|
1194
|
+
clearMatchedTocState();
|
|
1195
|
+
renderSearchResults();
|
|
1196
|
+
updateSearchStatus();
|
|
1197
|
+
resyncTocToScroll();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function performSearch() {
|
|
1201
|
+
const input = document.getElementById('doc-search-input');
|
|
1202
|
+
const rawQuery = input ? input.value.trim() : '';
|
|
1203
|
+
const query = normalizeText(rawQuery);
|
|
1204
|
+
readerState.query = rawQuery;
|
|
1205
|
+
clearSelectedHighlight();
|
|
1206
|
+
clearMatchedTocState();
|
|
1207
|
+
|
|
1208
|
+
if (!query) {
|
|
1209
|
+
readerState.results = [];
|
|
1210
|
+
readerState.selectedResultIndex = -1;
|
|
1211
|
+
renderSearchResults();
|
|
1212
|
+
updateSearchStatus();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
readerState.results = sections
|
|
1217
|
+
.filter((section) => normalizeText(section.searchText).includes(query))
|
|
1218
|
+
.map((section) => ({
|
|
1219
|
+
id: section.id,
|
|
1220
|
+
title: section.title,
|
|
1221
|
+
snippet: buildSnippet(section, query),
|
|
1222
|
+
}));
|
|
1223
|
+
readerState.selectedResultIndex = -1;
|
|
1224
|
+
renderSearchResults();
|
|
1225
|
+
updateSearchStatus();
|
|
1226
|
+
applyMatchedTocState();
|
|
1227
|
+
if (readerState.results.length) {
|
|
1228
|
+
selectResult(0);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const searchInput = document.getElementById('doc-search-input');
|
|
1233
|
+
if (searchInput) {
|
|
1234
|
+
searchInput.addEventListener('keydown', (event) => {
|
|
1235
|
+
if (event.key === 'Enter') {
|
|
1236
|
+
event.preventDefault();
|
|
1237
|
+
performSearch();
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
const submitBtn = document.getElementById('doc-search-submit');
|
|
1242
|
+
if (submitBtn) submitBtn.addEventListener('click', performSearch);
|
|
1243
|
+
const clearBtn = document.getElementById('doc-search-clear');
|
|
1244
|
+
if (clearBtn) clearBtn.addEventListener('click', clearSearchState);
|
|
1245
|
+
const prevBtn = document.getElementById('search-prev');
|
|
1246
|
+
if (prevBtn) prevBtn.addEventListener('click', () => selectResult(readerState.selectedResultIndex - 1));
|
|
1247
|
+
const nextBtn = document.getElementById('search-next');
|
|
1248
|
+
if (nextBtn) nextBtn.addEventListener('click', () => selectResult(readerState.selectedResultIndex + 1));
|
|
1249
|
+
const resultsList = document.getElementById('search-results-list');
|
|
1250
|
+
if (resultsList) {
|
|
1251
|
+
resultsList.addEventListener('click', (event) => {
|
|
1252
|
+
const button = event.target.closest('[data-result-index]');
|
|
1253
|
+
if (button) {
|
|
1254
|
+
selectResult(Number(button.getAttribute('data-result-index')));
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const contentRoot = document.querySelector('main.content');
|
|
1260
|
+
if (contentRoot) {
|
|
1261
|
+
contentRoot.addEventListener('click', () => {
|
|
1262
|
+
observerFrozen = false;
|
|
1263
|
+
resyncTocToScroll();
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
updateSearchStatus();
|
|
1268
|
+
|
|
1269
|
+
const sidebarToggle = document.getElementById('sidebar-toggle');
|
|
1270
|
+
const sidebarScrim = document.getElementById('sidebar-scrim');
|
|
1271
|
+
function setSidebarOpen(open) {
|
|
1272
|
+
if (open) {
|
|
1273
|
+
document.body.setAttribute('data-sidebar-open', '');
|
|
1274
|
+
} else {
|
|
1275
|
+
document.body.removeAttribute('data-sidebar-open');
|
|
1276
|
+
}
|
|
1277
|
+
if (sidebarToggle) sidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1278
|
+
}
|
|
1279
|
+
if (sidebarToggle) {
|
|
1280
|
+
sidebarToggle.addEventListener('click', () => {
|
|
1281
|
+
setSidebarOpen(!document.body.hasAttribute('data-sidebar-open'));
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
if (sidebarScrim) {
|
|
1285
|
+
sidebarScrim.addEventListener('click', () => setSidebarOpen(false));
|
|
1286
|
+
}
|
|
1287
|
+
document.addEventListener('keydown', (event) => {
|
|
1288
|
+
if (event.key === 'Escape' && document.body.hasAttribute('data-sidebar-open')) {
|
|
1289
|
+
setSidebarOpen(false);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
document.addEventListener('click', (event) => {
|
|
1293
|
+
if (!document.body.hasAttribute('data-sidebar-open')) return;
|
|
1294
|
+
if (!event.target || !event.target.closest) return;
|
|
1295
|
+
if (event.target.closest('.toc a[href^="#"]') || event.target.closest('.search-result-item')) {
|
|
1296
|
+
setSidebarOpen(false);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
})();
|
|
1300
|
+
</script>
|
|
1301
|
+
</body>
|
|
1302
|
+
</html>`;
|
|
1303
|
+
|
|
1304
|
+
// ── Output ───────────────────────────────────────────────────────────────────
|
|
1305
|
+
if (ext === '.html') {
|
|
1306
|
+
fs.writeFileSync(dst, html, 'utf8');
|
|
1307
|
+
console.log(`[HTML] ${src} → ${dst}`);
|
|
1308
|
+
|
|
1309
|
+
} else if (ext === '.pdf') {
|
|
1310
|
+
(async () => {
|
|
1311
|
+
let puppeteer;
|
|
1312
|
+
try {
|
|
1313
|
+
puppeteer = require('puppeteer');
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
console.error('[ERROR] puppeteer not found — install with: npm install puppeteer');
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Write temporary HTML, launch headless Chromium, export PDF
|
|
1320
|
+
const tmp = dst.replace(/\.pdf$/, '._tmp.html');
|
|
1321
|
+
fs.writeFileSync(tmp, html, 'utf8');
|
|
1322
|
+
|
|
1323
|
+
const browser = await puppeteer.launch({
|
|
1324
|
+
headless: 'new',
|
|
1325
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-crash-reporter', '--disable-dev-shm-usage'],
|
|
1326
|
+
});
|
|
1327
|
+
const page = await browser.newPage();
|
|
1328
|
+
|
|
1329
|
+
await page.goto('file://' + path.resolve(tmp), { waitUntil: 'load' });
|
|
1330
|
+
|
|
1331
|
+
// Allow WaveDrom / Mermaid scripts time to render diagrams
|
|
1332
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1333
|
+
|
|
1334
|
+
await page.pdf({
|
|
1335
|
+
path: dst,
|
|
1336
|
+
format: 'A4',
|
|
1337
|
+
printBackground: true,
|
|
1338
|
+
outline: true,
|
|
1339
|
+
tagged: true,
|
|
1340
|
+
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' }
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
await browser.close();
|
|
1344
|
+
fs.unlinkSync(tmp);
|
|
1345
|
+
console.log(`[PDF] ${src} → ${dst}`);
|
|
1346
|
+
})();
|
|
1347
|
+
|
|
1348
|
+
} else {
|
|
1349
|
+
console.error('[ERROR] Output extension must be .html or .pdf');
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|