@seanmars/tospec 0.26.0 → 0.27.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +3 -1
  3. package/assets/how/page.mjs +235 -0
  4. package/assets/how/skeleton.html +790 -0
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/config.js +8 -0
  7. package/dist/commands/config.js.map +1 -1
  8. package/dist/core/shared/skill-generation.d.ts +7 -1
  9. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  10. package/dist/core/shared/skill-generation.js +3 -1
  11. package/dist/core/shared/skill-generation.js.map +1 -1
  12. package/dist/core/templates/fragments/debugging.d.ts +32 -0
  13. package/dist/core/templates/fragments/debugging.d.ts.map +1 -0
  14. package/dist/core/templates/fragments/debugging.js +32 -0
  15. package/dist/core/templates/fragments/debugging.js.map +1 -0
  16. package/dist/core/templates/fragments/verify.d.ts +2 -2
  17. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  18. package/dist/core/templates/fragments/verify.js +5 -3
  19. package/dist/core/templates/fragments/verify.js.map +1 -1
  20. package/dist/core/templates/skill-templates.d.ts +2 -0
  21. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  22. package/dist/core/templates/skill-templates.js +2 -0
  23. package/dist/core/templates/skill-templates.js.map +1 -1
  24. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  25. package/dist/core/templates/workflows/apply.js +16 -2
  26. package/dist/core/templates/workflows/apply.js.map +1 -1
  27. package/dist/core/templates/workflows/explore.js +3 -3
  28. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  29. package/dist/core/templates/workflows/grill.js +3 -1
  30. package/dist/core/templates/workflows/grill.js.map +1 -1
  31. package/dist/core/templates/workflows/how.d.ts +3 -0
  32. package/dist/core/templates/workflows/how.d.ts.map +1 -0
  33. package/dist/core/templates/workflows/how.js +283 -0
  34. package/dist/core/templates/workflows/how.js.map +1 -0
  35. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  36. package/dist/core/templates/workflows/issue.js +13 -0
  37. package/dist/core/templates/workflows/issue.js.map +1 -1
  38. package/dist/core/templates/workflows/restate-your-goal.d.ts +17 -0
  39. package/dist/core/templates/workflows/restate-your-goal.d.ts.map +1 -0
  40. package/dist/core/templates/workflows/restate-your-goal.js +45 -0
  41. package/dist/core/templates/workflows/restate-your-goal.js.map +1 -0
  42. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  43. package/dist/core/templates/workflows/update.js +1 -0
  44. package/dist/core/templates/workflows/update.js.map +1 -1
  45. package/package.json +2 -2
  46. package/schemas/issue/schema.yaml +7 -3
