@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/astro.config.mjs +255 -0
  4. package/bin/anc.mjs +798 -0
  5. package/package.json +93 -0
  6. package/public/_headers +45 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/favicon.svg +9 -0
  9. package/public/fonts/Temml.woff2 +0 -0
  10. package/scripts/build-snapshot.js +69 -0
  11. package/scripts/build-wasm.js +67 -0
  12. package/scripts/build-workspace.js +50 -0
  13. package/scripts/copy-snapshot.js +48 -0
  14. package/scripts/copy-wasm.js +57 -0
  15. package/scripts/emit-redirects.js +64 -0
  16. package/scripts/generate-corpus.js +421 -0
  17. package/scripts/init-repository.js +249 -0
  18. package/scripts/load-config.js +975 -0
  19. package/scripts/markdown-to-artifact.js +1286 -0
  20. package/scripts/preview-site.js +364 -0
  21. package/scripts/publish-set-review.js +157 -0
  22. package/scripts/resolve-links.js +640 -0
  23. package/scripts/run-pagefind.js +125 -0
  24. package/scripts/scan-residue.js +1016 -0
  25. package/scripts/scan-secrets.js +268 -0
  26. package/scripts/seed-gitignore.js +410 -0
  27. package/scripts/snapshot-rows.js +205 -0
  28. package/scripts/validate-content.js +95 -0
  29. package/scripts/vendor-provenance.js +307 -0
  30. package/scripts/verify-output-inventory.js +307 -0
  31. package/scripts/write-report.js +351 -0
  32. package/scripts/write-snapshot.js +136 -0
  33. package/src/components/CollectionExplorer.astro +149 -0
  34. package/src/components/FacetIndex.astro +45 -0
  35. package/src/components/LinkedNotes.astro +94 -0
  36. package/src/components/NoteGraph.astro +434 -0
  37. package/src/components/NoteList.astro +54 -0
  38. package/src/components/SiteMetadata.astro +143 -0
  39. package/src/components/TableOfContents.astro +69 -0
  40. package/src/components/TagBrowser.astro +70 -0
  41. package/src/layouts/Layout.astro +305 -0
  42. package/src/lib/artifact-source.js +72 -0
  43. package/src/lib/build-bindings.js +11 -0
  44. package/src/lib/collection-navigation.js +145 -0
  45. package/src/lib/content.js +92 -0
  46. package/src/lib/diagram-mode.js +18 -0
  47. package/src/lib/graph-layout.js +219 -0
  48. package/src/lib/graph-selection.js +84 -0
  49. package/src/lib/graph.js +42 -0
  50. package/src/lib/link-resolution.js +389 -0
  51. package/src/lib/markdown.js +1469 -0
  52. package/src/lib/math-mode.js +31 -0
  53. package/src/lib/math-output.js +131 -0
  54. package/src/lib/math.js +165 -0
  55. package/src/lib/mermaid-environment.js +475 -0
  56. package/src/lib/mermaid-render.js +769 -0
  57. package/src/lib/preview-model.js +96 -0
  58. package/src/lib/relations.js +242 -0
  59. package/src/lib/rendered-marker.js +55 -0
  60. package/src/lib/route-path.js +192 -0
  61. package/src/lib/routes.js +342 -0
  62. package/src/lib/schema.js +578 -0
  63. package/src/lib/site.js +578 -0
  64. package/src/lib/snapshot-contract.js +216 -0
  65. package/src/lib/snapshot-operations.js +147 -0
  66. package/src/lib/snapshot-queries.js +216 -0
  67. package/src/lib/snapshot-reader.js +210 -0
  68. package/src/lib/snapshot.js +233 -0
  69. package/src/lib/sqlite-warning.js +22 -0
  70. package/src/lib/sqlite.js +14 -0
  71. package/src/lib/tag-browser-model.js +18 -0
  72. package/src/lib/translations.js +533 -0
  73. package/src/lib/wasm-asset.js +47 -0
  74. package/src/lib/worker-protocol.js +93 -0
  75. package/src/pages/404.astro +35 -0
  76. package/src/pages/about.astro +100 -0
  77. package/src/pages/collections/[slug].astro +36 -0
  78. package/src/pages/collections/index.astro +33 -0
  79. package/src/pages/graph.astro +64 -0
  80. package/src/pages/index.astro +33 -0
  81. package/src/pages/notes/[slug].astro +509 -0
  82. package/src/pages/privacy.astro +121 -0
  83. package/src/pages/private.astro +59 -0
  84. package/src/pages/recent.astro +33 -0
  85. package/src/pages/robots.txt.js +15 -0
  86. package/src/pages/rss.xml.js +21 -0
  87. package/src/pages/sitemap.xml.js +20 -0
  88. package/src/pages/tags/[tag].astro +56 -0
  89. package/src/pages/tags/index.astro +37 -0
  90. package/src/scripts/code-copy.js +45 -0
  91. package/src/scripts/diagram-disabled.js +16 -0
  92. package/src/scripts/diagram.js +322 -0
  93. package/src/scripts/graph-client.js +222 -0
  94. package/src/scripts/graph-draw.js +166 -0
  95. package/src/scripts/link-preview.js +334 -0
  96. package/src/scripts/math-disabled.js +18 -0
  97. package/src/scripts/math.js +133 -0
  98. package/src/scripts/preferences.js +123 -0
  99. package/src/scripts/search-dialog.js +651 -0
  100. package/src/scripts/snapshot-client.js +157 -0
  101. package/src/scripts/snapshot-worker.js +341 -0
  102. package/src/scripts/tag-browser.js +153 -0
  103. package/src/scripts/theme-init.js +35 -0
  104. package/src/scripts/toc-active.js +38 -0
  105. package/src/styles/code.css +152 -0
  106. package/src/styles/diagram.css +149 -0
  107. package/src/styles/global.css +1739 -0
  108. package/src/styles/graph.css +317 -0
  109. package/src/styles/math.css +376 -0
  110. package/src/styles/tokens.css +171 -0
  111. package/tsconfig.json +25 -0
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Every chrome string the site renders, in both public languages.
3
+ *
4
+ * **Chrome, never content.** A title, an excerpt, a tag, a collection name, and
5
+ * an article body are the author's words in the author's language, and they are
6
+ * reproduced verbatim on every page regardless of which locale resolved around
7
+ * them. A tag reading `security` stays `security` on a Chinese page. What lives
8
+ * here is the interface the site wraps that content in — the words this
9
+ * repository authored rather than the ones the artifact carried.
10
+ *
11
+ * **Per document, not per site.** {@link translate} is keyed on one document's
12
+ * own `language` field, so a zh-CN note renders Chinese chrome and an English
13
+ * note renders English chrome *in the same build*. That is the structural
14
+ * difference from Quartz, whose `cfg.locale` is a single global read by every
15
+ * plugin: there a zh-CN document renders `lang="zh"` around English chrome,
16
+ * because the document's language and the interface language cannot disagree
17
+ * when only one of them exists. Here `language` is a per-entry field on a
18
+ * validated contract (`src/lib/schema.ts`), so the two are separate facts and
19
+ * the interface follows the document.
20
+ *
21
+ * **Which routes are documents.** A route that renders exactly one artifact
22
+ * entry — `/notes/<slug>/` — resolves its chrome from that entry. Every other
23
+ * route resolves to {@link NAV_LANGUAGE}: the home page, `/recent/`, the tag and
24
+ * collection indexes, a facet page, and the 404 each aggregate many entries or
25
+ * none, so there is no single document language to read. Picking one member's
26
+ * language would make the chrome of `/tags/gardening/` depend on which note
27
+ * sorts first, which is a public page changing language because an unrelated
28
+ * note was published.
29
+ *
30
+ * **The contract is compile-checked.** Each locale ends `as const satisfies
31
+ * Translation`, so a key added to {@link Translation} and forgotten in either
32
+ * locale is a type error at `pnpm run check` — never a runtime fallback that
33
+ * renders the key name to a reader. `tests/translations.test.ts` proves that
34
+ * property by type-checking a locale with a key removed.
35
+ *
36
+ * **No ICU runtime, and none needed.** The strings that carry a count are
37
+ * functions. English pluralises and Chinese does not, and a function-valued
38
+ * entry expresses both without a plural-rules library — and without the defect a
39
+ * shared `count === 1 ? singular : plural` helper would bake in, which is that
40
+ * it produces nonsense the moment it is applied to a language with no plural.
41
+ * Each locale states its own rule.
42
+ *
43
+ * **Zero client-side locale lookup.** Resolution happens during the build. The
44
+ * scripts that write text — theme, search status, and code copy — read strings
45
+ * the build already resolved out of `data-` attributes, so none ships a locale
46
+ * table or knows a locale exists.
47
+ */
48
+ /** The theme names the toggle cycles through, in cycle order. */
49
+ export const THEME_NAMES = ['system', 'light', 'dark'];
50
+ /**
51
+ * The `dataset` key carrying each theme's label, and each search state's
52
+ * sentence, on the element the build writes them to.
53
+ *
54
+ * These pair a *script's* lookup with an *attribute* in the markup, and neither
55
+ * side type-checks the other: `dataset[key]` is `string | undefined` for any
56
+ * string, so a rename on either side is a toggle rendering the raw English state
57
+ * name inside Chinese chrome, or a status line that silently goes blank.
58
+ *
59
+ * They live here, in the module with no DOM, rather than beside the scripts that
60
+ * index them, for two reasons. The gate over `dist/` has to read *these objects*
61
+ * to check the pairing, and importing a client script under Node runs its
62
+ * top-level `document` access. And the layout writes its attributes from the
63
+ * same values through {@link datasetAttribute}, so the two spellings are now
64
+ * derived from one string rather than written twice — a rename here changes
65
+ * both sides together, which is what makes the divergence impossible rather
66
+ * than merely detectable.
67
+ *
68
+ * Restating the keys in the test instead was tried and is not equivalent: it
69
+ * catches a rename in the markup and misses one in the script, which is the
70
+ * direction that ships the defect. Verified by mutation.
71
+ */
72
+ export const THEME_DATASET = {
73
+ system: 'labelSystem',
74
+ light: 'labelLight',
75
+ dark: 'labelDark',
76
+ };
77
+ /**
78
+ * The attribute name the DOM maps a `dataset` key from: `messageIdle` becomes
79
+ * `data-message-idle`.
80
+ *
81
+ * The layout writes attributes through this and the scripts read the keys
82
+ * directly, so the two spellings are derived from one value rather than written
83
+ * twice — which is what makes a rename impossible to get half-right.
84
+ */
85
+ export function datasetAttribute(key) {
86
+ return `data-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
87
+ }
88
+ /** The four announceable search states. `ready` renders no sentence — see `announce`. */
89
+ export const MESSAGE_DATASET = {
90
+ idle: 'messageIdle',
91
+ loading: 'messageLoading',
92
+ empty: 'messageEmpty',
93
+ failed: 'messageFailed',
94
+ };
95
+ /** The tag browser's live-status sentences, carried from the build as data attributes. */
96
+ export const TAG_BROWSE_DATASET = {
97
+ loading: 'tagBrowseLoading',
98
+ empty: 'tagBrowseEmpty',
99
+ exhausted: 'tagBrowseExhausted',
100
+ unknown: 'tagBrowseUnknown',
101
+ failed: 'tagBrowseFailed',
102
+ };
103
+ /**
104
+ * The environment variable carrying the configured site `language` into the
105
+ * build: `configForBuild` in `scripts/load-config.ts` sets it from
106
+ * `publish.config.yaml`, beside the site title, for the reason
107
+ * `SITE_TITLE_VARIABLE` in `site.ts` records.
108
+ */
109
+ export const SITE_LANGUAGE_VARIABLE = 'PUBLISH_SITE_LANGUAGE';
110
+ /** The interface's own default when no site language is configured. */
111
+ export const DEFAULT_NAV_LANGUAGE = 'en';
112
+ /**
113
+ * The site navigation language: what a route that is not one document renders,
114
+ * and what a document carrying no `language` falls back to.
115
+ *
116
+ * The configured site `language` when there is one — already validated as
117
+ * BCP 47 by the loader — else {@link DEFAULT_NAV_LANGUAGE}. Read through
118
+ * `globalThis.process` rather than `process`, because browser scripts import
119
+ * this module (`partLanguage`) and have no `process`: there it is always the
120
+ * default, which is harmless because every note the browser receives from the
121
+ * snapshot already carries its effective language. The producer writes the
122
+ * configured default into every undeclared entry, so a note page, its
123
+ * `nodes.language` row, and its Pagefind index never depend on this fallback
124
+ * in a configured build.
125
+ */
126
+ export const NAV_LANGUAGE = globalThis.process?.env?.[SITE_LANGUAGE_VARIABLE] || DEFAULT_NAV_LANGUAGE;
127
+ const EN = {
128
+ skipToContent: 'Skip to content',
129
+ primaryNavLabel: 'Primary',
130
+ siteMapLabel: 'Site map',
131
+ siteSubtitle: 'Notes, published as a static site.',
132
+ siteDescription: 'A collection of notes, published as static pages.',
133
+ publishedAt: (url) => `Published at ${url}`,
134
+ feedTitle: (siteName) => `${siteName} — all notes`,
135
+ socialCardAlt: (siteName) => `${siteName} — a collection of notes`,
136
+ navHome: 'Home',
137
+ navNotes: 'Notes',
138
+ navRecent: 'Recent',
139
+ navTags: 'Tags',
140
+ navCollections: 'Collections',
141
+ navGraph: 'Graph',
142
+ navAbout: 'About',
143
+ navPrivacy: 'Privacy',
144
+ searchToggle: 'Search',
145
+ searchToggleLabel: 'Search this site (press slash)',
146
+ themeLabel: (theme) => `Theme: ${theme}`,
147
+ readerToggle: 'Reader mode',
148
+ searchDialogLabel: 'Search',
149
+ searchClose: 'Close search',
150
+ searchFieldLabel: 'Search this site',
151
+ searchIdle: 'Type to search this site.',
152
+ searchLoading: 'Loading the search index…',
153
+ searchEmpty: 'No results. Try a different word.',
154
+ searchFailed: 'The search index could not be loaded. The rest of this page still works — press Enter to try again.',
155
+ explorerHeading: 'Browse collections',
156
+ explorerIndex: (collection) => `All ${collection} notes`,
157
+ explorerBounded: (shown, total) => `Showing ${shown} of ${total}.`,
158
+ explorerExpand: 'See every published note →',
159
+ explorerExpandLabel: 'See every published note, including those this rail does not list',
160
+ uncollected: 'Uncollected',
161
+ readNote: 'Read note →',
162
+ noNotesYet: 'No notes are published yet.',
163
+ noteCount: (count) => `${count} ${count === 1 ? 'note' : 'notes'}`,
164
+ homeDescription: 'A collection of notes, published as static pages.',
165
+ homeEyebrow: 'NOTES',
166
+ homeHeading: 'Notes, in the open.',
167
+ homeIntro: 'Written as plain Markdown and published as static pages. Notes link to each other, and every ' +
168
+ 'link that appears here leads to a note that is published.',
169
+ homeNotesHeading: 'Notes',
170
+ publishedCount: (count) => `${count} published`,
171
+ recentTitle: 'Recent',
172
+ recentDescription: 'Published notes in order of when they last changed.',
173
+ recentOrderedByDate: 'Ordered by when each note was last updated, most recent first. Notes with no recorded date ' +
174
+ 'follow, in alphabetical order by slug.',
175
+ recentOrderedBySlug: 'These notes carry no publication dates, so they are listed in alphabetical order by slug. ' +
176
+ 'This page will order by update date as soon as any note records one.',
177
+ tagsTitle: 'Tags',
178
+ tagsDescription: 'Every tag on a published note.',
179
+ tagCount: (count) => `${count} ${count === 1 ? 'tag' : 'tags'}`,
180
+ tagsEmpty: 'No published note carries a tag yet. This page lists every tag as soon as one does.',
181
+ tagBrowseHeading: 'Browse this tag here',
182
+ tagBrowseStart: 'Load notes from the live index',
183
+ tagBrowseSelectLabel: 'Tag to browse',
184
+ tagBrowsePrompt: 'Choose a tag\u2026',
185
+ tagBrowseMore: 'Load more',
186
+ tagBrowseLoading: 'Loading notes\u2026',
187
+ tagBrowseEmpty: 'No published note carries this tag in the live index.',
188
+ tagBrowseExhausted: 'All matching notes are shown.',
189
+ tagBrowseUnknown: 'That tag is not in the live index.',
190
+ tagBrowseFailed: 'The live index could not be loaded. The static list above is complete.',
191
+ collectionsTitle: 'Collections',
192
+ collectionsDescription: 'The collections published notes are grouped into.',
193
+ collectionCount: (count) => `${count} ${count === 1 ? 'collection' : 'collections'}`,
194
+ collectionsEmpty: 'No published note belongs to a collection yet, so every note lives in one flat namespace. ' +
195
+ 'This page lists each collection as soon as one does.',
196
+ collectionsNote: 'Collections group notes for reading. They are a property of what is published here and do ' +
197
+ 'not reflect how anything is organized elsewhere.',
198
+ tagPageTitle: (tag) => `Tag: ${tag}`,
199
+ tagPageDescription: (tag) => `Published notes tagged ${tag}.`,
200
+ allTags: '← All tags',
201
+ collectionPageTitle: (collection) => `Collection: ${collection}`,
202
+ collectionPageDescription: (collection) => `Published notes in the ${collection} collection.`,
203
+ allCollections: '← All collections',
204
+ breadcrumbLabel: 'Breadcrumb',
205
+ metaPublished: 'Published',
206
+ metaUpdated: 'Updated',
207
+ metaCollection: 'Collection',
208
+ metaTags: 'Tags',
209
+ metaAliases: 'Also known as',
210
+ tocHeading: 'On this page',
211
+ headingAnchorLabel: (heading) => `Link to section: ${heading}`,
212
+ codeCopy: 'Copy code',
213
+ codeCopied: 'Copied',
214
+ codeCopyFailed: 'Copy failed',
215
+ footnotesHeading: 'Footnotes',
216
+ footnoteBackLabel: (reference) => `Back to reference ${reference}`,
217
+ diagramCaption: (kind) => `${kind} diagram`,
218
+ outgoingHeading: 'Links to',
219
+ outgoingEmpty: 'This note links to no other published note.',
220
+ backlinksHeading: 'Linked from',
221
+ backlinksEmpty: 'No published note links here yet.',
222
+ relatedHeading: 'Related notes',
223
+ relatedDerivation: 'Suggested by shared tags, the tag grouping fewest notes first. Notes already listed above ' +
224
+ 'are excluded.',
225
+ relatedNoTagPeer: 'No other published note shares a tag with this one.',
226
+ relatedAlreadyListed: 'Every note sharing a tag with this one is already listed above.',
227
+ moreInCollection: (collection) => `More in the ${collection} collection`,
228
+ pagerPrevious: 'Previous',
229
+ pagerNext: 'Next',
230
+ noteProvenance: 'Published from a note written in Markdown, and served as a static page. Links here lead only ' +
231
+ 'to notes that are published.',
232
+ canonicalUrlLine: (url) => `Canonical URL: ${url}`,
233
+ graphHeading: 'Nearby notes',
234
+ graphTitle: 'Graph',
235
+ graphDescription: 'How the published notes link to each other.',
236
+ graphLocalDerivation: 'Every note one link away from this one, drawn with the links between them. Laid out when the ' +
237
+ 'site was built, so it needs no scripting.',
238
+ graphGlobalDerivation: 'The most linked notes on the site, most connected at the centre. Laid out when the site was ' +
239
+ 'built, so it needs no scripting.',
240
+ graphFigureLabel: (nodes, edges) => `Link graph: ${nodes} ${nodes === 1 ? 'note' : 'notes'}, ${edges} ${edges === 1 ? 'link' : 'links'}`,
241
+ graphNodeLabel: (title, relation, degree) => `${title} — ${relation}, ${degree} ${degree === 1 ? 'link' : 'links'} drawn`,
242
+ graphSubjectRelation: 'this note',
243
+ graphOutgoingRelation: 'linked from this note',
244
+ graphIncomingRelation: 'links to this note',
245
+ graphMutualRelation: 'linked both ways',
246
+ graphLinkedRelation: 'published note',
247
+ graphExplorerOpen: 'Explore interactively',
248
+ graphExplorerReset: 'Reset',
249
+ graphExplorerRecenter: 'Re-center',
250
+ graphExplorerRecenterLabel: 'Re-center on {title}',
251
+ graphExplorerFilterLabel: 'Tag',
252
+ graphExplorerFilterAll: 'All notes',
253
+ graphExplorerStatusLocal: 'Live graph: {shown} of {total} neighbouring notes drawn.',
254
+ graphExplorerStatusGlobal: 'Live graph: {shown} of {total} published notes drawn.',
255
+ graphExplorerStatusFiltered: 'Live graph for tag {tag}: {shown} of {total} matching notes drawn.',
256
+ graphExplorerLoading: 'Loading the live graph…',
257
+ graphExplorerFailed: 'The live graph could not be loaded. The static figure above is complete.',
258
+ graphExplorerUnknownCenter: 'That note is not in the live index.',
259
+ graphExplorerEmptyFilter: 'No published note carries that tag.',
260
+ graphExplorerNodeLabel: '{title} — {relation}, {degree} links drawn',
261
+ graphExplorerNodeLabelOne: '{title} — {relation}, {degree} link drawn',
262
+ graphTableCaption: 'The same notes and links, as a table',
263
+ graphColumnNote: 'Note',
264
+ graphColumnRelation: 'Relationship',
265
+ graphColumnLinks: 'Links drawn',
266
+ graphColumnLinkedTo: 'Linked to',
267
+ graphNoLinks: 'Nothing else drawn here',
268
+ graphBoundedLocal: (shown, total) => `Drawing ${shown} of ${total} neighbouring notes, in alphabetical order. The lists above name ` +
269
+ 'every one of them.',
270
+ graphBounded: (shown, total) => `Drawing ${shown} of ${total} notes, the most linked first.`,
271
+ graphExpandLocal: 'See the whole graph →',
272
+ graphExpandLocalLabel: 'See the whole graph of every published note',
273
+ graphExpand: 'See every published note →',
274
+ graphExpandLabel: 'See every published note, including those this graph does not draw',
275
+ graphEmpty: 'No published note links to this one and it links to none, so there is no neighbourhood to draw.',
276
+ graphSiteEmpty: 'No two published notes link to each other yet, so there is no graph to draw. This page fills ' +
277
+ 'in as more notes link to each other.',
278
+ notFoundTitle: 'Page not found',
279
+ notFoundDescription: 'That page is not part of this site.',
280
+ notFoundBody: 'That address does not match a published page. It may have been withdrawn, moved, or never ' +
281
+ 'have existed here. Use one of the current destinations below.',
282
+ notFoundWhereTo: 'Where to go instead',
283
+ withheldTitle: 'Not published',
284
+ withheldDescription: 'The note this link points at is not part of this site.',
285
+ withheldBody: 'A published note links here, and the note it names was not published. Its text, its title, ' +
286
+ 'and everything else about it are absent from this site — the link you followed is the only ' +
287
+ 'trace of it here, and the words in that link were written by the author of the note you came ' +
288
+ 'from.',
289
+ withheldPermanence: 'This is not a page that fills in later. Nothing about the excluded note is scheduled for ' +
290
+ 'publication, and this address will say the same thing whenever you return to it.',
291
+ };
292
+ /**
293
+ * Simplified Chinese.
294
+ *
295
+ * Three things differ from a transliteration of the English, and each is a
296
+ * property of the language rather than a stylistic choice:
297
+ *
298
+ * 1. **No plural.** `noteCount`, `tagCount`, `collectionCount`, and
299
+ * `publishedCount` take a measure word and are invariant in number. Applying
300
+ * English's `count === 1` rule here would produce a form the language does
301
+ * not have.
302
+ * 2. **Full-width punctuation.** `:`, `,`, and `。` are the Chinese forms; the
303
+ * ASCII ones read as a typographic error. The em dash in `searchFailed`
304
+ * becomes a comma, because a Chinese sentence does not use one for that
305
+ * clause boundary.
306
+ * 3. **Modifier before head.** `explorerIndex` and `moreInCollection` place the
307
+ * collection name before the noun with `的` rather than after it, so the
308
+ * interpolated value cannot be assembled by the same template as English.
309
+ */
310
+ const ZH_CN = {
311
+ skipToContent: '跳到正文',
312
+ primaryNavLabel: '主导航',
313
+ siteMapLabel: '站点地图',
314
+ siteSubtitle: '笔记,以静态站点的形式发布。',
315
+ siteDescription: '一组以静态页面形式发布的笔记。',
316
+ publishedAt: (url) => `本页地址:${url}`,
317
+ feedTitle: (siteName) => `${siteName} — 全部笔记`,
318
+ socialCardAlt: (siteName) => `${siteName} — 一组笔记`,
319
+ navHome: '首页',
320
+ navNotes: '笔记',
321
+ navRecent: '最近更新',
322
+ navTags: '标签',
323
+ navCollections: '合集',
324
+ navGraph: '关系图',
325
+ navAbout: '关于',
326
+ navPrivacy: '隐私',
327
+ searchToggle: '搜索',
328
+ searchToggleLabel: '搜索本站(按斜杠键)',
329
+ themeLabel: (theme) => `主题:${{ system: '跟随系统', light: '浅色', dark: '深色' }[theme]}`,
330
+ readerToggle: '阅读模式',
331
+ searchDialogLabel: '搜索',
332
+ searchClose: '关闭搜索',
333
+ searchFieldLabel: '搜索本站',
334
+ searchIdle: '输入内容以搜索本站。',
335
+ searchLoading: '正在加载搜索索引…',
336
+ searchEmpty: '没有匹配的结果,换一个词试试。',
337
+ searchFailed: '搜索索引加载失败。本页其余内容仍可正常使用,按回车键重试。',
338
+ explorerHeading: '浏览合集',
339
+ explorerIndex: (collection) => `${collection} 的全部笔记`,
340
+ explorerBounded: (shown, total) => `共 ${total} 篇,此处列出 ${shown} 篇。`,
341
+ explorerExpand: '查看全部公开笔记 →',
342
+ explorerExpandLabel: '查看全部公开笔记,包括本导航栏未列出的部分',
343
+ uncollected: '未归入合集',
344
+ readNote: '阅读全文 →',
345
+ noNotesYet: '目前还没有公开的笔记。',
346
+ noteCount: (count) => `${count} 篇笔记`,
347
+ homeDescription: '一组以静态页面形式发布的笔记。',
348
+ homeEyebrow: '笔记',
349
+ homeHeading: '公开的笔记。',
350
+ homeIntro: '以纯 Markdown 写成,并以静态页面发布。笔记之间相互链接,站内出现的每一个链接都指向' +
351
+ '一篇已公开的笔记。',
352
+ homeNotesHeading: '笔记',
353
+ publishedCount: (count) => `已公开 ${count} 篇`,
354
+ recentTitle: '最近更新',
355
+ recentDescription: '按最后修改时间排列的公开笔记。',
356
+ recentOrderedByDate: '按每篇笔记最后更新的时间排列,最新的在前。没有记录日期的笔记排在其后,' +
357
+ '并按 slug 的字母顺序排列。',
358
+ recentOrderedBySlug: '这些笔记没有记录发布日期,因此按 slug 的字母顺序排列。' +
359
+ '一旦有笔记记录了日期,本页将改为按更新时间排序。',
360
+ tagsTitle: '标签',
361
+ tagsDescription: '公开笔记上的全部标签。',
362
+ tagCount: (count) => `${count} 个标签`,
363
+ tagsEmpty: '目前还没有公开笔记带有标签。一旦有笔记带上标签,本页将逐个列出。',
364
+ tagBrowseHeading: '在此浏览这个标签',
365
+ tagBrowseStart: '从实时索引加载笔记',
366
+ tagBrowseSelectLabel: '要浏览的标签',
367
+ tagBrowsePrompt: '选择一个标签\u2026',
368
+ tagBrowseMore: '加载更多',
369
+ tagBrowseLoading: '正在加载笔记\u2026',
370
+ tagBrowseEmpty: '实时索引中没有公开笔记带有这个标签。',
371
+ tagBrowseExhausted: '已显示全部匹配的笔记。',
372
+ tagBrowseUnknown: '实时索引中没有这个标签。',
373
+ tagBrowseFailed: '实时索引加载失败;上方静态列表仍然完整。',
374
+ collectionsTitle: '合集',
375
+ collectionsDescription: '公开笔记所归入的合集。',
376
+ collectionCount: (count) => `${count} 个合集`,
377
+ collectionsEmpty: '目前还没有公开笔记归入合集,因此所有笔记都位于同一个扁平命名空间中。' +
378
+ '一旦有笔记归入合集,本页将逐个列出。',
379
+ collectionsNote: '合集用于组织阅读。它们只反映本站公开内容的归类方式,并不反映其他地方的组织方式。',
380
+ tagPageTitle: (tag) => `标签:${tag}`,
381
+ tagPageDescription: (tag) => `带有 ${tag} 标签的公开笔记。`,
382
+ allTags: '← 全部标签',
383
+ collectionPageTitle: (collection) => `合集:${collection}`,
384
+ collectionPageDescription: (collection) => `${collection} 合集中的公开笔记。`,
385
+ allCollections: '← 全部合集',
386
+ breadcrumbLabel: '面包屑导航',
387
+ metaPublished: '发布于',
388
+ metaUpdated: '更新于',
389
+ metaCollection: '所属合集',
390
+ metaTags: '标签',
391
+ metaAliases: '别名',
392
+ tocHeading: '本页目录',
393
+ headingAnchorLabel: (heading) => `跳转到章节:${heading}`,
394
+ codeCopy: '复制代码',
395
+ codeCopied: '已复制',
396
+ codeCopyFailed: '复制失败',
397
+ footnotesHeading: '脚注',
398
+ footnoteBackLabel: (reference) => `返回正文引用 ${reference}`,
399
+ diagramCaption: (kind) => `${kind} 图示`,
400
+ outgoingHeading: '链出笔记',
401
+ outgoingEmpty: '这篇笔记没有链接到其他公开笔记。',
402
+ backlinksHeading: '链入笔记',
403
+ backlinksEmpty: '目前还没有公开笔记链接到这里。',
404
+ relatedHeading: '相关笔记',
405
+ relatedDerivation: '依据共同标签推荐,标签所含笔记最少的优先。上文已列出的笔记不再重复。',
406
+ relatedNoTagPeer: '没有其他公开笔记与这篇共享标签。',
407
+ relatedAlreadyListed: '与这篇共享标签的笔记都已在上文列出。',
408
+ moreInCollection: (collection) => `${collection} 合集中的更多笔记`,
409
+ pagerPrevious: '上一篇',
410
+ pagerNext: '下一篇',
411
+ noteProvenance: '本页由一篇以 Markdown 写成的笔记发布,并以静态页面提供。页内的链接只会指向' +
412
+ '已公开的笔记。',
413
+ canonicalUrlLine: (url) => `规范链接:${url}`,
414
+ graphHeading: '相邻笔记',
415
+ graphTitle: '关系图',
416
+ graphDescription: '公开笔记之间的链接关系。',
417
+ graphLocalDerivation: '与这篇笔记相隔一条链接的全部笔记,并画出它们彼此之间的链接。' + '布局在构建站点时完成,因此无需脚本。',
418
+ graphGlobalDerivation: '站内链接最多的笔记,连接越多越靠近中心。布局在构建站点时完成,因此无需脚本。',
419
+ graphFigureLabel: (nodes, edges) => `链接关系图:${nodes} 篇笔记,${edges} 条链接`,
420
+ graphNodeLabel: (title, relation, degree) => `${title} — ${relation},图中有 ${degree} 条链接`,
421
+ graphSubjectRelation: '本篇',
422
+ graphOutgoingRelation: '本篇链接到它',
423
+ graphIncomingRelation: '它链接到本篇',
424
+ graphMutualRelation: '互相链接',
425
+ graphLinkedRelation: '公开笔记',
426
+ graphExplorerOpen: '交互探索',
427
+ graphExplorerReset: '重置',
428
+ graphExplorerRecenter: '以此为中心',
429
+ graphExplorerRecenterLabel: '{title},以此为中心',
430
+ graphExplorerFilterLabel: '标签',
431
+ graphExplorerFilterAll: '全部笔记',
432
+ graphExplorerStatusLocal: '实时图:已画出 {total} 篇相邻笔记中的 {shown} 篇。',
433
+ graphExplorerStatusGlobal: '实时图:已画出 {total} 篇公开笔记中的 {shown} 篇。',
434
+ graphExplorerStatusFiltered: '标签 {tag} 的实时图:已画出 {total} 篇匹配笔记中的 {shown} 篇。',
435
+ graphExplorerLoading: '正在加载实时图…',
436
+ graphExplorerFailed: '实时图加载失败;上方静态图仍然完整。',
437
+ graphExplorerUnknownCenter: '实时索引中没有这篇笔记。',
438
+ graphExplorerEmptyFilter: '没有公开笔记带有这个标签。',
439
+ graphExplorerNodeLabel: '{title} — {relation},已绘制 {degree} 条连线',
440
+ graphExplorerNodeLabelOne: '{title} — {relation},已绘制 {degree} 条连线',
441
+ graphTableCaption: '同样的笔记与链接,以表格呈现',
442
+ graphColumnNote: '笔记',
443
+ graphColumnRelation: '关系',
444
+ graphColumnLinks: '图中链接数',
445
+ graphColumnLinkedTo: '链接到',
446
+ graphNoLinks: '图中没有与它相连的笔记',
447
+ graphBoundedLocal: (shown, total) => `共有 ${total} 篇相邻笔记,此处按字母顺序画出其中 ${shown} 篇。上文的列表已列出全部。`,
448
+ graphBounded: (shown, total) => `共 ${total} 篇笔记,此处画出链接最多的 ${shown} 篇。`,
449
+ graphExpandLocal: '查看整站关系图 →',
450
+ graphExpandLocalLabel: '查看整站关系图,包含全部公开笔记',
451
+ graphExpand: '查看全部公开笔记 →',
452
+ graphExpandLabel: '查看全部公开笔记,包括本图未画出的部分',
453
+ graphEmpty: '没有公开笔记链接到这篇,这篇也没有链接到其他笔记,因此没有可绘制的相邻关系。',
454
+ graphSiteEmpty: '目前还没有两篇公开笔记互相链接,因此没有可绘制的关系图。随着笔记之间的链接增多,本页会逐步充实。',
455
+ notFoundTitle: '页面未找到',
456
+ notFoundDescription: '该页面不属于本站。',
457
+ notFoundBody: '该地址不对应任何已公开的页面。它可能已被撤回、迁移,或从未存在于此。请从下方选择当前可用的页面。',
458
+ notFoundWhereTo: '可以去这些地方',
459
+ withheldTitle: '未公开',
460
+ withheldDescription: '此链接指向的笔记不属于本站。',
461
+ withheldBody: '一篇已公开的笔记链接到了这里,而它所指向的那篇笔记并未公开。它的正文、标题以及其余内容' +
462
+ '都不在本站之中 — 你刚才点击的那个链接是它在此处留下的唯一痕迹,而链接里的文字出自你' +
463
+ '来时那篇笔记的作者之手。',
464
+ withheldPermanence: '本页不会在日后补上内容。那篇未公开的笔记没有任何发布安排,无论何时回到这个地址,' +
465
+ '它都会是同样的说明。',
466
+ };
467
+ /**
468
+ * The locale each tag resolves to, keyed by the lowercased tag.
469
+ *
470
+ * The bare `zh` key is **not** redundant with the `zh-cn` one, and removing it
471
+ * was tried: {@link translate} falls back to the *primary subtag*, so `zh-TW`,
472
+ * `zh-Hant`, and `zh-Hans-CN` all reduce to `zh` — which without a key of its
473
+ * own falls through to English, putting English chrome around a Chinese
474
+ * document. That is the exact defect this ticket exists to close, reached from a
475
+ * schema-valid artifact.
476
+ *
477
+ * What it means is that a Traditional-script document renders Simplified chrome.
478
+ * A known limit, stated rather than hidden: this tool ships two locales, and
479
+ * Simplified chrome around a Traditional document is a closer answer than
480
+ * English. A Traditional locale is one more entry here and nothing else.
481
+ */
482
+ const LOCALES = {
483
+ en: EN,
484
+ 'zh-cn': ZH_CN,
485
+ zh: ZH_CN,
486
+ // ISO 639-2/T. `src/lib/schema.ts` admits a two- *or three*-letter primary
487
+ // subtag, so `zho` is a valid artifact spelling of the same language `zh`
488
+ // names, and without this key it resolved to English chrome on a Chinese
489
+ // document — the exact defect this ticket closes, reachable from a
490
+ // schema-valid artifact. `cmn` and `yue` are deliberately absent: they name
491
+ // Mandarin and Cantonese specifically, and mapping either onto this Simplified
492
+ // locale would be a guess about script rather than a spelling of a tag this
493
+ // site already publishes.
494
+ zho: ZH_CN,
495
+ };
496
+ /**
497
+ * The chrome for one document, from its own `language` field.
498
+ *
499
+ * Resolution is the exact-tag-then-primary-subtag order `search-dialog.ts`
500
+ * already uses for Pagefind's index partitions, so the two agree about what
501
+ * `zh-Hans-CN` is: the full lowercased tag first, then the primary subtag. A
502
+ * document declaring a language this site has no locale for — `fr`, or the
503
+ * `zh-TW` a traditional-script note would carry — resolves to the closest
504
+ * locale it does have rather than to a key name, and `undefined` resolves to
505
+ * {@link NAV_LANGUAGE}, which is the published corpus's own case.
506
+ */
507
+ export function translate(language) {
508
+ const tag = (language ?? NAV_LANGUAGE).toLowerCase();
509
+ return LOCALES[tag] ?? LOCALES[tag.split('-')[0]] ?? EN;
510
+ }
511
+ /**
512
+ * The `lang` attribute a listed title needs, or `undefined` when it needs none.
513
+ *
514
+ * WCAG 2.2 AA success criterion 3.1.2 is about *parts* of a page, and a list of
515
+ * notes is exactly that case: an English home page listing four Chinese titles
516
+ * is read out in an English voice by a screen reader unless each of those titles
517
+ * says what it is. The attribute is emitted only when the entry's language
518
+ * differs from the page's, so an all-English list carries no redundant markup.
519
+ *
520
+ * `entryLanguage` is the artifact's optional field, so an entry declaring none
521
+ * is treated as the navigation language — the same fallback {@link translate}
522
+ * applies, and for the same reason.
523
+ */
524
+ export function partLanguage(entryLanguage, pageLanguage) {
525
+ const own = entryLanguage ?? NAV_LANGUAGE;
526
+ // Compared case-insensitively, as BCP 47 says language tags are: `zh-CN` and
527
+ // `zh-cn` name one language, so an artifact that spells one entry's tag
528
+ // differently from another's must not make the two look foreign to each other
529
+ // and put a redundant `lang` on every title. `translate` lowercases for the
530
+ // same reason, and the two must agree or a page resolves Chinese chrome while
531
+ // marking its own Chinese titles as foreign.
532
+ return own.toLowerCase() === pageLanguage.toLowerCase() ? undefined : own;
533
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The pinned SQLite WASM runtime's public identity.
3
+ *
4
+ * The browser loads the official `@sqlite.org/sqlite-wasm` entry same-origin from
5
+ * `/wasm/`, and its WASM binary under a digest-named file the Worker verifies
6
+ * against the URL it bound. This module is pure data and string helpers so the
7
+ * producer, the output inventory, the scanners, and the Worker share one
8
+ * spelling of every name.
9
+ */
10
+ import { isHexDigest } from "./snapshot.js";
11
+ /** Directory segment the SQLite runtime lives under. `wasm` is a reserved slug. */
12
+ export const WASM_DIRECTORY = 'wasm';
13
+ /** The package's browser entry, copied under a stable public name. */
14
+ export const WASM_MODULE_NAME = 'sqlite-wasm.js';
15
+ /** Same-origin URL of the browser entry the Worker imports at runtime. */
16
+ export function wasmModuleUrl() {
17
+ return `/${WASM_DIRECTORY}/${WASM_MODULE_NAME}`;
18
+ }
19
+ /** The digest-named WASM member name, without a directory. */
20
+ export function wasmMemberName(digest) {
21
+ if (!isHexDigest(digest))
22
+ throw new Error(`wasm digest is not 64 lowercase hex: ${digest}`);
23
+ return `sqlite3.${digest}.wasm`;
24
+ }
25
+ /** Same-origin URL of the WASM named by its full digest. */
26
+ export function wasmRoute(digest) {
27
+ return `/${WASM_DIRECTORY}/${wasmMemberName(digest)}`;
28
+ }
29
+ /** Validate an untrusted runtime binding. */
30
+ export function readWasmBinding(value) {
31
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
32
+ return undefined;
33
+ const record = value;
34
+ const moduleUrl = record['moduleUrl'];
35
+ const url = record['url'];
36
+ const digest = record['digest'];
37
+ const members = record['members'];
38
+ if (moduleUrl !== wasmModuleUrl())
39
+ return undefined;
40
+ if (typeof url !== 'string' || typeof digest !== 'string' || !isHexDigest(digest))
41
+ return undefined;
42
+ if (url !== wasmRoute(digest))
43
+ return undefined;
44
+ if (!Array.isArray(members) || members.some((member) => typeof member !== 'string'))
45
+ return undefined;
46
+ return { moduleUrl, url, digest, members: [...members] };
47
+ }