From fc8633478d1fe27f85f0d305820a29966a2277af Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 23 Jul 2026 14:42:23 +0800 Subject: [PATCH] refactor: deepen four modules surfaced by the architecture review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acting on the candidates from /improve-codebase-architecture. Four refactors, each independently motivated but cohesive as a batch. == 1. Collapse the easter-egg scattering (the big one) == FingerprintPlayground.vue was 724 lines with each egg smeared across 6 locations: type union, EGGS array, EGG_DURATIONS, eggBackground switch, v-else-if template branch, and CSS block. Adding an egg meant editing all 6. Each egg is now a self-contained Vue SFC under src/components/vue/fingerprint/eggs/. The playground imports a single EGG_REGISTRY and renders the active egg via . Adding an egg is now: create one .vue file + append one entry to registry.ts. src/components/vue/FingerprintPlayground.vue 724 → 247 lines src/components/vue/fingerprint/types.ts new (EggDef, EggProps) src/components/vue/fingerprint/registry.ts new (15 entries) src/components/vue/fingerprint/eggs/*.vue new (15 files) Each egg owns its template, script (including any reactive state — matrix tick, OTP XOR, Enigma rotors), style, and prefers-reduced-motion rule. The engine stays tiny: hash, watch input, dispatch events, render the active egg component. == 2. Surface the implicit rnp:* event bus == Five custom events were referenced as string literals at ~12 sites across SiteHeader, SiteSearch, FingerprintPlayground, EasterEggs, and HeroDecrypt. No central registry; rename was unsafe. src/lib/events.ts now exports RNP_EVENTS (and RNP_STORAGE for the session key) as `as const` maps plus a dispatch() helper. All senders and listeners import the same symbol; rename = one edit; typo = compile error. == 3. Deepen the vendor-source concept == RNP_VERSION was a hand-synced constant in content.config.ts that the comment warned to "bump together with docs_ref". The man-page paths were duplicated between fetch-sources.mjs (EXTRA_PATHS) and content.config.ts (include regex). src/lib/vendor-source.mjs now owns: - RNP_RELEASE_TAG (read from docs_ref in software/rnp.md) - RNP_VERSION (derived from the tag, stripping the leading 'v') - RNP_MAN_PAGES + RNP_EXTRA_SPARSE (shared sparse-checkout paths) content.config.ts and fetch-sources.mjs both import. The hand-sync is gone — bump docs_ref and RNP_VERSION follows. src/lib/frontmatter.mjs extracts the YAML-front-matter parser shared between asciidoc.ts (TS) and fetch-sources.mjs (Node). Kept in .mjs so the standalone script can import it without a TS compiler. == 4. Delete the duplicated helpers == decodeEntities was inlined verbatim inside renderAdoc (asciidoc.ts); docSort was duplicated verbatim in two software route files. Both pass the deletion test as pass-throughs. - renderAdoc now calls the existing decodeEntities function - docSort moves to src/lib/docs.ts; both routes import == Skipped: front-matter parsing seam == The original review claimed fetch-sources.mjs and asciidoc.ts both parse YAML front matter. Verifying on disk: fetch-sources.mjs doesn't parse front matter itself — it sparse-clones repos and reads each product's front matter via the shared parser now in frontmatter.mjs (already done above as part of #3). The "seam" never existed as described; nothing to do here. == Verification == - `npm run build` clean (no errors, no warnings) - HTTP 200 on /, /blog/, /software/rnp/, /advisories/ - node -e import vendor-source.mjs → RNP_VERSION '0.18.1' - dev server HMR picked up all new files; egg files all 200 EOF --- scripts/fetch-sources.mjs | 15 +- src/components/vue/EasterEggs.vue | 21 +- src/components/vue/FingerprintPlayground.vue | 599 ++---------------- src/components/vue/HeroDecrypt.vue | 9 +- src/components/vue/SiteHeader.vue | 3 +- src/components/vue/SiteSearch.vue | 5 +- .../vue/fingerprint/eggs/ArmorEgg.vue | 13 + .../vue/fingerprint/eggs/DaveEgg.vue | 10 + src/components/vue/fingerprint/eggs/DhEgg.vue | 38 ++ .../vue/fingerprint/eggs/EnigmaEgg.vue | 65 ++ .../vue/fingerprint/eggs/HalEgg.vue | 31 + .../vue/fingerprint/eggs/LibrepgpEgg.vue | 26 + .../vue/fingerprint/eggs/MatrixEgg.vue | 52 ++ .../vue/fingerprint/eggs/OtpEgg.vue | 48 ++ .../vue/fingerprint/eggs/PanicEgg.vue | 27 + .../vue/fingerprint/eggs/PhilEgg.vue | 53 ++ .../vue/fingerprint/eggs/PqcEgg.vue | 31 + .../vue/fingerprint/eggs/PrivacyEgg.vue | 14 + .../vue/fingerprint/eggs/RedactedEgg.vue | 35 + .../vue/fingerprint/eggs/RiboseEgg.vue | 12 + .../vue/fingerprint/eggs/ThunderbirdEgg.vue | 39 ++ src/components/vue/fingerprint/registry.ts | 133 ++++ src/components/vue/fingerprint/types.ts | 39 ++ src/content.config.ts | 7 +- src/lib/asciidoc.ts | 24 +- src/lib/docs.ts | 9 + src/lib/events.ts | 31 + src/lib/frontmatter.mjs | 21 + src/lib/vendor-source.mjs | 31 + src/pages/software/[id].astro | 11 +- .../software/[product]/docs/[...slug].astro | 11 +- 31 files changed, 853 insertions(+), 610 deletions(-) create mode 100644 src/components/vue/fingerprint/eggs/ArmorEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/DaveEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/DhEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/EnigmaEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/HalEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/LibrepgpEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/MatrixEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/OtpEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/PanicEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/PhilEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/PqcEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/PrivacyEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/RedactedEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/RiboseEgg.vue create mode 100644 src/components/vue/fingerprint/eggs/ThunderbirdEgg.vue create mode 100644 src/components/vue/fingerprint/registry.ts create mode 100644 src/components/vue/fingerprint/types.ts create mode 100644 src/lib/docs.ts create mode 100644 src/lib/events.ts create mode 100644 src/lib/frontmatter.mjs create mode 100644 src/lib/vendor-source.mjs diff --git a/scripts/fetch-sources.mjs b/scripts/fetch-sources.mjs index c8156fb..5e45f87 100644 --- a/scripts/fetch-sources.mjs +++ b/scripts/fetch-sources.mjs @@ -12,11 +12,12 @@ * Existing vendor clones are reused (delete vendor/ to force a refresh). * Network failures are warnings, not errors — docs pages degrade gracefully. */ -import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'; +import { existsSync, mkdirSync, readdirSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; +import { parseFrontmatter } from '../src/lib/frontmatter.mjs'; +import { RNP_EXTRA_SPARSE } from '../src/lib/vendor-source.mjs'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const vendorDir = join(root, 'vendor'); @@ -24,15 +25,9 @@ const softwareDir = join(root, 'src/content/software'); /** Extra files to check out per product, gitignore-style sparse patterns. */ const EXTRA_PATHS = { - rnp: ['/src/rnp/rnp.1.adoc', '/src/rnpkeys/rnpkeys.1.adoc', '/src/lib/librnp.3.adoc'], + rnp: RNP_EXTRA_SPARSE, }; -function frontMatter(file) { - const raw = readFileSync(file, 'utf8'); - const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); - return m ? (parseYaml(m[1]) ?? {}) : {}; -} - function run(args, cwd) { execFileSync('git', args, { cwd, stdio: 'pipe' }); } @@ -40,7 +35,7 @@ function run(args, cwd) { let failures = 0; for (const file of readdirSync(softwareDir).filter((f) => f.endsWith('.md'))) { - const fm = frontMatter(join(softwareDir, file)); + const fm = parseFrontmatter(join(softwareDir, file)).frontMatter; if (!fm.docs_repo) continue; const name = file.replace(/\.md$/, ''); diff --git a/src/components/vue/EasterEggs.vue b/src/components/vue/EasterEggs.vue index d73c48e..0a54c49 100644 --- a/src/components/vue/EasterEggs.vue +++ b/src/components/vue/EasterEggs.vue @@ -1,5 +1,6 @@ @@ -333,183 +186,14 @@ onUnmounted(() => { :class="eggBackground" aria-live="polite" > - -
- {{ g }}{{ i < matrixGroups.length - 1 ? ' ' : '' }} -
- - -
-
- {{ g }}{{ i < redactedGroups.length - 1 ? ' ' : '' }} -
-
- Classified -
-
- - -
-