@@ -0,0 +1,790 @@
1
+ <!doctype html>
2
+ <html lang="{{LANG}}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <!-- Offline by construction: nothing outside this file can load. -->
7
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:">
8
+ <title>{{TITLE}}</title>
9
+ <style>
10
+ :root {
11
+ --bg: #fbfaf7;
12
+ --surface: #ffffff;
13
+ --ink: #1f2328;
14
+ --muted: #5b6470;
15
+ --line: #e2dfd8;
16
+ --accent: #2f5bd3;
17
+ --accent-soft: #e9effd;
18
+ --warn: #8a5300;
19
+ --warn-soft: #fdf4e3;
20
+ --open: #63389a;
21
+ --open-soft: #f3edfb;
22
+ --note: #1f6f4a;
23
+ --note-soft: #e9f6ef;
24
+ --sans: system-ui, -apple-system, "Segoe UI", "PingFang TC", "Microsoft JhengHei UI", "Microsoft JhengHei", "Noto Sans TC", "Noto Sans CJK TC", sans-serif;
25
+ --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
26
+ }
27
+ * { box-sizing: border-box; }
28
+ html { scroll-behavior: smooth; }
29
+ body {
30
+ margin: 0;
31
+ background: var(--bg);
32
+ color: var(--ink);
33
+ font: 17px/1.75 var(--sans);
34
+ -webkit-font-smoothing: antialiased;
35
+ }
36
+ body.dg-lock { overflow: hidden; }
37
+
38
+ .masthead { border-bottom: 1px solid var(--line); background: var(--surface); }
39
+ .masthead-inner { max-width: 74rem; margin: 0 auto; padding: 2.5rem 2rem 2rem; }
40
+ h1 { margin: 0 0 0.4rem; font-size: 2rem; line-height: 1.3; letter-spacing: -0.01em; }
41
+ .question { margin: 0; color: var(--muted); font-size: 1.05rem; }
42
+ .question::before { content: "\201C"; }
43
+ .question::after { content: "\201D"; }
44
+ .meta { margin: 0.9rem 0 0; color: var(--muted); font-size: 0.85rem; }
45
+
46
+ .layout { display: grid; grid-template-columns: 14rem minmax(0, 1fr); gap: 3rem; max-width: 74rem; margin: 0 auto; padding: 2rem; }
47
+ .toc { position: sticky; top: 1.5rem; align-self: start; max-height: calc(100vh - 3rem); overflow: auto; font-size: 0.9rem; }
48
+ .toc-title { margin: 0 0 0.5rem; color: var(--muted); font: 600 0.78rem/1 var(--sans); letter-spacing: 0.06em; text-transform: uppercase; }
49
+ .toc ol { list-style: none; margin: 0; padding: 0; border-left: 2px solid var(--line); }
50
+ .toc a { display: block; padding: 0.3rem 0 0.3rem 0.9rem; margin-left: -2px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; line-height: 1.45; }
51
+ .toc a:hover { color: var(--ink); }
52
+ .toc a.active { color: var(--accent); border-left-color: var(--accent); font-weight: 600; }
53
+
54
+ main { min-width: 0; max-width: 50rem; }
55
+ section { margin: 0 0 3.5rem; scroll-margin-top: 1.5rem; }
56
+ h2 { margin: 0 0 1rem; font-size: 1.45rem; line-height: 1.35; padding-bottom: 0.4rem; border-bottom: 1px solid var(--line); }
57
+ h3 { margin: 2rem 0 0.6rem; font-size: 1.12rem; line-height: 1.4; }
58
+ p { margin: 0 0 1rem; }
59
+ .lede { font-size: 1.12rem; }
60
+ a { color: var(--accent); }
61
+ strong { font-weight: 650; }
62
+ ul, ol { padding-left: 1.4rem; margin: 0 0 1rem; }
63
+ li { margin: 0.25rem 0; }
64
+
65
+ code { font: 0.86em/1.5 var(--mono); background: #f1efea; border-radius: 4px; padding: 0.1em 0.35em; }
66
+ code.ref { cursor: copy; color: var(--accent); background: var(--accent-soft); }
67
+ code.ref.copied { color: var(--note); background: var(--note-soft); }
68
+ pre { margin: 0 0 1.2rem; padding: 1rem 1.2rem; overflow: auto; background: #f6f5f1; border: 1px solid var(--line); border-radius: 8px; }
69
+ pre code { background: none; padding: 0; font-size: 0.85rem; }
70
+
71
+ .concepts { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: 0.9rem; margin: 0 0 1.2rem; }
72
+ .concepts > div { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; }
73
+ .concepts dt { font-weight: 650; margin: 0 0 0.3rem; }
74
+ .concepts dt code { font-size: 0.78em; margin-left: 0.3rem; }
75
+ .concepts dd { margin: 0; color: var(--muted); font-size: 0.95rem; line-height: 1.6; }
76
+
77
+ ol.steps { counter-reset: step; list-style: none; padding: 0; }
78
+ ol.steps > li { counter-increment: step; position: relative; padding: 0 0 1.1rem 2.6rem; margin: 0; }
79
+ ol.steps > li::before { content: counter(step); position: absolute; left: 0; top: 0.1rem; width: 1.7rem; height: 1.7rem; border-radius: 50%; background: var(--accent-soft); color: var(--accent); font: 700 0.85rem/1.7rem var(--sans); text-align: center; }
80
+ ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 0.83rem; top: 2rem; bottom: 0.2rem; width: 2px; background: var(--line); }
81
+
82
+ table { width: 100%; border-collapse: collapse; margin: 0 0 1.2rem; font-size: 0.95rem; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
83
+ th, td { text-align: left; vertical-align: top; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--line); }
84
+ th { background: #f6f5f1; font-weight: 650; }
85
+ tr:last-child td { border-bottom: none; }
86
+ td:first-child { white-space: nowrap; }
87
+
88
+ .callout { margin: 0 0 1rem; padding: 0.9rem 1.1rem; border-radius: 8px; border-left: 4px solid; }
89
+ .callout > :last-child { margin-bottom: 0; }
90
+ .callout > strong:first-child { display: block; margin-bottom: 0.25rem; }
91
+ .callout.warn { background: var(--warn-soft); border-color: var(--warn); }
92
+ .callout.warn > strong:first-child { color: var(--warn); }
93
+ .callout.open { background: var(--open-soft); border-color: var(--open); }
94
+ .callout.open > strong:first-child { color: var(--open); }
95
+ .callout.note { background: var(--note-soft); border-color: var(--note); }
96
+ .callout.note > strong:first-child { color: var(--note); }
97
+
98
+ details { margin: 0 0 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
99
+ summary { cursor: pointer; padding: 0.7rem 1rem; font-weight: 600; }
100
+ details > :not(summary) { margin-left: 1rem; margin-right: 1rem; }
101
+ details[open] > summary { border-bottom: 1px solid var(--line); margin-bottom: 0.8rem; }
102
+
103
+ figure.diagram { margin: 1.4rem 0 1.8rem; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
104
+ figure.diagram:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
105
+ .dg-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--line); background: #f8f7f3; font-size: 0.85rem; }
106
+ .dg-bar .dg-count { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 4.5rem; text-align: center; }
107
+ .dg-bar .dg-spacer { flex: 1; }
108
+ .dg-bar button { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding: 0.25rem 0.7rem; cursor: pointer; }
109
+ .dg-bar button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
110
+ .dg-bar button:disabled { opacity: 0.4; cursor: default; }
111
+ .dg-bar button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
112
+ .dg-canvas { overflow: auto; padding: 0.8rem; text-align: center; }
113
+ .dg-canvas svg { max-width: 100%; height: auto; }
114
+ .dg-panel { min-height: 3.2rem; padding: 0.7rem 1rem; border-top: 1px solid var(--line); background: #fcfbf9; font-size: 0.93rem; line-height: 1.6; }
115
+ .dg-panel .dg-hint { color: var(--muted); }
116
+ .dg-panel strong { display: block; }
117
+ .dg-panel p { margin: 0.2rem 0 0; }
118
+ figcaption { padding: 0.6rem 1rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.88rem; }
119
+ .dg-error { margin: 0.8rem; padding: 0.8rem 1rem; border-radius: 8px; background: #fdecec; color: #8f1d1d; font: 0.85rem/1.6 var(--mono); white-space: pre-wrap; text-align: left; }
120
+
121
+ figure.diagram.expanded { position: fixed; inset: 3vh 3vw; z-index: 50; margin: 0; display: flex; flex-direction: column; box-shadow: 0 0 0 100vmax rgba(31, 35, 40, 0.45), 0 20px 50px rgba(0, 0, 0, 0.25); }
122
+ figure.diagram.expanded .dg-canvas { flex: 1; }
123
+ figure.diagram.expanded .dg-canvas svg { max-width: none; width: calc(var(--w, 800px) * 1.35); }
124
+
125
+ /* Diagram elements: shared by generated and hand-written SVG. */
126
+ .dg text { font-family: var(--sans); font-size: 13px; fill: var(--ink); }
127
+ .dg .node { cursor: pointer; }
128
+ .dg .node rect, .dg .node path, .dg .node circle, .dg .node ellipse { fill: #ffffff; stroke: #3d4a5c; stroke-width: 1.4; }
129
+ .dg .node.k-store rect, .dg .node.k-store path, .dg .node.k-store ellipse { fill: #f3f7ec; stroke: #56762a; }
130
+ .dg .node.k-external rect { fill: #f7f7f5; stroke: #8a919b; stroke-dasharray: 5 4; }
131
+ .dg .node.k-actor rect { fill: #f4effc; stroke: #6a4a9c; }
132
+ .dg .node.k-decision path { fill: #fff7e5; stroke: #a3741b; }
133
+ .dg .node.k-state rect { fill: #eef3ff; stroke: #2f5bd3; }
134
+ .dg .node.k-start circle { fill: #1f2328; stroke: #1f2328; }
135
+ .dg .node.k-end circle { fill: #ffffff; stroke: #1f2328; stroke-width: 2; }
136
+ .dg .node.k-end circle.dot { fill: #1f2328; }
137
+ .dg .node:hover rect, .dg .node:hover path, .dg .node:hover ellipse { stroke: var(--accent); }
138
+ .dg .group rect { fill: #f7f6f2; stroke: #d6d2c8; stroke-dasharray: 6 4; }
139
+ .dg .group text { fill: var(--muted); font-size: 12px; font-weight: 600; letter-spacing: 0.04em; }
140
+ .dg .edge { cursor: pointer; }
141
+ .dg .edge path.line { fill: none; stroke: #6b7380; stroke-width: 1.5; }
142
+ .dg .edge.dashed path.line { stroke-dasharray: 6 4; }
143
+ .dg .edge path.hit { fill: none; stroke: transparent; stroke-width: 14; }
144
+ .dg .edge:hover path.line { stroke: var(--accent); }
145
+ .dg .label rect { fill: #ffffff; opacity: 0.94; }
146
+ .dg .label text { font-size: 12px; fill: #3d4550; }
147
+ .dg .lifeline { stroke: #b9b5ac; stroke-dasharray: 4 4; }
148
+ .dg .divider rect { fill: #f1efe9; }
149
+ .dg .divider text { font-size: 12px; font-weight: 600; fill: var(--muted); }
150
+ .dg [data-key] { transition: opacity 0.2s; }
151
+ .dg.stepping [data-key]:not(.on) { opacity: 0.18; }
152
+ .dg .on path.line { stroke: var(--accent); stroke-width: 2.4; }
153
+ .dg .node.on rect, .dg .node.on path, .dg .node.on ellipse { stroke: var(--accent); stroke-width: 2.4; }
154
+ .dg .picked rect, .dg .picked path, .dg .picked ellipse, .dg .picked path.line { stroke: var(--accent); stroke-width: 2.4; }
155
+
156
+ @media (max-width: 960px) {
157
+ .layout { grid-template-columns: minmax(0, 1fr); gap: 1rem; padding: 1.2rem; }
158
+ .toc { position: static; max-height: none; padding: 0.8rem 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
159
+ .masthead-inner { padding: 1.8rem 1.2rem 1.4rem; }
160
+ h1 { font-size: 1.6rem; }
161
+ }
162
+ @media print {
163
+ .toc, .dg-bar { display: none; }
164
+ .layout { display: block; }
165
+ figure.diagram, .callout, details { break-inside: avoid; }
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <header class="masthead">
171
+ <div class="masthead-inner">
172
+ <h1>{{TITLE}}</h1>
173
+ <p class="question">{{QUESTION}}</p>
174
+ <p class="meta">{{META}}</p>
175
+ </div>
176
+ </header>
177
+ <div class="layout">
178
+ <nav class="toc" aria-label="Contents"></nav>
179
+ <main>
180
+ <!-- CONTENT -->
181
+ </main>
182
+ </div>
183
+ <script>
184
+ (function () {
185
+ 'use strict';
186
+
187
+ var NS = 'http://www.w3.org/2000/svg';
188
+ var ZH = /^zh/i.test(document.documentElement.lang || '');
189
+ var T = ZH
190
+ ? { prev: '上一步', next: '下一步', all: '全部', expand: '放大', close: '關閉', contents: '目錄', copied: '已複製', error: '圖表定義有誤',
191
+ hint: '點選節點或箭頭查看說明.有步驟的圖可以用 ← → 逐步瀏覽.' }
192
+ : { prev: 'Previous', next: 'Next', all: 'All', expand: 'Expand', close: 'Close', contents: 'Contents', copied: 'Copied', error: 'Diagram spec error',
193
+ hint: 'Click a node or an arrow for details. Diagrams with steps can be walked with the arrow keys.' };
194
+ var uid = 0;
195
+
196
+ function make(name, attrs, parent) {
197
+ var node = document.createElementNS(NS, name);
198
+ for (var k in attrs) if (attrs[k] !== undefined && attrs[k] !== null) node.setAttribute(k, attrs[k]);
199
+ if (parent) parent.appendChild(node);
200
+ return node;
201
+ }
202
+ function html(name, className, parent, text) {
203
+ var node = document.createElement(name);
204
+ if (className) node.className = className;
205
+ if (text !== undefined) node.textContent = text;
206
+ if (parent) parent.appendChild(node);
207
+ return node;
208
+ }
209
+
210
+ // ---- text measurement: a CJK glyph is about twice as wide as a Latin one.
211
+ var CJK = 'ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩';
212
+ var CJK_CHAR = new RegExp('[' + CJK + ']');
213
+ var TOKENS = new RegExp('[' + CJK + ']|[^\\s' + CJK + ']+|\\s+', 'g');
214
+ function units(ch) { return CJK_CHAR.test(ch) ? 2 : 1; }
215
+ function width(s) { var n = 0; for (var i = 0; i < s.length; i++) n += units(s[i]); return n; }
216
+ function wrap(label, max) {
217
+ var out = [];
218
+ String(label).split('\n').forEach(function (para) {
219
+ var line = '';
220
+ var used = 0;
221
+ function flush() { if (line.trim()) out.push(line.trim()); line = ''; used = 0; }
222
+ (para.match(TOKENS) || []).forEach(function (tok) {
223
+ if (/^\s+$/.test(tok)) { if (line) { line += ' '; used += 1; } return; }
224
+ var w = width(tok);
225
+ if (used + w > max && line.trim()) flush();
226
+ while (w > max) {
227
+ var cut = '';
228
+ var cw = 0;
229
+ for (var i = 0; i < tok.length && cw + units(tok[i]) <= max - used; i++) { cut += tok[i]; cw += units(tok[i]); }
230
+ if (!cut) { flush(); continue; }
231
+ line += cut;
232
+ flush();
233
+ tok = tok.slice(cut.length);
234
+ w = width(tok);
235
+ }
236
+ line += tok;
237
+ used += w;
238
+ });
239
+ flush();
240
+ });
241
+ return out.length ? out : [''];
242
+ }
243
+ function textBlock(parent, lines, x, y, lineH, anchor) {
244
+ var t = make('text', { x: x, 'text-anchor': anchor || 'middle', 'dominant-baseline': 'central' }, parent);
245
+ var top = y - ((lines.length - 1) * lineH) / 2;
246
+ lines.forEach(function (line, i) {
247
+ var span = make('tspan', { x: x, y: top + i * lineH }, t);
248
+ span.textContent = line;
249
+ });
250
+ return t;
251
+ }
252
+ function labelSize(text, maxUnits) {
253
+ var lines = wrap(text, maxUnits || 24);
254
+ return { lines: lines, w: Math.max.apply(null, lines.map(width)) * 6.6 + 10, h: lines.length * 15 + 6 };
255
+ }
256
+ function labelBox(parent, size, x, y, key) {
257
+ var g = make('g', { 'class': 'label', 'data-key': key }, parent);
258
+ make('rect', { x: x - size.w / 2, y: y - size.h / 2, width: size.w, height: size.h, rx: 3 }, g);
259
+ textBlock(g, size.lines, x, y, 15);
260
+ return g;
261
+ }
262
+ function markers(svg, id) {
263
+ var defs = make('defs', {}, svg);
264
+ [['a', '#6b7380'], ['h', '#2f5bd3']].forEach(function (m) {
265
+ var mk = make('marker', { id: id + m[0], viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: 'auto-start-reverse' }, defs);
266
+ make('path', { d: 'M0 0 L10 5 L0 10 z', fill: m[1] }, mk);
267
+ });
268
+ var open = make('marker', { id: id + 'o', viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: 'auto-start-reverse' }, defs);
269
+ make('path', { d: 'M0 0 L10 5 L0 10', fill: 'none', stroke: '#6b7380', 'stroke-width': 1.5 }, open);
270
+ }
271
+ function newSvg(w, h) {
272
+ var id = 'dg' + (++uid);
273
+ var svg = make('svg', { viewBox: '0 0 ' + w + ' ' + h, width: w, height: h, 'class': 'dg', role: 'img' });
274
+ svg.style.setProperty('--w', w + 'px');
275
+ markers(svg, id);
276
+ return { svg: svg, id: id };
277
+ }
278
+
279
+ // ---- flow: architecture, flowchart and state diagrams on a grid.
280
+ var F = { colW: 220, rowH: 116, nodeW: 160, pad: 24, lineH: 17 };
281
+
282
+ function geometry(n) {
283
+ var kind = n.kind || 'box';
284
+ var cx = F.pad + n.col * F.colW + F.colW / 2;
285
+ var cy = F.pad + n.row * F.rowH + F.rowH / 2;
286
+ if (kind === 'start' || kind === 'end') return { n: n, kind: kind, shape: 'circle', cx: cx, cy: cy, hw: 11, hh: 11, lines: [] };
287
+ if (kind === 'decision') {
288
+ var dl = wrap(n.label || n.id, 17);
289
+ return { n: n, kind: kind, shape: 'diamond', cx: cx, cy: cy, hw: 86, hh: Math.max(36, dl.length * F.lineH / 2 + 24), lines: dl };
290
+ }
291
+ var lines = wrap(n.label || n.id, 21);
292
+ return { n: n, kind: kind, shape: 'rect', cx: cx, cy: cy, hw: F.nodeW / 2, hh: Math.max(24, lines.length * F.lineH / 2 + 12), lines: lines };
293
+ }
294
+ // Point where a ray from the node's centre leaves its outline, plus a small gap.
295
+ function boundary(g, dx, dy) {
296
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
297
+ var ux = dx / len;
298
+ var uy = dy / len;
299
+ var t;
300
+ if (g.shape === 'circle') t = g.hw;
301
+ else if (g.shape === 'diamond') t = 1 / (Math.abs(ux) / g.hw + Math.abs(uy) / g.hh);
302
+ else t = Math.min(Math.abs(ux) > 1e-6 ? g.hw / Math.abs(ux) : Infinity, Math.abs(uy) > 1e-6 ? g.hh / Math.abs(uy) : Infinity);
303
+ return { x: g.cx + ux * (t + 3), y: g.cy + uy * (t + 3) };
304
+ }
305
+ function route(a, b, bend, offset) {
306
+ var dx = b.cx - a.cx;
307
+ var dy = b.cy - a.cy;
308
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
309
+ var px = -dy / len;
310
+ var py = dx / len;
311
+ var mx = (a.cx + b.cx) / 2 + px * bend;
312
+ var my = (a.cy + b.cy) / 2 + py * bend;
313
+ var s = bend ? boundary(a, mx - a.cx, my - a.cy) : boundary(a, dx, dy);
314
+ var e = bend ? boundary(b, mx - b.cx, my - b.cy) : boundary(b, -dx, -dy);
315
+ s = { x: s.x + px * offset, y: s.y + py * offset };
316
+ e = { x: e.x + px * offset, y: e.y + py * offset };
317
+ if (!bend) return { s: s, e: e, d: 'M' + s.x + ' ' + s.y + ' L' + e.x + ' ' + e.y, lx: (s.x + e.x) / 2, ly: (s.y + e.y) / 2 };
318
+ // Control point chosen so the curve's midpoint lands on (mx, my).
319
+ var c = { x: 2 * mx - (s.x + e.x) / 2, y: 2 * my - (s.y + e.y) / 2 };
320
+ return { s: s, e: e, c: c, d: 'M' + s.x + ' ' + s.y + ' Q' + c.x + ' ' + c.y + ' ' + e.x + ' ' + e.y, lx: mx, ly: my };
321
+ }
322
+ function pointAt(r, t) {
323
+ if (!r.c) return { x: r.s.x + (r.e.x - r.s.x) * t, y: r.s.y + (r.e.y - r.s.y) * t };
324
+ var u = 1 - t;
325
+ return { x: u * u * r.s.x + 2 * u * t * r.c.x + t * t * r.e.x, y: u * u * r.s.y + 2 * u * t * r.c.y + t * t * r.e.y };
326
+ }
327
+ // Where on the edge a label covers no node and no other label: the midpoint
328
+ // first, then off to either side of the line, then further along it. Between
329
+ // two neighbouring nodes the midpoint alone would sit on top of them.
330
+ function labelSpot(r, size, geos, placed) {
331
+ var s = pointAt(r, 0.45);
332
+ var e = pointAt(r, 0.55);
333
+ var dx = e.x - s.x;
334
+ var dy = e.y - s.y;
335
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
336
+ var nx = -dy / len;
337
+ var ny = dx / len;
338
+ var side = size.h / 2 + 5;
339
+ var ts = [0.5, 0.38, 0.62, 0.28, 0.72];
340
+ // The outer shifts clear a whole node height: a label wider than the gap
341
+ // between two neighbours has no free spot on the line itself.
342
+ var shifts = [0, -side, side, -side * 2, side * 2, -side * 3, side * 3];
343
+ function free(x, y) {
344
+ var hw = size.w / 2 + 3;
345
+ var hh = size.h / 2 + 3;
346
+ for (var i = 0; i < geos.length; i++) {
347
+ var g = geos[i];
348
+ if (Math.abs(x - g.cx) < g.hw + hw && Math.abs(y - g.cy) < g.hh + hh) return false;
349
+ }
350
+ for (var j = 0; j < placed.length; j++) {
351
+ var p = placed[j];
352
+ if (Math.abs(x - p.x) < (p.w + size.w) / 2 + 2 && Math.abs(y - p.y) < (p.h + size.h) / 2 + 2) return false;
353
+ }
354
+ return true;
355
+ }
356
+ for (var a = 0; a < ts.length; a++) {
357
+ var q = pointAt(r, ts[a]);
358
+ for (var b = 0; b < shifts.length; b++) {
359
+ var x = q.x + nx * shifts[b];
360
+ var y = q.y + ny * shifts[b];
361
+ if (free(x, y)) return { x: x, y: y };
362
+ }
363
+ }
364
+ var mid = pointAt(r, 0.5);
365
+ return { x: mid.x, y: mid.y };
366
+ }
367
+ function crosses(r, geos, a, b) {
368
+ for (var i = 1; i < 30; i++) {
369
+ var q = pointAt(r, i / 30);
370
+ for (var j = 0; j < geos.length; j++) {
371
+ var g = geos[j];
372
+ if (g === a || g === b) continue;
373
+ if (Math.abs(q.x - g.cx) < g.hw + 6 && Math.abs(q.y - g.cy) < g.hh + 6) return true;
374
+ }
375
+ }
376
+ return false;
377
+ }
378
+
379
+ function validateFlow(spec) {
380
+ var errors = [];
381
+ var ids = {};
382
+ var cells = {};
383
+ (spec.nodes || []).forEach(function (n, i) {
384
+ if (!n.id) errors.push('nodes[' + i + ']: missing "id"');
385
+ if (ids[n.id]) errors.push('node "' + n.id + '" is defined twice');
386
+ ids[n.id] = true;
387
+ if (typeof n.col !== 'number' || typeof n.row !== 'number') errors.push('node "' + n.id + '": "col" and "row" must be numbers');
388
+ var cell = n.col + ',' + n.row;
389
+ if (cells[cell]) errors.push('nodes "' + cells[cell] + '" and "' + n.id + '" share cell col ' + n.col + ', row ' + n.row);
390
+ cells[cell] = n.id;
391
+ });
392
+ (spec.edges || []).forEach(function (e, i) {
393
+ if (!ids[e.from]) errors.push('edges[' + i + ']: unknown "from" node "' + e.from + '"');
394
+ if (!ids[e.to]) errors.push('edges[' + i + ']: unknown "to" node "' + e.to + '"');
395
+ });
396
+ (spec.steps || []).forEach(function (st, i) {
397
+ (st.show || []).forEach(function (key) {
398
+ var parts = String(key).split('->');
399
+ parts.forEach(function (p) { if (!ids[p]) errors.push('steps[' + i + ']: unknown node "' + p + '" in "' + key + '"'); });
400
+ });
401
+ });
402
+ return errors;
403
+ }
404
+
405
+ function renderFlow(spec, info) {
406
+ var geos = (spec.nodes || []).map(geometry);
407
+ var byId = {};
408
+ geos.forEach(function (g) { byId[g.n.id] = g; });
409
+ var cols = 1;
410
+ var rows = 1;
411
+ geos.forEach(function (g) { cols = Math.max(cols, g.n.col + 1); rows = Math.max(rows, g.n.row + 1); });
412
+ (spec.groups || []).forEach(function (gr) { cols = Math.max(cols, gr.cols[1] + 1); rows = Math.max(rows, gr.rows[1] + 1); });
413
+ var W = F.pad * 2 + Math.ceil(cols) * F.colW;
414
+ var H = F.pad * 2 + Math.ceil(rows) * F.rowH;
415
+ var made = newSvg(W, H);
416
+ var svg = made.svg;
417
+
418
+ (spec.groups || []).forEach(function (gr) {
419
+ var g = make('g', { 'class': 'group' }, svg);
420
+ var x = F.pad + gr.cols[0] * F.colW + 8;
421
+ var y = F.pad + gr.rows[0] * F.rowH + 4;
422
+ make('rect', { x: x, y: y, width: (gr.cols[1] - gr.cols[0] + 1) * F.colW - 16, height: (gr.rows[1] - gr.rows[0] + 1) * F.rowH - 8, rx: 10 }, g);
423
+ var t = make('text', { x: x + 12, y: y + 18 }, g);
424
+ t.textContent = gr.label || '';
425
+ });
426
+
427
+ var edgeLayer = make('g', {}, svg);
428
+ var labels = [];
429
+ var pairs = {};
430
+ (spec.edges || []).forEach(function (e) { pairs[e.from + '->' + e.to] = true; });
431
+ (spec.edges || []).forEach(function (e) {
432
+ var a = byId[e.from];
433
+ var b = byId[e.to];
434
+ var key = 'e:' + e.from + '->' + e.to;
435
+ var g = make('g', { 'class': 'edge' + (e.dashed ? ' dashed' : ''), 'data-key': key }, edgeLayer);
436
+ var r;
437
+ if (a === b) {
438
+ var sx = a.cx + a.hw * 0.35;
439
+ var sy = a.cy - a.hh - 2;
440
+ var ex = a.cx + a.hw + 2;
441
+ var ey = a.cy - a.hh * 0.35;
442
+ r = { d: 'M' + sx + ' ' + sy + ' C' + (sx + 6) + ' ' + (sy - 46) + ' ' + (ex + 46) + ' ' + (ey - 6) + ' ' + ex + ' ' + ey, loop: { x: ex + 30, y: sy - 30 } };
443
+ } else {
444
+ var offset = pairs[e.to + '->' + e.from] ? 7 : 0;
445
+ if (typeof e.bend === 'number') {
446
+ r = route(a, b, e.bend, offset);
447
+ } else {
448
+ // A straight line through another node reads as a connection to it,
449
+ // so bend around anything in the way.
450
+ var tries = [0, 50, -50, 95, -95];
451
+ for (var i = 0; i < tries.length; i++) {
452
+ r = route(a, b, tries[i], offset);
453
+ if (!crosses(r, geos, a, b)) break;
454
+ }
455
+ }
456
+ }
457
+ make('path', { 'class': 'hit', d: r.d }, g);
458
+ var line = make('path', { 'class': 'line', d: r.d, 'marker-end': 'url(#' + made.id + 'a)' }, g);
459
+ line.dataset.marker = made.id;
460
+ if (e.label) labels.push({ route: r, text: e.label, key: key });
461
+ info[key] = { title: e.label || (labelOf(a) + ' → ' + labelOf(b)), text: e.note, ref: e.ref, ends: [e.from, e.to] };
462
+ });
463
+
464
+ geos.forEach(function (g) {
465
+ var key = 'n:' + g.n.id;
466
+ var node = make('g', { 'class': 'node k-' + g.kind, 'data-key': key }, svg);
467
+ if (g.kind === 'start') {
468
+ make('circle', { cx: g.cx, cy: g.cy, r: 10 }, node);
469
+ } else if (g.kind === 'end') {
470
+ make('circle', { cx: g.cx, cy: g.cy, r: 11 }, node);
471
+ make('circle', { 'class': 'dot', cx: g.cx, cy: g.cy, r: 6 }, node);
472
+ } else if (g.shape === 'diamond') {
473
+ make('path', { d: 'M' + g.cx + ' ' + (g.cy - g.hh) + ' L' + (g.cx + g.hw) + ' ' + g.cy + ' L' + g.cx + ' ' + (g.cy + g.hh) + ' L' + (g.cx - g.hw) + ' ' + g.cy + ' z' }, node);
474
+ } else if (g.kind === 'store') {
475
+ var top = g.cy - g.hh;
476
+ var bottom = g.cy + g.hh;
477
+ var x0 = g.cx - g.hw;
478
+ var x1 = g.cx + g.hw;
479
+ make('path', { d: 'M' + x0 + ' ' + (top + 7) + ' L' + x0 + ' ' + (bottom - 7) + ' A' + g.hw + ' 7 0 0 0 ' + x1 + ' ' + (bottom - 7) + ' L' + x1 + ' ' + (top + 7) }, node);
480
+ make('ellipse', { cx: g.cx, cy: top + 7, rx: g.hw, ry: 7 }, node);
481
+ } else {
482
+ make('rect', { x: g.cx - g.hw, y: g.cy - g.hh, width: g.hw * 2, height: g.hh * 2, rx: g.kind === 'state' ? 18 : (g.kind === 'actor' ? 24 : 6) }, node);
483
+ }
484
+ if (g.lines.length) textBlock(node, g.lines, g.cx, g.kind === 'store' ? g.cy + 4 : g.cy, F.lineH);
485
+ else if (g.n.label) textBlock(node, wrap(g.n.label, 20), g.cx, g.cy + 26, 15);
486
+ info[key] = { title: labelOf(g), text: g.n.note, ref: g.n.ref };
487
+ });
488
+
489
+ // Labels go on top of the nodes, each sharing its edge's key so a step or
490
+ // a click treats the two as one.
491
+ var placed = [];
492
+ labels.forEach(function (l) {
493
+ var size = labelSize(l.text, 22);
494
+ var at = l.route.loop || labelSpot(l.route, size, geos, placed);
495
+ placed.push({ x: at.x, y: at.y, w: size.w, h: size.h });
496
+ labelBox(svg, size, at.x, at.y, l.key);
497
+ });
498
+
499
+ var steps = (spec.steps || []).map(function (st) {
500
+ var keys = [];
501
+ (st.show || []).forEach(function (k) {
502
+ var parts = String(k).split('->');
503
+ if (parts.length === 2) { keys.push('e:' + k, 'n:' + parts[0], 'n:' + parts[1]); } else keys.push('n:' + k);
504
+ });
505
+ return { title: st.title, text: st.text, keys: keys };
506
+ });
507
+ return { svg: svg, steps: steps };
508
+ }
509
+ function labelOf(g) { return (g.n && (g.n.label || g.n.id) || '').replace(/\n/g, ' '); }
510
+
511
+ // ---- sequence: who calls whom, in order.
512
+ var S = { colW: 184, pad: 24, headW: 152, rowH: 46 };
513
+
514
+ function validateSequence(spec) {
515
+ var errors = [];
516
+ var ids = {};
517
+ (spec.participants || []).forEach(function (p, i) {
518
+ if (!p.id) errors.push('participants[' + i + ']: missing "id"');
519
+ if (ids[p.id]) errors.push('participant "' + p.id + '" is defined twice');
520
+ ids[p.id] = true;
521
+ });
522
+ (spec.messages || []).forEach(function (m, i) {
523
+ if (m.divider !== undefined) return;
524
+ if (!ids[m.from]) errors.push('messages[' + i + ']: unknown "from" participant "' + m.from + '"');
525
+ if (!ids[m.to]) errors.push('messages[' + i + ']: unknown "to" participant "' + m.to + '"');
526
+ });
527
+ return errors;
528
+ }
529
+
530
+ function renderSequence(spec, info) {
531
+ var ps = spec.participants || [];
532
+ var xs = {};
533
+ ps.forEach(function (p, i) { xs[p.id] = S.pad + i * S.colW + S.colW / 2; });
534
+ var heads = ps.map(function (p) { return wrap(p.label || p.id, 19); });
535
+ var headH = Math.max.apply(null, heads.map(function (l) { return l.length * 16 + 20; }).concat([40]));
536
+ var W = S.pad * 2 + Math.max(1, ps.length) * S.colW;
537
+
538
+ // Lay rows out first: a wrapped label needs a taller row.
539
+ var y = S.pad + headH + 30;
540
+ var rows = (spec.messages || []).map(function (m) {
541
+ // The next message's label sits above its line, so leave it room below the band.
542
+ if (m.divider !== undefined) { var dy = y - 8; y += 46; return { m: m, y: dy }; }
543
+ var span = m.from === m.to ? 30 : Math.abs(xs[m.to] - xs[m.from]) / 6.6 - 2;
544
+ var lines = m.label ? wrap(m.label, Math.max(14, Math.floor(span))) : [];
545
+ var extra = Math.max(0, lines.length - 1) * 15;
546
+ var row = { m: m, y: y + extra, lines: lines };
547
+ y += S.rowH + extra + (m.from === m.to ? 16 : 0);
548
+ return row;
549
+ });
550
+ var H = y + S.pad;
551
+ var made = newSvg(W, H);
552
+ var svg = made.svg;
553
+
554
+ ps.forEach(function (p, i) {
555
+ var key = 'n:' + p.id;
556
+ var x = xs[p.id];
557
+ make('line', { 'class': 'lifeline', x1: x, y1: S.pad + headH, x2: x, y2: H - S.pad }, svg);
558
+ var g = make('g', { 'class': 'node k-' + (p.kind || 'box'), 'data-key': key }, svg);
559
+ make('rect', { x: x - S.headW / 2, y: S.pad, width: S.headW, height: headH, rx: p.kind === 'actor' ? 20 : 6 }, g);
560
+ textBlock(g, heads[i], x, S.pad + headH / 2, 16);
561
+ info[key] = { title: (p.label || p.id).replace(/\n/g, ' '), text: p.note, ref: p.ref };
562
+ });
563
+
564
+ var steps = [];
565
+ rows.forEach(function (row, i) {
566
+ var m = row.m;
567
+ if (m.divider !== undefined) {
568
+ var dg = make('g', { 'class': 'divider' }, svg);
569
+ make('rect', { x: S.pad / 2, y: row.y, width: W - S.pad, height: 24, rx: 4 }, dg);
570
+ var dt = make('text', { x: W / 2, y: row.y + 12, 'text-anchor': 'middle', 'dominant-baseline': 'central' }, dg);
571
+ dt.textContent = m.divider;
572
+ return;
573
+ }
574
+ var key = 'e:m' + i;
575
+ var g = make('g', { 'class': 'edge' + (m.reply ? ' dashed' : ''), 'data-key': key }, svg);
576
+ var x1 = xs[m.from];
577
+ var x2 = xs[m.to];
578
+ var d;
579
+ var lx;
580
+ var anchor = 'middle';
581
+ if (m.from === m.to) {
582
+ d = 'M' + x1 + ' ' + row.y + ' h34 v20 h-32';
583
+ lx = x1 + 42;
584
+ anchor = 'start';
585
+ } else {
586
+ var dir = x2 > x1 ? 1 : -1;
587
+ d = 'M' + (x1 + dir * 2) + ' ' + row.y + ' L' + (x2 - dir * 3) + ' ' + row.y;
588
+ lx = (x1 + x2) / 2;
589
+ }
590
+ make('path', { 'class': 'hit', d: d }, g);
591
+ var line = make('path', { 'class': 'line', d: d, 'marker-end': 'url(#' + made.id + (m.reply ? 'o' : 'a') + ')' }, g);
592
+ line.dataset.marker = made.id;
593
+ if (row.lines.length) {
594
+ var ty = m.from === m.to ? row.y + 10 : row.y - 10 - (row.lines.length - 1) * 15;
595
+ var t = make('text', { x: lx, 'text-anchor': anchor, 'class': 'msg' }, g);
596
+ row.lines.forEach(function (line2, j) {
597
+ var span = make('tspan', { x: lx, y: ty + j * 15 }, t);
598
+ span.textContent = line2;
599
+ });
600
+ }
601
+ info[key] = { title: m.label || (m.from + ' → ' + m.to), text: m.note, ref: m.ref, ends: [m.from, m.to] };
602
+ steps.push({ title: m.label, text: m.note, keys: [key, 'n:' + m.from, 'n:' + m.to] });
603
+ });
604
+ return { svg: svg, steps: spec.steps === false ? [] : steps };
605
+ }
606
+
607
+ // ---- figure chrome: toolbar, step-through, detail panel, expand.
608
+ function setupFigure(fig, svg, steps, info) {
609
+ var bar = html('div', 'dg-bar');
610
+ var canvas = html('div', 'dg-canvas');
611
+ var panel = html('div', 'dg-panel');
612
+ panel.setAttribute('aria-live', 'polite');
613
+ var caption = fig.querySelector('figcaption');
614
+ fig.insertBefore(bar, caption);
615
+ fig.insertBefore(canvas, caption);
616
+ fig.insertBefore(panel, caption);
617
+ canvas.appendChild(svg);
618
+ fig.tabIndex = 0;
619
+
620
+ var current = -1;
621
+ var prev;
622
+ var next;
623
+ var all;
624
+ var count;
625
+ if (steps.length) {
626
+ prev = html('button', '', bar, '◀ ' + T.prev);
627
+ count = html('span', 'dg-count', bar);
628
+ next = html('button', '', bar, T.next + ' ▶');
629
+ all = html('button', '', bar, T.all);
630
+ prev.type = next.type = all.type = 'button';
631
+ prev.onclick = function () { show(current - 1); };
632
+ next.onclick = function () { show(current + 1); };
633
+ all.onclick = function () { show(-1); };
634
+ }
635
+ html('span', 'dg-spacer', bar);
636
+ var expand = html('button', '', bar, T.expand);
637
+ expand.type = 'button';
638
+ expand.setAttribute('aria-pressed', 'false');
639
+ expand.onclick = function () { setExpanded(!fig.classList.contains('expanded')); };
640
+
641
+ function setExpanded(on) {
642
+ fig.classList.toggle('expanded', on);
643
+ document.body.classList.toggle('dg-lock', on);
644
+ expand.textContent = on ? T.close : T.expand;
645
+ expand.setAttribute('aria-pressed', on ? 'true' : 'false');
646
+ if (on) fig.focus();
647
+ }
648
+ function setMarkers(keys) {
649
+ svg.querySelectorAll('path.line').forEach(function (p) {
650
+ var lit = keys && p.parentNode.classList.contains('on');
651
+ // Hand-drawn SVG brings its own markers; a reply keeps its open head.
652
+ var m = p.getAttribute('marker-end') || '';
653
+ if (!p.dataset.marker || /o\)$/.test(m)) return;
654
+ p.setAttribute('marker-end', 'url(#' + p.dataset.marker + (lit ? 'h' : 'a') + ')');
655
+ });
656
+ }
657
+ function highlight(keys) {
658
+ svg.classList.toggle('stepping', !!keys);
659
+ svg.querySelectorAll('[data-key]').forEach(function (el) {
660
+ el.classList.toggle('on', !!keys && keys.indexOf(el.getAttribute('data-key')) !== -1);
661
+ });
662
+ setMarkers(keys);
663
+ }
664
+ function describe(title, text, ref, prefix) {
665
+ panel.textContent = '';
666
+ if (!title && !text) { html('span', 'dg-hint', panel, T.hint); return; }
667
+ var head = html('strong', '', panel, (prefix || '') + (title || ''));
668
+ if (ref) { head.appendChild(document.createTextNode(' ')); html('code', 'ref', head, ref); }
669
+ if (text) html('p', '', panel, text);
670
+ }
671
+ function show(i) {
672
+ svg.querySelectorAll('.picked').forEach(function (el) { el.classList.remove('picked'); });
673
+ if (!steps.length || i < 0 || i >= steps.length) {
674
+ current = -1;
675
+ highlight(null);
676
+ describe();
677
+ } else {
678
+ current = i;
679
+ highlight(steps[i].keys);
680
+ describe(steps[i].title, steps[i].text, null, (i + 1) + '. ');
681
+ }
682
+ if (count) {
683
+ count.textContent = current < 0 ? steps.length + ' ' + (ZH ? '步' : 'steps') : (current + 1) + ' / ' + steps.length;
684
+ prev.disabled = current <= 0;
685
+ next.disabled = current >= steps.length - 1;
686
+ all.disabled = current < 0;
687
+ }
688
+ }
689
+
690
+ svg.addEventListener('click', function (ev) {
691
+ var el = ev.target.closest('[data-key]');
692
+ if (!el) return;
693
+ var data = info[el.getAttribute('data-key')] || {};
694
+ if (current < 0) {
695
+ svg.querySelectorAll('.picked').forEach(function (p) { p.classList.remove('picked'); });
696
+ el.classList.add('picked');
697
+ }
698
+ describe(data.title || el.getAttribute('data-title'), data.text || el.getAttribute('data-note'), data.ref || el.getAttribute('data-ref'));
699
+ });
700
+ fig.addEventListener('keydown', function (ev) {
701
+ if (ev.key === 'Escape' && fig.classList.contains('expanded')) { setExpanded(false); ev.preventDefault(); }
702
+ if (!steps.length) return;
703
+ if (ev.key === 'ArrowRight') { show(Math.min(current + 1, steps.length - 1)); ev.preventDefault(); }
704
+ if (ev.key === 'ArrowLeft') { show(Math.max(current - 1, 0)); ev.preventDefault(); }
705
+ });
706
+ show(-1);
707
+ }
708
+
709
+ function initDiagram(fig) {
710
+ var info = {};
711
+ var specEl = fig.querySelector('script[type="application/json"]');
712
+ var handSvg = fig.querySelector(':scope > svg');
713
+ var result;
714
+ try {
715
+ if (specEl) {
716
+ var spec = JSON.parse(specEl.textContent);
717
+ var errors = spec.type === 'sequence' ? validateSequence(spec) : validateFlow(spec);
718
+ if (errors.length) throw new Error(errors.join('\n'));
719
+ result = spec.type === 'sequence' ? renderSequence(spec, info) : renderFlow(spec, info);
720
+ } else if (handSvg) {
721
+ handSvg.classList.add('dg');
722
+ if (!handSvg.style.getPropertyValue('--w') && handSvg.viewBox && handSvg.viewBox.baseVal) {
723
+ handSvg.style.setProperty('--w', handSvg.viewBox.baseVal.width + 'px');
724
+ }
725
+ result = { svg: handSvg, steps: [] };
726
+ } else {
727
+ return;
728
+ }
729
+ } catch (err) {
730
+ var box = html('div', 'dg-error', null, T.error + ':\n' + err.message);
731
+ fig.insertBefore(box, fig.querySelector('figcaption'));
732
+ if (window.console) console.error('[diagram]', err.message);
733
+ return;
734
+ }
735
+ setupFigure(fig, result.svg, result.steps, info);
736
+ }
737
+
738
+ // ---- page: contents, scroll spy, copyable code references.
739
+ function slug(text, i) {
740
+ var s = text.toLowerCase().trim().replace(/[^\wÀ-￿]+/g, '-').replace(/^-+|-+$/g, '');
741
+ return s || 'section-' + (i + 1);
742
+ }
743
+ function buildToc() {
744
+ var toc = document.querySelector('.toc');
745
+ var heads = document.querySelectorAll('main h2');
746
+ if (!toc || !heads.length) { if (toc) toc.remove(); return; }
747
+ html('p', 'toc-title', toc, T.contents);
748
+ var list = html('ol', '', toc);
749
+ var links = [];
750
+ heads.forEach(function (h, i) {
751
+ var target = h.closest('section') || h;
752
+ if (!target.id) target.id = slug(h.textContent, i);
753
+ var li = html('li', '', list);
754
+ var a = html('a', '', li, h.textContent);
755
+ a.href = '#' + target.id;
756
+ links.push({ a: a, target: target });
757
+ });
758
+ if (!('IntersectionObserver' in window)) return;
759
+ var io = new IntersectionObserver(function (entries) {
760
+ entries.forEach(function (en) {
761
+ if (!en.isIntersecting) return;
762
+ links.forEach(function (l) { l.a.classList.toggle('active', l.target === en.target); });
763
+ });
764
+ }, { rootMargin: '0px 0px -70% 0px' });
765
+ links.forEach(function (l) { io.observe(l.target); });
766
+ }
767
+ function copy(text) {
768
+ if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
769
+ var ta = html('textarea', '', document.body);
770
+ ta.value = text;
771
+ ta.select();
772
+ try { document.execCommand('copy'); } finally { ta.remove(); }
773
+ return Promise.resolve();
774
+ }
775
+ document.addEventListener('click', function (ev) {
776
+ var ref = ev.target.closest('code.ref');
777
+ if (!ref) return;
778
+ copy(ref.textContent).then(function () {
779
+ ref.classList.add('copied');
780
+ ref.title = T.copied;
781
+ setTimeout(function () { ref.classList.remove('copied'); ref.removeAttribute('title'); }, 1200);
782
+ });
783
+ });
784
+
785
+ document.querySelectorAll('figure.diagram').forEach(initDiagram);
786
+ buildToc();
787
+ })();
788
+ </script>
789
+ </body>
790
+ </html>