@wxxb789/anc 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/astro.config.mjs +255 -0
- package/bin/anc.mjs +798 -0
- package/package.json +93 -0
- package/public/_headers +45 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/fonts/Temml.woff2 +0 -0
- package/scripts/build-snapshot.js +69 -0
- package/scripts/build-wasm.js +67 -0
- package/scripts/build-workspace.js +50 -0
- package/scripts/copy-snapshot.js +48 -0
- package/scripts/copy-wasm.js +57 -0
- package/scripts/emit-redirects.js +64 -0
- package/scripts/generate-corpus.js +421 -0
- package/scripts/init-repository.js +249 -0
- package/scripts/load-config.js +975 -0
- package/scripts/markdown-to-artifact.js +1286 -0
- package/scripts/preview-site.js +364 -0
- package/scripts/publish-set-review.js +157 -0
- package/scripts/resolve-links.js +640 -0
- package/scripts/run-pagefind.js +125 -0
- package/scripts/scan-residue.js +1016 -0
- package/scripts/scan-secrets.js +268 -0
- package/scripts/seed-gitignore.js +410 -0
- package/scripts/snapshot-rows.js +205 -0
- package/scripts/validate-content.js +95 -0
- package/scripts/vendor-provenance.js +307 -0
- package/scripts/verify-output-inventory.js +307 -0
- package/scripts/write-report.js +351 -0
- package/scripts/write-snapshot.js +136 -0
- package/src/components/CollectionExplorer.astro +149 -0
- package/src/components/FacetIndex.astro +45 -0
- package/src/components/LinkedNotes.astro +94 -0
- package/src/components/NoteGraph.astro +434 -0
- package/src/components/NoteList.astro +54 -0
- package/src/components/SiteMetadata.astro +143 -0
- package/src/components/TableOfContents.astro +69 -0
- package/src/components/TagBrowser.astro +70 -0
- package/src/layouts/Layout.astro +305 -0
- package/src/lib/artifact-source.js +72 -0
- package/src/lib/build-bindings.js +11 -0
- package/src/lib/collection-navigation.js +145 -0
- package/src/lib/content.js +92 -0
- package/src/lib/diagram-mode.js +18 -0
- package/src/lib/graph-layout.js +219 -0
- package/src/lib/graph-selection.js +84 -0
- package/src/lib/graph.js +42 -0
- package/src/lib/link-resolution.js +389 -0
- package/src/lib/markdown.js +1469 -0
- package/src/lib/math-mode.js +31 -0
- package/src/lib/math-output.js +131 -0
- package/src/lib/math.js +165 -0
- package/src/lib/mermaid-environment.js +475 -0
- package/src/lib/mermaid-render.js +769 -0
- package/src/lib/preview-model.js +96 -0
- package/src/lib/relations.js +242 -0
- package/src/lib/rendered-marker.js +55 -0
- package/src/lib/route-path.js +192 -0
- package/src/lib/routes.js +342 -0
- package/src/lib/schema.js +578 -0
- package/src/lib/site.js +578 -0
- package/src/lib/snapshot-contract.js +216 -0
- package/src/lib/snapshot-operations.js +147 -0
- package/src/lib/snapshot-queries.js +216 -0
- package/src/lib/snapshot-reader.js +210 -0
- package/src/lib/snapshot.js +233 -0
- package/src/lib/sqlite-warning.js +22 -0
- package/src/lib/sqlite.js +14 -0
- package/src/lib/tag-browser-model.js +18 -0
- package/src/lib/translations.js +533 -0
- package/src/lib/wasm-asset.js +47 -0
- package/src/lib/worker-protocol.js +93 -0
- package/src/pages/404.astro +35 -0
- package/src/pages/about.astro +100 -0
- package/src/pages/collections/[slug].astro +36 -0
- package/src/pages/collections/index.astro +33 -0
- package/src/pages/graph.astro +64 -0
- package/src/pages/index.astro +33 -0
- package/src/pages/notes/[slug].astro +509 -0
- package/src/pages/privacy.astro +121 -0
- package/src/pages/private.astro +59 -0
- package/src/pages/recent.astro +33 -0
- package/src/pages/robots.txt.js +15 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +20 -0
- package/src/pages/tags/[tag].astro +56 -0
- package/src/pages/tags/index.astro +37 -0
- package/src/scripts/code-copy.js +45 -0
- package/src/scripts/diagram-disabled.js +16 -0
- package/src/scripts/diagram.js +322 -0
- package/src/scripts/graph-client.js +222 -0
- package/src/scripts/graph-draw.js +166 -0
- package/src/scripts/link-preview.js +334 -0
- package/src/scripts/math-disabled.js +18 -0
- package/src/scripts/math.js +133 -0
- package/src/scripts/preferences.js +123 -0
- package/src/scripts/search-dialog.js +651 -0
- package/src/scripts/snapshot-client.js +157 -0
- package/src/scripts/snapshot-worker.js +341 -0
- package/src/scripts/tag-browser.js +153 -0
- package/src/scripts/theme-init.js +35 -0
- package/src/scripts/toc-active.js +38 -0
- package/src/styles/code.css +152 -0
- package/src/styles/diagram.css +149 -0
- package/src/styles/global.css +1739 -0
- package/src/styles/graph.css +317 -0
- package/src/styles/math.css +376 -0
- package/src/styles/tokens.css +171 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive graph exploration: re-center and tag-filter over the shared
|
|
3
|
+
* snapshot Worker.
|
|
4
|
+
*
|
|
5
|
+
* Progressive enhancement only. The server-rendered SVG, table, and lists remain
|
|
6
|
+
* the baseline and are never blanked by a failure here; the controls are hidden
|
|
7
|
+
* until this module runs. It imports only leaf modules (`graph-selection`,
|
|
8
|
+
* `graph-layout`, `route-path`, `translations`, the shared client), never the
|
|
9
|
+
* content artifact or the route model, so graph-free pages do not carry it and
|
|
10
|
+
* the bundle stays small. `partLanguage` is the one thing it takes from the
|
|
11
|
+
* translations contract, and it is a BCP 47 comparison rather than a locale
|
|
12
|
+
* lookup, so the built bundle still ships no locale table.
|
|
13
|
+
*
|
|
14
|
+
* This file is the stateful wiring (center, tag filter, request sequence); the
|
|
15
|
+
* stateless figure and table construction lives in `graph-draw.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import { layoutGlobal, layoutLocal, boundCounts } from "../lib/graph-layout.js";
|
|
18
|
+
import { noteSlugFromPath } from "../lib/route-path.js";
|
|
19
|
+
import { drawFigure, drawTable, fillTemplate } from "./graph-draw.js";
|
|
20
|
+
import { requestGlobalGraph, requestLocalGraph } from "./snapshot-client.js";
|
|
21
|
+
const region = document.querySelector('[data-graph-region]');
|
|
22
|
+
const controls = document.querySelector('[data-graph-controls]');
|
|
23
|
+
if (region && controls)
|
|
24
|
+
install(region, controls);
|
|
25
|
+
function install(region, controls) {
|
|
26
|
+
const data = controls.dataset;
|
|
27
|
+
const origin = data['graphCenter'] || noteSlugFromPath(location.pathname) || null;
|
|
28
|
+
const resetButton = controls.querySelector('[data-graph-reset-control]');
|
|
29
|
+
const view = {
|
|
30
|
+
region,
|
|
31
|
+
scope: data['graphScope'] ?? 'local',
|
|
32
|
+
chooser: controls.querySelector('[data-graph-tag]'),
|
|
33
|
+
status: region.querySelector('[data-graph-status]'),
|
|
34
|
+
canvas: region.querySelector('[data-graph-canvas]'),
|
|
35
|
+
tableBody: region.querySelector('[data-graph-body]'),
|
|
36
|
+
template: (name) => data[name] ?? '',
|
|
37
|
+
};
|
|
38
|
+
const { chooser, status, template } = view;
|
|
39
|
+
const state = { center: origin, tag: data['graphInitialTag'] || null, sequence: 0, live: false };
|
|
40
|
+
const recenter = (slug) => {
|
|
41
|
+
state.center = slug;
|
|
42
|
+
void refresh();
|
|
43
|
+
};
|
|
44
|
+
controls.hidden = false;
|
|
45
|
+
controls.querySelector('[data-graph-activate]')?.addEventListener('click', () => void refresh());
|
|
46
|
+
resetButton?.addEventListener('click', () => {
|
|
47
|
+
state.center = origin;
|
|
48
|
+
state.tag = null;
|
|
49
|
+
if (chooser)
|
|
50
|
+
chooser.value = '';
|
|
51
|
+
void refresh();
|
|
52
|
+
});
|
|
53
|
+
chooser?.addEventListener('change', () => {
|
|
54
|
+
state.tag = chooser.value || null;
|
|
55
|
+
void refresh();
|
|
56
|
+
});
|
|
57
|
+
async function refresh() {
|
|
58
|
+
const mine = ++state.sequence;
|
|
59
|
+
controls.setAttribute('aria-busy', 'true');
|
|
60
|
+
// A cold load fetches the Worker, the WASM, and the snapshot before any
|
|
61
|
+
// reply, and `aria-busy` alone announces nothing. The polite status region
|
|
62
|
+
// says the live graph is loading; the reply's own sentence replaces it.
|
|
63
|
+
if (status)
|
|
64
|
+
status.textContent = template('graphLoading');
|
|
65
|
+
let answer;
|
|
66
|
+
try {
|
|
67
|
+
answer = await load(view.scope, state.center, state.tag);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (mine !== state.sequence)
|
|
71
|
+
return;
|
|
72
|
+
controls.removeAttribute('aria-busy');
|
|
73
|
+
if (status)
|
|
74
|
+
status.textContent = template('graphFailed');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (mine !== state.sequence)
|
|
78
|
+
return; // a newer center/filter already won
|
|
79
|
+
controls.removeAttribute('aria-busy');
|
|
80
|
+
if (resetButton)
|
|
81
|
+
resetButton.hidden = false;
|
|
82
|
+
render(view, state, answer, recenter);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function load(scope, center, tag) {
|
|
86
|
+
if (scope === 'local') {
|
|
87
|
+
const result = await requestLocalGraph(center ?? '');
|
|
88
|
+
if (result.graph === null)
|
|
89
|
+
return { center: null, nodes: [], edges: [], omitted: 0 };
|
|
90
|
+
return { center: result.graph.center, nodes: result.graph.nodes, edges: result.graph.edges, omitted: result.graph.omitted };
|
|
91
|
+
}
|
|
92
|
+
const result = await requestGlobalGraph(tag);
|
|
93
|
+
return { center: null, nodes: result.graph.nodes, edges: result.graph.edges, omitted: result.graph.omitted };
|
|
94
|
+
}
|
|
95
|
+
function render(view, state, answer, recenter) {
|
|
96
|
+
const { scope, status, canvas, tableBody, template } = view;
|
|
97
|
+
const { tag } = state;
|
|
98
|
+
if (answer.center === null && scope === 'local') {
|
|
99
|
+
// A reply with no center is a successful no-match, not a runtime failure:
|
|
100
|
+
// the failure-preservation contract (`docs/core-design/build-and-runtime.md`,
|
|
101
|
+
// "Failure, accessibility, and security") keeps the static article,
|
|
102
|
+
// figure, and table for fetch, integrity, schema, CSP, Worker, and WASM
|
|
103
|
+
// failures, and none of those happened here. On first load this branch is
|
|
104
|
+
// a stale binding whose subject is absent from the snapshot — no drawing
|
|
105
|
+
// has replaced the baseline, and that baseline is still this page's own
|
|
106
|
+
// graph, so it stays visible while the status says what was not found.
|
|
107
|
+
// Only a drawing this client already replaced is cleared: it depicts a
|
|
108
|
+
// center the snapshot does not have, and leaving it would contradict the
|
|
109
|
+
// status sentence.
|
|
110
|
+
if (status)
|
|
111
|
+
status.textContent = template('graphUnknownCenter');
|
|
112
|
+
if (state.live)
|
|
113
|
+
clearLiveDrawing(view);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (scope === 'global' && tag && answer.nodes.length === 0) {
|
|
117
|
+
// The status states the empty result, so any drawing shown beside it would
|
|
118
|
+
// contradict it: picture and status would describe different sets. Unlike
|
|
119
|
+
// the unknown-center branch above, there is no baseline that is still the
|
|
120
|
+
// requested graph — a first load on `/graph/` is the unfiltered static
|
|
121
|
+
// figure, not the filter's — so this clears unconditionally. The empty
|
|
122
|
+
// selection is a successful answer and is depicted as one.
|
|
123
|
+
if (status)
|
|
124
|
+
status.textContent = template('graphEmptyFilter');
|
|
125
|
+
clearLiveDrawing(view);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const drawingStarted = performance.now();
|
|
129
|
+
// The button that started this redraw is about to be replaced with the
|
|
130
|
+
// table's new rows; remember which note it named so focus can follow it to
|
|
131
|
+
// the fresh control, and a keyboard re-center stays one Tab away from the
|
|
132
|
+
// next one instead of falling back to the document.
|
|
133
|
+
const focused = focusedRecenter();
|
|
134
|
+
const graph = scope === 'local' && answer.center !== null
|
|
135
|
+
? layoutLocal({ center: answer.center, drawn: answer.nodes, edges: answer.edges, omitted: answer.omitted })
|
|
136
|
+
: layoutGlobal({ nodes: answer.nodes, edges: answer.edges, omitted: answer.omitted });
|
|
137
|
+
if (canvas)
|
|
138
|
+
drawFigure(canvas, graph, template);
|
|
139
|
+
if (tableBody)
|
|
140
|
+
drawTable(tableBody, graph, template, scope === 'local' ? recenter : undefined);
|
|
141
|
+
if (focused !== null) {
|
|
142
|
+
tableBody?.querySelector(`[data-graph-recenter="${CSS.escape(focused)}"]`)?.focus();
|
|
143
|
+
}
|
|
144
|
+
// Layout and drawing only: the Worker wait that produced `answer` is not
|
|
145
|
+
// part of what this render costs the main thread. Goal 0008 consumes the
|
|
146
|
+
// event below beside the snapshot timing it does not replace.
|
|
147
|
+
const drawingMs = performance.now() - drawingStarted;
|
|
148
|
+
state.live = true;
|
|
149
|
+
// Both static count sentences describe the build-time figure. A live redraw
|
|
150
|
+
// makes them second, stale totals for the same picture, so they go and the
|
|
151
|
+
// status sentence below is the live home for counts.
|
|
152
|
+
hideStaticCounts(view.region);
|
|
153
|
+
if (status) {
|
|
154
|
+
const counts = boundCounts(graph);
|
|
155
|
+
const templateName = scope === 'local' ? 'graphStatusLocal' : tag ? 'graphStatusFiltered' : 'graphStatusGlobal';
|
|
156
|
+
status.textContent = fillTemplate(template(templateName), {
|
|
157
|
+
shown: counts.shown,
|
|
158
|
+
total: counts.total,
|
|
159
|
+
tag: tag === null ? '' : tagLabel(view.chooser, tag),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// The render instrument, armed explicitly by the measurer: no corpus data
|
|
163
|
+
// is carried, and an ordinary reader dispatches nothing. Only the actual
|
|
164
|
+
// drawing path reaches this line — an unknown center or an empty filter
|
|
165
|
+
// returns above with a status and no render to time.
|
|
166
|
+
if (window.__snapshotMeasurement === true) {
|
|
167
|
+
document.dispatchEvent(new CustomEvent('graph-render', { detail: { scope, ms: drawingMs } }));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The chosen tag's display label, for the filtered status sentence.
|
|
172
|
+
*
|
|
173
|
+
* The option's value is the route key (`c-sharp`), and the sentence names
|
|
174
|
+
* the tag the reader chose, which is the label on the option (`C#`) — the
|
|
175
|
+
* same text the chooser showed. Falls back to the key only for a tag no
|
|
176
|
+
* option carries.
|
|
177
|
+
*/
|
|
178
|
+
function tagLabel(chooser, key) {
|
|
179
|
+
return [...(chooser?.options ?? [])].find((option) => option.value === key)?.textContent?.trim() || key;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Blank a live drawing's figure and table.
|
|
183
|
+
*
|
|
184
|
+
* The children are replaced rather than the containers hidden: an empty
|
|
185
|
+
* picture and an empty equivalent table are the depiction of an empty result,
|
|
186
|
+
* and the static baseline cannot be restored — the client replaced it in
|
|
187
|
+
* place. Leaving the previous selection on screen would state two different
|
|
188
|
+
* sets at once, which is the contradiction this removes. The static count
|
|
189
|
+
* sentences and the count-bearing group name go with it; the status sentence
|
|
190
|
+
* is the live home for counts.
|
|
191
|
+
*/
|
|
192
|
+
function clearLiveDrawing(view) {
|
|
193
|
+
const svg = view.canvas?.querySelector('svg');
|
|
194
|
+
svg?.querySelector('.graph-edges')?.replaceChildren();
|
|
195
|
+
svg?.querySelector('.graph-nodes')?.replaceChildren();
|
|
196
|
+
view.tableBody?.replaceChildren();
|
|
197
|
+
hideStaticCounts(view.region);
|
|
198
|
+
const figureName = view.template('graphFigureName');
|
|
199
|
+
if (svg && figureName !== '')
|
|
200
|
+
svg.setAttribute('aria-label', figureName);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Hide the static count sentences.
|
|
204
|
+
*
|
|
205
|
+
* Both describe the build-time figure and neither can be updated by a live
|
|
206
|
+
* query, so once the picture is redrawn or cleared they would state a second
|
|
207
|
+
* total beside the live status sentence. The bound element's expansion link
|
|
208
|
+
* stays: "see the whole graph" is useful whatever this figure holds.
|
|
209
|
+
*/
|
|
210
|
+
function hideStaticCounts(region) {
|
|
211
|
+
const figureLabel = region.querySelector('[data-graph-figure-label]');
|
|
212
|
+
if (figureLabel)
|
|
213
|
+
figureLabel.hidden = true;
|
|
214
|
+
const bound = region.querySelector('[data-graph-bound]');
|
|
215
|
+
if (bound)
|
|
216
|
+
bound.hidden = true;
|
|
217
|
+
}
|
|
218
|
+
/** The note slug of the re-center control that currently holds focus, if any. */
|
|
219
|
+
function focusedRecenter() {
|
|
220
|
+
const active = document.activeElement;
|
|
221
|
+
return active instanceof HTMLElement ? active.getAttribute('data-graph-recenter') : null;
|
|
222
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM construction for a live graph redraw: the SVG figure and its equivalent
|
|
3
|
+
* table, plus the label helpers both share.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `graph-client.ts` so that file holds only the stateful wiring
|
|
6
|
+
* (center, tag filter, request sequence). Nothing here keeps state: every
|
|
7
|
+
* function takes the graph, the page's template lookup, and whatever callback it
|
|
8
|
+
* needs as explicit arguments. It imports only the same leaf modules the client
|
|
9
|
+
* already does, so the bundle's import graph is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { drawnNeighbours, relationKey, truncateLabel, SUBJECT_RADIUS, NODE_RADIUS, } from "../lib/graph-layout.js";
|
|
12
|
+
import { noteRoute } from "../lib/route-path.js";
|
|
13
|
+
import { partLanguage } from "../lib/translations.js";
|
|
14
|
+
/** Replace each `{key}` in a page-emitted template with its value. */
|
|
15
|
+
export function fillTemplate(text, values) {
|
|
16
|
+
return text.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ''));
|
|
17
|
+
}
|
|
18
|
+
export function relationLabel(template, node) {
|
|
19
|
+
// One classifier decides the state; the attribute name is that same key, as
|
|
20
|
+
// `data-graph-relation-<key>` in NoteGraph.astro and `dataset` camelize it.
|
|
21
|
+
// Keys are single words, so the camel spelling is the first letter up.
|
|
22
|
+
const key = relationKey(node);
|
|
23
|
+
return template(`graphRelation${key.charAt(0).toUpperCase()}${key.slice(1)}`);
|
|
24
|
+
}
|
|
25
|
+
function nodeLabel(template, node) {
|
|
26
|
+
return fillTemplate(template(node.degree === 1 ? 'graphNodeLabelOne' : 'graphNodeLabel'), {
|
|
27
|
+
title: node.entry.title,
|
|
28
|
+
relation: relationLabel(template, node),
|
|
29
|
+
degree: String(node.degree),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function drawFigure(target, graph, template) {
|
|
33
|
+
const svg = target.querySelector('svg');
|
|
34
|
+
if (!svg)
|
|
35
|
+
return;
|
|
36
|
+
const edgeGroup = svg.querySelector('.graph-edges');
|
|
37
|
+
const nodesGroup = svg.querySelector('.graph-nodes');
|
|
38
|
+
if (!edgeGroup || !nodesGroup)
|
|
39
|
+
return;
|
|
40
|
+
// The static SVG carries the build-time frame and a count-bearing name, so
|
|
41
|
+
// a redraw has to move both with the new selection: without the frame the
|
|
42
|
+
// filtered graph draws inside the old box, and without the name a screen
|
|
43
|
+
// reader hears the static total beside the live one. The live counts are in
|
|
44
|
+
// `[data-graph-status]`, which is why the live name is count-free.
|
|
45
|
+
svg.setAttribute('viewBox', graph.viewBox);
|
|
46
|
+
svg.setAttribute('width', String(graph.width));
|
|
47
|
+
svg.setAttribute('height', String(graph.height));
|
|
48
|
+
const figureName = template('graphFigureName');
|
|
49
|
+
if (figureName !== '')
|
|
50
|
+
svg.setAttribute('aria-label', figureName);
|
|
51
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
52
|
+
const markerId = svg.querySelector('marker')?.id;
|
|
53
|
+
const arrow = markerId === undefined ? undefined : `url(#${markerId})`;
|
|
54
|
+
edgeGroup.replaceChildren(...graph.edges.map((edge) => {
|
|
55
|
+
const line = document.createElementNS(namespace, 'line');
|
|
56
|
+
line.setAttribute('class', `graph-edge${edge.isMutual ? ' graph-edge-mutual' : ''}${edge.direction === 'incoming' ? ' graph-edge-incoming' : ''}`);
|
|
57
|
+
line.setAttribute('x1', String(edge.x1));
|
|
58
|
+
line.setAttribute('y1', String(edge.y1));
|
|
59
|
+
line.setAttribute('x2', String(edge.x2));
|
|
60
|
+
line.setAttribute('y2', String(edge.y2));
|
|
61
|
+
if (arrow !== undefined)
|
|
62
|
+
line.setAttribute('marker-end', arrow);
|
|
63
|
+
if (edge.isMutual)
|
|
64
|
+
line.setAttribute('marker-start', arrow);
|
|
65
|
+
return line;
|
|
66
|
+
}));
|
|
67
|
+
const language = document.documentElement.lang || 'en';
|
|
68
|
+
nodesGroup.replaceChildren(...graph.nodes.map((node) => {
|
|
69
|
+
const anchor = document.createElementNS(namespace, 'a');
|
|
70
|
+
anchor.setAttribute('class', node.isSubject ? 'graph-node graph-node-subject' : 'graph-node');
|
|
71
|
+
anchor.setAttribute('href', noteRoute(node.entry.slug));
|
|
72
|
+
anchor.setAttribute('aria-label', nodeLabel(template, node));
|
|
73
|
+
// The same `partLanguage` result the static SVG anchor carries, so a
|
|
74
|
+
// live redraw cannot drop a foreign title's language where the static
|
|
75
|
+
// figure had it — including an entry declaring no language, which the
|
|
76
|
+
// helper resolves to the navigation language.
|
|
77
|
+
const lang = partLanguage(node.entry.language, language);
|
|
78
|
+
if (lang !== undefined)
|
|
79
|
+
anchor.setAttribute('lang', lang);
|
|
80
|
+
const circle = document.createElementNS(namespace, 'circle');
|
|
81
|
+
circle.setAttribute('class', 'graph-dot');
|
|
82
|
+
circle.setAttribute('cx', String(node.x));
|
|
83
|
+
circle.setAttribute('cy', String(node.y));
|
|
84
|
+
circle.setAttribute('r', String(node.isSubject ? SUBJECT_RADIUS : NODE_RADIUS));
|
|
85
|
+
circle.setAttribute('aria-hidden', 'true');
|
|
86
|
+
const text = document.createElementNS(namespace, 'text');
|
|
87
|
+
text.setAttribute('class', 'graph-label');
|
|
88
|
+
text.setAttribute('x', String(node.x));
|
|
89
|
+
text.setAttribute('y', String(node.y));
|
|
90
|
+
text.setAttribute('dy', '1.9em');
|
|
91
|
+
text.setAttribute('aria-hidden', 'true');
|
|
92
|
+
text.textContent = node.label || truncateLabel(node.entry.title);
|
|
93
|
+
anchor.append(circle, text);
|
|
94
|
+
return anchor;
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/** The "linked to" cell: the joined notes as a list, or the page's "none" text. */
|
|
98
|
+
function joinedCell(links, template, language) {
|
|
99
|
+
const linked = document.createElement('td');
|
|
100
|
+
if (links.length === 0) {
|
|
101
|
+
const none = document.createElement('span');
|
|
102
|
+
none.className = 'graph-none';
|
|
103
|
+
none.textContent = template('graphNoLinks');
|
|
104
|
+
linked.append(none);
|
|
105
|
+
return linked;
|
|
106
|
+
}
|
|
107
|
+
const list = document.createElement('ul');
|
|
108
|
+
list.className = 'graph-joined';
|
|
109
|
+
for (const other of links) {
|
|
110
|
+
const item = document.createElement('li');
|
|
111
|
+
const anchor = document.createElement('a');
|
|
112
|
+
anchor.href = noteRoute(other.slug);
|
|
113
|
+
anchor.textContent = other.title;
|
|
114
|
+
// As in the static "linked to" list, each joined note is marked
|
|
115
|
+
// with its own language.
|
|
116
|
+
const otherLang = partLanguage(other.language, language);
|
|
117
|
+
if (otherLang !== undefined)
|
|
118
|
+
anchor.setAttribute('lang', otherLang);
|
|
119
|
+
item.append(anchor);
|
|
120
|
+
list.append(item);
|
|
121
|
+
}
|
|
122
|
+
linked.append(list);
|
|
123
|
+
return linked;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Redraw the equivalent table. `recenter` is present only on a local graph, and
|
|
127
|
+
* each row then carries a re-center control that calls it with the row's slug.
|
|
128
|
+
*/
|
|
129
|
+
export function drawTable(target, graph, template, recenter) {
|
|
130
|
+
const joined = drawnNeighbours(graph);
|
|
131
|
+
const language = document.documentElement.lang || 'en';
|
|
132
|
+
target.replaceChildren(...graph.nodes.map((node) => {
|
|
133
|
+
const row = document.createElement('tr');
|
|
134
|
+
const head = document.createElement('th');
|
|
135
|
+
head.scope = 'row';
|
|
136
|
+
const link = document.createElement('a');
|
|
137
|
+
link.href = noteRoute(node.entry.slug);
|
|
138
|
+
link.textContent = node.entry.title;
|
|
139
|
+
// The static table marks its title link with `partLanguage`; the live
|
|
140
|
+
// one has to carry the same attribute or a redraw quietly drops it.
|
|
141
|
+
const titleLang = partLanguage(node.entry.language, language);
|
|
142
|
+
if (titleLang !== undefined)
|
|
143
|
+
link.setAttribute('lang', titleLang);
|
|
144
|
+
head.append(link);
|
|
145
|
+
const relation = document.createElement('td');
|
|
146
|
+
relation.textContent = relationLabel(template, node);
|
|
147
|
+
const degree = document.createElement('td');
|
|
148
|
+
degree.textContent = String(node.degree);
|
|
149
|
+
const linked = joinedCell(joined.get(node.entry.slug) ?? [], template, language);
|
|
150
|
+
row.append(head, relation, degree, linked);
|
|
151
|
+
if (recenter !== undefined) {
|
|
152
|
+
const control = document.createElement('button');
|
|
153
|
+
control.type = 'button';
|
|
154
|
+
control.setAttribute('data-graph-recenter', node.entry.slug);
|
|
155
|
+
control.textContent = template('graphRecenter');
|
|
156
|
+
// The visible text is the same on every row, so the accessible name
|
|
157
|
+
// carries the note this control acts on: the page emits the
|
|
158
|
+
// `{title}` template and the client fills it, as it does for the node
|
|
159
|
+
// labels, which keeps the bundle free of a locale table.
|
|
160
|
+
control.setAttribute('aria-label', fillTemplate(template('graphRecenterLabel'), { title: node.entry.title }));
|
|
161
|
+
head.append(control);
|
|
162
|
+
control.addEventListener('click', () => recenter(node.entry.slug));
|
|
163
|
+
}
|
|
164
|
+
return row;
|
|
165
|
+
}));
|
|
166
|
+
}
|