visual-spec 0.1.10 → 0.1.11

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.
@@ -0,0 +1,923 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>VSpec Details</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0b1220;
10
+ --panel: rgba(17, 27, 46, 0.76);
11
+ --panel2: rgba(10, 16, 28, 0.82);
12
+ --text: #e6eefc;
13
+ --muted: #8aa0c3;
14
+ --border: rgba(255, 255, 255, 0.09);
15
+ --primary: #3b82f6;
16
+ --accent: #22c55e;
17
+ --warn: #f59e0b;
18
+ }
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ background: linear-gradient(180deg, var(--bg), #060a13);
25
+ color: var(--text);
26
+ font-family:
27
+ -apple-system,
28
+ BlinkMacSystemFont,
29
+ "Segoe UI",
30
+ Roboto,
31
+ Helvetica,
32
+ Arial,
33
+ "Apple Color Emoji",
34
+ "Segoe UI Emoji";
35
+ }
36
+ .top-banner {
37
+ position: sticky;
38
+ top: 0;
39
+ z-index: 20;
40
+ background: rgba(245, 158, 11, 0.14);
41
+ border-bottom: 1px solid rgba(245, 158, 11, 0.35);
42
+ backdrop-filter: blur(10px);
43
+ }
44
+ .top-banner .inner {
45
+ max-width: 1400px;
46
+ margin: 0 auto;
47
+ padding: 10px 16px;
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ gap: 10px;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ }
54
+ .top-banner .msg {
55
+ font-size: 13px;
56
+ color: rgba(245, 158, 11, 0.98);
57
+ }
58
+ .top-banner .msg b {
59
+ color: rgba(255, 255, 255, 0.92);
60
+ }
61
+ .wrap {
62
+ max-width: 1400px;
63
+ margin: 0 auto;
64
+ padding: 12px 12px 28px;
65
+ }
66
+ .shell {
67
+ display: grid;
68
+ grid-template-columns: 340px 1fr;
69
+ gap: 12px;
70
+ min-height: calc(100vh - 90px);
71
+ }
72
+ @media (max-width: 980px) {
73
+ .shell {
74
+ grid-template-columns: 1fr;
75
+ }
76
+ }
77
+ .panel {
78
+ border: 1px solid var(--border);
79
+ border-radius: 14px;
80
+ background: var(--panel);
81
+ backdrop-filter: blur(10px);
82
+ overflow: hidden;
83
+ }
84
+ .sidebar-header {
85
+ padding: 12px;
86
+ border-bottom: 1px solid var(--border);
87
+ background: rgba(255, 255, 255, 0.03);
88
+ }
89
+ .sidebar-title {
90
+ font-size: 13px;
91
+ color: #cfe0ff;
92
+ margin: 0 0 8px;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: 10px;
97
+ }
98
+ .sidebar-meta {
99
+ font-size: 12px;
100
+ color: var(--muted);
101
+ }
102
+ .search {
103
+ display: flex;
104
+ gap: 8px;
105
+ align-items: center;
106
+ margin-top: 10px;
107
+ }
108
+ input[type="text"] {
109
+ width: 100%;
110
+ border-radius: 10px;
111
+ border: 1px solid var(--border);
112
+ background: rgba(10, 16, 28, 0.78);
113
+ color: var(--text);
114
+ padding: 9px 10px;
115
+ font-size: 13px;
116
+ outline: none;
117
+ }
118
+ button {
119
+ border: 1px solid var(--border);
120
+ background: rgba(255, 255, 255, 0.06);
121
+ color: var(--text);
122
+ border-radius: 10px;
123
+ padding: 8px 10px;
124
+ cursor: pointer;
125
+ font-size: 13px;
126
+ }
127
+ button.primary {
128
+ background: rgba(59, 130, 246, 0.18);
129
+ border-color: rgba(59, 130, 246, 0.45);
130
+ }
131
+ .tree {
132
+ padding: 8px 8px 12px;
133
+ overflow: auto;
134
+ max-height: calc(100vh - 190px);
135
+ }
136
+ @media (max-width: 980px) {
137
+ .tree {
138
+ max-height: 46vh;
139
+ }
140
+ }
141
+ details {
142
+ border-radius: 10px;
143
+ }
144
+ details > summary {
145
+ list-style: none;
146
+ cursor: pointer;
147
+ user-select: none;
148
+ padding: 7px 8px;
149
+ border-radius: 10px;
150
+ color: #dbe8ff;
151
+ font-size: 13px;
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ }
156
+ details > summary::-webkit-details-marker {
157
+ display: none;
158
+ }
159
+ details > summary:hover {
160
+ background: rgba(255, 255, 255, 0.04);
161
+ }
162
+ .node {
163
+ margin-left: 10px;
164
+ padding-left: 8px;
165
+ border-left: 1px dashed rgba(255, 255, 255, 0.12);
166
+ }
167
+ .file {
168
+ display: flex;
169
+ width: 100%;
170
+ padding: 7px 10px;
171
+ border-radius: 10px;
172
+ border: 1px solid transparent;
173
+ background: transparent;
174
+ text-align: left;
175
+ cursor: pointer;
176
+ color: var(--text);
177
+ font-size: 13px;
178
+ }
179
+ .file:hover {
180
+ background: rgba(255, 255, 255, 0.04);
181
+ }
182
+ .file.active {
183
+ background: rgba(59, 130, 246, 0.16);
184
+ border-color: rgba(59, 130, 246, 0.34);
185
+ }
186
+ .main-header {
187
+ padding: 12px 12px 0;
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 8px;
191
+ }
192
+ .pathbar {
193
+ display: flex;
194
+ flex-wrap: wrap;
195
+ gap: 10px;
196
+ align-items: center;
197
+ justify-content: space-between;
198
+ padding: 10px 12px;
199
+ border: 1px solid var(--border);
200
+ border-radius: 14px;
201
+ background: var(--panel2);
202
+ }
203
+ .pathbar .left {
204
+ display: flex;
205
+ flex-wrap: wrap;
206
+ gap: 10px;
207
+ align-items: center;
208
+ }
209
+ .badge {
210
+ font-size: 12px;
211
+ color: var(--muted);
212
+ border: 1px solid var(--border);
213
+ border-radius: 999px;
214
+ padding: 4px 10px;
215
+ background: rgba(255, 255, 255, 0.03);
216
+ }
217
+ .path {
218
+ font-size: 13px;
219
+ color: #dbe8ff;
220
+ word-break: break-all;
221
+ }
222
+ .content {
223
+ padding: 12px;
224
+ }
225
+ .doc {
226
+ border: 1px solid var(--border);
227
+ border-radius: 14px;
228
+ background: rgba(8, 12, 20, 0.6);
229
+ padding: 16px 16px 18px;
230
+ }
231
+ .doc h1,
232
+ .doc h2,
233
+ .doc h3,
234
+ .doc h4,
235
+ .doc h5,
236
+ .doc h6 {
237
+ margin: 18px 0 10px;
238
+ color: #dbe8ff;
239
+ }
240
+ .doc h1 {
241
+ font-size: 22px;
242
+ }
243
+ .doc h2 {
244
+ font-size: 18px;
245
+ }
246
+ .doc h3 {
247
+ font-size: 16px;
248
+ }
249
+ .doc p {
250
+ margin: 10px 0;
251
+ color: rgba(230, 238, 252, 0.92);
252
+ line-height: 1.55;
253
+ }
254
+ .doc a {
255
+ color: rgba(59, 130, 246, 0.95);
256
+ }
257
+ .doc code {
258
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
259
+ monospace;
260
+ background: rgba(255, 255, 255, 0.06);
261
+ padding: 2px 5px;
262
+ border-radius: 6px;
263
+ border: 1px solid rgba(255, 255, 255, 0.08);
264
+ }
265
+ .doc pre {
266
+ background: #0a1020;
267
+ border: 1px solid rgba(255, 255, 255, 0.12);
268
+ border-radius: 12px;
269
+ padding: 12px;
270
+ overflow: auto;
271
+ line-height: 1.45;
272
+ }
273
+ .doc pre code {
274
+ background: transparent;
275
+ border: none;
276
+ padding: 0;
277
+ }
278
+ .doc table {
279
+ width: 100%;
280
+ border-collapse: collapse;
281
+ margin: 12px 0 14px;
282
+ }
283
+ .doc th,
284
+ .doc td {
285
+ border: 1px solid rgba(255, 255, 255, 0.14);
286
+ padding: 8px 10px;
287
+ vertical-align: top;
288
+ }
289
+ .doc th {
290
+ background: rgba(255, 255, 255, 0.04);
291
+ color: #dbe8ff;
292
+ text-align: left;
293
+ }
294
+ .doc ul,
295
+ .doc ol {
296
+ margin: 10px 0 10px 20px;
297
+ line-height: 1.55;
298
+ }
299
+ .puml {
300
+ width: 100%;
301
+ text-align: center;
302
+ margin: 14px 0;
303
+ }
304
+ .puml img {
305
+ max-width: 100%;
306
+ border-radius: 12px;
307
+ border: 1px solid rgba(255, 255, 255, 0.12);
308
+ background: rgba(255, 255, 255, 0.02);
309
+ }
310
+ iframe {
311
+ width: 100%;
312
+ height: calc(100vh - 240px);
313
+ border: 1px solid rgba(255, 255, 255, 0.12);
314
+ border-radius: 12px;
315
+ background: #fff;
316
+ }
317
+ .empty {
318
+ color: var(--muted);
319
+ font-size: 13px;
320
+ line-height: 1.5;
321
+ }
322
+ </style>
323
+ </head>
324
+ <body>
325
+ <div class="top-banner">
326
+ <div class="inner">
327
+ <div class="msg">
328
+ <b>Word export:</b> run <b>/vspec:doc</b> to generate <b>/docs/current/requirement_detail.doc</b>
329
+ </div>
330
+ </div>
331
+ </div>
332
+
333
+ <div class="wrap">
334
+ <div class="shell">
335
+ <div class="panel">
336
+ <div class="sidebar-header">
337
+ <div class="sidebar-title">
338
+ <span>Details Index</span>
339
+ <span class="badge" id="fileCount">0 files</span>
340
+ </div>
341
+ <div class="sidebar-meta">Click a file to render it on the right.</div>
342
+ <div class="search">
343
+ <input id="search" type="text" placeholder="Search path..." />
344
+ <button id="collapseAll">Collapse</button>
345
+ </div>
346
+ </div>
347
+ <div class="tree" id="tree"></div>
348
+ </div>
349
+
350
+ <div class="panel">
351
+ <div class="main-header">
352
+ <div class="pathbar">
353
+ <div class="left">
354
+ <span class="badge" id="typeBadge">—</span>
355
+ <span class="path" id="currentPath">No file selected</span>
356
+ </div>
357
+ <div class="right">
358
+ <button id="copyPath">Copy Path</button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ <div class="content">
363
+ <div class="doc" id="viewer">
364
+ <div class="empty">
365
+ Select a file from the left tree. Supported:
366
+ <ul>
367
+ <li><code>.md</code> rendered as markdown</li>
368
+ <li><code>.html</code> rendered via iframe</li>
369
+ <li><code>.puml</code> and markdown <code>```plantuml</code> blocks rendered via PlantUML server</li>
370
+ </ul>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <script>
379
+ window.__VSPEC_DETAILS_FILES__ = window.__VSPEC_DETAILS_FILES__ || {};
380
+
381
+ const PLANTUML_SERVER = "https://www.plantuml.com/plantuml/svg/";
382
+
383
+ const dom = {
384
+ tree: document.getElementById("tree"),
385
+ search: document.getElementById("search"),
386
+ fileCount: document.getElementById("fileCount"),
387
+ viewer: document.getElementById("viewer"),
388
+ currentPath: document.getElementById("currentPath"),
389
+ typeBadge: document.getElementById("typeBadge"),
390
+ copyPath: document.getElementById("copyPath"),
391
+ collapseAll: document.getElementById("collapseAll"),
392
+ };
393
+
394
+ const state = {
395
+ files: {},
396
+ paths: [],
397
+ activePath: null,
398
+ activeButton: null,
399
+ };
400
+
401
+ function escapeHtml(str) {
402
+ return String(str)
403
+ .replaceAll("&", "&amp;")
404
+ .replaceAll("<", "&lt;")
405
+ .replaceAll(">", "&gt;")
406
+ .replaceAll('"', "&quot;")
407
+ .replaceAll("'", "&#039;");
408
+ }
409
+
410
+ function fileType(path) {
411
+ const i = path.lastIndexOf(".");
412
+ const ext = i >= 0 ? path.slice(i + 1).toLowerCase() : "";
413
+ if (ext === "md") return "Markdown";
414
+ if (ext === "html") return "HTML";
415
+ if (ext === "puml") return "PlantUML";
416
+ return ext ? ext.toUpperCase() : "FILE";
417
+ }
418
+
419
+ function setActive(path, button) {
420
+ if (state.activeButton) state.activeButton.classList.remove("active");
421
+ state.activeButton = button || null;
422
+ if (state.activeButton) state.activeButton.classList.add("active");
423
+ state.activePath = path;
424
+ dom.currentPath.textContent = path || "No file selected";
425
+ dom.typeBadge.textContent = path ? fileType(path) : "—";
426
+ if (path) location.hash = encodeURIComponent(path);
427
+ }
428
+
429
+ function exists(path) {
430
+ return state.files[path] != null;
431
+ }
432
+
433
+ function pickFirst(candidates) {
434
+ for (const p of candidates) {
435
+ if (exists(p)) return p;
436
+ }
437
+ return null;
438
+ }
439
+
440
+ function listByPrefix(prefix) {
441
+ return state.paths.filter((p) => p.startsWith(prefix));
442
+ }
443
+
444
+ function basename(path) {
445
+ const s = (path || "").split("/").filter(Boolean);
446
+ return s.length ? s[s.length - 1] : path;
447
+ }
448
+
449
+ function filterMatch(filterLower, label, path) {
450
+ if (!filterLower) return true;
451
+ return `${label || ""} ${path || ""}`.toLowerCase().includes(filterLower);
452
+ }
453
+
454
+ function getModuleSlug(path) {
455
+ const parts = String(path || "").split("/").filter(Boolean);
456
+ if (parts.length >= 3 && parts[0] === "specs" && parts[1] === "details") return parts[2];
457
+ return null;
458
+ }
459
+
460
+ function groupBy(arr, getKey) {
461
+ const m = new Map();
462
+ for (const x of arr) {
463
+ const k = getKey(x);
464
+ if (!m.has(k)) m.set(k, []);
465
+ m.get(k).push(x);
466
+ }
467
+ return m;
468
+ }
469
+
470
+ function buildChapters() {
471
+ const original = pickFirst(["/specs/background/original.md"]);
472
+ const stakeholder = pickFirst(["/specs/background/stakeholder.md", "/specs/background/stakeholders.md"]);
473
+ const roles = pickFirst(["/specs/background/roles.md"]);
474
+ const terms = pickFirst(["/specs/background/terms.md"]);
475
+ const scenarios = pickFirst(["/specs/background/scenarios.md"]);
476
+ const dependencies = pickFirst(["/specs/background/dependencies.md"]);
477
+ const questions = pickFirst(["/specs/background/questions.md"]);
478
+
479
+ const functions = listByPrefix("/specs/functions/").filter((p) => p.toLowerCase().endsWith(".md"));
480
+ const flows = listByPrefix("/specs/flows/").filter((p) => p.toLowerCase().endsWith(".puml"));
481
+ const models = listByPrefix("/specs/models/").filter((p) => p.toLowerCase().endsWith(".md"));
482
+
483
+ const detailTypes = [
484
+ { dir: "rbac", label: "RBAC" },
485
+ { dir: "data_permission", label: "Data Permission" },
486
+ { dir: "page_load", label: "Page Load" },
487
+ { dir: "interaction", label: "Interaction" },
488
+ { dir: "service_logic", label: "Service Logic" },
489
+ { dir: "job_logic", label: "Job Logic" },
490
+ { dir: "mq", label: "MQ" },
491
+ { dir: "logging_matrix", label: "Logging Matrix" },
492
+ { dir: "notification_matrix", label: "Notification Matrix" },
493
+ { dir: "nfp", label: "Non-Functional" },
494
+ { dir: "validation_matrix", label: "Validation Matrix" },
495
+ { dir: "judgemental_matrix", label: "Decision Matrix" },
496
+ { dir: "code_rules", label: "Code Rules" },
497
+ { dir: "timeline", label: "Timeline" },
498
+ { dir: "formula", label: "Formula" },
499
+ { dir: "expression_tree", label: "Expression Tree" },
500
+ { dir: "file_import", label: "File Import" },
501
+ { dir: "file_export", label: "File Export" },
502
+ { dir: "cron_job", label: "Cron Jobs" },
503
+ ];
504
+
505
+ const detailGroups = [];
506
+ for (const dt of detailTypes) {
507
+ const prefix = `/specs/details/`;
508
+ const hits = state.paths.filter((p) => p.startsWith(prefix) && p.includes(`/${dt.dir}/`));
509
+ if (!hits.length) continue;
510
+ const byModule = groupBy(hits, (p) => getModuleSlug(p) || "unknown");
511
+ const modules = Array.from(byModule.entries()).sort((a, b) => a[0].localeCompare(b[0]));
512
+ const children = modules.map(([module, files]) => ({
513
+ type: "group",
514
+ label: module,
515
+ children: files
516
+ .slice()
517
+ .sort((a, b) => a.localeCompare(b))
518
+ .map((p) => ({ type: "file", label: basename(p), path: p })),
519
+ }));
520
+ detailGroups.push({ type: "group", label: dt.label, children });
521
+ }
522
+
523
+ const chapters = [];
524
+ chapters.push({
525
+ label: "Requirement",
526
+ children: [
527
+ original ? { type: "file", label: "Original Requirement", path: original } : null,
528
+ stakeholder ? { type: "file", label: "Stakeholders", path: stakeholder } : null,
529
+ roles ? { type: "file", label: "Roles", path: roles } : null,
530
+ terms ? { type: "file", label: "Terms", path: terms } : null,
531
+ scenarios ? { type: "file", label: "Scenarios", path: scenarios } : null,
532
+ dependencies ? { type: "file", label: "Dependencies", path: dependencies } : null,
533
+ questions ? { type: "file", label: "Questions", path: questions } : null,
534
+ ].filter(Boolean),
535
+ });
536
+
537
+ const coreFunctions = pickFirst(["/specs/functions/core.md"]);
538
+ chapters.push({
539
+ label: "Scenarios ↔ Functions",
540
+ children: [
541
+ scenarios ? { type: "file", label: "Scenario List", path: scenarios } : null,
542
+ flows.length
543
+ ? { type: "group", label: "Flow Diagrams", children: flows.map((p) => ({ type: "file", label: basename(p), path: p })) }
544
+ : null,
545
+ coreFunctions ? { type: "file", label: "Core Functions", path: coreFunctions } : null,
546
+ ].filter(Boolean),
547
+ });
548
+
549
+ chapters.push({
550
+ label: "Flows",
551
+ children: flows.map((p) => ({ type: "file", label: basename(p), path: p })),
552
+ });
553
+
554
+ chapters.push({
555
+ label: "Functions",
556
+ children: functions.map((p) => ({ type: "file", label: basename(p), path: p })),
557
+ });
558
+
559
+ chapters.push({
560
+ label: "Details",
561
+ children: detailGroups,
562
+ });
563
+
564
+ chapters.push({
565
+ label: "Models",
566
+ children: models.map((p) => ({ type: "file", label: basename(p), path: p })),
567
+ });
568
+
569
+ return chapters.filter((c) => (c.children || []).length);
570
+ }
571
+
572
+ function anyChildMatches(node, filterLower) {
573
+ if (!node) return false;
574
+ if (node.type === "file") return filterMatch(filterLower, node.label, node.path);
575
+ if (node.children && node.children.length) return node.children.some((c) => anyChildMatches(c, filterLower));
576
+ return false;
577
+ }
578
+
579
+ function renderNavNode(node, container, filterLower) {
580
+ if (node.type === "file") {
581
+ if (!filterMatch(filterLower, node.label, node.path)) return;
582
+ const btn = document.createElement("button");
583
+ btn.className = "file";
584
+ btn.textContent = node.label;
585
+ btn.addEventListener("click", () => openFile(node.path, btn));
586
+ if (state.activePath === node.path) btn.classList.add("active");
587
+ container.appendChild(btn);
588
+ return;
589
+ }
590
+
591
+ if (!anyChildMatches(node, filterLower)) return;
592
+ const det = document.createElement("details");
593
+ det.open = !filterLower;
594
+ const sum = document.createElement("summary");
595
+ sum.textContent = node.label;
596
+ det.appendChild(sum);
597
+ const inner = document.createElement("div");
598
+ inner.className = "node";
599
+ det.appendChild(inner);
600
+ container.appendChild(det);
601
+ for (const c of node.children || []) renderNavNode(c, inner, filterLower);
602
+ }
603
+
604
+ function renderSidebar() {
605
+ dom.tree.innerHTML = "";
606
+ const filterLower = (dom.search.value || "").trim().toLowerCase();
607
+ const chapters = buildChapters();
608
+ for (const ch of chapters) {
609
+ renderNavNode({ type: "group", label: ch.label, children: ch.children }, dom.tree, filterLower);
610
+ }
611
+ }
612
+
613
+ function encode6bit(b) {
614
+ if (b < 10) return String.fromCharCode(48 + b);
615
+ b -= 10;
616
+ if (b < 26) return String.fromCharCode(65 + b);
617
+ b -= 26;
618
+ if (b < 26) return String.fromCharCode(97 + b);
619
+ b -= 26;
620
+ if (b === 0) return "-";
621
+ if (b === 1) return "_";
622
+ return ".";
623
+ }
624
+
625
+ function append3bytes(b1, b2, b3) {
626
+ const c1 = b1 >> 2;
627
+ const c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
628
+ const c3 = ((b2 & 0xf) << 2) | (b3 >> 6);
629
+ const c4 = b3 & 0x3f;
630
+ return "" + encode6bit(c1 & 0x3f) + encode6bit(c2 & 0x3f) + encode6bit(c3 & 0x3f) + encode6bit(c4 & 0x3f);
631
+ }
632
+
633
+ function encode64(bytes) {
634
+ let r = "";
635
+ for (let i = 0; i < bytes.length; i += 3) {
636
+ if (i + 2 === bytes.length) {
637
+ r += append3bytes(bytes[i], bytes[i + 1], 0);
638
+ } else if (i + 1 === bytes.length) {
639
+ r += append3bytes(bytes[i], 0, 0);
640
+ } else {
641
+ r += append3bytes(bytes[i], bytes[i + 1], bytes[i + 2]);
642
+ }
643
+ }
644
+ return r;
645
+ }
646
+
647
+ async function deflateToBytes(text) {
648
+ if (!("CompressionStream" in window)) {
649
+ throw new Error("CompressionStream not supported");
650
+ }
651
+ const enc = new TextEncoder();
652
+ const input = enc.encode(text);
653
+ const cs = new CompressionStream("deflate");
654
+ const writer = cs.writable.getWriter();
655
+ await writer.write(input);
656
+ await writer.close();
657
+ const resp = new Response(cs.readable);
658
+ const buf = await resp.arrayBuffer();
659
+ return new Uint8Array(buf);
660
+ }
661
+
662
+ async function plantUmlSvgUrl(source) {
663
+ const bytes = await deflateToBytes(source);
664
+ return PLANTUML_SERVER + encode64(bytes);
665
+ }
666
+
667
+ function parseMarkdown(md) {
668
+ const lines = md.replace(/\r\n/g, "\n").split("\n");
669
+ const blocks = [];
670
+ let i = 0;
671
+ while (i < lines.length) {
672
+ const line = lines[i];
673
+ const fence = line.match(/^```(\w+)?\s*$/);
674
+ if (fence) {
675
+ const lang = (fence[1] || "").toLowerCase();
676
+ i++;
677
+ const code = [];
678
+ while (i < lines.length && !/^```/.test(lines[i])) {
679
+ code.push(lines[i]);
680
+ i++;
681
+ }
682
+ if (i < lines.length) i++;
683
+ blocks.push({ type: "code", lang, text: code.join("\n") });
684
+ continue;
685
+ }
686
+
687
+ const h = line.match(/^(#{1,6})\s+(.*)\s*$/);
688
+ if (h) {
689
+ blocks.push({ type: "heading", level: h[1].length, text: h[2] });
690
+ i++;
691
+ continue;
692
+ }
693
+
694
+ if (/^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
695
+ const header = line.trim();
696
+ const sep = lines[i + 1];
697
+ i += 2;
698
+ const rows = [];
699
+ while (i < lines.length && /^\s*\|/.test(lines[i])) {
700
+ rows.push(lines[i].trim());
701
+ i++;
702
+ }
703
+ blocks.push({ type: "table", header, sep, rows });
704
+ continue;
705
+ }
706
+
707
+ const ul = line.match(/^\s*-\s+(.*)\s*$/);
708
+ if (ul) {
709
+ const items = [];
710
+ while (i < lines.length) {
711
+ const m = lines[i].match(/^\s*-\s+(.*)\s*$/);
712
+ if (!m) break;
713
+ items.push(m[1]);
714
+ i++;
715
+ }
716
+ blocks.push({ type: "ul", items });
717
+ continue;
718
+ }
719
+
720
+ const ol = line.match(/^\s*\d+\.\s+(.*)\s*$/);
721
+ if (ol) {
722
+ const items = [];
723
+ while (i < lines.length) {
724
+ const m = lines[i].match(/^\s*\d+\.\s+(.*)\s*$/);
725
+ if (!m) break;
726
+ items.push(m[1]);
727
+ i++;
728
+ }
729
+ blocks.push({ type: "ol", items });
730
+ continue;
731
+ }
732
+
733
+ if (!line.trim()) {
734
+ i++;
735
+ continue;
736
+ }
737
+
738
+ const para = [];
739
+ while (i < lines.length && lines[i].trim() && !/^```/.test(lines[i]) && !/^(#{1,6})\s+/.test(lines[i]) && !/^\s*-\s+/.test(lines[i]) && !/^\s*\d+\.\s+/.test(lines[i])) {
740
+ para.push(lines[i]);
741
+ i++;
742
+ }
743
+ blocks.push({ type: "p", text: para.join("\n") });
744
+ }
745
+ return blocks;
746
+ }
747
+
748
+ function renderInline(text) {
749
+ let t = escapeHtml(text);
750
+ t = t.replace(/`([^`]+)`/g, (m, g1) => `<code>${escapeHtml(g1)}</code>`);
751
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, g1, g2) => `<a href="${escapeHtml(g2)}" target="_blank" rel="noreferrer">${escapeHtml(g1)}</a>`);
752
+ return t.replace(/\n/g, "<br />");
753
+ }
754
+
755
+ function splitTableRow(line) {
756
+ let s = line.trim();
757
+ if (s.startsWith("|")) s = s.slice(1);
758
+ if (s.endsWith("|")) s = s.slice(0, -1);
759
+ return s.split("|").map((c) => c.trim());
760
+ }
761
+
762
+ async function renderMarkdown(md) {
763
+ const root = document.createElement("div");
764
+ const blocks = parseMarkdown(md);
765
+ for (const b of blocks) {
766
+ if (b.type === "heading") {
767
+ const h = document.createElement("h" + b.level);
768
+ h.innerHTML = renderInline(b.text);
769
+ root.appendChild(h);
770
+ continue;
771
+ }
772
+ if (b.type === "p") {
773
+ const p = document.createElement("p");
774
+ p.innerHTML = renderInline(b.text);
775
+ root.appendChild(p);
776
+ continue;
777
+ }
778
+ if (b.type === "ul") {
779
+ const ul = document.createElement("ul");
780
+ for (const it of b.items) {
781
+ const li = document.createElement("li");
782
+ li.innerHTML = renderInline(it);
783
+ ul.appendChild(li);
784
+ }
785
+ root.appendChild(ul);
786
+ continue;
787
+ }
788
+ if (b.type === "ol") {
789
+ const ol = document.createElement("ol");
790
+ for (const it of b.items) {
791
+ const li = document.createElement("li");
792
+ li.innerHTML = renderInline(it);
793
+ ol.appendChild(li);
794
+ }
795
+ root.appendChild(ol);
796
+ continue;
797
+ }
798
+ if (b.type === "table") {
799
+ const table = document.createElement("table");
800
+ const thead = document.createElement("thead");
801
+ const trh = document.createElement("tr");
802
+ for (const c of splitTableRow(b.header)) {
803
+ const th = document.createElement("th");
804
+ th.innerHTML = renderInline(c);
805
+ trh.appendChild(th);
806
+ }
807
+ thead.appendChild(trh);
808
+ table.appendChild(thead);
809
+ const tbody = document.createElement("tbody");
810
+ for (const row of b.rows) {
811
+ const tr = document.createElement("tr");
812
+ for (const c of splitTableRow(row)) {
813
+ const td = document.createElement("td");
814
+ td.innerHTML = renderInline(c);
815
+ tr.appendChild(td);
816
+ }
817
+ tbody.appendChild(tr);
818
+ }
819
+ table.appendChild(tbody);
820
+ root.appendChild(table);
821
+ continue;
822
+ }
823
+ if (b.type === "code") {
824
+ const lang = (b.lang || "").toLowerCase();
825
+ if (lang === "plantuml" || lang === "puml") {
826
+ const wrap = document.createElement("div");
827
+ wrap.className = "puml";
828
+ const img = document.createElement("img");
829
+ img.alt = "PlantUML";
830
+ wrap.appendChild(img);
831
+ root.appendChild(wrap);
832
+ try {
833
+ img.src = await plantUmlSvgUrl(b.text);
834
+ } catch (e) {
835
+ const pre = document.createElement("pre");
836
+ const code = document.createElement("code");
837
+ code.textContent = b.text;
838
+ pre.appendChild(code);
839
+ wrap.innerHTML = "";
840
+ wrap.appendChild(pre);
841
+ }
842
+ continue;
843
+ }
844
+ const pre = document.createElement("pre");
845
+ const code = document.createElement("code");
846
+ code.textContent = b.text;
847
+ pre.appendChild(code);
848
+ root.appendChild(pre);
849
+ continue;
850
+ }
851
+ }
852
+ return root;
853
+ }
854
+
855
+ async function openFile(path, btn) {
856
+ const content = state.files[path];
857
+ setActive(path, btn);
858
+ dom.viewer.innerHTML = "";
859
+ const t = fileType(path);
860
+ if (t === "HTML") {
861
+ const iframe = document.createElement("iframe");
862
+ iframe.srcdoc = content || "";
863
+ dom.viewer.appendChild(iframe);
864
+ return;
865
+ }
866
+ if (t === "PlantUML") {
867
+ const wrap = document.createElement("div");
868
+ wrap.className = "puml";
869
+ const img = document.createElement("img");
870
+ img.alt = "PlantUML";
871
+ wrap.appendChild(img);
872
+ dom.viewer.appendChild(wrap);
873
+ try {
874
+ img.src = await plantUmlSvgUrl(content || "");
875
+ } catch (e) {
876
+ const pre = document.createElement("pre");
877
+ const code = document.createElement("code");
878
+ code.textContent = content || "";
879
+ pre.appendChild(code);
880
+ dom.viewer.appendChild(pre);
881
+ }
882
+ return;
883
+ }
884
+ const rendered = await renderMarkdown(content || "");
885
+ dom.viewer.appendChild(rendered);
886
+ }
887
+
888
+ function setFiles(files) {
889
+ state.files = files || {};
890
+ state.paths = Object.keys(state.files)
891
+ .filter((p) => p && p !== "/specs/details/index.html" && p !== "index.html" && !p.endsWith("/specs/details/index.html"))
892
+ .sort((a, b) => a.localeCompare(b));
893
+ dom.fileCount.textContent = `${state.paths.length} files`;
894
+ renderSidebar();
895
+ const fromHash = decodeURIComponent((location.hash || "").replace(/^#/, "")) || "";
896
+ if (fromHash && state.files[fromHash] != null) {
897
+ openFile(fromHash, null);
898
+ return;
899
+ }
900
+ const fallback = pickFirst([
901
+ "/specs/background/original.md",
902
+ "/specs/background/scenarios.md",
903
+ "/specs/functions/core.md",
904
+ ]);
905
+ if (fallback && state.files[fallback] != null) openFile(fallback, null);
906
+ }
907
+
908
+ dom.search.addEventListener("input", () => renderSidebar());
909
+ dom.copyPath.addEventListener("click", async () => {
910
+ if (!state.activePath) return;
911
+ try {
912
+ await navigator.clipboard.writeText(state.activePath);
913
+ } catch (e) {}
914
+ });
915
+ dom.collapseAll.addEventListener("click", () => {
916
+ const ds = dom.tree.querySelectorAll("details");
917
+ for (const d of ds) d.open = false;
918
+ });
919
+
920
+ setFiles(window.__VSPEC_DETAILS_FILES__);
921
+ </script>
922
+ </body>
923
+ </html>