@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,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The article's headings, as a real nested list.
|
|
4
|
+
*
|
|
5
|
+
* Three properties are load-bearing, and each is a decision the parity plan's
|
|
6
|
+
* L4 records against Quartz:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Nested `<ol>`, not a flat list with a `depth-N` class.** The nesting is
|
|
9
|
+
* the structure, so a screen reader announces "list, 3 items, item 2, list,
|
|
10
|
+
* 2 items" rather than reading eleven siblings and leaving the reader to
|
|
11
|
+
* infer the hierarchy from indentation they cannot see.
|
|
12
|
+
* 2. **Complete in the server-rendered HTML.** Requirements section 5.3 makes
|
|
13
|
+
* reading and navigation work without JavaScript. A small progressive script
|
|
14
|
+
* may mark the current section, but it never creates or removes a TOC link.
|
|
15
|
+
* 3. **Collapse costs zero JavaScript.** `<details>`/`<summary>` is the native
|
|
16
|
+
* disclosure widget: keyboard-operable, announced correctly, and `open` by
|
|
17
|
+
* default so a reader who never interacts sees the whole list.
|
|
18
|
+
*
|
|
19
|
+
* Recursion is `Astro.self`, which renders at build time — the nesting is real
|
|
20
|
+
* markup by the time the page is written, not a component boundary a browser
|
|
21
|
+
* has to resolve.
|
|
22
|
+
*
|
|
23
|
+
* Quartz's own table of contents is the component worth studying, but its
|
|
24
|
+
* contrast is not: its unread links measure 2.04:1 in light and 2.92:1 in dark,
|
|
25
|
+
* both WCAG AA failures in the default state. Ours uses `--color-link`, which
|
|
26
|
+
* `tests/design-tokens.test.ts` proves at 6.72:1 and 10.37:1.
|
|
27
|
+
*/
|
|
28
|
+
import type { TocEntry } from '../lib/markdown.js';
|
|
29
|
+
import { translate } from '../lib/translations.js';
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
entries: readonly TocEntry[];
|
|
33
|
+
/**
|
|
34
|
+
* Set on the outermost list only. Nested lists are rendered by `Astro.self`
|
|
35
|
+
* with it absent, which is what keeps the `<details>` shell and the labelled
|
|
36
|
+
* `<nav>` from being emitted once per level.
|
|
37
|
+
*/
|
|
38
|
+
isRoot?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The note's own language. Only the root renders a chrome string, so only the
|
|
41
|
+
* root needs it — the nested calls carry heading text, which is the article's
|
|
42
|
+
* own words in the article's own language either way.
|
|
43
|
+
*/
|
|
44
|
+
lang?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { entries, isRoot = false, lang } = Astro.props;
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
{isRoot ? (
|
|
51
|
+
<nav class="toc" aria-labelledby="toc-title">
|
|
52
|
+
{/* `open` so the list is visible by default and the disclosure is an
|
|
53
|
+
affordance to *close* it. A reader who never touches it loses nothing,
|
|
54
|
+
and with JavaScript disabled the element still opens and closes. */}
|
|
55
|
+
<details class="toc-details" open>
|
|
56
|
+
<summary id="toc-title">{translate(lang).tocHeading}</summary>
|
|
57
|
+
<Astro.self entries={entries} />
|
|
58
|
+
</details>
|
|
59
|
+
</nav>
|
|
60
|
+
) : (
|
|
61
|
+
<ol class="toc-list">
|
|
62
|
+
{entries.map((entry) => (
|
|
63
|
+
<li>
|
|
64
|
+
<a href={`#${entry.id}`}>{entry.text}</a>
|
|
65
|
+
{entry.children.length > 0 && <Astro.self entries={entry.children} />}
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ol>
|
|
69
|
+
)}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The enhanced tag browser, shared by `/tags/` and `/tags/<tag>/`.
|
|
4
|
+
*
|
|
5
|
+
* Both routes render the region's ids and the five `data-tag-browse-*`
|
|
6
|
+
* sentences that `src/scripts/tag-browser.ts` queries. When the shape lived on
|
|
7
|
+
* both pages a one-sided rename was silent: the client's `querySelector`
|
|
8
|
+
* returned null and the enhancement degraded without an error. The component
|
|
9
|
+
* owns that shape once; the script and this markup now move together or not at
|
|
10
|
+
* all.
|
|
11
|
+
*
|
|
12
|
+
* `mode` is the one thing that differs. `switch` renders the tag chooser;
|
|
13
|
+
* `fixed` renders the start button for the page's own tag. The complete static
|
|
14
|
+
* `FacetIndex`/`NoteList` beside it remains the no-JS route, so this region
|
|
15
|
+
* stays hidden until the script runs and unhides the static list on failure;
|
|
16
|
+
* `data-pagefind-ignore` keeps the region out of the Pagefind index.
|
|
17
|
+
*/
|
|
18
|
+
import { TAG_PAGE_SIZE } from '../lib/tag-browser-model.js';
|
|
19
|
+
import { NAV_LANGUAGE, TAG_BROWSE_DATASET, datasetAttribute, translate } from '../lib/translations.js';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
mode: 'switch' | 'fixed';
|
|
23
|
+
/** The fixed page's tag key, sent to the snapshot exactly as the route spells it. */
|
|
24
|
+
tagKey?: string;
|
|
25
|
+
/** The chooser's options; ignored when `mode` is `fixed`. */
|
|
26
|
+
facets?: readonly { key: string; label: string }[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { mode, tagKey, facets = [] } = Astro.props;
|
|
30
|
+
const t = translate(NAV_LANGUAGE);
|
|
31
|
+
// The five status sentences travel as data attributes so no locale table ships
|
|
32
|
+
// to the browser (`tag-browser.ts` reads them from `root.dataset`).
|
|
33
|
+
const browserData = Object.fromEntries([
|
|
34
|
+
[datasetAttribute(TAG_BROWSE_DATASET.loading), t.tagBrowseLoading],
|
|
35
|
+
[datasetAttribute(TAG_BROWSE_DATASET.empty), t.tagBrowseEmpty],
|
|
36
|
+
[datasetAttribute(TAG_BROWSE_DATASET.exhausted), t.tagBrowseExhausted],
|
|
37
|
+
[datasetAttribute(TAG_BROWSE_DATASET.unknown), t.tagBrowseUnknown],
|
|
38
|
+
[datasetAttribute(TAG_BROWSE_DATASET.failed), t.tagBrowseFailed],
|
|
39
|
+
]);
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<section
|
|
43
|
+
id="tag-browser"
|
|
44
|
+
hidden
|
|
45
|
+
data-pagefind-ignore
|
|
46
|
+
data-mode={mode}
|
|
47
|
+
{...mode === 'fixed' ? { 'data-tag-key': tagKey } : {}}
|
|
48
|
+
data-page-size={String(TAG_PAGE_SIZE)}
|
|
49
|
+
aria-labelledby="tag-browser-title"
|
|
50
|
+
{...browserData}
|
|
51
|
+
>
|
|
52
|
+
<h2 id="tag-browser-title">{t.tagBrowseHeading}</h2>
|
|
53
|
+
{mode === 'switch' ? (
|
|
54
|
+
<>
|
|
55
|
+
<label for="tag-browser-select">{t.tagBrowseSelectLabel}</label>
|
|
56
|
+
<select id="tag-browser-select">
|
|
57
|
+
<option value="">{t.tagBrowsePrompt}</option>
|
|
58
|
+
{facets.map((facet) => (
|
|
59
|
+
<option value={facet.key}>{facet.label}</option>
|
|
60
|
+
))}
|
|
61
|
+
</select>
|
|
62
|
+
</>
|
|
63
|
+
) : (
|
|
64
|
+
<button id="tag-browse-start" type="button">{t.tagBrowseStart}</button>
|
|
65
|
+
)}
|
|
66
|
+
<p id="tag-browser-current" hidden></p>
|
|
67
|
+
<p id="tag-browser-status" role="status"></p>
|
|
68
|
+
<ol id="tag-browser-results"></ol>
|
|
69
|
+
<button id="tag-browse-more" type="button" hidden>{t.tagBrowseMore}</button>
|
|
70
|
+
</section>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/global.css';
|
|
3
|
+
// `?url` keeps this a classic, render-blocking script: Astro compiles a normal
|
|
4
|
+
// `<script>` to `type="module"`, which defers and would paint one frame of the
|
|
5
|
+
// wrong theme. Importing the URL keeps the source under `src/scripts/` and
|
|
6
|
+
// content-hashed, while the tag stays inline-free and same-origin.
|
|
7
|
+
import themeInit from '../scripts/theme-init.js?url';
|
|
8
|
+
// The deployed policy's one source. `?raw` inlines the file's text at build
|
|
9
|
+
// time, so the `<meta>` below is derived from the same bytes `_headers` ships
|
|
10
|
+
// rather than restated, and cannot drift from it.
|
|
11
|
+
import shippedHeaders from '../../public/_headers?raw';
|
|
12
|
+
import SiteMetadata from '../components/SiteMetadata.astro';
|
|
13
|
+
import CollectionExplorer from '../components/CollectionExplorer.astro';
|
|
14
|
+
import { entries } from '../lib/content.js';
|
|
15
|
+
import { collectionNavigation } from '../lib/collection-navigation.js';
|
|
16
|
+
import { PRIMARY_NAV, SITE_MAP } from '../lib/routes.js';
|
|
17
|
+
import { canonicalUrl, describe, SITE_NAME } from '../lib/site.js';
|
|
18
|
+
import {
|
|
19
|
+
MESSAGE_DATASET,
|
|
20
|
+
NAV_LANGUAGE,
|
|
21
|
+
THEME_DATASET,
|
|
22
|
+
THEME_NAMES,
|
|
23
|
+
datasetAttribute,
|
|
24
|
+
translate,
|
|
25
|
+
} from '../lib/translations.js';
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** BCP 47 tag from the document's own metadata, when the artifact carries one. */
|
|
31
|
+
lang?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { title, lang = NAV_LANGUAGE } = Astro.props;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This document's chrome, resolved from this document's own language.
|
|
38
|
+
*
|
|
39
|
+
* The whole of TK-16 is that this line reads `lang` — a per-entry field on the
|
|
40
|
+
* validated contract — rather than one site-wide locale. A zh-CN note therefore
|
|
41
|
+
* renders Chinese chrome inside `<html lang="zh-CN">` while an English note in
|
|
42
|
+
* the same build renders English chrome, which is a shape Quartz's global
|
|
43
|
+
* `cfg.locale` cannot express. Routes that are not a single document pass no
|
|
44
|
+
* `lang` and land on `NAV_LANGUAGE`; `translations.ts` documents why.
|
|
45
|
+
*/
|
|
46
|
+
const t = translate(lang);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `describe` rather than a default parameter, and that is a fix rather than a
|
|
50
|
+
* refactor: a default only fires on `undefined`, while the note page passes
|
|
51
|
+
* `entry.excerpt` — the one required string the content contract admits *empty*.
|
|
52
|
+
* A note with an empty excerpt therefore shipped
|
|
53
|
+
* `<meta name="description" content="">`, which is worse than no tag at all,
|
|
54
|
+
* since a consumer scrapes the page when the tag is absent and renders nothing
|
|
55
|
+
* when it is present and empty. The fixture corpus has exactly one such note.
|
|
56
|
+
*
|
|
57
|
+
* The fallback is this document's own, so a Chinese note with an empty excerpt
|
|
58
|
+
* describes itself in Chinese rather than falling back across a language.
|
|
59
|
+
*/
|
|
60
|
+
const description = describe(Astro.props.description, t.siteDescription);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The explorer rail's groups, resolved against the route being rendered.
|
|
64
|
+
*
|
|
65
|
+
* Computed in the layout rather than passed in by each page, so every route
|
|
66
|
+
* carries the same navigation frame by construction — a page that forgets to
|
|
67
|
+
* ask is the failure mode this prevents, and it is the same reason
|
|
68
|
+
* `SiteMetadata` lives here. `Astro.url.pathname` is what makes the rail
|
|
69
|
+
* contextual: it opens the group holding the current page and no other, and it
|
|
70
|
+
* is what centres each group's window on the note being read.
|
|
71
|
+
*
|
|
72
|
+
* **This line is why the rail's size is a scaling property rather than a page's
|
|
73
|
+
* own concern.** It renders on every route, so whatever it costs is paid once
|
|
74
|
+
* per published page — the rail listing the whole corpus made a site O(n²) in
|
|
75
|
+
* bytes. `GROUP_WINDOW` in `collection-navigation.ts` is the bound that fixed
|
|
76
|
+
* it and carries the measurements.
|
|
77
|
+
*/
|
|
78
|
+
const explorerGroups = collectionNavigation(entries, Astro.url.pathname, t.uncollected);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The site-wide Content-Security-Policy, as a `<meta http-equiv>`.
|
|
82
|
+
*
|
|
83
|
+
* `public/_headers` is Cloudflare Pages' format, so a host that ignores it —
|
|
84
|
+
* GitHub Pages, the documented zero-secret path, or any plain static server —
|
|
85
|
+
* served no CSP at all. The meta element carries the same policy to every host.
|
|
86
|
+
*
|
|
87
|
+
* Read from the `/*` rule of `_headers` rather than restated, minus the
|
|
88
|
+
* directives a meta-delivered policy cannot carry and the browser ignores with a
|
|
89
|
+
* console warning (CSP3 §3.3): `frame-ancestors`, `report-uri`, `report-to`, and
|
|
90
|
+
* `sandbox`. Clickjacking protection therefore still depends on a host that
|
|
91
|
+
* sends the header. Where `_headers` is honoured both policies apply; they are
|
|
92
|
+
* the same policy, so the intersection is unchanged.
|
|
93
|
+
*
|
|
94
|
+
* Omitted under `astro dev` only, whose client injects inline scripts the
|
|
95
|
+
* shipped policy forbids. Keyed on Vite's `MODE`, not `PROD`: `PROD` follows
|
|
96
|
+
* `NODE_ENV`, so a build started with `NODE_ENV=test` — every build a test
|
|
97
|
+
* harness spawns — would silently ship no policy. `tests/csp-meta.test.ts`
|
|
98
|
+
* asserts the emitted value against `_headers` and its enforcement in Chromium.
|
|
99
|
+
*/
|
|
100
|
+
const META_UNSUPPORTED = new Set(['frame-ancestors', 'report-uri', 'report-to', 'sandbox']);
|
|
101
|
+
const metaPolicy = (() => {
|
|
102
|
+
let inSiteRule = false;
|
|
103
|
+
for (const line of shippedHeaders.split(/\r?\n/)) {
|
|
104
|
+
if (line.trim() === '' || line.trimStart().startsWith('#')) continue;
|
|
105
|
+
if (!/^\s/.test(line)) {
|
|
106
|
+
inSiteRule = line.trim() === '/*';
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const match = inSiteRule ? /^\s*Content-Security-Policy:\s*(.+)$/i.exec(line) : null;
|
|
110
|
+
if (match === null) continue;
|
|
111
|
+
return match[1]!
|
|
112
|
+
.split(';')
|
|
113
|
+
.map((directive) => directive.trim())
|
|
114
|
+
.filter((directive) => directive !== '' && !META_UNSUPPORTED.has(directive.split(/\s+/)[0]!.toLowerCase()))
|
|
115
|
+
.join('; ');
|
|
116
|
+
}
|
|
117
|
+
throw new Error('public/_headers declares no site-wide Content-Security-Policy for the <meta> policy');
|
|
118
|
+
})();
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
<!doctype html>
|
|
122
|
+
<html lang={lang}>
|
|
123
|
+
<head>
|
|
124
|
+
<meta charset="UTF-8" />
|
|
125
|
+
{/* First after the charset: a meta policy governs only what follows it. */}
|
|
126
|
+
{import.meta.env.MODE !== 'development' && <meta http-equiv="Content-Security-Policy" content={metaPolicy} />}
|
|
127
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
128
|
+
<meta name="description" content={description} />
|
|
129
|
+
<meta name="generator" content={Astro.generator} />
|
|
130
|
+
<title>{title}</title>
|
|
131
|
+
<script is:inline src={themeInit}></script>
|
|
132
|
+
{/* TK-08: the canonical URL, the Open Graph and Twitter card metadata, and
|
|
133
|
+
the feed discovery link. Rendered here rather than by each page so that
|
|
134
|
+
every route carries them by construction — a page that forgets to ask
|
|
135
|
+
is the failure mode this prevents. It derives everything from
|
|
136
|
+
`Astro.site`, `Astro.url`, and the artifact; it takes no new props
|
|
137
|
+
beyond the three this layout already has. */}
|
|
138
|
+
<SiteMetadata title={title} description={description} lang={lang} />
|
|
139
|
+
{/* The one seam a page has into the document head.
|
|
140
|
+
TK-15 fills it with the math stylesheet and font, gated on
|
|
141
|
+
`RenderedNote.hasMath`, and the Mermaid assets gated on `.hasMermaid`.
|
|
142
|
+
`src/pages/notes/[slug].astro` already uses it for the `hasCode` case,
|
|
143
|
+
so the mechanism is exercised rather than speculative.
|
|
144
|
+
|
|
145
|
+
A named slot rather than a widening `Props` interface: a per-ticket prop
|
|
146
|
+
would make this file the contended one every time a ticket needs a tag
|
|
147
|
+
in the head, which is the collision the wave plan exists to avoid. */}
|
|
148
|
+
<slot name="head" />
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<a class="skip-link" href="#main">{t.skipToContent}</a>
|
|
152
|
+
<header class="site-header">
|
|
153
|
+
{/* The site's name is a proper noun, so it is the one word in this header
|
|
154
|
+
that is not translated — see `SITE_NAME`. */}
|
|
155
|
+
<a class="brand" href="/">{SITE_NAME}</a>
|
|
156
|
+
<nav class="site-nav" aria-label={t.primaryNavLabel}>
|
|
157
|
+
{PRIMARY_NAV.map((item) => <a href={item.href}>{t[item.label]}</a>)}
|
|
158
|
+
{/* The shortcut is in the accessible name rather than in a `title`,
|
|
159
|
+
which no screen reader reliably announces and no keyboard reader
|
|
160
|
+
can discover. `aria-keyshortcuts` is the attribute that exists for
|
|
161
|
+
exactly this, and the visible text stays one word. */}
|
|
162
|
+
<button
|
|
163
|
+
id="search-toggle"
|
|
164
|
+
type="button"
|
|
165
|
+
aria-keyshortcuts="/"
|
|
166
|
+
aria-label={t.searchToggleLabel}
|
|
167
|
+
data-js-only
|
|
168
|
+
>{t.searchToggle}</button>
|
|
169
|
+
{/* The three theme labels travel as `data-` attributes rather than as a
|
|
170
|
+
locale table in the bundle: `preferences.ts` cycles the attribute and
|
|
171
|
+
reads back the label the build already resolved, so the script ships
|
|
172
|
+
no strings, knows no locale, and adds no bytes that scale with the
|
|
173
|
+
number of languages. The rendered text is the `system` label, which
|
|
174
|
+
is the state before any preference is stored. */}
|
|
175
|
+
<button
|
|
176
|
+
id="theme-toggle"
|
|
177
|
+
type="button"
|
|
178
|
+
data-js-only
|
|
179
|
+
{...Object.fromEntries(
|
|
180
|
+
THEME_NAMES.map((theme) => [datasetAttribute(THEME_DATASET[theme]), t.themeLabel(theme)]),
|
|
181
|
+
)}
|
|
182
|
+
>{t.themeLabel(THEME_NAMES[0])}</button>
|
|
183
|
+
<button id="reader-toggle" type="button" aria-pressed="false" data-js-only>{t.readerToggle}</button>
|
|
184
|
+
</nav>
|
|
185
|
+
</header>
|
|
186
|
+
{/* TK-06. Every region the search script fills is declared here in HTML
|
|
187
|
+
rather than constructed in JavaScript, for three reasons: the `<label>`
|
|
188
|
+
and the `<input>` stay a real labelled pair instead of Pagefind's
|
|
189
|
+
`data-`-attribute substitute; the dialog has something to focus the
|
|
190
|
+
instant it opens, before the bundle has loaded; and the status line is a
|
|
191
|
+
live region that exists before the first message is written into it, so
|
|
192
|
+
the message is announced rather than silently appearing.
|
|
193
|
+
|
|
194
|
+
`data-pagefind-ignore` on the whole dialog: it is on every page, so
|
|
195
|
+
without it the index would carry this chrome on all 32 notes — the same
|
|
196
|
+
defect TK-12 fixed for the back link and the heading anchors.
|
|
197
|
+
|
|
198
|
+
The close button is a `<form method="dialog">`, which needs no script
|
|
199
|
+
and — per the parity plan §7.6 — never navigates, so `form-action 'none'`
|
|
200
|
+
does not touch it. */}
|
|
201
|
+
<dialog id="search-dialog" aria-label={t.searchDialogLabel} data-pagefind-ignore>
|
|
202
|
+
<form method="dialog" class="search-close"><button>{t.searchClose}</button></form>
|
|
203
|
+
<label class="search-label" for="search-input">{t.searchFieldLabel}</label>
|
|
204
|
+
{/* `type="search"` for the browser's own clear control and the right
|
|
205
|
+
on-screen keyboard. `autocomplete="off"` because a site search field
|
|
206
|
+
offering the reader's previous queries from other sites is noise. */}
|
|
207
|
+
<input id="search-input" type="search" autocomplete="off" autocapitalize="none" enterkeyhint="search" />
|
|
208
|
+
<div id="search-filters">
|
|
209
|
+
<div id="search-filter-tag"></div>
|
|
210
|
+
<div id="search-filter-collection"></div>
|
|
211
|
+
</div>
|
|
212
|
+
{/* `role="status"` is an `aria-live="polite"` region: the reader hears
|
|
213
|
+
"Loading the search index" or "No results" without focus moving off
|
|
214
|
+
the field they are typing into.
|
|
215
|
+
|
|
216
|
+
The four load-state sentences travel as `data-` attributes for the same
|
|
217
|
+
reason the theme labels do: `search-dialog.ts` reads the one it needs
|
|
218
|
+
off this element, so the bundle carries no locale table and the page
|
|
219
|
+
announces its own language. There is no rendered text — the region is
|
|
220
|
+
empty until a state is announced, which is what makes the first message
|
|
221
|
+
an announcement rather than a change nobody hears. */}
|
|
222
|
+
<p id="search-status" role="status" {...Object.fromEntries([
|
|
223
|
+
[datasetAttribute(MESSAGE_DATASET.idle), t.searchIdle],
|
|
224
|
+
[datasetAttribute(MESSAGE_DATASET.loading), t.searchLoading],
|
|
225
|
+
[datasetAttribute(MESSAGE_DATASET.empty), t.searchEmpty],
|
|
226
|
+
[datasetAttribute(MESSAGE_DATASET.failed), t.searchFailed],
|
|
227
|
+
])}></p>
|
|
228
|
+
<ol id="search-results"></ol>
|
|
229
|
+
</dialog>
|
|
230
|
+
{/* The reading frame: the article, and the collection rail beside it.
|
|
231
|
+
Requirements section 16 asks for an explorer, a centred article, and a
|
|
232
|
+
contextual rail on desktop; collapsible panels on tablet; one column on
|
|
233
|
+
mobile. This grid is all three — see `.page-shell` in `global.css`.
|
|
234
|
+
|
|
235
|
+
The rail follows `<main>` in document order at every width, and the
|
|
236
|
+
stylesheet never reorders it. That is what keeps the reading order, the
|
|
237
|
+
tab order, and the visual order the same thing: a reader tabbing forward
|
|
238
|
+
reaches the article before the site-wide navigation, and a reader with no
|
|
239
|
+
CSS gets the article first. Placing the explorer visually before the
|
|
240
|
+
article — Quartz's left-hand shape — would mean either a DOM order that
|
|
241
|
+
puts the site's whole navigation ahead of the page's own content, or a
|
|
242
|
+
`grid-column` that makes the tab order disagree with the layout. */}
|
|
243
|
+
<div class="page-shell">
|
|
244
|
+
<main id="main" tabindex="-1">
|
|
245
|
+
<slot />
|
|
246
|
+
{/* The link preview panel (TK-07). Empty until a preview opens, and
|
|
247
|
+
`position: fixed`, so it contributes no layout wherever it sits — but
|
|
248
|
+
it must sit inside a landmark. axe-core's `region` rule reports every
|
|
249
|
+
element of page content that is in none, and an open panel parented to
|
|
250
|
+
`<body>` was exactly that: it was reported on every route, in both
|
|
251
|
+
themes, at both widths, and moving it here is what cleared it.
|
|
252
|
+
|
|
253
|
+
That audit is run out of band and is not reproducible from this
|
|
254
|
+
repository — axe-core is in neither `dependencies` nor
|
|
255
|
+
`devDependencies`, so the check needs an `axe.min.js` fetched to a
|
|
256
|
+
scratch path. TK-09 owns the question of whether it becomes a release
|
|
257
|
+
gate. Recorded this way deliberately: the rule the panel was breaking
|
|
258
|
+
is nameable and checkable by anyone who runs axe, which is worth more
|
|
259
|
+
than a violation count nothing here can recompute.
|
|
260
|
+
|
|
261
|
+
After the slot, so the document order a screen reader walks puts a
|
|
262
|
+
transient description of a link after the content that contains the
|
|
263
|
+
link, rather than before the article it describes. */}
|
|
264
|
+
<div id="link-preview" role="tooltip" hidden></div>
|
|
265
|
+
</main>
|
|
266
|
+
{/* TK-05c. Built entirely at build time and collapsed with native
|
|
267
|
+
`<details>`, so it is complete and operable with scripting disabled —
|
|
268
|
+
the Quartz explorer failure the parity plan's L5 names. It renders
|
|
269
|
+
nothing at all on a corpus with fewer than two notes. */}
|
|
270
|
+
<CollectionExplorer groups={explorerGroups} lang={lang} />
|
|
271
|
+
</div>
|
|
272
|
+
<footer class="site-footer">
|
|
273
|
+
{/* Every fixed route is linked from here, on every page, so no route is an
|
|
274
|
+
orphan. This nav deliberately survives reader mode, which hides the
|
|
275
|
+
header's secondary links: hiding the only path to a route would make
|
|
276
|
+
reader mode remove information rather than chrome. */}
|
|
277
|
+
<nav class="site-map" aria-label={t.siteMapLabel}>
|
|
278
|
+
{SITE_MAP.map((item) => <a href={item.href}>{t[item.label]}</a>)}
|
|
279
|
+
</nav>
|
|
280
|
+
<p>{t.siteSubtitle}</p>
|
|
281
|
+
{/* Scope item 10 requires the printed page to carry its canonical URL.
|
|
282
|
+
A printed page has no address bar, so the full absolute URL is what
|
|
283
|
+
makes it findable again — TK-05a emitted the route and left this for
|
|
284
|
+
TK-08 to upgrade once `site:` was configured. */}
|
|
285
|
+
<p class="print-only">{t.publishedAt(canonicalUrl(Astro.site, Astro.url.pathname))}</p>
|
|
286
|
+
</footer>
|
|
287
|
+
<script>
|
|
288
|
+
import '../scripts/preferences.js';
|
|
289
|
+
import '../scripts/search-dialog.js';
|
|
290
|
+
import '../scripts/link-preview.js';
|
|
291
|
+
if (document.querySelector('.prose[data-code-copy] .code-block')) {
|
|
292
|
+
void import('../scripts/code-copy.js');
|
|
293
|
+
}
|
|
294
|
+
if (document.querySelector('.toc')) {
|
|
295
|
+
void import('../scripts/toc-active.js');
|
|
296
|
+
}
|
|
297
|
+
if (document.querySelector('#tag-browser')) {
|
|
298
|
+
void import('../scripts/tag-browser.js');
|
|
299
|
+
}
|
|
300
|
+
if (document.querySelector('[data-graph-controls]')) {
|
|
301
|
+
void import('../scripts/graph-client.js');
|
|
302
|
+
}
|
|
303
|
+
</script>
|
|
304
|
+
</body>
|
|
305
|
+
</html>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which artifact file this build reads.
|
|
3
|
+
*
|
|
4
|
+
* The default is the producer-generated `src/data/content.json`. Setting
|
|
5
|
+
* `CONTENT_ARTIFACT` to another path — `pnpm run build:fixture` does exactly
|
|
6
|
+
* that — builds the whole site from a different artifact without touching the
|
|
7
|
+
* generated one, which the agent contract forbids editing by hand.
|
|
8
|
+
*
|
|
9
|
+
* Every consumer resolves the path through here rather than hardcoding it, so
|
|
10
|
+
* the build gate, the redirect emitter, the pages, and the tests over `dist/`
|
|
11
|
+
* can never end up reading two different artifacts and agreeing about it.
|
|
12
|
+
*
|
|
13
|
+
* The path is resolved against `process.cwd()` rather than against
|
|
14
|
+
* `import.meta.url`, and that is load-bearing: Astro bundles this module into
|
|
15
|
+
* `dist/.prerender/`, so `import.meta.url` points inside the build output at
|
|
16
|
+
* the moment the pages actually read it. `pnpm` runs every script with the
|
|
17
|
+
* working directory set to the directory holding `package.json`, and Astro
|
|
18
|
+
* resolves its own project root the same way — so cwd is the one anchor all
|
|
19
|
+
* three consumers agree on.
|
|
20
|
+
*
|
|
21
|
+
* ponytail: this means the build must be started from the repository root. It
|
|
22
|
+
* already had to be — `astro build` finds `astro.config.mjs` the same way.
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { resolve } from 'node:path';
|
|
26
|
+
import { validateArtifact } from "./schema.js";
|
|
27
|
+
const DEFAULT_ARTIFACT = 'src/data/content.json';
|
|
28
|
+
/**
|
|
29
|
+
* Repository-relative path of the artifact, suitable for an error message.
|
|
30
|
+
*
|
|
31
|
+
* `||` rather than `??`: an empty `CONTENT_ARTIFACT` is a caller who meant to
|
|
32
|
+
* pass a path and passed nothing. With `??` it would resolve to the working
|
|
33
|
+
* directory and fail on a directory read, without naming the empty variable.
|
|
34
|
+
*/
|
|
35
|
+
export const ARTIFACT_PATH = process.env['CONTENT_ARTIFACT'] || DEFAULT_ARTIFACT;
|
|
36
|
+
/**
|
|
37
|
+
* Absolute filesystem path to the artifact, anchored at the repository root.
|
|
38
|
+
*
|
|
39
|
+
* `path.resolve` rather than a hand-built `file://` URL: a URL parses `#` and
|
|
40
|
+
* `?` in the *directory* name as a fragment and a query, so a checkout under
|
|
41
|
+
* `Q:/re#po` silently resolves to the drive root, and it percent-decodes, so a
|
|
42
|
+
* literal `%20` in a path becomes a space. Neither is hypothetical on a machine
|
|
43
|
+
* whose worktrees live under generated directory names.
|
|
44
|
+
*
|
|
45
|
+
* A function rather than a constant so a test can change `CONTENT_ARTIFACT` and
|
|
46
|
+
* see the effect without re-importing the module.
|
|
47
|
+
*/
|
|
48
|
+
export function artifactPath(path = ARTIFACT_PATH) {
|
|
49
|
+
return resolve(process.cwd(), path);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The artifact's raw bytes, exactly as they are on disk.
|
|
53
|
+
*
|
|
54
|
+
* Returned as text rather than parsed because two callers need the bytes
|
|
55
|
+
* themselves: `emit-redirects.ts` hashes them into the redirect map's
|
|
56
|
+
* `content_version`, and `built-routes.test.ts` recomputes that hash to prove
|
|
57
|
+
* the map came from the artifact `dist/` was built from.
|
|
58
|
+
*/
|
|
59
|
+
export function readArtifact(path = ARTIFACT_PATH) {
|
|
60
|
+
return readFileSync(artifactPath(path), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The validated artifact, read from whichever file this build selected.
|
|
64
|
+
*
|
|
65
|
+
* One place where read, parse, and validate happen together, because three
|
|
66
|
+
* callers were doing all three and a fourth would have got the error label
|
|
67
|
+
* subtly wrong. Every caller sees an artifact that has already passed the
|
|
68
|
+
* contract, and a violation names the file it actually came from.
|
|
69
|
+
*/
|
|
70
|
+
export function loadArtifact(path = ARTIFACT_PATH) {
|
|
71
|
+
return validateArtifact(JSON.parse(readArtifact(path)), path);
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time globals substituted by `vite.define` from the private snapshot
|
|
3
|
+
* workspace (`readStaged` in `astro.config.mjs`).
|
|
4
|
+
*
|
|
5
|
+
* They carry the digest-named same-origin binding for the snapshot and the
|
|
6
|
+
* SQLite WASM, or `null` when a build staged none, so the Worker fails closed
|
|
7
|
+
* and fetches nothing. Declared here rather than in a `.d.ts` because
|
|
8
|
+
* `scripts/compile-package.ts` builds its TypeScript program from `src` and
|
|
9
|
+
* `scripts` sources, not from ambient declaration files.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|