- DON'T PANIC -

-

- — in big, friendly letters on its cover -

-
- - -
-
- - Powered by Thunderbird's wings. -
-
- - -
-
-----BEGIN PGP MESSAGE-----
-
- {{ hex.slice(0, 48) }}
{{ hex.slice(48, 96) }}
{{ hex.slice(96) }} -
-
-----END PGP MESSAGE-----
-
- - -
-
-
-

I'M SORRY, DAVE.

-

I'm afraid I can't do that.

-
-
- - -
-

> Open the pod bay doors, HAL.

-

— transmission pending —

-
- - -
-

- Harvest now. -

-

- Decrypt later. -

-

— the post-quantum adversary is patient

-
- - -
-

LibrePGP

-

— the open OpenPGP specification

-

- brought to you by g10 · Intevation · Ribose -

-
- - -
-

- "Open source is a development methodology; free software is a social movement." -

-

— Ribose · the people behind RNP

-
- - -
-

- "Privacy is the power to selectively reveal oneself to the world." -

-

- — Eric Hughes · A Cypherpunk's Manifesto (1993) -

-
- - -
-
- - + - - - -
-

- Public exchange, private result. (Diffie–Hellman, 1976) -

-
- - -
-
-
- P - {{ otpData.plain }} -
-
- K - {{ otpData.key }} -
-
- C - {{ otpData.cipher }} -
-
-

