@disusered/okf-cli 1.1.0 → 1.3.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.
@@ -0,0 +1,915 @@
1
+ /**
2
+ * The generated page's behavior, as text.
3
+ *
4
+ * Marked parses stored Markdown and DOMPurify sanitizes it before insertion into the document.
5
+ * The script uses ES5 syntax because it is embedded in a TypeScript template literal. String.raw
6
+ * preserves the regular-expression backslashes.
7
+ */
8
+ export const PAGE_SCRIPT = String.raw `
9
+ (function () {
10
+ "use strict";
11
+
12
+ var payload = JSON.parse(document.getElementById("okf-graph").textContent);
13
+ var graph = payload.graph;
14
+
15
+ var nodeById = Object.create(null);
16
+ var linksBySource = Object.create(null);
17
+ var backlinks = Object.create(null);
18
+
19
+ graph.nodes.forEach(function (node) {
20
+ nodeById[node.id] = node;
21
+ var map = Object.create(null);
22
+ node.links.forEach(function (link) {
23
+ map[link.href] = link.pending ? "pending:" + link.target : link.target;
24
+ });
25
+ linksBySource[node.id] = map;
26
+ });
27
+
28
+ graph.edges.forEach(function (edge) {
29
+ (backlinks[edge.target] = backlinks[edge.target] || []).push(edge.source);
30
+ });
31
+
32
+ var detailEl = document.getElementById("detail");
33
+ var emptyEl = document.getElementById("detail-empty");
34
+ var contentEl = document.getElementById("detail-content");
35
+ var typeEl = document.getElementById("detail-type");
36
+ var titleEl = document.getElementById("detail-title");
37
+ var pathEl = document.getElementById("detail-path");
38
+ var descriptionEl = document.getElementById("detail-description");
39
+ var statusEl = document.getElementById("detail-status");
40
+ var sourcesEl = document.getElementById("detail-sources");
41
+ var bodyEl = document.getElementById("detail-body");
42
+ var outSection = document.getElementById("detail-out");
43
+ var outList = document.getElementById("links-out");
44
+ var backSection = document.getElementById("detail-back");
45
+ var backList = document.getElementById("links-back");
46
+ var searchInput = document.getElementById("search");
47
+ var typeSelect = document.getElementById("type");
48
+ var trustSelect = document.getElementById("trust");
49
+ var statusSelect = document.getElementById("status");
50
+ var staleOnly = document.getElementById("stale-only");
51
+ var staleOnlyLabel = document.getElementById("stale-only-label");
52
+ var layoutSelect = document.getElementById("layout");
53
+ var orientationSelect = document.getElementById("orientation");
54
+ var verifiedEl = document.getElementById("detail-verified");
55
+ var generatedTerm = document.getElementById("dt-generated");
56
+ var generatedEl = document.getElementById("detail-generated");
57
+ var resourceTerm = document.getElementById("dt-resource");
58
+ var resourceEl = document.getElementById("detail-resource");
59
+ var computationSection = document.getElementById("detail-computation");
60
+ var staleTerm = document.getElementById("dt-stale");
61
+ var staleEl = document.getElementById("detail-stale");
62
+ var tagsTerm = document.getElementById("dt-tags");
63
+ var tagsEl = document.getElementById("detail-tags");
64
+ var flagEl = document.getElementById("detail-flag");
65
+ var mainEl = document.querySelector("main");
66
+ var splitEl = document.getElementById("split");
67
+ var graphEl = document.getElementById("graph");
68
+
69
+ var TRUST_LABEL = {
70
+ "human-reviewed": "reviewed by a person",
71
+ "machine-confirmed": "confirmed by a machine only",
72
+ "unverified": "unverified"
73
+ };
74
+
75
+ var AUTHOR_LABEL = {
76
+ "human": "written by a person",
77
+ "agent": "written by an agent",
78
+ "process": "written by an automated process",
79
+ "unknown": "nobody recorded who wrote it"
80
+ };
81
+
82
+ var authored = graph.nodes.filter(function (node) { return !node.pending; });
83
+ var pendingCount = graph.nodes.length - authored.length;
84
+
85
+ document.getElementById("bundle").textContent = payload.bundle;
86
+ var counts = authored.length + " pages, " + graph.edges.length + " relationships";
87
+ if (pendingCount) { counts += ", " + pendingCount + " not written yet"; }
88
+ // Say it outright when the whole bundle shares one trust tier. A filter with one option is
89
+ // noise, but silence would let a reader infer a verification nobody performed.
90
+ if (graph.trustTiers.length === 1) { counts += ", all " + graph.trustTiers[0]; }
91
+ document.getElementById("counts").textContent = counts;
92
+
93
+ document.getElementById("evaluated").textContent =
94
+ payload.evaluatedAt ? "freshness evaluated " + payload.evaluatedAt : "";
95
+
96
+ function fillSelect(select, values, labeler) {
97
+ values.forEach(function (value) {
98
+ var option = document.createElement("option");
99
+ option.value = value;
100
+ option.textContent = labeler ? labeler(value) : value;
101
+ select.appendChild(option);
102
+ });
103
+ }
104
+
105
+ fillSelect(typeSelect, graph.types);
106
+ fillSelect(trustSelect, graph.trustTiers, function (t) { return TRUST_LABEL[t] || t; });
107
+ fillSelect(statusSelect, graph.statuses);
108
+
109
+ // Hiding a control is only ever about filtering. Every node draws its own trust tier
110
+ // regardless, so a uniform bundle still shows what that tier is.
111
+ trustSelect.hidden = graph.trustTiers.length < 2;
112
+ statusSelect.hidden = graph.statuses.length < 2;
113
+ var anyStale = graph.nodes.some(function (node) { return node.stale === true; });
114
+ staleOnlyLabel.hidden = !anyStale;
115
+
116
+ function runLayout(name) {
117
+ cy.layout({
118
+ name: name,
119
+ animate: false,
120
+ padding: 36,
121
+ nodeRepulsion: 6000,
122
+ idealEdgeLength: 100,
123
+ componentSpacing: 80,
124
+ nodeOverlap: 20,
125
+ // Fit once the layout settles, or a graph larger than its pane is silently clipped.
126
+ stop: function () { fitGraph(); }
127
+ }).run();
128
+ }
129
+
130
+ // The encoding is okf-viz's, computed when the page was generated and applied here. Two
131
+ // stylesheets travel with the page because the authorship ring is chosen at view time: a
132
+ // near-black ring is invisible on a dark background, and that ring is a signal that must
133
+ // always read. Neither the rules nor the node classes are decided in this file any more,
134
+ // so the graph is drawn one way by one implementation.
135
+ var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
136
+ var MUTED = dark ? payload.encoding.muted.dark : payload.encoding.muted.light;
137
+
138
+ var cy = cytoscape({
139
+ container: document.getElementById("graph"),
140
+ elements: graph.nodes.map(function (node) {
141
+ return {
142
+ data: {
143
+ id: node.id,
144
+ label: node.title,
145
+ color: node.color,
146
+ size: node.size,
147
+ shape: node.shape
148
+ },
149
+ classes: (payload.encoding.classes[node.id] || []).join(" ")
150
+ };
151
+ }).concat(graph.edges.map(function (edge) {
152
+ return {
153
+ data: { id: edge.id, source: edge.source, target: edge.target, relation: edge.relation },
154
+ classes: edge.attested ? "attested" : ""
155
+ };
156
+ })),
157
+ style: dark ? payload.encoding.style.dark : payload.encoding.style.light,
158
+ // Labels sit below their node, so the default packing overlaps them. Give the layout room.
159
+ layout: payload.encoding.layout
160
+ });
161
+
162
+ // Layout preferences live in localStorage, keyed generically so the page carries no origin.
163
+ // Every access is guarded: a file:// page throws SecurityError on storage in some browsers.
164
+ var PREFS_KEY = "okf.viz.prefs.v1";
165
+
166
+ function readPrefs() {
167
+ try {
168
+ var all = JSON.parse(window.localStorage.getItem(PREFS_KEY) || "{}");
169
+ return (all && all[payload.bundle]) || {};
170
+ } catch (error) { return {}; }
171
+ }
172
+
173
+ function writePrefs(patch) {
174
+ try {
175
+ var all = JSON.parse(window.localStorage.getItem(PREFS_KEY) || "{}");
176
+ if (!all || typeof all !== "object") { all = {}; }
177
+ var current = all[payload.bundle] || {};
178
+ for (var key in patch) { current[key] = patch[key]; }
179
+ all[payload.bundle] = current;
180
+ window.localStorage.setItem(PREFS_KEY, JSON.stringify(all));
181
+ } catch (error) { /* storage is a convenience; never let it break the page */ }
182
+ }
183
+
184
+ function setSplit(percent) {
185
+ var clamped = Math.max(20, Math.min(85, percent));
186
+ mainEl.style.setProperty("--split", clamped + "%");
187
+ splitEl.setAttribute("aria-valuenow", String(Math.round(clamped)));
188
+ return clamped;
189
+ }
190
+
191
+ function setOrientation(value) {
192
+ // Columns is the default and carries no attribute; rows is the opt-in.
193
+ if (value === "rows") { mainEl.setAttribute("data-orientation", "rows"); }
194
+ else { mainEl.removeAttribute("data-orientation"); }
195
+ orientationSelect.value = value;
196
+ splitEl.setAttribute("aria-orientation", value === "rows" ? "horizontal" : "vertical");
197
+ }
198
+
199
+ var prefs = readPrefs();
200
+ setOrientation(prefs.orientation === "rows" ? "rows" : "columns");
201
+ setSplit(typeof prefs.split === "number" ? prefs.split : 60);
202
+ if (prefs.layout && prefs.layout !== layoutSelect.value) {
203
+ layoutSelect.value = prefs.layout;
204
+ runLayout(prefs.layout);
205
+ }
206
+
207
+ // The constructor's layout ran against the pane as it was before the split was applied, so
208
+ // it fitted to the wrong width. Refit on the next frame, once flex has settled, and again
209
+ // whenever the window changes size.
210
+ window.requestAnimationFrame(fitGraph);
211
+ window.addEventListener("resize", fitGraph);
212
+
213
+ orientationSelect.addEventListener("change", function () {
214
+ setOrientation(orientationSelect.value);
215
+ writePrefs({ orientation: orientationSelect.value });
216
+ cy.resize();
217
+ });
218
+
219
+ var dragging = false;
220
+
221
+ splitEl.addEventListener("pointerdown", function (event) {
222
+ dragging = true;
223
+ splitEl.setPointerCapture(event.pointerId);
224
+ event.preventDefault();
225
+ });
226
+
227
+ splitEl.addEventListener("pointermove", function (event) {
228
+ if (!dragging) { return; }
229
+ var box = mainEl.getBoundingClientRect();
230
+ var columns = mainEl.getAttribute("data-orientation") === "columns";
231
+ var percent = columns
232
+ ? ((event.clientX - box.left) / box.width) * 100
233
+ : ((event.clientY - box.top) / box.height) * 100;
234
+ setSplit(percent);
235
+ });
236
+
237
+ splitEl.addEventListener("pointerup", function (event) {
238
+ if (!dragging) { return; }
239
+ dragging = false;
240
+ splitEl.releasePointerCapture(event.pointerId);
241
+ writePrefs({ split: Number(splitEl.getAttribute("aria-valuenow")) });
242
+ });
243
+
244
+ splitEl.addEventListener("keydown", function (event) {
245
+ var now = Number(splitEl.getAttribute("aria-valuenow"));
246
+ var next = event.key === "ArrowUp" || event.key === "ArrowLeft" ? now - 2
247
+ : event.key === "ArrowDown" || event.key === "ArrowRight" ? now + 2
248
+ : event.key === "Home" ? 20
249
+ : event.key === "End" ? 85
250
+ : null;
251
+ if (next === null) { return; }
252
+ event.preventDefault();
253
+ writePrefs({ split: setSplit(next) });
254
+ });
255
+
256
+ // Resize the canvas, but never re-fit: dragging the splitter must not throw away the
257
+ // reader's pan and zoom.
258
+ if (typeof ResizeObserver === "function") {
259
+ new ResizeObserver(function () {
260
+ cy.resize();
261
+ // The first real size may only arrive here, after the frame loop above gave up waiting.
262
+ if (!fitted) {
263
+ var box = document.getElementById("graph").getBoundingClientRect();
264
+ if (box.width >= 1 && box.height >= 1) { fitted = true; fitGraph(); }
265
+ }
266
+ }).observe(document.getElementById("graph"));
267
+ }
268
+
269
+ // Fit after a frame, not now: the flex container has not been laid out yet, so cytoscape
270
+ // would measure a zero or stale viewport and compute a zoom that leaves nodes off-screen.
271
+ // The padding is generous because fit measures nodes, not the labels drawn beneath them.
272
+ function fitGraph() {
273
+ cy.resize();
274
+ cy.fit(undefined, 60);
275
+ // fit measures node bounds; labels are drawn beneath and outside them, so ease off the
276
+ // zoom a little or the outermost captions sit past the edge of the pane.
277
+ cy.zoom(cy.zoom() * 0.82);
278
+ cy.center();
279
+ }
280
+
281
+ // One frame is not reliably enough: the flex container may still measure zero, and a fit
282
+ // against a zero viewport leaves every node off-screen. Wait until it has real dimensions.
283
+ var fitted = false;
284
+ function fitWhenSized() {
285
+ var box = document.getElementById("graph").getBoundingClientRect();
286
+ if (box.width < 1 || box.height < 1) { requestAnimationFrame(fitWhenSized); return; }
287
+ fitted = true;
288
+ fitGraph();
289
+ }
290
+ requestAnimationFrame(fitWhenSized);
291
+
292
+ cy.on("tap", "node", function (event) { showNode(event.target.id()); });
293
+ cy.on("tap", function (event) { if (event.target === cy) { clearSelection(); } });
294
+
295
+ // Pointing at a node answers "what is this" without answering it in the reader: hovering must
296
+ // never change the selection, or a reader loses the page they were reading by moving a mouse.
297
+ // Built with createElement and textContent, so a title can never become markup.
298
+ var hoverCard = document.createElement("div");
299
+ hoverCard.className = "hovercard";
300
+ hoverCard.hidden = true;
301
+ // Presented from here rather than the stylesheet so the card carries its own appearance; the
302
+ // class name is the hook if the page stylesheet ever takes it over.
303
+ hoverCard.style.position = "fixed";
304
+ hoverCard.style.zIndex = "20";
305
+ hoverCard.style.pointerEvents = "none";
306
+ hoverCard.style.maxWidth = "260px";
307
+ hoverCard.style.padding = "6px 8px";
308
+ hoverCard.style.borderRadius = "4px";
309
+ hoverCard.style.fontSize = "12px";
310
+ hoverCard.style.lineHeight = "1.45";
311
+ hoverCard.style.border = "1px solid " + (dark ? "#1f2937" : "#e2e8f0");
312
+ hoverCard.style.background = dark ? "#111827" : "#ffffff";
313
+ hoverCard.style.color = dark ? "#e5e7eb" : "#0f172a";
314
+ hoverCard.style.boxShadow = "0 2px 10px rgba(15, 23, 42, 0.22)";
315
+ document.body.appendChild(hoverCard);
316
+
317
+ function hoverLine(text, muted) {
318
+ var line = document.createElement("div");
319
+ if (muted) { line.style.color = MUTED; }
320
+ line.textContent = text;
321
+ return line;
322
+ }
323
+
324
+ function hideHover() { hoverCard.hidden = true; }
325
+
326
+ /** Place the card beside the node, then pull it back inside the window if it would overhang. */
327
+ function placeHover(rendered) {
328
+ var pane = graphEl.getBoundingClientRect();
329
+ var card = hoverCard.getBoundingClientRect();
330
+ var left = pane.left + rendered.x + 16;
331
+ var top = pane.top + rendered.y + 16;
332
+ if (left + card.width > window.innerWidth - 8) { left = window.innerWidth - card.width - 8; }
333
+ if (top + card.height > window.innerHeight - 8) { top = top - card.height - 32; }
334
+ hoverCard.style.left = Math.max(8, left) + "px";
335
+ hoverCard.style.top = Math.max(8, top) + "px";
336
+ }
337
+
338
+ function showHover(node, rendered) {
339
+ hoverCard.replaceChildren();
340
+ var title = hoverCard.appendChild(hoverLine(node.title, false));
341
+ title.style.fontWeight = "600";
342
+ hoverCard.appendChild(hoverLine(node.type, true));
343
+ // Who wrote it and when, in the words the page recorded. Say so plainly when nobody did:
344
+ // an empty line here would read as though the question had not been asked.
345
+ hoverCard.appendChild(hoverLine(
346
+ node.generated
347
+ ? node.generated.by + (node.generated.at ? " · " + node.generated.at : "")
348
+ : AUTHOR_LABEL[node.authorKind],
349
+ true
350
+ ));
351
+ hoverCard.hidden = false;
352
+ placeHover(rendered);
353
+ }
354
+
355
+ cy.on("mouseover", "node", function (event) {
356
+ var node = nodeById[event.target.id()];
357
+ if (node) { showHover(node, event.target.renderedPosition()); }
358
+ });
359
+ cy.on("mouseout", "node", hideHover);
360
+ // The card is anchored to a rendered position, so anything that moves the camera invalidates
361
+ // it. Dropping it is honest; leaving it pointing at empty canvas is not.
362
+ cy.on("pan zoom drag", hideHover);
363
+ graphEl.addEventListener("mouseleave", hideHover);
364
+
365
+ layoutSelect.addEventListener("change", function () {
366
+ runLayout(layoutSelect.value);
367
+ writePrefs({ layout: layoutSelect.value });
368
+ });
369
+
370
+ document.getElementById("reset").addEventListener("click", function () {
371
+ searchInput.value = "";
372
+ typeSelect.value = "";
373
+ trustSelect.value = "";
374
+ statusSelect.value = "";
375
+ staleOnly.checked = false;
376
+ setOrientation("columns");
377
+ setSplit(60);
378
+ applyFilters();
379
+ fitGraph();
380
+ clearSelection();
381
+ });
382
+
383
+ searchInput.addEventListener("input", applyFilters);
384
+ typeSelect.addEventListener("change", applyFilters);
385
+ trustSelect.addEventListener("change", applyFilters);
386
+ statusSelect.addEventListener("change", applyFilters);
387
+ staleOnly.addEventListener("change", applyFilters);
388
+
389
+ document.addEventListener("keydown", function (event) {
390
+ if (event.key === "Escape") { clearSelection(); }
391
+ });
392
+
393
+ function applyFilters() {
394
+ var query = searchInput.value.trim().toLowerCase();
395
+ var type = typeSelect.value;
396
+ var trust = trustSelect.value;
397
+ var status = statusSelect.value;
398
+ var onlyStale = staleOnly.checked;
399
+ cy.nodes().forEach(function (element) {
400
+ var node = nodeById[element.id()];
401
+ var matches = !type || node.type === type;
402
+ if (matches && trust) { matches = node.trustTier === trust; }
403
+ if (matches && status) { matches = node.status === status; }
404
+ if (matches && onlyStale) { matches = node.stale === true; }
405
+ if (matches && query) {
406
+ var haystack = (
407
+ node.title + " " + node.path + " " + node.description + " " + node.tags.join(" ")
408
+ ).toLowerCase();
409
+ matches = haystack.indexOf(query) !== -1;
410
+ }
411
+ element.toggleClass("dim", !matches);
412
+ });
413
+ cy.edges().forEach(function (element) {
414
+ element.toggleClass("dim", element.source().hasClass("dim") || element.target().hasClass("dim"));
415
+ });
416
+ }
417
+
418
+ /** Show a definition row only when it has a value, keeping term and value in step. */
419
+ function setRow(termSuffix, valueId, value) {
420
+ var term = document.getElementById("dt-" + termSuffix);
421
+ var element = document.getElementById(valueId);
422
+ var shown = value !== null && value !== undefined && value !== "";
423
+ term.hidden = !shown;
424
+ element.hidden = !shown;
425
+ element.textContent = shown ? value : "";
426
+ }
427
+
428
+ function clearSelection() {
429
+ cy.elements().unselect();
430
+ emptyEl.hidden = false;
431
+ contentEl.hidden = true;
432
+ }
433
+
434
+ function showNode(id, keepCamera) {
435
+ var node = nodeById[id];
436
+ if (!node) { return; }
437
+
438
+ cy.elements().unselect();
439
+ var element = cy.getElementById(id);
440
+ if (element && element.length) { element.select(); }
441
+
442
+ emptyEl.hidden = true;
443
+ contentEl.hidden = false;
444
+
445
+ typeEl.textContent = node.type;
446
+ typeEl.style.background = node.color;
447
+ titleEl.textContent = node.title;
448
+ pathEl.textContent = node.path;
449
+ descriptionEl.textContent = node.description || "—";
450
+ statusEl.textContent = node.status || (node.pending ? "not written yet" : "—");
451
+
452
+ // Show the checks themselves, newest first, with the derived rating underneath as the
453
+ // conclusion drawn from them. The row is never hidden: silence must not read as approval.
454
+ verifiedEl.replaceChildren();
455
+ if (node.verified.length) {
456
+ var events = node.verified.slice().sort(function (a, b) {
457
+ return String(b.at || "").localeCompare(String(a.at || ""));
458
+ });
459
+ var list = document.createElement("ul");
460
+ list.className = "plain";
461
+ events.forEach(function (event) {
462
+ var item = document.createElement("li");
463
+ var who = document.createElement("span");
464
+ who.className = "actor";
465
+ who.textContent = event.by;
466
+ item.appendChild(who);
467
+ if (event.at) {
468
+ var when = document.createElement("span");
469
+ when.className = "muted";
470
+ when.textContent = " · " + event.at;
471
+ item.appendChild(when);
472
+ }
473
+ list.appendChild(item);
474
+ });
475
+ verifiedEl.appendChild(list);
476
+ } else {
477
+ var none = document.createElement("div");
478
+ none.textContent = "no verification recorded";
479
+ verifiedEl.appendChild(none);
480
+ }
481
+ var rating = document.createElement("div");
482
+ rating.className = "muted";
483
+ rating.textContent = TRUST_LABEL[node.trustTier] || node.trustTier;
484
+ verifiedEl.appendChild(rating);
485
+
486
+ generatedTerm.hidden = !node.generated;
487
+ generatedEl.hidden = !node.generated;
488
+ generatedEl.textContent = node.generated
489
+ ? node.generated.by + (node.generated.at ? " · " + node.generated.at : "")
490
+ : "";
491
+
492
+ resourceTerm.hidden = !node.resource;
493
+ resourceEl.hidden = !node.resource;
494
+ resourceEl.replaceChildren();
495
+ if (node.resource) { appendExternal(node.resource, node.resource, resourceEl); }
496
+
497
+ var flag = node.pending ? "Pending" : node.stale === true ? "Stale"
498
+ : node.status === "deprecated" ? "Deprecated" : "";
499
+ flagEl.textContent = flag;
500
+ flagEl.hidden = !flag;
501
+
502
+ var staleText = "";
503
+ if (node.staleAfter) {
504
+ staleText = node.staleAfter;
505
+ if (node.stale === true) { staleText += " — passed"; }
506
+ else if (node.stale === false) { staleText += " — current as of " + payload.evaluatedAt; }
507
+ }
508
+ staleTerm.hidden = !staleText;
509
+ staleEl.hidden = !staleText;
510
+ staleEl.textContent = staleText;
511
+
512
+ tagsTerm.hidden = !node.tags.length;
513
+ tagsEl.hidden = !node.tags.length;
514
+ tagsEl.replaceChildren();
515
+ node.tags.forEach(function (tag) {
516
+ var button = document.createElement("button");
517
+ button.type = "button";
518
+ button.className = "tag";
519
+ button.textContent = tag;
520
+ button.addEventListener("click", function () {
521
+ searchInput.value = tag;
522
+ applyFilters();
523
+ });
524
+ tagsEl.appendChild(button);
525
+ });
526
+
527
+ sourcesEl.replaceChildren();
528
+ if (node.sources.length) {
529
+ var sourceList = document.createElement("ul");
530
+ sourceList.className = "plain";
531
+ node.sources.forEach(function (source) {
532
+ var item = document.createElement("li");
533
+ var label = source.title || source.resource;
534
+
535
+ // A source pointing inside the bundle selects that page, the same as a body link.
536
+ // Only an http(s) resource leaves the document.
537
+ if (source.resolvedPath && source.exists) {
538
+ var button = document.createElement("button");
539
+ button.type = "button";
540
+ button.className = "internal";
541
+ button.textContent = label;
542
+ button.addEventListener("click", function () { showNode(source.resolvedPath); });
543
+ item.appendChild(button);
544
+ } else {
545
+ appendExternal(source.resource, label, item);
546
+ }
547
+
548
+ if (!source.title) {
549
+ var untitled = document.createElement("span");
550
+ untitled.className = "muted";
551
+ untitled.textContent = " (untitled)";
552
+ item.appendChild(untitled);
553
+ }
554
+ if (source.exists === false) {
555
+ var missing = document.createElement("span");
556
+ missing.className = "warn-text";
557
+ missing.textContent = " — not written yet";
558
+ item.appendChild(missing);
559
+ }
560
+
561
+ // The credibility signals OKF records so a reader can judge the source itself.
562
+ var facts = [];
563
+ if (source.author) { facts.push("by " + source.author); }
564
+ if (source.lastModified) { facts.push("source last changed " + source.lastModified); }
565
+ if (source.id) { facts.push("cited as [^" + source.id + "]"); }
566
+ if (facts.length) {
567
+ var detail = document.createElement("span");
568
+ detail.className = "muted";
569
+ detail.textContent = facts.join(" · ");
570
+ item.appendChild(detail);
571
+ }
572
+ sourceList.appendChild(item);
573
+ });
574
+ sourcesEl.appendChild(sourceList);
575
+ } else {
576
+ sourcesEl.textContent = "none recorded";
577
+ }
578
+
579
+ // Only an Attested Computation carries a sanctioned way to produce its value.
580
+ var attestation = node.attestation;
581
+ computationSection.hidden = !attestation;
582
+ if (attestation) {
583
+ setRow("runtime", "comp-runtime", attestation.runtime);
584
+ setRow("computation", "comp-computation", attestation.computation);
585
+ setRow("executor", "comp-executor", attestation.executorResource);
586
+ setRow("attester", "comp-attester", attestation.attesterResource);
587
+ setRow("receipt", "comp-receipt", attestation.executorReceipt.join(", "));
588
+ setRow(
589
+ "parameters",
590
+ "comp-parameters",
591
+ attestation.parameters.map(function (parameter) {
592
+ var shown = parameter.name;
593
+ if (parameter.type) { shown += ": " + parameter.type; }
594
+ if (parameter.required === true) { shown += " (required)"; }
595
+ return shown;
596
+ }).join(", "),
597
+ );
598
+ }
599
+
600
+ bodyEl.replaceChildren();
601
+ renderMarkdown(node.body, bodyEl, node.id);
602
+
603
+ var outgoing = [];
604
+ var already = Object.create(null);
605
+ node.links.forEach(function (link) {
606
+ var id = link.pending ? "pending:" + link.target : link.target;
607
+ if (id !== node.id && !already[id]) {
608
+ already[id] = true;
609
+ outgoing.push(id);
610
+ }
611
+ });
612
+ fillReferences(outSection, outList, outgoing);
613
+ fillReferences(backSection, backList, backlinks[node.id] || []);
614
+
615
+ detailEl.scrollTop = 0;
616
+ // The camera only follows a deliberate selection. On first paint the page opens a node to
617
+ // fill the reader, and moving the view there would fight the initial fit and can leave the
618
+ // graph parked on empty canvas.
619
+ if (element && element.length && !keepCamera) {
620
+ cy.animate({ center: { eles: element }, zoom: Math.max(cy.zoom(), 0.9) }, { duration: 180 });
621
+ }
622
+ }
623
+
624
+ function fillReferences(section, list, ids) {
625
+ list.replaceChildren();
626
+ if (!ids.length) {
627
+ section.hidden = true;
628
+ return;
629
+ }
630
+ section.hidden = false;
631
+ ids.forEach(function (id) {
632
+ var target = nodeById[id];
633
+ var item = document.createElement("li");
634
+ item.appendChild(internalButton(id, target ? target.title : id));
635
+ var path = document.createElement("span");
636
+ path.className = "muted";
637
+ path.textContent = target ? target.path : id;
638
+ item.appendChild(path);
639
+ list.appendChild(item);
640
+ });
641
+ }
642
+
643
+ function internalButton(id, label) {
644
+ var button = document.createElement("button");
645
+ button.type = "button";
646
+ button.className = "internal";
647
+ button.textContent = label;
648
+ button.addEventListener("click", function () { showNode(id); });
649
+ return button;
650
+ }
651
+
652
+ function appendExternal(url, label, container) {
653
+ if (/^https?:\/\//i.test(url)) {
654
+ var anchor = document.createElement("a");
655
+ anchor.href = url;
656
+ anchor.target = "_blank";
657
+ anchor.rel = "noopener noreferrer";
658
+ anchor.textContent = label;
659
+ container.appendChild(anchor);
660
+ return;
661
+ }
662
+ container.appendChild(document.createTextNode(label));
663
+ }
664
+
665
+ // Parse, sanitize, then rewrite navigation. Images and inline styles are forbidden.
666
+ var SANITIZE = {
667
+ USE_PROFILES: { html: true },
668
+ FORBID_TAGS: ["img", "form", "input", "textarea", "select", "style"],
669
+ FORBID_ATTR: ["style"]
670
+ };
671
+
672
+ function renderMarkdown(text, container, sourceId) {
673
+ var html = marked.parse(String(text), { gfm: true, breaks: false, async: false });
674
+ container.innerHTML = DOMPurify.sanitize(html, SANITIZE);
675
+ rewriteLinks(container, sourceId);
676
+ }
677
+
678
+ // The bundle analysis already resolved internal links; do not resolve them again here.
679
+ function rewriteLinks(root, sourceId) {
680
+ var known = linksBySource[sourceId] || Object.create(null);
681
+ var anchors = Array.prototype.slice.call(root.querySelectorAll("a[href]"));
682
+
683
+ anchors.forEach(function (anchor) {
684
+ var href = anchor.getAttribute("href") || "";
685
+ var id = known[href];
686
+ if (id === undefined) {
687
+ try {
688
+ id = known[decodeURIComponent(href)];
689
+ } catch {
690
+ id = undefined;
691
+ }
692
+ }
693
+
694
+ var label = anchor.textContent || href;
695
+
696
+ if (id !== undefined && nodeById[id]) {
697
+ anchor.replaceWith(internalButton(id, label));
698
+ return;
699
+ }
700
+ if (/^https?:\/\//i.test(href)) {
701
+ anchor.target = "_blank";
702
+ anchor.rel = "noopener noreferrer";
703
+ return;
704
+ }
705
+ // Keep the label but remove an unresolved or disallowed link.
706
+ anchor.replaceWith(document.createTextNode(label));
707
+ });
708
+ }
709
+
710
+ var opening = graph.nodes.find(function (node) { return node.path === "index.md"; }) || graph.nodes[0];
711
+ if (opening) { showNode(opening.id, true); }
712
+
713
+ /* === dated strip and source usage bars ====================================================
714
+ * Appended as one block so the reader panel gains a timeline and proportional usage bars
715
+ * without editing showNode above. Everything here only reads the node payload and writes
716
+ * into elements the markup already carries.
717
+ * ======================================================================================= */
718
+
719
+ var timelineSection = document.getElementById("detail-timeline");
720
+ var timelineMarks = document.getElementById("timeline-marks");
721
+ var timelineOverrun = document.getElementById("timeline-overrun");
722
+ var timelineNote = document.getElementById("timeline-note");
723
+
724
+ var DAY = 86400000;
725
+
726
+ /** Milliseconds for a date or a datetime, or null when the value is neither. */
727
+ function instant(value) {
728
+ if (!value) { return null; }
729
+ var parsed = Date.parse(value);
730
+ return isNaN(parsed) ? null : parsed;
731
+ }
732
+
733
+ /** Strips a time off a datetime: the strip is about days, and a clock crowds the label. */
734
+ function calendarDay(value) { return String(value).slice(0, 10); }
735
+
736
+ /**
737
+ * Every dated event on a page, chronological. The evaluation date is only ever a companion
738
+ * to the page's own dates: it anchors them, but it never earns the strip on its own.
739
+ */
740
+ function timelineEvents(node) {
741
+ var events = [];
742
+
743
+ function add(kind, label, at, title) {
744
+ var time = instant(at);
745
+ if (time === null) { return; }
746
+ events.push({ kind: kind, label: label, at: calendarDay(at), time: time, title: title });
747
+ }
748
+
749
+ if (node.generated) {
750
+ add("written", "written", node.generated.at, "written by " + node.generated.by);
751
+ }
752
+ node.verified.forEach(function (event) {
753
+ add("checked", event.by, event.at, "checked by " + event.by);
754
+ });
755
+ if (node.staleAfter) {
756
+ var expiry = node.stale === true ? "expired" : "expires";
757
+ add("expires", expiry, node.staleAfter, "stale after");
758
+ }
759
+ if (!events.length) { return events; }
760
+ add("today", "today", payload.evaluatedAt, "freshness evaluated");
761
+ events.sort(function (a, b) { return a.time - b.time; });
762
+ return events;
763
+ }
764
+
765
+ function renderTimeline(node) {
766
+ timelineMarks.replaceChildren();
767
+ timelineOverrun.hidden = true;
768
+ timelineNote.hidden = true;
769
+ timelineNote.textContent = "";
770
+
771
+ var events = node ? timelineEvents(node) : [];
772
+ timelineSection.hidden = !events.length;
773
+ if (!events.length) { return; }
774
+
775
+ // One date, or several landing on the same instant, has no span to scale against.
776
+ var low = events[0].time;
777
+ var high = events[events.length - 1].time;
778
+ var flat = high <= low;
779
+ var pad = flat ? 0 : (high - low) * 0.08;
780
+ var from = low - pad;
781
+ var span = flat ? 0 : (high - low) + pad * 2;
782
+
783
+ function place(time) { return flat ? 50 : ((time - from) / span) * 100; }
784
+
785
+ // Alternate sides by whichever one was used less recently, so a cluster of dates spreads
786
+ // across both instead of stacking its labels on one.
787
+ var lastAbove = -1000;
788
+ var lastBelow = -1000;
789
+ var drawn = [];
790
+
791
+ events.forEach(function (event) {
792
+ var x = place(event.time);
793
+ var above = lastAbove <= lastBelow;
794
+ if (above) { lastAbove = x; } else { lastBelow = x; }
795
+
796
+ var mark = document.createElement("div");
797
+ mark.className = "tl-mark tl-" + event.kind + (above ? " tl-above" : " tl-below");
798
+ mark.style.left = x + "%";
799
+ mark.title = event.title + " " + event.at;
800
+
801
+ var stem = document.createElement("span");
802
+ stem.className = "tl-stem";
803
+ mark.appendChild(stem);
804
+
805
+ var dot = document.createElement("span");
806
+ dot.className = "tl-dot";
807
+ mark.appendChild(dot);
808
+
809
+ var label = document.createElement("span");
810
+ label.className = "tl-label";
811
+ var what = document.createElement("span");
812
+ what.className = "tl-what";
813
+ what.textContent = event.label;
814
+ var when = document.createElement("span");
815
+ when.className = "tl-when";
816
+ when.textContent = event.at;
817
+ label.appendChild(what);
818
+ label.appendChild(when);
819
+ mark.appendChild(label);
820
+
821
+ timelineMarks.appendChild(mark);
822
+ drawn.push({ mark: mark, label: label });
823
+ });
824
+
825
+ // Anchor a label inward only when centring it would actually run off the strip. Measuring
826
+ // beats a percentage threshold here: the labels vary from "today" to a namespaced actor,
827
+ // so how close to the end is too close depends on the label, not on the date.
828
+ var strip = timelineMarks.getBoundingClientRect();
829
+ if (strip.width > 0) {
830
+ drawn.forEach(function (entry) {
831
+ var box = entry.label.getBoundingClientRect();
832
+ if (box.left < strip.left) { entry.mark.classList.add("tl-start"); }
833
+ else if (box.right > strip.right) { entry.mark.classList.add("tl-end"); }
834
+ });
835
+ }
836
+
837
+ // The overrun is the stretch between an expiry already passed and the evaluation date.
838
+ var expired = instant(node.staleAfter);
839
+ var today = instant(payload.evaluatedAt);
840
+ if (expired !== null && today !== null && today > expired) {
841
+ var left = place(expired);
842
+ timelineOverrun.style.left = left + "%";
843
+ timelineOverrun.style.width = (place(today) - left) + "%";
844
+ timelineOverrun.hidden = false;
845
+ var days = Math.round((today - expired) / DAY);
846
+ timelineNote.textContent = days + (days === 1 ? " day" : " days") + " past its stale-after date";
847
+ timelineNote.hidden = false;
848
+ }
849
+ }
850
+
851
+ /**
852
+ * Proportional usage bars, appended to the source rows showNode already drew. The scale is
853
+ * the busiest source on this page: the counts answer "which of these carries the weight",
854
+ * and a bundle-wide scale would flatten every page that cites nothing popular.
855
+ */
856
+ function renderUsage(node) {
857
+ var list = sourcesEl.querySelector("ul.plain");
858
+ if (!list) { return; }
859
+
860
+ var largest = 0;
861
+ node.sources.forEach(function (source) {
862
+ if (source.usageCount !== null && source.usageCount > largest) { largest = source.usageCount; }
863
+ });
864
+ if (!largest) { return; }
865
+
866
+ var rows = list.children;
867
+ node.sources.forEach(function (source, index) {
868
+ var row = rows[index];
869
+ if (!row || source.usageCount === null) { return; }
870
+
871
+ var usage = document.createElement("span");
872
+ usage.className = "usage";
873
+ var bar = document.createElement("span");
874
+ bar.className = "usage-bar";
875
+ var fill = document.createElement("span");
876
+ fill.className = "usage-fill";
877
+ fill.style.width = ((source.usageCount / largest) * 100) + "%";
878
+ bar.appendChild(fill);
879
+ var count = document.createElement("span");
880
+ count.className = "usage-count";
881
+ count.textContent = "used " + source.usageCount.toLocaleString() + " times";
882
+ usage.appendChild(bar);
883
+ usage.appendChild(count);
884
+ row.appendChild(usage);
885
+ });
886
+
887
+ // The window is a property of the measurement, so it is stated once rather than per row.
888
+ var measured = node.usageWindow;
889
+ if (measured && (measured.from || measured.to)) {
890
+ var caption = document.createElement("div");
891
+ caption.className = "usage-window";
892
+ caption.textContent = "counted between " + (measured.from || "?") + " and " + (measured.to || "?");
893
+ sourcesEl.appendChild(caption);
894
+ }
895
+ }
896
+
897
+ // Wrap rather than edit: every existing call site reads this binding, so the strip and the
898
+ // bars follow any selection, including one made from inside the panel.
899
+ var baseShowNode = showNode;
900
+ showNode = function (id, keepCamera) {
901
+ baseShowNode(id, keepCamera);
902
+ var node = nodeById[id];
903
+ if (!node) { return; }
904
+ renderTimeline(node);
905
+ renderUsage(node);
906
+ };
907
+
908
+ // The opening page was drawn above, before this block existed. Catch it up.
909
+ if (opening) {
910
+ renderTimeline(opening);
911
+ renderUsage(opening);
912
+ }
913
+ })();
914
+ `;
915
+ //# sourceMappingURL=script.js.map