@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,509 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The note page: requirements section 9.2's thirteen-element anatomy.
|
|
4
|
+
*
|
|
5
|
+
* Elements 1, 2, and part of 13 come from `Layout.astro` — the skip link, the
|
|
6
|
+
* header with its search trigger, and the printed canonical route. Everything
|
|
7
|
+
* else is here, in the order the requirements list them, and every region is
|
|
8
|
+
* either rendered below or left as a named, empty slot with the ticket that
|
|
9
|
+
* owns it. **Do not fill a slot from another ticket**: the slot is the contract.
|
|
10
|
+
*
|
|
11
|
+
* | § 9.2 element | Where |
|
|
12
|
+
* | --- | --- |
|
|
13
|
+
* | 1 skip link | `Layout.astro` |
|
|
14
|
+
* | 2 header and search trigger | `Layout.astro` |
|
|
15
|
+
* | 3 breadcrumbs | here |
|
|
16
|
+
* | 4 title and public metadata | here |
|
|
17
|
+
* | 5 optional summary | here |
|
|
18
|
+
* | 6 table of contents | here, via `TableOfContents.astro` |
|
|
19
|
+
* | 7 sanitized article content | here |
|
|
20
|
+
* | 8 outgoing public links | here |
|
|
21
|
+
* | 9 static backlinks | here |
|
|
22
|
+
* | 10 related-note fallback | here, derived by `relations.ts` |
|
|
23
|
+
* | 11 local-graph island | here, via `NoteGraph.astro`, laid out by `graph.ts` |
|
|
24
|
+
* | 12 previous/next or collection navigation | here |
|
|
25
|
+
* | 13 footer with provenance and canonical URL | here; the absolute URL is TK-08 |
|
|
26
|
+
*
|
|
27
|
+
* Reading and navigation need no JavaScript. The table of contents is server
|
|
28
|
+
* rendered, the breadcrumbs are static, and code-copy is a progressive control
|
|
29
|
+
* inserted only when scripting runs.
|
|
30
|
+
*/
|
|
31
|
+
import Layout from '../../layouts/Layout.astro';
|
|
32
|
+
import LinkedNotes from '../../components/LinkedNotes.astro';
|
|
33
|
+
import NoteGraph from '../../components/NoteGraph.astro';
|
|
34
|
+
import TableOfContents from '../../components/TableOfContents.astro';
|
|
35
|
+
import { entries, getEntry, tagFacets, tagRouteForLabel } from '../../lib/content.js';
|
|
36
|
+
import { hasDrawableGraph, localGraph } from '../../lib/graph.js';
|
|
37
|
+
import { renderMarkdown } from '../../lib/markdown.js';
|
|
38
|
+
import {
|
|
39
|
+
collectionNeighbours,
|
|
40
|
+
hasTagPeer,
|
|
41
|
+
notesForSlugs,
|
|
42
|
+
relatedNotes,
|
|
43
|
+
} from '../../lib/relations.js';
|
|
44
|
+
import { GRAPH_SEGMENT, collectionRoute, noteRoute } from '../../lib/routes.js';
|
|
45
|
+
import { canonicalUrl, TITLE_SUFFIX } from '../../lib/site.js';
|
|
46
|
+
import { translate, NAV_LANGUAGE, partLanguage } from '../../lib/translations.js';
|
|
47
|
+
// Loaded as a URL rather than imported, so the stylesheet becomes its own
|
|
48
|
+
// content-hashed asset and the `<link>` below can be emitted conditionally.
|
|
49
|
+
// Importing it would merge it into the layout's sheet and ship it everywhere,
|
|
50
|
+
// which is precisely what owner decision 7 rules out.
|
|
51
|
+
import codeStylesheet from '../../styles/code.css?url';
|
|
52
|
+
import mathStylesheet from '../../styles/math.css?url';
|
|
53
|
+
import diagramStylesheet from '../../styles/diagram.css?url';
|
|
54
|
+
import graphStylesheet from '../../styles/graph.css?url';
|
|
55
|
+
import { DIAGRAM_MODE } from '../../lib/diagram-mode.js';
|
|
56
|
+
import { MATH_MODE } from '../../lib/math-mode.js';
|
|
57
|
+
|
|
58
|
+
export function getStaticPaths() {
|
|
59
|
+
return entries.map((entry) => ({ params: { slug: entry.slug }, props: { entry } }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { entry } = Astro.props;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* **The one route that is a document**, and therefore the whole point of TK-16.
|
|
66
|
+
*
|
|
67
|
+
* Every other page on this site aggregates many entries or none and resolves its
|
|
68
|
+
* chrome from the navigation language. This one renders exactly one artifact
|
|
69
|
+
* entry, so it resolves from that entry's own `language` — and the result is
|
|
70
|
+
* that a zh-CN note renders Chinese chrome under `<html lang="zh-CN">` while an
|
|
71
|
+
* English note in the same build renders English chrome. `translate` falls back
|
|
72
|
+
* to the navigation language for an entry carrying no `language`, which is the
|
|
73
|
+
* published corpus's single note.
|
|
74
|
+
*/
|
|
75
|
+
const t = translate(entry.language);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This page's language as an actual tag, which is what `<html lang>` carries and
|
|
79
|
+
* what a listed foreign title is compared against.
|
|
80
|
+
*
|
|
81
|
+
* `entry.language` is optional and `translate` resolves that to the navigation
|
|
82
|
+
* language; this resolves it to the same value so that a note declaring none and
|
|
83
|
+
* a note declaring `en` mark exactly the same set of foreign titles. Passing
|
|
84
|
+
* `entry.language` raw would compare `undefined` against `'en'` and put a
|
|
85
|
+
* redundant `lang="en"` on every English title on the published corpus.
|
|
86
|
+
*/
|
|
87
|
+
const lang = entry.language ?? NAV_LANGUAGE;
|
|
88
|
+
// The renderer takes this mapping rather than knowing the route shape itself,
|
|
89
|
+
// so `/notes/<slug>/` is defined in exactly one place. Returning undefined for
|
|
90
|
+
// an unpublished slug leaves that href untouched rather than pointing it at a
|
|
91
|
+
// route that does not exist.
|
|
92
|
+
//
|
|
93
|
+
// `pageTitle` is what lets the anatomy put the title above the article: the
|
|
94
|
+
// producer writes it into the body as a leading `# Title`, and the renderer
|
|
95
|
+
// drops that one duplicate rather than the page rendering it twice. A body that
|
|
96
|
+
// keeps some *other* `h1` is left alone and fails the "exactly one h1" gate,
|
|
97
|
+
// which is the honest outcome — see `RenderOptions.pageTitle`.
|
|
98
|
+
const { html, toc, hasCode, hasMath, hasMermaid } = await renderMarkdown(entry.markdown, {
|
|
99
|
+
pageTitle: entry.title,
|
|
100
|
+
routeForSlug: (slug) => (getEntry(slug) ? noteRoute(slug) : undefined),
|
|
101
|
+
// The two strings the renderer writes into the article that are this
|
|
102
|
+
// repository's words rather than the author's: a heading anchor's accessible
|
|
103
|
+
// name and an untitled diagram's caption. They are chrome, so they follow the
|
|
104
|
+
// document like every other chrome string — a Chinese article had English
|
|
105
|
+
// accessible names on every heading before TK-16 threaded this through.
|
|
106
|
+
chrome: t,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `Home / Notes / <collection> / <title>`, from public fields only.
|
|
111
|
+
*
|
|
112
|
+
* `collection` is validated as a flat slug and there is no hierarchy behind it,
|
|
113
|
+
* so the trail is at most four deep. TK-05c owns the collection tree; when it
|
|
114
|
+
* lands, this gains ancestors and nothing else changes. The `/#notes` target is
|
|
115
|
+
* the home page's own listing — requirements section 9.1 defines no `/notes/`
|
|
116
|
+
* index, and `PRIMARY_NAV` already points there.
|
|
117
|
+
*
|
|
118
|
+
* Every segment is a fixed route or an artifact field. Nothing reads a path, so
|
|
119
|
+
* a breadcrumb can never disclose a private folder name.
|
|
120
|
+
*/
|
|
121
|
+
const collection = entry.collection;
|
|
122
|
+
const breadcrumbs: { label: string; href?: string }[] = [
|
|
123
|
+
{ label: t.navHome, href: '/' },
|
|
124
|
+
{ label: t.navNotes, href: '/#notes' },
|
|
125
|
+
...(collection === undefined ? [] : [{ label: collection, href: collectionRoute(collection) }]),
|
|
126
|
+
{ label: entry.title },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The calendar date a reader sees, from an artifact date.
|
|
131
|
+
*
|
|
132
|
+
* The first ten characters, not a parsed and reformatted `Date`. TK-01 has
|
|
133
|
+
* already validated both fields as ISO 8601, so the prefix *is* the calendar
|
|
134
|
+
* date exactly as the producer wrote it — and taking it verbatim avoids the two
|
|
135
|
+
* ways formatting gets this wrong: a date-only value parsed as UTC midnight and
|
|
136
|
+
* rendered in the build machine's zone shows the previous day west of
|
|
137
|
+
* Greenwich, and a locale format would be a chrome string TK-16 then has to
|
|
138
|
+
* translate. `YYYY-MM-DD` is unambiguous in both of this site's languages.
|
|
139
|
+
*
|
|
140
|
+
* The `<time datetime>` attribute carries the full precise value, so nothing
|
|
141
|
+
* the artifact expressed is lost.
|
|
142
|
+
*/
|
|
143
|
+
function displayDate(value: string): string {
|
|
144
|
+
return value.slice(0, 10);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const canonicalRoute = noteRoute(entry.slug);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The three relationship surfaces, resolved once.
|
|
151
|
+
*
|
|
152
|
+
* `outgoing` and `backlinks` are the artifact's authored edges; `related` is
|
|
153
|
+
* derived and says so on the page. All three are computed here rather than in
|
|
154
|
+
* the template so that the markup below reads as the anatomy it is.
|
|
155
|
+
*
|
|
156
|
+
* **The incompleteness this used to state is gone, and the heading still says
|
|
157
|
+
* "Links to".** The producer derived `outgoing` from wikilinks only, so a plain
|
|
158
|
+
* Markdown link rendered a live `<a>` with no edge behind it. Since TK-27 one
|
|
159
|
+
* traversal produces the rewritten href *and* the edge from a single
|
|
160
|
+
* resolution (`scripts/resolve-links.ts`), across all five link forms — so that
|
|
161
|
+
* direction of disagreement is now unrepresentable rather than merely untested.
|
|
162
|
+
*
|
|
163
|
+
* The heading is unchanged deliberately, because "every link on this page"
|
|
164
|
+
* would be a lie in three ways the data still cannot support. An external link
|
|
165
|
+
* is an anchor a reader can click and is never an edge. A link that resolved to
|
|
166
|
+
* nothing, and one to a note the user withheld, are both on the page as the
|
|
167
|
+
* author's own words — degraded to text rather than deleted — and neither
|
|
168
|
+
* reaches anything here. And a link to this page's own heading renders as a
|
|
169
|
+
* real anchor inside the article while contributing no edge, because an entry
|
|
170
|
+
* may not link to itself. "Links to" describes the list that is actually there:
|
|
171
|
+
* the published notes this one reaches.
|
|
172
|
+
*
|
|
173
|
+
* What did change is the *empty* state, from possibly-false to true.
|
|
174
|
+
* `outgoingEmpty` reads "This note links to no other published note", and
|
|
175
|
+
* before TK-27 that sentence was printed for a note whose body was full of
|
|
176
|
+
* Markdown-syntax links to published notes. Now it can only be printed when it
|
|
177
|
+
* holds. `tests/backlink-surfaces.test.ts` gates it over a built site, which
|
|
178
|
+
* nothing did before — an empty state that states a fact was the one region
|
|
179
|
+
* here with no gate on the fact.
|
|
180
|
+
*/
|
|
181
|
+
const outgoing = notesForSlugs(entry.outgoing, getEntry);
|
|
182
|
+
const backlinks = notesForSlugs(entry.backlinks, getEntry);
|
|
183
|
+
const facets = tagFacets();
|
|
184
|
+
const related = relatedNotes(entry, entries, facets);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Why the related list is empty, when it is.
|
|
188
|
+
*
|
|
189
|
+
* Two different facts produce the same empty list and a reader deserves to be
|
|
190
|
+
* told which: a note with no tag-sharer anywhere, and a note whose every
|
|
191
|
+
* tag-sharer is already rendered in the two sections above. Saying "nothing
|
|
192
|
+
* shares a tag" in the second case is a claim the artifact contradicts, which
|
|
193
|
+
* is precisely what an honest empty state must not do.
|
|
194
|
+
*
|
|
195
|
+
* `hasTagPeer` runs only for an empty list, which is the only case
|
|
196
|
+
* `LinkedNotes` renders the message; a page with related notes skips a second
|
|
197
|
+
* full scan of the facet index.
|
|
198
|
+
*/
|
|
199
|
+
const relatedEmpty =
|
|
200
|
+
related.length === 0 && !hasTagPeer(entry, entries, facets)
|
|
201
|
+
? t.relatedNoTagPeer
|
|
202
|
+
: t.relatedAlreadyListed;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The pager: its two links and the name it announces, or nothing at all.
|
|
206
|
+
*
|
|
207
|
+
* One value rather than a neighbour pair plus a separate label, so the template
|
|
208
|
+
* has a single condition to branch on. Two conditions is how a pager ends up
|
|
209
|
+
* rendered as an empty `<nav>` for the one shape neither corpus contains — a
|
|
210
|
+
* note alone in its collection. `collectionNeighbours` returns no neighbour for
|
|
211
|
+
* that case and `tests/relations.test.ts` pins it; this keeps the page from
|
|
212
|
+
* being able to disagree.
|
|
213
|
+
*
|
|
214
|
+
* Naming the collection matters because a page carries several `<nav>` elements
|
|
215
|
+
* and a screen reader lists them by name.
|
|
216
|
+
*/
|
|
217
|
+
const neighbours = collectionNeighbours(entry, entries);
|
|
218
|
+
const pager =
|
|
219
|
+
collection !== undefined && (neighbours.previous !== undefined || neighbours.next !== undefined)
|
|
220
|
+
? { ...neighbours, label: t.moreInCollection(collection) }
|
|
221
|
+
: undefined;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* § 9.2.11's neighbourhood, laid out during the build.
|
|
225
|
+
*
|
|
226
|
+
* The same edges the two lists above render, drawn as a figure — plus the edges
|
|
227
|
+
* *between* those neighbours, which is what makes it a graph rather than a
|
|
228
|
+
* picture of a list. `checkCorpus` has already proven every one of them
|
|
229
|
+
* resolves, which is why the layout is affordable here and structurally
|
|
230
|
+
* unavailable to a whole-vault renderer.
|
|
231
|
+
*/
|
|
232
|
+
const graph = localGraph(entry, getEntry);
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
<Layout
|
|
236
|
+
title={`${entry.title}${TITLE_SUFFIX}`}
|
|
237
|
+
description={entry.excerpt}
|
|
238
|
+
lang={entry.language}
|
|
239
|
+
>
|
|
240
|
+
{/*
|
|
241
|
+
Owner decision 7, the live case. Each flag is true only when the body
|
|
242
|
+
contains the construct, so each sheet reaches only the pages that render
|
|
243
|
+
one. Three conditional `<link>` elements rather than one merged sheet:
|
|
244
|
+
merging Mermaid's per-diagram-type stylesheets was measured at 6,354 B gzip
|
|
245
|
+
against a 972 B median for a single type, and the build-time renderer
|
|
246
|
+
removes the need for either — `diagram.css` is 14 rules because the theming
|
|
247
|
+
is flattened onto the SVG itself.
|
|
248
|
+
|
|
249
|
+
`hasMath` also gates the ~9 KB Temml stylesheet and, through its
|
|
250
|
+
`unicode-range`, the 9.4 KB `Temml.woff2` — which a browser fetches only for
|
|
251
|
+
an expression that actually uses `\mathscr`.
|
|
252
|
+
*/}
|
|
253
|
+
{hasCode && <link slot="head" rel="stylesheet" href={codeStylesheet} />}
|
|
254
|
+
{hasMath && <link slot="head" rel="stylesheet" href={mathStylesheet} />}
|
|
255
|
+
{hasMermaid && <link slot="head" rel="stylesheet" href={diagramStylesheet} />}
|
|
256
|
+
{/* The graph sheet reaches only a note that actually draws one — a note with
|
|
257
|
+
no neighbourhood renders a sentence and requests nothing. The published
|
|
258
|
+
corpus's single note is exactly that case. `hasDrawableGraph` is the same
|
|
259
|
+
predicate the component branches on, so the stylesheet and the figure
|
|
260
|
+
cannot disagree about whether there is a graph. */}
|
|
261
|
+
{hasDrawableGraph(graph) && <link slot="head" rel="stylesheet" href={graphStylesheet} />}
|
|
262
|
+
{/*
|
|
263
|
+
Client mode only, and only on a page that has a diagram.
|
|
264
|
+
|
|
265
|
+
Astro bundles a script tag **eagerly**: the `hasMermaid` condition decides
|
|
266
|
+
whether the tag is rendered, not whether the module is built. Keeping
|
|
267
|
+
Mermaid out of `dist/` in build-time mode is therefore done in
|
|
268
|
+
`astro.config.mjs`, which resolves `src/scripts/diagram.ts` to an empty stub
|
|
269
|
+
unless client mode is selected. Both halves are needed — this one so a
|
|
270
|
+
diagram-free page requests nothing, that one so a diagram-free *build*
|
|
271
|
+
contains nothing. `tests/math-and-diagrams.test.ts` asserts both over
|
|
272
|
+
`dist/`.
|
|
273
|
+
|
|
274
|
+
`DIAGRAM_MODE` is checked here as well, so build-time mode emits no tag at
|
|
275
|
+
all rather than one pointing at a 20-byte empty chunk. It is not what keeps
|
|
276
|
+
Mermaid out — the resolver is — but a request for nothing is still a
|
|
277
|
+
request.
|
|
278
|
+
*/}
|
|
279
|
+
{hasMermaid && DIAGRAM_MODE === 'client' && (
|
|
280
|
+
<script>
|
|
281
|
+
import '../../scripts/diagram.js';
|
|
282
|
+
</script>
|
|
283
|
+
)}
|
|
284
|
+
{/*
|
|
285
|
+
The same arrangement for math, and it needs both halves for the same reason
|
|
286
|
+
the diagram one does: this condition keeps a math-free page from requesting
|
|
287
|
+
anything, and `astro.config.mjs`'s resolver keeps a build-time *build* from
|
|
288
|
+
containing Temml at all. `clientRuntimePlugin` is parameterised over the two
|
|
289
|
+
runtimes rather than duplicated.
|
|
290
|
+
|
|
291
|
+
`hasMath` needs no change to serve this: it is set in `mathPlugin` before the
|
|
292
|
+
mode branch, so it means "this page has an expression" in either mode — which
|
|
293
|
+
is what both the stylesheet above and this tag ask of it.
|
|
294
|
+
*/}
|
|
295
|
+
{hasMath && MATH_MODE === 'client' && (
|
|
296
|
+
<script>
|
|
297
|
+
import '../../scripts/math.js';
|
|
298
|
+
</script>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
<header class="note-header">
|
|
302
|
+
{/* § 9.2.3 — the trail. Public fields only; never a private folder name. */}
|
|
303
|
+
<nav class="breadcrumbs" aria-label={t.breadcrumbLabel}>
|
|
304
|
+
<ol>
|
|
305
|
+
{breadcrumbs.map((crumb) => (
|
|
306
|
+
<li>
|
|
307
|
+
{crumb.href === undefined ? (
|
|
308
|
+
<span aria-current="page">{crumb.label}</span>
|
|
309
|
+
) : (
|
|
310
|
+
<a href={crumb.href}>{crumb.label}</a>
|
|
311
|
+
)}
|
|
312
|
+
</li>
|
|
313
|
+
))}
|
|
314
|
+
</ol>
|
|
315
|
+
</nav>
|
|
316
|
+
|
|
317
|
+
{/* § 9.2.4 — the title, and the public metadata the artifact actually
|
|
318
|
+
carries. Every row below is optional in the contract, so each is
|
|
319
|
+
conditional: a note with none of them renders a title and nothing else,
|
|
320
|
+
which is the published corpus today. */}
|
|
321
|
+
<h1 class="note-title">{entry.title}</h1>
|
|
322
|
+
|
|
323
|
+
{(entry.created !== undefined ||
|
|
324
|
+
entry.updated !== undefined ||
|
|
325
|
+
collection !== undefined ||
|
|
326
|
+
(entry.tags?.length ?? 0) > 0 ||
|
|
327
|
+
(entry.aliases?.length ?? 0) > 0) && (
|
|
328
|
+
<dl class="note-meta">
|
|
329
|
+
{entry.created !== undefined && (
|
|
330
|
+
<div>
|
|
331
|
+
<dt>{t.metaPublished}</dt>
|
|
332
|
+
<dd><time datetime={entry.created}>{displayDate(entry.created)}</time></dd>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
{entry.updated !== undefined && (
|
|
336
|
+
<div>
|
|
337
|
+
<dt>{t.metaUpdated}</dt>
|
|
338
|
+
<dd><time datetime={entry.updated}>{displayDate(entry.updated)}</time></dd>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
{collection !== undefined && (
|
|
342
|
+
<div>
|
|
343
|
+
<dt>{t.metaCollection}</dt>
|
|
344
|
+
<dd><a href={collectionRoute(collection)}>{collection}</a></dd>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
{entry.tags !== undefined && entry.tags.length > 0 && (
|
|
348
|
+
<div>
|
|
349
|
+
<dt>{t.metaTags}</dt>
|
|
350
|
+
<dd>
|
|
351
|
+
<ul class="note-tags">
|
|
352
|
+
{entry.tags.map((tag) => (
|
|
353
|
+
<li><a href={tagRouteForLabel(tag)}>{tag}</a></li>
|
|
354
|
+
))}
|
|
355
|
+
</ul>
|
|
356
|
+
</dd>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
{/* Pagefind searches metadata outside `data-pagefind-body`, so aliases
|
|
360
|
+
remain findable without becoming the body-derived result excerpt. */}
|
|
361
|
+
{entry.aliases !== undefined && entry.aliases.length > 0 && (
|
|
362
|
+
<div>
|
|
363
|
+
<dt>{t.metaAliases}</dt>
|
|
364
|
+
<dd>
|
|
365
|
+
<ul class="note-aliases" data-pagefind-meta="aliases">
|
|
366
|
+
{entry.aliases.map((alias) => <li>{alias}</li>)}
|
|
367
|
+
</ul>
|
|
368
|
+
</dd>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
</dl>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{/* § 9.2.5 — the optional summary.
|
|
375
|
+
`description` verbatim, with no comparison against `excerpt`: the two
|
|
376
|
+
are separate artifact fields, the excerpt is never rendered on this page
|
|
377
|
+
(it is the `<meta name="description">` and the card body elsewhere), and
|
|
378
|
+
a note whose author wrote the same sentence into both still asked for a
|
|
379
|
+
summary. Suppressing on equality was tried and was a product decision
|
|
380
|
+
this ticket has no reason to make. */}
|
|
381
|
+
{entry.description !== undefined && <p class="note-summary">{entry.description}</p>}
|
|
382
|
+
</header>
|
|
383
|
+
|
|
384
|
+
{/* § 9.2.6 — the table of contents, on pages above `TOC_MIN_HEADINGS`. The
|
|
385
|
+
renderer returns an empty `toc` below the threshold, so the threshold
|
|
386
|
+
lives in one place and this is only the rendering decision. */}
|
|
387
|
+
{toc.length > 0 && <TableOfContents entries={toc} isRoot lang={entry.language} />}
|
|
388
|
+
|
|
389
|
+
{/* § 9.2.7 — the sanitized article body. */}
|
|
390
|
+
<article
|
|
391
|
+
class="prose"
|
|
392
|
+
data-pagefind-body
|
|
393
|
+
data-code-copy={t.codeCopy}
|
|
394
|
+
data-code-copied={t.codeCopied}
|
|
395
|
+
data-code-copy-failed={t.codeCopyFailed}
|
|
396
|
+
>
|
|
397
|
+
<div set:html={html} />
|
|
398
|
+
</article>
|
|
399
|
+
|
|
400
|
+
{/* § 9.2.8 — outgoing public links: the notes this one links to, by title.
|
|
401
|
+
Every target is a published entry — `checkCorpus` proves it — so this can
|
|
402
|
+
never be a dead link or a private title. */}
|
|
403
|
+
<LinkedNotes
|
|
404
|
+
region="outgoing"
|
|
405
|
+
heading={t.outgoingHeading}
|
|
406
|
+
entries={outgoing}
|
|
407
|
+
empty={t.outgoingEmpty}
|
|
408
|
+
lang={lang}
|
|
409
|
+
/>
|
|
410
|
+
|
|
411
|
+
{/* § 9.2.9 — static backlinks. Requirements section 13.1 asks for five
|
|
412
|
+
things: source title, source route, an optional source heading, a bounded
|
|
413
|
+
sanitized context excerpt, and a stable sort order. Three are rendered.
|
|
414
|
+
The heading and the context excerpt are not, because the artifact carries
|
|
415
|
+
one slug per edge and nothing about *where* in the source that edge is —
|
|
416
|
+
no heading, no offset, no surrounding text.
|
|
417
|
+
|
|
418
|
+
`ContentEntry.excerpt` is deliberately not substituted for the missing
|
|
419
|
+
one. It is the source note's own summary — the same string the card, the
|
|
420
|
+
`<meta name="description">`, and the hover preview all use — and it is
|
|
421
|
+
derived from the note's opening, not from the passage that links here.
|
|
422
|
+
Printed under a backlink it would read as the context of the link while
|
|
423
|
+
being unrelated to it, which is the fabrication the empty states exist to
|
|
424
|
+
avoid: a wrong answer presented in the shape of a right one is worse than
|
|
425
|
+
no answer. A real context excerpt needs a per-edge field; that is TK-19's
|
|
426
|
+
producer contract work. */}
|
|
427
|
+
<LinkedNotes
|
|
428
|
+
region="backlinks"
|
|
429
|
+
heading={t.backlinksHeading}
|
|
430
|
+
entries={backlinks}
|
|
431
|
+
empty={t.backlinksEmpty}
|
|
432
|
+
lang={lang}
|
|
433
|
+
/>
|
|
434
|
+
|
|
435
|
+
{/* § 9.2.10 — the related-note fallback. Derived, not authored, and the
|
|
436
|
+
markup says so: the rule is stated below the heading so a reader is never
|
|
437
|
+
left to guess whether the owner put these here. `relations.ts` documents
|
|
438
|
+
the rule and the alternatives it was chosen over. */}
|
|
439
|
+
<LinkedNotes
|
|
440
|
+
region="related"
|
|
441
|
+
heading={t.relatedHeading}
|
|
442
|
+
entries={related}
|
|
443
|
+
empty={relatedEmpty}
|
|
444
|
+
derivation={t.relatedDerivation}
|
|
445
|
+
lang={lang}
|
|
446
|
+
/>
|
|
447
|
+
|
|
448
|
+
{/* § 9.2.11 — the local-graph neighborhood: build-time inline SVG with real
|
|
449
|
+
`<a>` elements plus the equivalent table section 17 requires. TK-21's
|
|
450
|
+
interactive layer must not be stubbed in here — this is the baseline that
|
|
451
|
+
survives when that enhancement fails. */}
|
|
452
|
+
<NoteGraph
|
|
453
|
+
graph={graph}
|
|
454
|
+
region="note-graph"
|
|
455
|
+
heading={t.graphHeading}
|
|
456
|
+
derivation={t.graphLocalDerivation}
|
|
457
|
+
empty={t.graphEmpty}
|
|
458
|
+
bounded={t.graphBoundedLocal}
|
|
459
|
+
expand={{
|
|
460
|
+
href: `/${GRAPH_SEGMENT}/`,
|
|
461
|
+
text: t.graphExpandLocal,
|
|
462
|
+
label: t.graphExpandLocalLabel,
|
|
463
|
+
}}
|
|
464
|
+
lang={lang}
|
|
465
|
+
explore={{ scope: 'local', centerSlug: entry.slug }}
|
|
466
|
+
/>
|
|
467
|
+
|
|
468
|
+
{/* § 9.2.12 — collection navigation, "where meaningful": only inside a
|
|
469
|
+
collection that has another note, and in the same order
|
|
470
|
+
`/collections/<key>/` lists them. A note with no collection, or the only
|
|
471
|
+
note in one, renders nothing rather than a pager with two dead ends. */}
|
|
472
|
+
{pager !== undefined && (
|
|
473
|
+
<nav class="collection-pager" aria-label={pager.label}>
|
|
474
|
+
{/* The link carries the destination's own title, so it is a meaningful
|
|
475
|
+
link out of context — "Previous" alone would be four identical link
|
|
476
|
+
names on a screen reader's link list. The direction word is a
|
|
477
|
+
separate element rather than part of the link text for the same
|
|
478
|
+
reason — and the title carries its own `lang` when the neighbour is in
|
|
479
|
+
another language, which the direction word never is. */}
|
|
480
|
+
{pager.previous !== undefined && (
|
|
481
|
+
<a href={noteRoute(pager.previous.slug)} rel="prev">
|
|
482
|
+
<span class="pager-direction">{t.pagerPrevious}</span>
|
|
483
|
+
<span
|
|
484
|
+
class="pager-title"
|
|
485
|
+
lang={partLanguage(pager.previous.language, lang)}
|
|
486
|
+
>{pager.previous.title}</span>
|
|
487
|
+
</a>
|
|
488
|
+
)}
|
|
489
|
+
{pager.next !== undefined && (
|
|
490
|
+
<a class="pager-next" href={noteRoute(pager.next.slug)} rel="next">
|
|
491
|
+
<span class="pager-direction">{t.pagerNext}</span>
|
|
492
|
+
<span
|
|
493
|
+
class="pager-title"
|
|
494
|
+
lang={partLanguage(pager.next.language, lang)}
|
|
495
|
+
>{pager.next.title}</span>
|
|
496
|
+
</a>
|
|
497
|
+
)}
|
|
498
|
+
</nav>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{/* § 9.2.13 — the footer: public provenance and the canonical route. */}
|
|
502
|
+
<footer class="note-footer">
|
|
503
|
+
<p>{t.noteProvenance}</p>
|
|
504
|
+
{/* TK-08 configured `site:`, so this is the full canonical URL rather than
|
|
505
|
+
the route TK-05a could honestly emit. A reader who copies it from a
|
|
506
|
+
printed or quoted page gets an address that resolves. */}
|
|
507
|
+
<p class="note-canonical">{t.canonicalUrlLine(canonicalUrl(Astro.site, canonicalRoute))}</p>
|
|
508
|
+
</footer>
|
|
509
|
+
</Layout>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* What this site collects, which is nothing.
|
|
4
|
+
*
|
|
5
|
+
* **Rewritten by TK-31 for one false claim and one unsupportable one.** The
|
|
6
|
+
* previous version ended with "Every page here was individually reviewed and
|
|
7
|
+
* approved for publication. Material that was not approved is not represented
|
|
8
|
+
* on this site in any form." The first sentence describes the retired owner-specific bridge
|
|
9
|
+
* and is false of a build from a directory of Markdown; the second is *true*
|
|
10
|
+
* and had been resting on the first, so it read as a consequence of a review
|
|
11
|
+
* process rather than of what the build actually does.
|
|
12
|
+
*
|
|
13
|
+
* That distinction matters more here than on `/about/`. A privacy page is the
|
|
14
|
+
* document a reader consults to decide whether to trust the rest, so a claim in
|
|
15
|
+
* it that the tool cannot keep is worse than no page: it converts a technical
|
|
16
|
+
* guarantee into a promise about someone's diligence, and a reader has no way
|
|
17
|
+
* to tell which they were given.
|
|
18
|
+
*
|
|
19
|
+
* **Every claim below is a property this build enforces**, and each is scoped to
|
|
20
|
+
* what is actually held. `tests/built-output.test.ts` asserts every `<script>`
|
|
21
|
+
* in `dist/` is same-origin and none is inline; `public/_headers` sets a CSP
|
|
22
|
+
* whose `script-src`, `style-src`, `font-src`, and `connect-src` are all
|
|
23
|
+
* `'self'`. The absence of cookies and of analytics follows from there being no
|
|
24
|
+
* server and no such script.
|
|
25
|
+
*
|
|
26
|
+
* **`img-src` is the one directive that is not `'self'`** — it permits `https:`,
|
|
27
|
+
* so a note whose author embedded a remote image renders it, and that request
|
|
28
|
+
* reaches a third party. Measured, not inferred. The page therefore says
|
|
29
|
+
* "script, stylesheet, and font" where it once said "every asset", and names the
|
|
30
|
+
* image case separately: the earlier wording was a claim this build does not
|
|
31
|
+
* keep, on the page a reader consults to calibrate trust.
|
|
32
|
+
*
|
|
33
|
+
* The other thing this page cannot speak for is the host that serves the files,
|
|
34
|
+
* so it says who that is and stops rather than promising on their behalf.
|
|
35
|
+
*
|
|
36
|
+
* See `about.astro` for why this is still a shipped page rather than a note
|
|
37
|
+
* `init` seeds into the user's own repository.
|
|
38
|
+
*
|
|
39
|
+
* **The Content section was rewritten on 2026-08-17**, for the same reason the
|
|
40
|
+
* paragraphs above it were: it said an excluded note "produces no page, no
|
|
41
|
+
* search entry, and no link", and the last clause stopped being true when the
|
|
42
|
+
* owner made links to withheld notes live. It now says what a reader can
|
|
43
|
+
* actually observe — the link exists, it carries the note's own path as often
|
|
44
|
+
* as not, and it leads to a page that says the note is not published.
|
|
45
|
+
*/
|
|
46
|
+
import Layout from '../layouts/Layout.astro';
|
|
47
|
+
import { DEFAULT_NAV_LANGUAGE } from '../lib/translations.js';
|
|
48
|
+
import { TITLE_SUFFIX } from '../lib/site.js';
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
{/* The prose below is English whatever the site language, so the page says so. */}
|
|
52
|
+
<Layout
|
|
53
|
+
title={`Privacy${TITLE_SUFFIX}`}
|
|
54
|
+
lang={DEFAULT_NAV_LANGUAGE}
|
|
55
|
+
description="No analytics, no comments, no third-party tracking."
|
|
56
|
+
>
|
|
57
|
+
<article class="prose">
|
|
58
|
+
<h1>Privacy</h1>
|
|
59
|
+
|
|
60
|
+
<h2>What this site does not do</h2>
|
|
61
|
+
|
|
62
|
+
<p>
|
|
63
|
+
There are no analytics on this site. No page view, click, scroll, reading time, or search
|
|
64
|
+
query is measured, recorded, or reported anywhere.
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<p>
|
|
68
|
+
There are no comments, reactions, accounts, or sign-in. There is nothing to submit and no
|
|
69
|
+
form to fill in.
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<p>
|
|
73
|
+
No third-party tracking runs here. There are no advertising or analytics scripts, no
|
|
74
|
+
tracking pixels, no social widgets, and no embedded players. Every script, stylesheet, and
|
|
75
|
+
font a page loads comes from this site's own address.
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<p>
|
|
79
|
+
Images are the exception, and only where a note's author linked one. A note that embeds an
|
|
80
|
+
image hosted elsewhere renders that image from wherever it lives, so displaying it tells that
|
|
81
|
+
host you loaded the page — the same as following a link there. Nothing on this site does that
|
|
82
|
+
on its own; it happens only where the writing itself points outward.
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<p>
|
|
86
|
+
This site sets no cookies. Your theme and reader-mode choices are stored by your own browser,
|
|
87
|
+
on your own device, and are never sent anywhere. Clearing your browser's site data removes
|
|
88
|
+
them.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<h2>Search</h2>
|
|
92
|
+
|
|
93
|
+
<p>
|
|
94
|
+
Search runs entirely inside your browser against a search index served as a static file.
|
|
95
|
+
Nothing you type is transmitted to this site or to anyone else.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<h2>What is unavoidable</h2>
|
|
99
|
+
|
|
100
|
+
<p>
|
|
101
|
+
Requesting a page from any website necessarily reveals your IP address and browser details to
|
|
102
|
+
whoever serves it. This site is served by a static hosting provider, which may keep
|
|
103
|
+
operational request logs of its own under its own policy. Nothing beyond that is collected by
|
|
104
|
+
this site, which has no server of its own to collect it with.
|
|
105
|
+
</p>
|
|
106
|
+
|
|
107
|
+
<h2>Content</h2>
|
|
108
|
+
|
|
109
|
+
<p>
|
|
110
|
+
This site is generated from a set of notes, and a note excluded from it produces no page, no
|
|
111
|
+
search entry, and no content here. None of what it says appears. What does still appear is
|
|
112
|
+
the words a published note used to refer to it: a link pointing at an excluded note keeps its
|
|
113
|
+
own text — often the note's name and the path it had — and leads to a page saying the note is
|
|
114
|
+
not published. See <a href="/about/">About</a> for how that works.
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<p>
|
|
118
|
+
If any of this changes, this page changes with it.
|
|
119
|
+
</p>
|
|
120
|
+
</article>
|
|
121
|
+
</Layout>
|