- ⊕ — mathematically proven unbreakable. Use once. -

-
- - -
-
-
- {{ letter }} -
-
-

- ³-rotor cipher · Wehrmacht, 1934–1945 -

-
- - -
-
- Munition - ITAR · US Customs · 1993–1996 -
-

- PGP was a weapon. Zimmermann under investigation for 3 years. -

-
+ - diff --git a/src/components/vue/HeroDecrypt.vue b/src/components/vue/HeroDecrypt.vue index 1aab055..e0eb8c5 100644 --- a/src/components/vue/HeroDecrypt.vue +++ b/src/components/vue/HeroDecrypt.vue @@ -1,5 +1,6 @@ diff --git a/src/components/vue/SiteHeader.vue b/src/components/vue/SiteHeader.vue index a5e9dab..a4cfd73 100644 --- a/src/components/vue/SiteHeader.vue +++ b/src/components/vue/SiteHeader.vue @@ -1,6 +1,7 @@ diff --git a/src/components/vue/fingerprint/eggs/ArmorEgg.vue b/src/components/vue/fingerprint/eggs/ArmorEgg.vue new file mode 100644 index 0000000..770c444 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/ArmorEgg.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/components/vue/fingerprint/eggs/DaveEgg.vue b/src/components/vue/fingerprint/eggs/DaveEgg.vue new file mode 100644 index 0000000..ed6f86e --- /dev/null +++ b/src/components/vue/fingerprint/eggs/DaveEgg.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/components/vue/fingerprint/eggs/DhEgg.vue b/src/components/vue/fingerprint/eggs/DhEgg.vue new file mode 100644 index 0000000..5d63c93 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/DhEgg.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/EnigmaEgg.vue b/src/components/vue/fingerprint/eggs/EnigmaEgg.vue new file mode 100644 index 0000000..b8eeb4e --- /dev/null +++ b/src/components/vue/fingerprint/eggs/EnigmaEgg.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/HalEgg.vue b/src/components/vue/fingerprint/eggs/HalEgg.vue new file mode 100644 index 0000000..8c011c1 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/HalEgg.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/LibrepgpEgg.vue b/src/components/vue/fingerprint/eggs/LibrepgpEgg.vue new file mode 100644 index 0000000..acf20e1 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/LibrepgpEgg.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/MatrixEgg.vue b/src/components/vue/fingerprint/eggs/MatrixEgg.vue new file mode 100644 index 0000000..995fc9d --- /dev/null +++ b/src/components/vue/fingerprint/eggs/MatrixEgg.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/OtpEgg.vue b/src/components/vue/fingerprint/eggs/OtpEgg.vue new file mode 100644 index 0000000..aaf2bdb --- /dev/null +++ b/src/components/vue/fingerprint/eggs/OtpEgg.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/vue/fingerprint/eggs/PanicEgg.vue b/src/components/vue/fingerprint/eggs/PanicEgg.vue new file mode 100644 index 0000000..d3bcaed --- /dev/null +++ b/src/components/vue/fingerprint/eggs/PanicEgg.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/PhilEgg.vue b/src/components/vue/fingerprint/eggs/PhilEgg.vue new file mode 100644 index 0000000..db10c9b --- /dev/null +++ b/src/components/vue/fingerprint/eggs/PhilEgg.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/PqcEgg.vue b/src/components/vue/fingerprint/eggs/PqcEgg.vue new file mode 100644 index 0000000..cdcdaf2 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/PqcEgg.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/PrivacyEgg.vue b/src/components/vue/fingerprint/eggs/PrivacyEgg.vue new file mode 100644 index 0000000..acc477f --- /dev/null +++ b/src/components/vue/fingerprint/eggs/PrivacyEgg.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/components/vue/fingerprint/eggs/RedactedEgg.vue b/src/components/vue/fingerprint/eggs/RedactedEgg.vue new file mode 100644 index 0000000..a2eff32 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/RedactedEgg.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/components/vue/fingerprint/eggs/RiboseEgg.vue b/src/components/vue/fingerprint/eggs/RiboseEgg.vue new file mode 100644 index 0000000..5e38f89 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/RiboseEgg.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/vue/fingerprint/eggs/ThunderbirdEgg.vue b/src/components/vue/fingerprint/eggs/ThunderbirdEgg.vue new file mode 100644 index 0000000..dcdd215 --- /dev/null +++ b/src/components/vue/fingerprint/eggs/ThunderbirdEgg.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/components/vue/fingerprint/registry.ts b/src/components/vue/fingerprint/registry.ts new file mode 100644 index 0000000..541cb8d --- /dev/null +++ b/src/components/vue/fingerprint/registry.ts @@ -0,0 +1,133 @@ +import type { EggDef } from './types'; + +import MatrixEgg from './eggs/MatrixEgg.vue'; +import RedactedEgg from './eggs/RedactedEgg.vue'; +import PanicEgg from './eggs/PanicEgg.vue'; +import ThunderbirdEgg from './eggs/ThunderbirdEgg.vue'; +import ArmorEgg from './eggs/ArmorEgg.vue'; +import HalEgg from './eggs/HalEgg.vue'; +import DaveEgg from './eggs/DaveEgg.vue'; +import PqcEgg from './eggs/PqcEgg.vue'; +import LibrepgpEgg from './eggs/LibrepgpEgg.vue'; +import RiboseEgg from './eggs/RiboseEgg.vue'; +import PrivacyEgg from './eggs/PrivacyEgg.vue'; +import DhEgg from './eggs/DhEgg.vue'; +import OtpEgg from './eggs/OtpEgg.vue'; +import EnigmaEgg from './eggs/EnigmaEgg.vue'; +import PhilEgg from './eggs/PhilEgg.vue'; + +/** + * Each egg is one entry — trigger, duration, background, and the component that + * renders inside the fingerprint box. Add a new egg by creating `eggs/FooEgg.vue` + * and appending one entry here. + */ +export const EGG_REGISTRY: EggDef[] = [ + { + name: 'matrix', + trigger: /\b(matrix|neo|wake\s*up|mr\.?\s*anderson)\b/i, + duration: 5000, + background: 'border-emerald-500/40 bg-[#00150a]', + component: MatrixEgg, + }, + { + name: 'redacted', + trigger: /\b(snowden|nsa|prism|classified|top\s*secret|redact(ed)?)\b/i, + duration: 5000, + background: 'border-red-900/60 bg-black', + component: RedactedEgg, + }, + { + name: 'panic', + trigger: /\b(42|don'?t\s*panic|hitchhiker|arthur\s*dent)\b/i, + duration: 5000, + background: 'border-amber-500/50 bg-[#fdf6e3]', + component: PanicEgg, + }, + { + name: 'thunderbird', + trigger: /\bthunderbird\b/i, + duration: 5000, + background: 'border-line bg-surface-dim', + component: ThunderbirdEgg, + }, + { + name: 'armor', + trigger: /\b(pgp\s*message|ascii\s*armor|openpgp|4880)\b/i, + duration: 5000, + background: 'border-line bg-surface-dim', + component: ArmorEgg, + }, + { + name: 'hal', + trigger: /\b(hal(\s*9000)?|hal9000|9000|monolith|space\s*odyssey|2001)\b/i, + duration: 9000, + background: 'border-[#ff1744]/40 bg-[#1a0606]', + component: HalEgg, + sceneEvent: 'halScene', + }, + { + name: 'dave', + trigger: /\b(dave(\s*bowman)?|bowman|pod\s*bay\s*doors?)\b/i, + duration: 11000, + background: 'border-cyan-900/50 bg-[#040810]', + component: DaveEgg, + sceneEvent: 'daveScene', + }, + { + name: 'pqc', + trigger: /\b(pqc|post[-\s]*quantum|quantum|9980)\b/i, + duration: 5000, + background: 'border-cyan-500/40 bg-[#061a1f]', + component: PqcEgg, + }, + { + name: 'librepgp', + trigger: /\blibrepgp\b/i, + duration: 5000, + background: + 'border-gold/50 bg-[color-mix(in_oklab,var(--gold)_14%,var(--surface-dim))]', + component: LibrepgpEgg, + }, + { + name: 'ribose', + trigger: /\bribose\b/i, + duration: 5000, + background: 'border-line bg-surface-dim', + component: RiboseEgg, + }, + { + name: 'privacy', + trigger: /\bprivacy\b/i, + duration: 5000, + background: 'border-line bg-surface-dim', + component: PrivacyEgg, + }, + { + name: 'dh', + trigger: /\b(dh|diffie[\s-]*hellman)\b/i, + duration: 6000, + background: 'border-purple-500/40 bg-[#0e0420]', + component: DhEgg, + }, + { + name: 'otp', + trigger: /\b(otp|one[\s-]*time[\s-]*pad|vernam)\b/i, + duration: 6000, + background: 'border-emerald-500/40 bg-[#021810]', + component: OtpEgg, + }, + { + name: 'enigma', + trigger: /\benigma\b/i, + duration: 6000, + background: 'border-gray-500/40 bg-[#0a0a0c]', + component: EnigmaEgg, + }, + { + name: 'phil', + trigger: /\b(phil|zimmermann|munitions?)\b/i, + duration: 6000, + background: 'border-amber-700/40 bg-[#f5edd6]', + component: PhilEgg, + }, +]; diff --git a/src/components/vue/fingerprint/types.ts b/src/components/vue/fingerprint/types.ts new file mode 100644 index 0000000..2d53057 --- /dev/null +++ b/src/components/vue/fingerprint/types.ts @@ -0,0 +1,39 @@ +import type { Component } from 'vue'; + +export type EggName = + | 'matrix' + | 'redacted' + | 'panic' + | 'thunderbird' + | 'armor' + | 'hal' + | 'dave' + | 'pqc' + | 'librepgp' + | 'ribose' + | 'privacy' + | 'dh' + | 'otp' + | 'enigma' + | 'phil'; + +export interface EggProps { + /** Current input value (some eggs read it, e.g. otp). */ + input: string; + /** Current SHA-256 hex (some eggs read it, e.g. armor). */ + hex: string; +} + +export interface EggDef { + name: EggName; + /** Input value is tested against this regex to trigger the egg. */ + trigger: RegExp; + /** How long the egg stays active before auto-clearing (ms). */ + duration: number; + /** Tailwind classes applied to the fingerprint box while this egg is active. */ + background: string; + /** Vue component rendered inside the box. */ + component: Component; + /** If set, dispatch this event when the egg fires (screenwide scene). */ + sceneEvent?: 'halScene' | 'daveScene'; +} diff --git a/src/content.config.ts b/src/content.config.ts index 1a511d2..d0db697 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -2,12 +2,12 @@ import { defineCollection } from 'astro:content'; import { glob } from 'astro/loaders'; import { z } from 'astro/zod'; import { adocLoader } from './lib/asciidoc'; +import { RNP_VERSION, RNP_MAN_PAGES } from './lib/vendor-source.mjs'; /** * Version of the RNP release whose man pages are rendered at /docs/. - * Bump together with `docs_ref` in src/content/software/rnp.md. + * Derived from `docs_ref` in src/content/software/rnp.md via vendor-source.mjs. */ -const RNP_VERSION = '0.18.1'; const categories = z .union([z.string(), z.array(z.string())]) @@ -157,8 +157,7 @@ const softwareDocs = defineCollection({ const manpages = defineCollection({ loader: adocLoader({ base: './vendor/rnp/src', - include: (rel) => - ['rnp/rnp.1.adoc', 'rnpkeys/rnpkeys.1.adoc', 'lib/librnp.3.adoc'].includes(rel), + include: (rel) => RNP_MAN_PAGES.includes(rel), attributes: { 'component-version': RNP_VERSION, 'release-version': RNP_VERSION, diff --git a/src/lib/asciidoc.ts b/src/lib/asciidoc.ts index a9a1057..8ffebf2 100644 --- a/src/lib/asciidoc.ts +++ b/src/lib/asciidoc.ts @@ -1,8 +1,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { join, relative, sep } from 'node:path'; -import { parse as parseYaml } from 'yaml'; import { load as loadAdoc } from 'asciidoctor'; import type { Loader } from 'astro/loaders'; +import { parseFrontmatter } from './frontmatter.mjs'; export interface SplitAdoc { frontMatter: Record; @@ -11,16 +11,7 @@ export interface SplitAdoc { /** Split YAML front matter (Jekyll-style) from an AsciiDoc body. */ export function splitFrontMatter(raw: string): SplitAdoc { - const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); - if (!m) return { frontMatter: {}, body: raw }; - let frontMatter: Record = {}; - try { - frontMatter = (parseYaml(m[1]) ?? {}) as Record; - } catch { - // malformed front matter — treat the file as body-only - return { frontMatter: {}, body: raw }; - } - return { frontMatter, body: raw.slice(m[0].length) }; + return parseFrontmatter(raw); } function decodeEntities(s: string): string { @@ -60,16 +51,7 @@ export async function renderAdoc(body: string, attributes: Record]+>/g, '') - .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))) - .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))) - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .trim(); + const docTitle = docTitleRaw !== undefined ? decodeEntities(docTitleRaw) : undefined; const html = (await doc.convert()) as unknown as string; return { html, docTitle }; } diff --git a/src/lib/docs.ts b/src/lib/docs.ts new file mode 100644 index 0000000..58a4be9 --- /dev/null +++ b/src/lib/docs.ts @@ -0,0 +1,9 @@ +/** Sort order for docs entries within a product. */ +export function docSort(a: string, b: string): number { + if (a === 'README') return -1; + if (b === 'README') return 1; + const aDev = a.startsWith('develop/') ? 1 : 0; + const bDev = b.startsWith('develop/') ? 1 : 0; + if (aDev !== bDev) return aDev - bDev; + return a.localeCompare(b); +} diff --git a/src/lib/events.ts b/src/lib/events.ts new file mode 100644 index 0000000..deb6e62 --- /dev/null +++ b/src/lib/events.ts @@ -0,0 +1,31 @@ +/** + * Central registry of window custom-event names used by Vue islands. + * + * Importing from here lets TypeScript flag typos and gives a single rename + * site per event. Both senders and listeners reference the same symbol. + */ + +export const RNP_EVENTS = { + /** SiteHeader → SiteSearch: open the Cmd+K modal. */ + openSearch: 'rnp:open-search', + /** FingerprintPlayground or EasterEggs → EasterEggs: full-screen oracle rain. */ + hexRain: 'rnp:hex-rain', + /** FingerprintPlayground → EasterEggs: play the HAL 9000 scene. */ + halScene: 'rnp:hal-scene', + /** FingerprintPlayground → EasterEggs: play the Dave Bowman scene. */ + daveScene: 'rnp:dave-scene', + /** FingerprintPlayground or EasterEggs → HeroDecrypt: scramble to readable. */ + replayHero: 'rnp:replay-hero', + /** FingerprintPlayground or EasterEggs → HeroDecrypt: readable to scrambled. */ + encryptHero: 'rnp:encrypt-hero', +} as const; + +/** sessionStorage keys (distinct from custom-event names). */ +export const RNP_STORAGE = { + helpHinted: 'rnp:help-hinted', +} as const; + +/** Dispatch a registered event with no payload. */ +export function dispatch(name: (typeof RNP_EVENTS)[keyof typeof RNP_EVENTS]): void { + window.dispatchEvent(new CustomEvent(name)); +} diff --git a/src/lib/frontmatter.mjs b/src/lib/frontmatter.mjs new file mode 100644 index 0000000..29d552f --- /dev/null +++ b/src/lib/frontmatter.mjs @@ -0,0 +1,21 @@ +/** + * YAML front-matter parser shared between the Astro layer (.ts) and the + * standalone Node scripts (.mjs). Kept in .mjs so fetch-sources.mjs can + * import it without a TS compiler. + */ +import { parse as parseYaml } from 'yaml'; + +/** + * Split YAML front matter (Jekyll-style `---\n...\n---`) from a raw text body. + * Returns `{ frontMatter, body }`; if no front matter is present the whole + * input is returned as `body` and `frontMatter` is `{}`. + */ +export function parseFrontmatter(raw) { + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!m) return { frontMatter: {}, body: raw }; + try { + return { frontMatter: parseYaml(m[1]) ?? {}, body: raw.slice(m[0].length) }; + } catch { + return { frontMatter: {}, body: raw }; + } +} diff --git a/src/lib/vendor-source.mjs b/src/lib/vendor-source.mjs new file mode 100644 index 0000000..3628e59 --- /dev/null +++ b/src/lib/vendor-source.mjs @@ -0,0 +1,31 @@ +/** + * Vendor-source conventions shared between content.config.ts and + * scripts/fetch-sources.mjs. Kept in .mjs so the standalone fetch script can + * import it without a TS compiler. + */ +import { readFileSync } from 'node:fs'; +import { parseFrontmatter } from './frontmatter.mjs'; + +/** Project-relative directory sparse-cloned by fetch-sources.mjs. */ +export const VENDOR_DIR = 'vendor'; + +/** + * Man-page sources for the RNP product, relative to `vendor/rnp/src/`. + * Sparse-checkout patterns are derived from this list. + */ +export const RNP_MAN_PAGES = ['rnp/rnp.1.adoc', 'rnpkeys/rnpkeys.1.adoc', 'lib/librnp.3.adoc']; + +/** Sparse-checkout patterns (repo-root relative) for RNP's extra paths. */ +export const RNP_EXTRA_SPARSE = RNP_MAN_PAGES.map((p) => `/src/${p}`); + +/** + * Pinned RNP release tag, read from `docs_ref` in `src/content/software/rnp.md`. + * `RNP_VERSION` (without the leading `v`) is derived from the tag so the two + * can no longer drift apart. + */ +const rnpFrontMatter = parseFrontmatter( + readFileSync('src/content/software/rnp.md', 'utf8'), +).frontMatter; + +export const RNP_RELEASE_TAG = String(rnpFrontMatter.docs_ref ?? '').trim(); +export const RNP_VERSION = RNP_RELEASE_TAG.replace(/^v/, ''); diff --git a/src/pages/software/[id].astro b/src/pages/software/[id].astro index 7d20c58..e78b155 100644 --- a/src/pages/software/[id].astro +++ b/src/pages/software/[id].astro @@ -5,22 +5,13 @@ import PageHero from '@/components/PageHero.astro'; import Prose from '@/components/Prose.astro'; import InstallTabs from '@/components/vue/InstallTabs.vue'; import { tagLabel } from '@/lib/tags'; +import { docSort } from '@/lib/docs'; export async function getStaticPaths() { const products = await getCollection('software'); return products.map((entry) => ({ params: { id: entry.id }, props: { entry } })); } -/** README first, then alphabetical by slug, with develop/* pages last. */ -function docSort(a: string, b: string): number { - if (a === 'README') return -1; - if (b === 'README') return 1; - const aDev = a.startsWith('develop/') ? 1 : 0; - const bDev = b.startsWith('develop/') ? 1 : 0; - if (aDev !== bDev) return aDev - bDev; - return a.localeCompare(b); -} - const { entry } = Astro.props; const { data } = entry; const { Content } = await render(entry); diff --git a/src/pages/software/[product]/docs/[...slug].astro b/src/pages/software/[product]/docs/[...slug].astro index 3ab5ed0..f147cf5 100644 --- a/src/pages/software/[product]/docs/[...slug].astro +++ b/src/pages/software/[product]/docs/[...slug].astro @@ -1,6 +1,7 @@ --- import { getCollection, render } from 'astro:content'; import DocsLayout from '@/layouts/DocsLayout.astro'; +import { docSort } from '@/lib/docs'; export async function getStaticPaths() { const entries = await getCollection('softwareDocs'); @@ -15,16 +16,6 @@ export async function getStaticPaths() { }); } -/** README first, then alphabetical by slug, with develop/* pages last. */ -function docSort(a: string, b: string): number { - if (a === 'README') return -1; - if (b === 'README') return 1; - const aDev = a.startsWith('develop/') ? 1 : 0; - const bDev = b.startsWith('develop/') ? 1 : 0; - if (aDev !== bDev) return aDev - bDev; - return a.localeCompare(b); -} - const { entry } = Astro.props; const { product } = Astro.params; const docPath = entry.id.split('/').slice(1).join('/');