HASH c4de7e0973c7
DATE 2026-07-17
SUBJECT web: wikilinks, docs collection, and a nav that wraps
FILES 21 CHANGED HASH c4de7e0973c7
DATE 2026-07-17
SUBJECT web: wikilinks, docs collection, and a
nav that wraps
FILES 21 CHANGED ┌─ ooknet-design/astro.config.ts ─────────────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/astro.config.ts b/ooknet-design/astro.config.ts │
│ index 1eb8919..1d569a4 100644 │
│ --- a/ooknet-design/astro.config.ts │
│ +++ b/ooknet-design/astro.config.ts │
│ @@ -1,9 +1,11 @@ │
│ import { defineConfig } from "astro/config"; │
│ import { rehypeAscii } from "./src/lib/rehype-ascii"; │
│ +import { remarkWikilinks } from "./src/lib/remark-wikilinks"; │
│ │
│ export default defineConfig({ │
│ markdown: { │
│ syntaxHighlight: false, │
│ + remarkPlugins: [remarkWikilinks], │
│ rehypePlugins: [rehypeAscii], │
│ }, │
│ }); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ooknet-design/astro.config.ts ───── [ COPY ] ─┐
│ diff --git a/ooknet-design/astro.config.ts b │
│ /ooknet-design/astro.config.ts │
│ index 1eb8919..1d569a4 100644 │
│ --- a/ooknet-design/astro.config.ts │
│ +++ b/ooknet-design/astro.config.ts │
│ @@ -1,9 +1,11 @@ │
│ import { defineConfig } from "astro/config" │
│ ; │
│ import { rehypeAscii } from "./src/lib/rehy │
│ pe-ascii"; │
│ +import { remarkWikilinks } from "./src/lib/ │
│ remark-wikilinks"; │
│ │
│ export default defineConfig({ │
│ markdown: { │
│ syntaxHighlight: false, │
│ + remarkPlugins: [remarkWikilinks], │
│ rehypePlugins: [rehypeAscii], │
│ }, │
│ }); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/NavBar/NavBar.astro ──────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/NavBar/NavBar.astro b/ooknet-design/src/ │
│ components/NavBar/NavBar.astro │
│ index 8382305..3b7828d 100644 │
│ --- a/ooknet-design/src/components/NavBar/NavBar.astro │
│ +++ b/ooknet-design/src/components/NavBar/NavBar.astro │
│ @@ -3,7 +3,7 @@ import Pre from "../Pre/Pre.astro"; │
│ import Button from "../Button/Button.astro"; │
│ import NavLanguage from "../NavLanguage/NavLanguage.astro"; │
│ import { FRAME_W } from "../../lib/config"; │
│ -import { NAV_ITEMS, NAV_GAP, NAV_LANG_W, isActive, itemsWidth, type Props } from │
│ "./NavBar"; │
│ +import { NAV_ITEMS, NAV_GAP, NAV_LANG_W, isActive, itemsWidth, navRows, type Prop │
│ s } from "./NavBar"; │
│ │
│ const { │
│ width = FRAME_W, │
│ @@ -12,9 +12,11 @@ const { │
│ lang = true, │
│ } = Astro.props as Props; │
│ const path = Astro.url.pathname; │
│ -const langW = lang ? NAV_LANG_W : 0; │
│ -const padW = Math.max(1, width - itemsWidth(items, gap) - langW); │
│ -const padStr = " ".repeat(padW); │
│ +const rows = navRows(items, width, gap, lang ? NAV_LANG_W + 1 : 0); │
│ const gapStr = " ".repeat(gap); │
│ +const pad = (row: typeof items, first: boolean) => │
│ + " ".repeat(Math.max(1, width - itemsWidth(row, gap) - (first && lang ? NAV_LANG │
│ _W : 0))); │
│ --- │
│ -<Pre class="nav-bar">{items.map((it, i) => (<Fragment>{i > 0 ? gapStr : ""}<Butto │
│ n label={it.label} href={it.href} variant={isActive(it.href, path) ? "primary" : " │
│ default"} current={isActive(it.href, path)} /></Fragment>))}{padStr}{lang && <NavL │
│ anguage />}</Pre> │
│ +{rows.map((row, ri) => ( │
│ + <Pre class="nav-bar">{row.map((it, i) => (<Fragment>{i > 0 ? gapStr : ""}<Butto │
│ n label={it.label} href={it.href} variant={isActive(it.href, path) ? "primary" : " │
│ default"} current={isActive(it.href, path)} /></Fragment>))}{ri === 0 && lang ? pa │
│ d(row, true) : ""}{ri === 0 && lang && <NavLanguage />}</Pre> │
│ +))} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...omponents/NavBar/NavBar.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Na │
│ vBar/NavBar.astro b/ooknet-design/src/compon │
│ ents/NavBar/NavBar.astro │
│ index 8382305..3b7828d 100644 │
│ --- a/ooknet-design/src/components/NavBar/Na │
│ vBar.astro │
│ +++ b/ooknet-design/src/components/NavBar/Na │
│ vBar.astro │
│ @@ -3,7 +3,7 @@ import Pre from "../Pre/Pre. │
│ astro"; │
│ import Button from "../Button/Button.astro" │
│ ; │
│ import NavLanguage from "../NavLanguage/Nav │
│ Language.astro"; │
│ import { FRAME_W } from "../../lib/config"; │
│ -import { NAV_ITEMS, NAV_GAP, NAV_LANG_W, is │
│ Active, itemsWidth, type Props } from "./Nav │
│ Bar"; │
│ +import { NAV_ITEMS, NAV_GAP, NAV_LANG_W, is │
│ Active, itemsWidth, navRows, type Props } fr │
│ om "./NavBar"; │
│ │
│ const { │
│ width = FRAME_W, │
│ @@ -12,9 +12,11 @@ const { │
│ lang = true, │
│ } = Astro.props as Props; │
│ const path = Astro.url.pathname; │
│ -const langW = lang ? NAV_LANG_W : 0; │
│ -const padW = Math.max(1, width - itemsWidth │
│ (items, gap) - langW); │
│ -const padStr = " ".repeat(padW); │
│ +const rows = navRows(items, width, gap, lan │
│ g ? NAV_LANG_W + 1 : 0); │
│ const gapStr = " ".repeat(gap); │
│ +const pad = (row: typeof items, first: bool │
│ ean) => │
│ + " ".repeat(Math.max(1, width - itemsWidth │
│ (row, gap) - (first && lang ? NAV_LANG_W : 0 │
│ ))); │
│ --- │
│ -<Pre class="nav-bar">{items.map((it, i) => │
│ (<Fragment>{i > 0 ? gapStr : ""}<Button labe │
│ l={it.label} href={it.href} variant={isActiv │
│ e(it.href, path) ? "primary" : "default"} cu │
│ rrent={isActive(it.href, path)} /></Fragment │
│ >))}{padStr}{lang && <NavLanguage />}</Pre> │
│ +{rows.map((row, ri) => ( │
│ + <Pre class="nav-bar">{row.map((it, i) => │
│ (<Fragment>{i > 0 ? gapStr : ""}<Button labe │
│ l={it.label} href={it.href} variant={isActiv │
│ e(it.href, path) ? "primary" : "default"} cu │
│ rrent={isActive(it.href, path)} /></Fragment │
│ >))}{ri === 0 && lang ? pad(row, true) : ""} │
│ {ri === 0 && lang && <NavLanguage />}</Pre> │
│ +))} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/NavBar/NavBar.test.ts ────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/NavBar/NavBar.test.ts b/ooknet-design/sr │
│ c/components/NavBar/NavBar.test.ts │
│ new file mode 100644 │
│ index 0000000..6a9fbc9 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/NavBar/NavBar.test.ts │
│ @@ -0,0 +1,23 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { NAV_ITEMS, NAV_LANG_W, itemsWidth, navRows } from "./NavBar"; │
│ +import { MOBILE_FRAME_W, FRAME_W } from "../../lib/config"; │
│ + │
│ +describe("navRows", () => { │
│ + it("keeps all five items on one row at desktop width", () => { │
│ + const rows = navRows(NAV_ITEMS, FRAME_W, 4, NAV_LANG_W + 1); │
│ + expect(rows).toHaveLength(1); │
│ + }); │
│ + │
│ + it("wraps onto rows at the narrow width", () => { │
│ + const rows = navRows(NAV_ITEMS, MOBILE_FRAME_W, 1); │
│ + expect(rows.length).toBeGreaterThan(1); │
│ + for (const row of rows) { │
│ + expect(itemsWidth(row, 1)).toBeLessThanOrEqual(MOBILE_FRAME_W); │
│ + } │
│ + }); │
│ + │
│ + it("preserves item order across rows", () => { │
│ + const rows = navRows(NAV_ITEMS, MOBILE_FRAME_W, 1); │
│ + expect(rows.flat().map((it) => it.label)).toEqual(NAV_ITEMS.map((it) => it.la │
│ bel)); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ponents/NavBar/NavBar.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Na │
│ vBar/NavBar.test.ts b/ooknet-design/src/comp │
│ onents/NavBar/NavBar.test.ts │
│ new file mode 100644 │
│ index 0000000..6a9fbc9 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/NavBar/Na │
│ vBar.test.ts │
│ @@ -0,0 +1,23 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { NAV_ITEMS, NAV_LANG_W, itemsWidth, │
│ navRows } from "./NavBar"; │
│ +import { MOBILE_FRAME_W, FRAME_W } from ".. │
│ /../lib/config"; │
│ + │
│ +describe("navRows", () => { │
│ + it("keeps all five items on one row at de │
│ sktop width", () => { │
│ + const rows = navRows(NAV_ITEMS, FRAME_W │
│ , 4, NAV_LANG_W + 1); │
│ + expect(rows).toHaveLength(1); │
│ + }); │
│ + │
│ + it("wraps onto rows at the narrow width", │
│ () => { │
│ + const rows = navRows(NAV_ITEMS, MOBILE_ │
│ FRAME_W, 1); │
│ + expect(rows.length).toBeGreaterThan(1); │
│ + for (const row of rows) { │
│ + expect(itemsWidth(row, 1)).toBeLessTh │
│ anOrEqual(MOBILE_FRAME_W); │
│ + } │
│ + }); │
│ + │
│ + it("preserves item order across rows", () │
│ => { │
│ + const rows = navRows(NAV_ITEMS, MOBILE_ │
│ FRAME_W, 1); │
│ + expect(rows.flat().map((it) => it.label │
│ )).toEqual(NAV_ITEMS.map((it) => it.label)); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/NavBar/NavBar.ts ─────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/NavBar/NavBar.ts b/ooknet-design/src/com │
│ ponents/NavBar/NavBar.ts │
│ index 98c9f7c..14adddd 100644 │
│ --- a/ooknet-design/src/components/NavBar/NavBar.ts │
│ +++ b/ooknet-design/src/components/NavBar/NavBar.ts │
│ @@ -19,6 +19,7 @@ export interface Props { │
│ │
│ export const NAV_ITEMS: NavItem[] = [ │
│ { label: "NOTES", href: "/" }, │
│ + { label: "DOCS", href: "/docs/" }, │
│ { label: "PROJECTS", href: "/projects/" }, │
│ { label: "RESOURCES", href: "/resources/" }, │
│ { label: "ABOUT", href: "/about/" }, │
│ @@ -35,6 +36,26 @@ export function itemsWidth(items: NavItem[], gap = NAV_GAP): nu │
│ mber { │
│ ); │
│ } │
│ │
│ +/** Greedy row wrap. The first row reserves `reserved` cells (the EN │
│ + * label and a space) so nav grows downward instead of overflowing the │
│ + * narrow frame. */ │
│ +export function navRows(items: NavItem[], width: number, gap = NAV_GAP, reserved │
│ = 0): NavItem[][] { │
│ + const rows: NavItem[][] = [[]]; │
│ + let used = reserved; │
│ + for (const it of items) { │
│ + const row = rows[rows.length - 1]; │
│ + const w = buttonWidth(it.label) + (row.length ? gap : 0); │
│ + if (row.length && used + w > width) { │
│ + rows.push([it]); │
│ + used = buttonWidth(it.label); │
│ + } else { │
│ + row.push(it); │
│ + used += w; │
│ + } │
│ + } │
│ + return rows; │
│ +} │
│ + │
│ /** Active match. Root `/` also claims any /notes/ subpath since the │
│ * index is the notes list. */ │
│ export function isActive(href: string, path: string): boolean { │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...c/components/NavBar/NavBar.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Na │
│ vBar/NavBar.ts b/ooknet-design/src/component │
│ s/NavBar/NavBar.ts │
│ index 98c9f7c..14adddd 100644 │
│ --- a/ooknet-design/src/components/NavBar/Na │
│ vBar.ts │
│ +++ b/ooknet-design/src/components/NavBar/Na │
│ vBar.ts │
│ @@ -19,6 +19,7 @@ export interface Props { │
│ │
│ export const NAV_ITEMS: NavItem[] = [ │
│ { label: "NOTES", href: "/" }, │
│ + { label: "DOCS", href: "/docs/" }, │
│ { label: "PROJECTS", href: "/projects/" │
│ }, │
│ { label: "RESOURCES", href: "/resources/" │
│ }, │
│ { label: "ABOUT", href: "/about/" }, │
│ @@ -35,6 +36,26 @@ export function itemsWidt │
│ h(items: NavItem[], gap = NAV_GAP): number { │
│ ); │
│ } │
│ │
│ +/** Greedy row wrap. The first row reserves │
│ `reserved` cells (the EN │
│ + * label and a space) so nav grows downwar │
│ d instead of overflowing the │
│ + * narrow frame. */ │
│ +export function navRows(items: NavItem[], w │
│ idth: number, gap = NAV_GAP, reserved = 0): │
│ NavItem[][] { │
│ + const rows: NavItem[][] = [[]]; │
│ + let used = reserved; │
│ + for (const it of items) { │
│ + const row = rows[rows.length - 1]; │
│ + const w = buttonWidth(it.label) + (row. │
│ length ? gap : 0); │
│ + if (row.length && used + w > width) { │
│ + rows.push([it]); │
│ + used = buttonWidth(it.label); │
│ + } else { │
│ + row.push(it); │
│ + used += w; │
│ + } │
│ + } │
│ + return rows; │
│ +} │
│ + │
│ /** Active match. Root `/` also claims any │
│ /notes/ subpath since the │
│ * index is the notes list. */ │
│ export function isActive(href: string, path │
│ : string): boolean { │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/Prose/Prose.scss ─────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Prose/Prose.scss b/ooknet-design/src/com │
│ ponents/Prose/Prose.scss │
│ index 7319592..a0179df 100644 │
│ --- a/ooknet-design/src/components/Prose/Prose.scss │
│ +++ b/ooknet-design/src/components/Prose/Prose.scss │
│ @@ -17,6 +17,14 @@ │
│ text-decoration: underline; │
│ text-underline-offset: 3px; │
│ } │
│ + │
│ + // Unresolved [[wikilinks]] - dead-link style, in the wiki tradition. │
│ + .wikilink-dead { │
│ + color: var(--ink-soft); │
│ + text-decoration: underline dashed; │
│ + text-underline-offset: 3px; │
│ + cursor: help; │
│ + } │
│ a:focus-visible { │
│ outline: 1px dashed currentColor; │
│ outline-offset: 2px; │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...c/components/Prose/Prose.scss ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Pr │
│ ose/Prose.scss b/ooknet-design/src/component │
│ s/Prose/Prose.scss │
│ index 7319592..a0179df 100644 │
│ --- a/ooknet-design/src/components/Prose/Pro │
│ se.scss │
│ +++ b/ooknet-design/src/components/Prose/Pro │
│ se.scss │
│ @@ -17,6 +17,14 @@ │
│ text-decoration: underline; │
│ text-underline-offset: 3px; │
│ } │
│ + │
│ + // Unresolved [[wikilinks]] - dead-link s │
│ tyle, in the wiki tradition. │
│ + .wikilink-dead { │
│ + color: var(--ink-soft); │
│ + text-decoration: underline dashed; │
│ + text-underline-offset: 3px; │
│ + cursor: help; │
│ + } │
│ a:focus-visible { │
│ outline: 1px dashed currentColor; │
│ outline-offset: 2px; │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/TopHeader/TopHeader.astro ────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/TopHeader/TopHeader.astro b/ooknet-desig │
│ n/src/components/TopHeader/TopHeader.astro │
│ index 5b5c877..ae32ca8 100644 │
│ --- a/ooknet-design/src/components/TopHeader/TopHeader.astro │
│ +++ b/ooknet-design/src/components/TopHeader/TopHeader.astro │
│ @@ -41,7 +41,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ <script> │
│ import Fuse from "fuse.js"; │
│ │
│ - type Note = { id: string; note: string; title: string; category: string; tags: │
│ string[]; filed: string }; │
│ + type Note = { note: string; title: string; category: string; tags: string[]; fi │
│ led: string; url: string }; │
│ let fuse: Fuse<Note> | null = null; │
│ │
│ async function ensureLoaded() { │
│ @@ -88,7 +88,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ .slice(0, 8) │
│ .map((h, i) => { │
│ const safe = (s: string) => s.replace(/&/g, "&").replace(/</g, "<" │
│ ).replace(/>/g, ">"); │
│ - return `<li${i === 0 ? ' class="active"' : ""}><a href="/notes/${encodeUR │
│ IComponent(h.item.id)}/">${safe(h.item.note)} - ${safe(h.item.title)}</a></li>`; │
│ + return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(h.item.url │
│ )}">${safe(h.item.note)} - ${safe(h.item.title)}</a></li>`; │
│ }) │
│ .join(""); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...nts/TopHeader/TopHeader.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/To │
│ pHeader/TopHeader.astro b/ooknet-design/src/ │
│ components/TopHeader/TopHeader.astro │
│ index 5b5c877..ae32ca8 100644 │
│ --- a/ooknet-design/src/components/TopHeader │
│ /TopHeader.astro │
│ +++ b/ooknet-design/src/components/TopHeader │
│ /TopHeader.astro │
│ @@ -41,7 +41,7 @@ const PLACEHOLDER_NARROW = │
│ "_".repeat(18); │
│ <script> │
│ import Fuse from "fuse.js"; │
│ │
│ - type Note = { id: string; note: string; t │
│ itle: string; category: string; tags: string │
│ []; filed: string }; │
│ + type Note = { note: string; title: string │
│ ; category: string; tags: string[]; filed: s │
│ tring; url: string }; │
│ let fuse: Fuse<Note> | null = null; │
│ │
│ async function ensureLoaded() { │
│ @@ -88,7 +88,7 @@ const PLACEHOLDER_NARROW = │
│ "_".repeat(18); │
│ .slice(0, 8) │
│ .map((h, i) => { │
│ const safe = (s: string) => s.repla │
│ ce(/&/g, "&").replace(/</g, "<").repl │
│ ace(/>/g, ">"); │
│ - return `<li${i === 0 ? ' class="act │
│ ive"' : ""}><a href="/notes/${encodeURICompo │
│ nent(h.item.id)}/">${safe(h.item.note)} - ${ │
│ safe(h.item.title)}</a></li>`; │
│ + return `<li${i === 0 ? ' class="act │
│ ive"' : ""}><a href="${safe(h.item.url)}">${ │
│ safe(h.item.note)} - ${safe(h.item.title)}</ │
│ a></li>`; │
│ }) │
│ .join(""); │
│ } │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content.config.ts ───────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content.config.ts b/ooknet-design/src/content.confi │
│ g.ts │
│ index ce664a6..2721063 100644 │
│ --- a/ooknet-design/src/content.config.ts │
│ +++ b/ooknet-design/src/content.config.ts │
│ @@ -30,4 +30,16 @@ const notes = defineCollection({ │
│ }), │
│ }); │
│ │
│ -export const collections = { notes }; │
│ +// Docs: hierarchical wiki pages. Directory structure is the taxonomy; │
│ +// `order` sorts pages within a directory. │
│ +const docs = defineCollection({ │
│ + loader: glob({ pattern: "**/*.md", base: "./src/content/docs" }), │
│ + schema: z.object({ │
│ + title: z.string(), │
│ + description: z.string().optional(), │
│ + order: z.number().optional(), │
│ + draft: z.boolean().default(false), │
│ + }), │
│ +}); │
│ + │
│ +export const collections = { notes, docs }; │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...-design/src/content.config.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content.confi │
│ g.ts b/ooknet-design/src/content.config.ts │
│ index ce664a6..2721063 100644 │
│ --- a/ooknet-design/src/content.config.ts │
│ +++ b/ooknet-design/src/content.config.ts │
│ @@ -30,4 +30,16 @@ const notes = defineColle │
│ ction({ │
│ }), │
│ }); │
│ │
│ -export const collections = { notes }; │
│ +// Docs: hierarchical wiki pages. Directory │
│ structure is the taxonomy; │
│ +// `order` sorts pages within a directory. │
│ +const docs = defineCollection({ │
│ + loader: glob({ pattern: "**/*.md", base: │
│ "./src/content/docs" }), │
│ + schema: z.object({ │
│ + title: z.string(), │
│ + description: z.string().optional(), │
│ + order: z.number().optional(), │
│ + draft: z.boolean().default(false), │
│ + }), │
│ +}); │
│ + │
│ +export const collections = { notes, docs }; │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/authoring/filing-notes.md ──────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/authoring/filing-notes.md b/ooknet-des │
│ ign/src/content/docs/authoring/filing-notes.md │
│ new file mode 100644 │
│ index 0000000..6350f4c │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/authoring/filing-notes.md │
│ @@ -0,0 +1,39 @@ │
│ +--- │
│ +title: "Filing notes" │
│ +order: 1 │
│ +--- │
│ + │
│ +# Anatomy of a note │
│ + │
│ +Notes live in `src/content/notes/` as markdown with frontmatter: │
│ + │
│ +```yaml │
│ +title: "ENTROPY BOUNDS IN SPARSE GRAPHS" │
│ +note: "N217" │
│ +published: "2026-04-19" │
│ +status: "PEER-CHECKED [PASS]" │
│ +filed: "/INFO/GRAPH/ENTROPY/BOUND" │
│ +meta: │
│ + category: "graph-theory" │
│ + tags: ["entropy", "sparse-graphs"] │
│ +``` │
│ + │
│ +Only `title`, `note`, and `published` are required; everything else │
│ +defaults so a bare draft can't crash the content sync. │
│ + │
│ +# The filed path │
│ + │
│ +`filed` is the taxonomy - it drives the breadcrumbs on the note page │
│ +and the tree on the index. Segments are searchable, so │
│ +`/INFO/GRAPH/ENTROPY/BOUND` makes the note reachable from any of its │
│ +levels. │
│ + │
│ +# Cross-references │
│ + │
│ +Link other notes by number: [[N217]], by title, or with an alias: │
│ +[[N217|the entropy bounds note]]. Unresolved targets like [[N999]] │
│ +render as dead links instead of failing the build, so drafts can │
│ +reference notes that don't exist yet. │
│ + │
│ +Set `draft: true` to keep a note out of the index, the search, and the │
│ +wikilink map. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ocs/authoring/filing-notes.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ authoring/filing-notes.md b/ooknet-design/sr │
│ c/content/docs/authoring/filing-notes.md │
│ new file mode 100644 │
│ index 0000000..6350f4c │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/authori │
│ ng/filing-notes.md │
│ @@ -0,0 +1,39 @@ │
│ +--- │
│ +title: "Filing notes" │
│ +order: 1 │
│ +--- │
│ + │
│ +# Anatomy of a note │
│ + │
│ +Notes live in `src/content/notes/` as markd │
│ own with frontmatter: │
│ + │
│ +```yaml │
│ +title: "ENTROPY BOUNDS IN SPARSE GRAPHS" │
│ +note: "N217" │
│ +published: "2026-04-19" │
│ +status: "PEER-CHECKED [PASS]" │
│ +filed: "/INFO/GRAPH/ENTROPY/BOUND" │
│ +meta: │
│ + category: "graph-theory" │
│ + tags: ["entropy", "sparse-graphs"] │
│ +``` │
│ + │
│ +Only `title`, `note`, and `published` are r │
│ equired; everything else │
│ +defaults so a bare draft can't crash the co │
│ ntent sync. │
│ + │
│ +# The filed path │
│ + │
│ +`filed` is the taxonomy - it drives the bre │
│ adcrumbs on the note page │
│ +and the tree on the index. Segments are sea │
│ rchable, so │
│ +`/INFO/GRAPH/ENTROPY/BOUND` makes the note │
│ reachable from any of its │
│ +levels. │
│ + │
│ +# Cross-references │
│ + │
│ +Link other notes by number: [[N217]], by ti │
│ tle, or with an alias: │
│ +[[N217|the entropy bounds note]]. Unresolve │
│ d targets like [[N999]] │
│ +render as dead links instead of failing the │
│ build, so drafts can │
│ +reference notes that don't exist yet. │
│ + │
│ +Set `draft: true` to keep a note out of the │
│ index, the search, and the │
│ +wikilink map. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/authoring/wikilinks.md ─────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/authoring/wikilinks.md b/ooknet-design │
│ /src/content/docs/authoring/wikilinks.md │
│ new file mode 100644 │
│ index 0000000..3e57328 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/authoring/wikilinks.md │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +title: "Wikilinks" │
│ +order: 2 │
│ +--- │
│ + │
│ +# Syntax │
│ + │
│ +| FORM | RESOLVES BY | │
│ +| ----------------------- | ------------------------------ | │
│ +| `[[N217]]` | note number | │
│ +| `[[n0-style-sheet]]` | file id | │
│ +| `[[Style Sheet]]` | title, case-insensitive | │
│ +| `[[design/frames]]` | doc path | │
│ +| `[[N217\|see bounds]]` | alias - target before the pipe | │
│ + │
│ +Live: [[N217]] by number, [[Style Sheet]] by title, │
│ +[[N217|aliased to this]] with an alias. │
│ + │
│ +# Resolution │
│ + │
│ +The map is built once per build by scanning notes and docs │
│ +frontmatter. Every published page is indexed under its id, its note │
│ +number (notes), and its title. Targets are trimmed and matched │
│ +case-insensitively. │
│ + │
│ +# Dead links │
│ + │
│ +An unresolved target renders soft with a dashed underline - like │
│ +[[N999]] - rather than failing the build. Hover shows the unresolved │
│ +target. This is deliberate: drafts reference future notes, and the │
│ +wiki tradition is to show the gap. │
│ + │
│ +# Where they work │
│ + │
│ +Anywhere markdown renders: notes, docs, and any prose the pipeline │
│ +touches. They don't fire inside code fences or existing links. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...t/docs/authoring/wikilinks.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ authoring/wikilinks.md b/ooknet-design/src/c │
│ ontent/docs/authoring/wikilinks.md │
│ new file mode 100644 │
│ index 0000000..3e57328 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/authori │
│ ng/wikilinks.md │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +title: "Wikilinks" │
│ +order: 2 │
│ +--- │
│ + │
│ +# Syntax │
│ + │
│ +| FORM | RESOLVES BY │
│ | │
│ +| ----------------------- | --------------- │
│ --------------- | │
│ +| `[[N217]]` | note number │
│ | │
│ +| `[[n0-style-sheet]]` | file id │
│ | │
│ +| `[[Style Sheet]]` | title, case-ins │
│ ensitive | │
│ +| `[[design/frames]]` | doc path │
│ | │
│ +| `[[N217\|see bounds]]` | alias - target │
│ before the pipe | │
│ + │
│ +Live: [[N217]] by number, [[Style Sheet]] b │
│ y title, │
│ +[[N217|aliased to this]] with an alias. │
│ + │
│ +# Resolution │
│ + │
│ +The map is built once per build by scanning │
│ notes and docs │
│ +frontmatter. Every published page is indexe │
│ d under its id, its note │
│ +number (notes), and its title. Targets are │
│ trimmed and matched │
│ +case-insensitively. │
│ + │
│ +# Dead links │
│ + │
│ +An unresolved target renders soft with a da │
│ shed underline - like │
│ +[[N999]] - rather than failing the build. H │
│ over shows the unresolved │
│ +target. This is deliberate: drafts referenc │
│ e future notes, and the │
│ +wiki tradition is to show the gap. │
│ + │
│ +# Where they work │
│ + │
│ +Anywhere markdown renders: notes, docs, and │
│ any prose the pipeline │
│ +touches. They don't fire inside code fences │
│ or existing links. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/design/components.md ───────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/design/components.md b/ooknet-design/s │
│ rc/content/docs/design/components.md │
│ new file mode 100644 │
│ index 0000000..36a7cea │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/components.md │
│ @@ -0,0 +1,28 @@ │
│ +--- │
│ +title: "Component library" │
│ +order: 3 │
│ +--- │
│ + │
│ +# Two kinds of component │
│ + │
│ +**Static** components are pure builders: a `.ts` function takes a │
│ +width and returns a string, unit-tested, rendered at both frame widths │
│ +through `Frame`. Tables, trees, timelines, gauges, calendars. │
│ + │
│ +**Interactive** components render once and wear ASCII chrome over │
│ +native elements - a checkbox behind `[x]`, a range input over │
│ +`├───█───┤`. They can't dual-render: duplicating an input duplicates │
│ +its name and splits state across variants. │
│ + │
│ +# Conventions │
│ + │
│ +- one directory per component: `.astro`, `.ts`, optional `.scss` and `.test.ts` │
│ +- every frame string passes `checkGrid` before it ships │
│ +- motion is discrete and glyph-stepped, never a CSS fade │
│ +- the live inventory renders on the [components page](/components) │
│ + │
│ +# Where to look first │
│ + │
│ +`Frame.astro` is the bridge between builders and the page. `Pre.astro` │
│ +is the strict grid. Copy an existing component's directory and follow │
│ +its shape. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ent/docs/design/components.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ design/components.md b/ooknet-design/src/con │
│ tent/docs/design/components.md │
│ new file mode 100644 │
│ index 0000000..36a7cea │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/ │
│ components.md │
│ @@ -0,0 +1,28 @@ │
│ +--- │
│ +title: "Component library" │
│ +order: 3 │
│ +--- │
│ + │
│ +# Two kinds of component │
│ + │
│ +**Static** components are pure builders: a │
│ `.ts` function takes a │
│ +width and returns a string, unit-tested, re │
│ ndered at both frame widths │
│ +through `Frame`. Tables, trees, timelines, │
│ gauges, calendars. │
│ + │
│ +**Interactive** components render once and │
│ wear ASCII chrome over │
│ +native elements - a checkbox behind `[x]`, │
│ a range input over │
│ +`├───█───┤`. They can't dual-render: duplic │
│ ating an input duplicates │
│ +its name and splits state across variants. │
│ + │
│ +# Conventions │
│ + │
│ +- one directory per component: `.astro`, `. │
│ ts`, optional `.scss` and `.test.ts` │
│ +- every frame string passes `checkGrid` bef │
│ ore it ships │
│ +- motion is discrete and glyph-stepped, nev │
│ er a CSS fade │
│ +- the live inventory renders on the [compon │
│ ents page](/components) │
│ + │
│ +# Where to look first │
│ + │
│ +`Frame.astro` is the bridge between builder │
│ s and the page. `Pre.astro` │
│ +is the strict grid. Copy an existing compon │
│ ent's directory and follow │
│ +its shape. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/design/frames.md ───────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/design/frames.md b/ooknet-design/src/c │
│ ontent/docs/design/frames.md │
│ new file mode 100644 │
│ index 0000000..0f2225f │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/frames.md │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +title: "Frames & the grid" │
│ +order: 1 │
│ +--- │
│ + │
│ +# The grid is the contract │
│ + │
│ +Every visual element on this site is text on a monospace grid. The │
│ +frame is 86 cells wide on desktop and 48 on mobile; components render │
│ +both variants at build time and CSS shows one per viewport. │
│ + │
│ +| CONSTANT | CELLS | WHERE | │
│ +| -------------- | ----: | -------------------- | │
│ +| FRAME_W | 86 | src/lib/config.ts | │
│ +| MOBILE_FRAME_W | 48 | src/lib/config.ts | │
│ + │
│ +# checkGrid │
│ + │
│ +Nothing ships that would bend a border. Every frame passes through │
│ +`checkGrid`, which fails the build if a line overflows its width or │
│ +uses a glyph the frame font doesn't ship at one cell wide: │
│ + │
│ +```ts │
│ +export function checkGrid(text: string, maxW: number, ctx: string): string { │
│ + // throws on overflow or off-grid codepoints │
│ +} │
│ +``` │
│ + │
│ +The full glyph policy lives in [[N0]] - the style sheet note that │
│ +exercises every construct the pipeline renders. │
│ + │
│ +# Fonts │
│ + │
│ +Prose sets in IBM Plex Mono; frames set in Cascadia Mono, whose │
│ +box-drawing glyphs are uniformly one cell. Never mix them on the same │
│ +line of a frame. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...content/docs/design/frames.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ design/frames.md b/ooknet-design/src/content │
│ /docs/design/frames.md │
│ new file mode 100644 │
│ index 0000000..0f2225f │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/ │
│ frames.md │
│ @@ -0,0 +1,36 @@ │
│ +--- │
│ +title: "Frames & the grid" │
│ +order: 1 │
│ +--- │
│ + │
│ +# The grid is the contract │
│ + │
│ +Every visual element on this site is text o │
│ n a monospace grid. The │
│ +frame is 86 cells wide on desktop and 48 on │
│ mobile; components render │
│ +both variants at build time and CSS shows o │
│ ne per viewport. │
│ + │
│ +| CONSTANT | CELLS | WHERE │
│ | │
│ +| -------------- | ----: | ---------------- │
│ ---- | │
│ +| FRAME_W | 86 | src/lib/config.t │
│ s | │
│ +| MOBILE_FRAME_W | 48 | src/lib/config.t │
│ s | │
│ + │
│ +# checkGrid │
│ + │
│ +Nothing ships that would bend a border. Eve │
│ ry frame passes through │
│ +`checkGrid`, which fails the build if a lin │
│ e overflows its width or │
│ +uses a glyph the frame font doesn't ship at │
│ one cell wide: │
│ + │
│ +```ts │
│ +export function checkGrid(text: string, max │
│ W: number, ctx: string): string { │
│ + // throws on overflow or off-grid codepoi │
│ nts │
│ +} │
│ +``` │
│ + │
│ +The full glyph policy lives in [[N0]] - the │
│ style sheet note that │
│ +exercises every construct the pipeline rend │
│ ers. │
│ + │
│ +# Fonts │
│ + │
│ +Prose sets in IBM Plex Mono; frames set in │
│ Cascadia Mono, whose │
│ +box-drawing glyphs are uniformly one cell. │
│ Never mix them on the same │
│ +line of a frame. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/design/pipeline.md ─────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/design/pipeline.md b/ooknet-design/src │
│ /content/docs/design/pipeline.md │
│ new file mode 100644 │
│ index 0000000..e2e342b │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/pipeline.md │
│ @@ -0,0 +1,30 @@ │
│ +--- │
│ +title: "Markdown pipeline" │
│ +order: 2 │
│ +--- │
│ + │
│ +# One source, ASCII out │
│ + │
│ +Notes and docs are plain markdown - previewable in Obsidian and │
│ +GitHub - transformed at build time by `rehype-ascii`: │
│ + │
│ +| CONSTRUCT | BECOMES | │
│ +| -------------- | ---------------------------------------- | │
│ +| fenced code | framed box with [ COPY ] | │
│ +| table | ASCII table + sr-only semantic table | │
│ +| [!NOTE] quote | labeled callout | │
│ +| lone image | numbered figure | │
│ +| diagram fence | verbatim ASCII art as a figure | │
│ +| mermaid fence | generated diagram as a figure | │
│ +| diff fence | framed diff, adds bold, deletions soft | │
│ + │
│ +# Diagrams │
│ + │
│ +A `mermaid` fence renders linear chains through the house layout and │
│ +delegates branching graphs to mermaid-ascii. A `diagram` fence keeps │
│ +hand-drawn art verbatim. Both take a `:: CAPTION` first line. │
│ + │
│ +# Wikilinks │
│ + │
│ +`[[target]]` resolves against notes and docs at build time - see │
│ +[[Wikilinks]] for the syntax. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ntent/docs/design/pipeline.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ design/pipeline.md b/ooknet-design/src/conte │
│ nt/docs/design/pipeline.md │
│ new file mode 100644 │
│ index 0000000..e2e342b │
│ --- /dev/null │
│ +++ b/ooknet-design/src/content/docs/design/ │
│ pipeline.md │
│ @@ -0,0 +1,30 @@ │
│ +--- │
│ +title: "Markdown pipeline" │
│ +order: 2 │
│ +--- │
│ + │
│ +# One source, ASCII out │
│ + │
│ +Notes and docs are plain markdown - preview │
│ able in Obsidian and │
│ +GitHub - transformed at build time by `rehy │
│ pe-ascii`: │
│ + │
│ +| CONSTRUCT | BECOMES │
│ | │
│ +| -------------- | ------------------------ │
│ ---------------- | │
│ +| fenced code | framed box with [ COPY ] │
│ | │
│ +| table | ASCII table + sr-only se │
│ mantic table | │
│ +| [!NOTE] quote | labeled callout │
│ | │
│ +| lone image | numbered figure │
│ | │
│ +| diagram fence | verbatim ASCII art as a │
│ figure | │
│ +| mermaid fence | generated diagram as a f │
│ igure | │
│ +| diff fence | framed diff, adds bold, │
│ deletions soft | │
│ + │
│ +# Diagrams │
│ + │
│ +A `mermaid` fence renders linear chains thr │
│ ough the house layout and │
│ +delegates branching graphs to mermaid-ascii │
│ . A `diagram` fence keeps │
│ +hand-drawn art verbatim. Both take a `:: CA │
│ PTION` first line. │
│ + │
│ +# Wikilinks │
│ + │
│ +`[[target]]` resolves against notes and doc │
│ s at build time - see │
│ +[[Wikilinks]] for the syntax. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/notes/n0-style-sheet.md ─────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/notes/n0-style-sheet.md b/ooknet-design/src │
│ /content/notes/n0-style-sheet.md │
│ index f2064fb..4de4e42 100644 │
│ --- a/ooknet-design/src/content/notes/n0-style-sheet.md │
│ +++ b/ooknet-design/src/content/notes/n0-style-sheet.md │
│ @@ -118,6 +118,12 @@ palette stays monochrome: │
│ } │
│ ``` │
│ │
│ +# Wikilinks │
│ + │
│ +A wikilink resolves by note number [[N217]], title [[Style Sheet]], │
│ +or doc path [[design/frames]], and takes an alias: │
│ +[[N217|the entropy note]]. Unresolved targets render dead: [[N999]]. │
│ + │
│ # Lists │
│ │
│ - frames are strings, components are chrome │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ntent/notes/n0-style-sheet.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/notes │
│ /n0-style-sheet.md b/ooknet-design/src/conte │
│ nt/notes/n0-style-sheet.md │
│ index f2064fb..4de4e42 100644 │
│ --- a/ooknet-design/src/content/notes/n0-sty │
│ le-sheet.md │
│ +++ b/ooknet-design/src/content/notes/n0-sty │
│ le-sheet.md │
│ @@ -118,6 +118,12 @@ palette stays monochrom │
│ e: │
│ } │
│ ``` │
│ │
│ +# Wikilinks │
│ + │
│ +A wikilink resolves by note number [[N217]] │
│ , title [[Style Sheet]], │
│ +or doc path [[design/frames]], and takes an │
│ alias: │
│ +[[N217|the entropy note]]. Unresolved targe │
│ ts render dead: [[N999]]. │
│ + │
│ # Lists │
│ │
│ - frames are strings, components are chrome │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/docs.test.ts ────────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/docs.test.ts b/ooknet-design/src/lib/docs.test. │
│ ts │
│ new file mode 100644 │
│ index 0000000..78c6460 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/docs.test.ts │
│ @@ -0,0 +1,37 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { docsOrder, docsTree } from "./docs"; │
│ + │
│ +const DOCS = [ │
│ + { id: "design/pipeline", title: "Pipeline", order: 2 }, │
│ + { id: "design/frames", title: "Frames", order: 1 }, │
│ + { id: "authoring/wikilinks", title: "Wikilinks", order: 2 }, │
│ + { id: "authoring/filing-notes", title: "Filing notes", order: 1 }, │
│ + { id: "design/zeta", title: "Zeta" }, │
│ +]; │
│ + │
│ +describe("docsOrder", () => { │
│ + it("sorts directories alphabetically, pages by order then title", () => { │
│ + expect(docsOrder(DOCS).map((d) => d.id)).toEqual([ │
│ + "authoring/filing-notes", │
│ + "authoring/wikilinks", │
│ + "design/frames", │
│ + "design/pipeline", │
│ + "design/zeta", │
│ + ]); │
│ + }); │
│ + │
│ + it("sorts a top-level page before directory contents", () => { │
│ + const withIndex = [...DOCS, { id: "overview", title: "Overview" }]; │
│ + expect(docsOrder(withIndex)[0].id).toBe("overview"); │
│ + }); │
│ +}); │
│ + │
│ +describe("docsTree", () => { │
│ + it("builds branches from path segments with linked leaves", () => { │
│ + const tree = docsTree(DOCS); │
│ + expect(tree.map((n) => n.label)).toEqual(["/AUTHORING", "/DESIGN"]); │
│ + const design = tree[1]; │
│ + expect(design.children!.map((n) => n.label)).toEqual(["FRAMES", "PIPELINE", " │
│ ZETA"]); │
│ + expect(design.children![0].href).toBe("/docs/design/frames/"); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...t-design/src/lib/docs.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/docs.test │
│ .ts b/ooknet-design/src/lib/docs.test.ts │
│ new file mode 100644 │
│ index 0000000..78c6460 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/docs.test.ts │
│ @@ -0,0 +1,37 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { docsOrder, docsTree } from "./docs │
│ "; │
│ + │
│ +const DOCS = [ │
│ + { id: "design/pipeline", title: "Pipeline │
│ ", order: 2 }, │
│ + { id: "design/frames", title: "Frames", o │
│ rder: 1 }, │
│ + { id: "authoring/wikilinks", title: "Wiki │
│ links", order: 2 }, │
│ + { id: "authoring/filing-notes", title: "F │
│ iling notes", order: 1 }, │
│ + { id: "design/zeta", title: "Zeta" }, │
│ +]; │
│ + │
│ +describe("docsOrder", () => { │
│ + it("sorts directories alphabetically, pag │
│ es by order then title", () => { │
│ + expect(docsOrder(DOCS).map((d) => d.id) │
│ ).toEqual([ │
│ + "authoring/filing-notes", │
│ + "authoring/wikilinks", │
│ + "design/frames", │
│ + "design/pipeline", │
│ + "design/zeta", │
│ + ]); │
│ + }); │
│ + │
│ + it("sorts a top-level page before directo │
│ ry contents", () => { │
│ + const withIndex = [...DOCS, { id: "over │
│ view", title: "Overview" }]; │
│ + expect(docsOrder(withIndex)[0].id).toBe │
│ ("overview"); │
│ + }); │
│ +}); │
│ + │
│ +describe("docsTree", () => { │
│ + it("builds branches from path segments wi │
│ th linked leaves", () => { │
│ + const tree = docsTree(DOCS); │
│ + expect(tree.map((n) => n.label)).toEqua │
│ l(["/AUTHORING", "/DESIGN"]); │
│ + const design = tree[1]; │
│ + expect(design.children!.map((n) => n.la │
│ bel)).toEqual(["FRAMES", "PIPELINE", "ZETA"] │
│ ); │
│ + expect(design.children![0].href).toBe(" │
│ /docs/design/frames/"); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/docs.ts ─────────────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/docs.ts b/ooknet-design/src/lib/docs.ts │
│ new file mode 100644 │
│ index 0000000..59ee60d │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/docs.ts │
│ @@ -0,0 +1,59 @@ │
│ +// Pure docs helpers: reading order and nav tree from doc ids like │
│ +// "design/frames". astro:content stays in the pages, as with notes.ts. │
│ +import type { TreeNode } from "../components/Tree/Tree"; │
│ + │
│ +export interface DocEntry { │
│ + id: string; │
│ + title: string; │
│ + order?: number; │
│ +} │
│ + │
│ +/** Depth-first reading order: directories alphabetical, pages within a │
│ + * directory by `order` then title. */ │
│ +export function docsOrder(docs: DocEntry[]): DocEntry[] { │
│ + return [...docs].sort((a, b) => { │
│ + const as = a.id.split("/"); │
│ + const bs = b.id.split("/"); │
│ + const dirs = Math.max(as.length, bs.length) - 1; │
│ + for (let i = 0; i < dirs; i++) { │
│ + const ad = as[i] ?? ""; │
│ + const bd = bs[i] ?? ""; │
│ + // a page sorts before its sibling directories' contents │
│ + if (i === as.length - 1 && i < bs.length - 1) return -1; │
│ + if (i === bs.length - 1 && i < as.length - 1) return 1; │
│ + if (ad !== bd) return ad.localeCompare(bd); │
│ + } │
│ + const ao = a.order ?? Number.MAX_SAFE_INTEGER; │
│ + const bo = b.order ?? Number.MAX_SAFE_INTEGER; │
│ + if (ao !== bo) return ao - bo; │
│ + return a.title.localeCompare(b.title); │
│ + }); │
│ +} │
│ + │
│ +/** Nav tree: branches are path segments, leaves link to the doc. */ │
│ +export function docsTree(docs: DocEntry[]): TreeNode[] { │
│ + type Branch = { children: Map<string, Branch>; leaves: DocEntry[] }; │
│ + const root: Branch = { children: new Map(), leaves: [] }; │
│ + │
│ + for (const d of docsOrder(docs)) { │
│ + const segments = d.id.split("/"); │
│ + let branch = root; │
│ + for (const seg of segments.slice(0, -1)) { │
│ + if (!branch.children.has(seg)) branch.children.set(seg, { children: new Map │
│ (), leaves: [] }); │
│ + branch = branch.children.get(seg)!; │
│ + } │
│ + branch.leaves.push(d); │
│ + } │
│ + │
│ + const toNodes = (b: Branch, depth: number): TreeNode[] => [ │
│ + ...b.leaves.map((d) => ({ │
│ + label: d.title.toUpperCase(), │
│ + href: `/docs/${d.id}/`, │
│ + })), │
│ + ...[...b.children.entries()].map(([seg, child]) => ({ │
│ + label: depth === 0 ? `/${seg.toUpperCase()}` : seg.toUpperCase(), │
│ + children: toNodes(child, depth + 1), │
│ + })), │
│ + ]; │
│ + return toNodes(root, 0); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/docs.ts ───── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/docs.ts b │
│ /ooknet-design/src/lib/docs.ts │
│ new file mode 100644 │
│ index 0000000..59ee60d │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/docs.ts │
│ @@ -0,0 +1,59 @@ │
│ +// Pure docs helpers: reading order and nav │
│ tree from doc ids like │
│ +// "design/frames". astro:content stays in │
│ the pages, as with notes.ts. │
│ +import type { TreeNode } from "../component │
│ s/Tree/Tree"; │
│ + │
│ +export interface DocEntry { │
│ + id: string; │
│ + title: string; │
│ + order?: number; │
│ +} │
│ + │
│ +/** Depth-first reading order: directories │
│ alphabetical, pages within a │
│ + * directory by `order` then title. */ │
│ +export function docsOrder(docs: DocEntry[]) │
│ : DocEntry[] { │
│ + return [...docs].sort((a, b) => { │
│ + const as = a.id.split("/"); │
│ + const bs = b.id.split("/"); │
│ + const dirs = Math.max(as.length, bs.len │
│ gth) - 1; │
│ + for (let i = 0; i < dirs; i++) { │
│ + const ad = as[i] ?? ""; │
│ + const bd = bs[i] ?? ""; │
│ + // a page sorts before its sibling di │
│ rectories' contents │
│ + if (i === as.length - 1 && i < bs.len │
│ gth - 1) return -1; │
│ + if (i === bs.length - 1 && i < as.len │
│ gth - 1) return 1; │
│ + if (ad !== bd) return ad.localeCompar │
│ e(bd); │
│ + } │
│ + const ao = a.order ?? Number.MAX_SAFE_I │
│ NTEGER; │
│ + const bo = b.order ?? Number.MAX_SAFE_I │
│ NTEGER; │
│ + if (ao !== bo) return ao - bo; │
│ + return a.title.localeCompare(b.title); │
│ + }); │
│ +} │
│ + │
│ +/** Nav tree: branches are path segments, l │
│ eaves link to the doc. */ │
│ +export function docsTree(docs: DocEntry[]): │
│ TreeNode[] { │
│ + type Branch = { children: Map<string, Bra │
│ nch>; leaves: DocEntry[] }; │
│ + const root: Branch = { children: new Map( │
│ ), leaves: [] }; │
│ + │
│ + for (const d of docsOrder(docs)) { │
│ + const segments = d.id.split("/"); │
│ + let branch = root; │
│ + for (const seg of segments.slice(0, -1) │
│ ) { │
│ + if (!branch.children.has(seg)) branch │
│ .children.set(seg, { children: new Map(), le │
│ aves: [] }); │
│ + branch = branch.children.get(seg)!; │
│ + } │
│ + branch.leaves.push(d); │
│ + } │
│ + │
│ + const toNodes = (b: Branch, depth: number │
│ ): TreeNode[] => [ │
│ + ...b.leaves.map((d) => ({ │
│ + label: d.title.toUpperCase(), │
│ + href: `/docs/${d.id}/`, │
│ + })), │
│ + ...[...b.children.entries()].map(([seg, │
│ child]) => ({ │
│ + label: depth === 0 ? `/${seg.toUpperC │
│ ase()}` : seg.toUpperCase(), │
│ + children: toNodes(child, depth + 1), │
│ + })), │
│ + ]; │
│ + return toNodes(root, 0); │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/remark-wikilinks.ts ─────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/remark-wikilinks.ts b/ooknet-design/src/lib/rem │
│ ark-wikilinks.ts │
│ new file mode 100644 │
│ index 0000000..b309b84 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/remark-wikilinks.ts │
│ @@ -0,0 +1,64 @@ │
│ +// [[target]] and [[target|alias]] ? links, resolved against the wiki │
│ +// map at build time. Unresolved targets render as soft dead links, in │
│ +// the wiki tradition, rather than failing the build mid-draft. │
│ +import { buildWikiMap, resolveWikilink, type WikiMap } from "./wikilinks"; │
│ + │
│ +const WIKI_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; │
│ + │
│ +interface MdNode { │
│ + type: string; │
│ + value?: string; │
│ + children?: MdNode[]; │
│ + [key: string]: unknown; │
│ +} │
│ + │
│ +const esc = (s: string) => │
│ + s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); │
│ + │
│ +export function wikiNodes(text: string, map: WikiMap): MdNode[] | null { │
│ + if (!text.includes("[[")) return null; │
│ + const out: MdNode[] = []; │
│ + let last = 0; │
│ + for (const m of text.matchAll(WIKI_RE)) { │
│ + if (m.index! > last) out.push({ type: "text", value: text.slice(last, m.index │
│ ) }); │
│ + const target = m[1].trim(); │
│ + const label = (m[2] ?? m[1]).trim(); │
│ + const url = resolveWikilink(target, map); │
│ + out.push( │
│ + url │
│ + ? { │
│ + type: "link", │
│ + url, │
│ + children: [{ type: "text", value: label }], │
│ + data: { hProperties: { className: ["wikilink"] } }, │
│ + } │
│ + : { type: "html", value: `<span class="wikilink-dead" title="unresolved: │
│ ${esc(target)}">${esc(label)}</span>` }, │
│ + ); │
│ + last = m.index! + m[0].length; │
│ + } │
│ + if (!out.length) return null; │
│ + if (last < text.length) out.push({ type: "text", value: text.slice(last) }); │
│ + return out; │
│ +} │
│ + │
│ +function walk(node: MdNode, map: WikiMap) { │
│ + if (!node.children) return; │
│ + if (node.type === "link") return; // don't rewrite inside real links │
│ + node.children = node.children.flatMap((child) => { │
│ + if (child.type === "text" && child.value) { │
│ + const replaced = wikiNodes(child.value, map); │
│ + if (replaced) return replaced; │
│ + } │
│ + walk(child, map); │
│ + return [child]; │
│ + }); │
│ +} │
│ + │
│ +let cache: WikiMap | null = null; │
│ + │
│ +export function remarkWikilinks() { │
│ + return (tree: MdNode) => { │
│ + cache ??= buildWikiMap(); │
│ + walk(tree, cache); │
│ + }; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...n/src/lib/remark-wikilinks.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/remark-wi │
│ kilinks.ts b/ooknet-design/src/lib/remark-wi │
│ kilinks.ts │
│ new file mode 100644 │
│ index 0000000..b309b84 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/remark-wikilinks │
│ .ts │
│ @@ -0,0 +1,64 @@ │
│ +// [[target]] and [[target|alias]] ? links, │
│ resolved against the wiki │
│ +// map at build time. Unresolved targets re │
│ nder as soft dead links, in │
│ +// the wiki tradition, rather than failing │
│ the build mid-draft. │
│ +import { buildWikiMap, resolveWikilink, typ │
│ e WikiMap } from "./wikilinks"; │
│ + │
│ +const WIKI_RE = /\[\[([^\]|]+)(?:\|([^\]]+) │
│ )?\]\]/g; │
│ + │
│ +interface MdNode { │
│ + type: string; │
│ + value?: string; │
│ + children?: MdNode[]; │
│ + [key: string]: unknown; │
│ +} │
│ + │
│ +const esc = (s: string) => │
│ + s.replace(/&/g, "&").replace(/</g, "& │
│ lt;").replace(/>/g, ">"); │
│ + │
│ +export function wikiNodes(text: string, map │
│ : WikiMap): MdNode[] | null { │
│ + if (!text.includes("[[")) return null; │
│ + const out: MdNode[] = []; │
│ + let last = 0; │
│ + for (const m of text.matchAll(WIKI_RE)) { │
│ + if (m.index! > last) out.push({ type: " │
│ text", value: text.slice(last, m.index) }); │
│ + const target = m[1].trim(); │
│ + const label = (m[2] ?? m[1]).trim(); │
│ + const url = resolveWikilink(target, map │
│ ); │
│ + out.push( │
│ + url │
│ + ? { │
│ + type: "link", │
│ + url, │
│ + children: [{ type: "text", valu │
│ e: label }], │
│ + data: { hProperties: { classNam │
│ e: ["wikilink"] } }, │
│ + } │
│ + : { type: "html", value: `<span cla │
│ ss="wikilink-dead" title="unresolved: ${esc( │
│ target)}">${esc(label)}</span>` }, │
│ + ); │
│ + last = m.index! + m[0].length; │
│ + } │
│ + if (!out.length) return null; │
│ + if (last < text.length) out.push({ type: │
│ "text", value: text.slice(last) }); │
│ + return out; │
│ +} │
│ + │
│ +function walk(node: MdNode, map: WikiMap) { │
│ + if (!node.children) return; │
│ + if (node.type === "link") return; // don' │
│ t rewrite inside real links │
│ + node.children = node.children.flatMap((ch │
│ ild) => { │
│ + if (child.type === "text" && child.valu │
│ e) { │
│ + const replaced = wikiNodes(child.valu │
│ e, map); │
│ + if (replaced) return replaced; │
│ + } │
│ + walk(child, map); │
│ + return [child]; │
│ + }); │
│ +} │
│ + │
│ +let cache: WikiMap | null = null; │
│ + │
│ +export function remarkWikilinks() { │
│ + return (tree: MdNode) => { │
│ + cache ??= buildWikiMap(); │
│ + walk(tree, cache); │
│ + }; │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/wikilinks.test.ts ───────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks.test.ts b/ooknet-design/src/lib/wikil │
│ inks.test.ts │
│ new file mode 100644 │
│ index 0000000..7ee1d0f │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/wikilinks.test.ts │
│ @@ -0,0 +1,46 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { buildWikiMap, resolveWikilink } from "./wikilinks"; │
│ +import { wikiNodes } from "./remark-wikilinks"; │
│ + │
│ +describe("buildWikiMap", () => { │
│ + const map = buildWikiMap(); │
│ + │
│ + it("indexes notes by id, note number, and title", () => { │
│ + expect(resolveWikilink("n0-style-sheet", map)).toBe("/notes/n0-style-sheet/") │
│ ; │
│ + expect(resolveWikilink("N0", map)).toBe("/notes/n0-style-sheet/"); │
│ + expect(resolveWikilink("Style Sheet", map)).toBe("/notes/n0-style-sheet/"); │
│ + }); │
│ + │
│ + it("is case-insensitive and trims", () => { │
│ + expect(resolveWikilink(" n217 ", map)).toBe("/notes/n217-entropy-bounds/"); │
│ + }); │
│ + │
│ + it("returns null for unknown targets", () => { │
│ + expect(resolveWikilink("N999", map)).toBeNull(); │
│ + }); │
│ +}); │
│ + │
│ +describe("wikiNodes", () => { │
│ + const map = new Map([["n217", "/notes/n217-entropy-bounds/"]]); │
│ + │
│ + it("leaves plain text untouched", () => { │
│ + expect(wikiNodes("no links here", map)).toBeNull(); │
│ + }); │
│ + │
│ + it("splits text around a resolved link", () => { │
│ + const nodes = wikiNodes("see [[N217]] for bounds", map)!; │
│ + expect(nodes.map((n) => n.type)).toEqual(["text", "link", "text"]); │
│ + expect(nodes[1].url).toBe("/notes/n217-entropy-bounds/"); │
│ + }); │
│ + │
│ + it("uses the alias as the label", () => { │
│ + const nodes = wikiNodes("[[N217|the entropy note]]", map)!; │
│ + expect((nodes[0].children as { value: string }[])[0].value).toBe("the entropy │
│ note"); │
│ + }); │
│ + │
│ + it("renders unresolved targets as dead-link spans", () => { │
│ + const nodes = wikiNodes("[[N999]]", map)!; │
│ + expect(nodes[0].type).toBe("html"); │
│ + expect(nodes[0].value).toContain("wikilink-dead"); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ign/src/lib/wikilinks.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks │
│ .test.ts b/ooknet-design/src/lib/wikilinks.t │
│ est.ts │
│ new file mode 100644 │
│ index 0000000..7ee1d0f │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/wikilinks.test.t │
│ s │
│ @@ -0,0 +1,46 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { buildWikiMap, resolveWikilink } fr │
│ om "./wikilinks"; │
│ +import { wikiNodes } from "./remark-wikilin │
│ ks"; │
│ + │
│ +describe("buildWikiMap", () => { │
│ + const map = buildWikiMap(); │
│ + │
│ + it("indexes notes by id, note number, and │
│ title", () => { │
│ + expect(resolveWikilink("n0-style-sheet" │
│ , map)).toBe("/notes/n0-style-sheet/"); │
│ + expect(resolveWikilink("N0", map)).toBe │
│ ("/notes/n0-style-sheet/"); │
│ + expect(resolveWikilink("Style Sheet", m │
│ ap)).toBe("/notes/n0-style-sheet/"); │
│ + }); │
│ + │
│ + it("is case-insensitive and trims", () => │
│ { │
│ + expect(resolveWikilink(" n217 ", map) │
│ ).toBe("/notes/n217-entropy-bounds/"); │
│ + }); │
│ + │
│ + it("returns null for unknown targets", () │
│ => { │
│ + expect(resolveWikilink("N999", map)).to │
│ BeNull(); │
│ + }); │
│ +}); │
│ + │
│ +describe("wikiNodes", () => { │
│ + const map = new Map([["n217", "/notes/n21 │
│ 7-entropy-bounds/"]]); │
│ + │
│ + it("leaves plain text untouched", () => { │
│ + expect(wikiNodes("no links here", map)) │
│ .toBeNull(); │
│ + }); │
│ + │
│ + it("splits text around a resolved link", │
│ () => { │
│ + const nodes = wikiNodes("see [[N217]] f │
│ or bounds", map)!; │
│ + expect(nodes.map((n) => n.type)).toEqua │
│ l(["text", "link", "text"]); │
│ + expect(nodes[1].url).toBe("/notes/n217- │
│ entropy-bounds/"); │
│ + }); │
│ + │
│ + it("uses the alias as the label", () => { │
│ + const nodes = wikiNodes("[[N217|the ent │
│ ropy note]]", map)!; │
│ + expect((nodes[0].children as { value: s │
│ tring }[])[0].value).toBe("the entropy note" │
│ ); │
│ + }); │
│ + │
│ + it("renders unresolved targets as dead-li │
│ nk spans", () => { │
│ + const nodes = wikiNodes("[[N999]]", map │
│ )!; │
│ + expect(nodes[0].type).toBe("html"); │
│ + expect(nodes[0].value).toContain("wikil │
│ ink-dead"); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/wikilinks.ts ────────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks.ts b/ooknet-design/src/lib/wikilinks. │
│ ts │
│ new file mode 100644 │
│ index 0000000..eecda2d │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/wikilinks.ts │
│ @@ -0,0 +1,55 @@ │
│ +// Wikilink resolution. The map is built by scanning content frontmatter │
│ +// directly (remark plugins run outside astro:content), keyed by every │
│ +// name an author might reach for: note number, id, title, doc path. │
│ +import { readdirSync, readFileSync } from "node:fs"; │
│ +import { join } from "node:path"; │
│ + │
│ +export type WikiMap = Map<string, string>; │
│ + │
│ +const FM_RE = /^---\n([\s\S]*?)\n---/; │
│ + │
│ +function fmField(fm: string, key: string): string | null { │
│ + const m = fm.match(new RegExp(`^${key}:\\s*["']?(.+?)["']?\\s*$`, "m")); │
│ + return m ? m[1] : null; │
│ +} │
│ + │
│ +function mdFiles(dir: string, prefix = ""): string[] { │
│ + let out: string[] = []; │
│ + for (const entry of readdirSync(dir, { withFileTypes: true })) { │
│ + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; │
│ + if (entry.isDirectory()) out = out.concat(mdFiles(join(dir, entry.name), rel) │
│ ); │
│ + else if (entry.name.endsWith(".md")) out.push(rel); │
│ + } │
│ + return out; │
│ +} │
│ + │
│ +function scan(map: WikiMap, dir: string, urlFor: (id: string) => string) { │
│ + let files: string[]; │
│ + try { │
│ + files = mdFiles(dir); │
│ + } catch { │
│ + return; // collection dir may not exist yet │
│ + } │
│ + for (const file of files) { │
│ + const id = file.replace(/\.md$/, ""); │
│ + const url = urlFor(id); │
│ + const fm = readFileSync(join(dir, file), "utf8").match(FM_RE)?.[1] ?? ""; │
│ + if (fmField(fm, "draft") === "true") continue; │
│ + map.set(id.toLowerCase(), url); │
│ + const note = fmField(fm, "note"); │
│ + if (note) map.set(note.toLowerCase(), url); │
│ + const title = fmField(fm, "title"); │
│ + if (title) map.set(title.toLowerCase(), url); │
│ + } │
│ +} │
│ + │
│ +export function buildWikiMap(contentDir = "src/content"): WikiMap { │
│ + const map: WikiMap = new Map(); │
│ + scan(map, join(contentDir, "notes"), (id) => `/notes/${id}/`); │
│ + scan(map, join(contentDir, "docs"), (id) => `/docs/${id}/`); │
│ + return map; │
│ +} │
│ + │
│ +export function resolveWikilink(target: string, map: WikiMap): string | null { │
│ + return map.get(target.trim().toLowerCase()) ?? null; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...t-design/src/lib/wikilinks.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks │
│ .ts b/ooknet-design/src/lib/wikilinks.ts │
│ new file mode 100644 │
│ index 0000000..eecda2d │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/wikilinks.ts │
│ @@ -0,0 +1,55 @@ │
│ +// Wikilink resolution. The map is built by │
│ scanning content frontmatter │
│ +// directly (remark plugins run outside ast │
│ ro:content), keyed by every │
│ +// name an author might reach for: note num │
│ ber, id, title, doc path. │
│ +import { readdirSync, readFileSync } from " │
│ node:fs"; │
│ +import { join } from "node:path"; │
│ + │
│ +export type WikiMap = Map<string, string>; │
│ + │
│ +const FM_RE = /^---\n([\s\S]*?)\n---/; │
│ + │
│ +function fmField(fm: string, key: string): │
│ string | null { │
│ + const m = fm.match(new RegExp(`^${key}:\\ │
│ s*["']?(.+?)["']?\\s*$`, "m")); │
│ + return m ? m[1] : null; │
│ +} │
│ + │
│ +function mdFiles(dir: string, prefix = ""): │
│ string[] { │
│ + let out: string[] = []; │
│ + for (const entry of readdirSync(dir, { wi │
│ thFileTypes: true })) { │
│ + const rel = prefix ? `${prefix}/${entry │
│ .name}` : entry.name; │
│ + if (entry.isDirectory()) out = out.conc │
│ at(mdFiles(join(dir, entry.name), rel)); │
│ + else if (entry.name.endsWith(".md")) ou │
│ t.push(rel); │
│ + } │
│ + return out; │
│ +} │
│ + │
│ +function scan(map: WikiMap, dir: string, ur │
│ lFor: (id: string) => string) { │
│ + let files: string[]; │
│ + try { │
│ + files = mdFiles(dir); │
│ + } catch { │
│ + return; // collection dir may not exist │
│ yet │
│ + } │
│ + for (const file of files) { │
│ + const id = file.replace(/\.md$/, ""); │
│ + const url = urlFor(id); │
│ + const fm = readFileSync(join(dir, file) │
│ , "utf8").match(FM_RE)?.[1] ?? ""; │
│ + if (fmField(fm, "draft") === "true") co │
│ ntinue; │
│ + map.set(id.toLowerCase(), url); │
│ + const note = fmField(fm, "note"); │
│ + if (note) map.set(note.toLowerCase(), u │
│ rl); │
│ + const title = fmField(fm, "title"); │
│ + if (title) map.set(title.toLowerCase(), │
│ url); │
│ + } │
│ +} │
│ + │
│ +export function buildWikiMap(contentDir = " │
│ src/content"): WikiMap { │
│ + const map: WikiMap = new Map(); │
│ + scan(map, join(contentDir, "notes"), (id) │
│ => `/notes/${id}/`); │
│ + scan(map, join(contentDir, "docs"), (id) │
│ => `/docs/${id}/`); │
│ + return map; │
│ +} │
│ + │
│ +export function resolveWikilink(target: str │
│ ing, map: WikiMap): string | null { │
│ + return map.get(target.trim().toLowerCase( │
│ )) ?? null; │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/pages/docs/[...slug].astro ──────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/[...slug].astro b/ooknet-design/src/page │
│ s/docs/[...slug].astro │
│ new file mode 100644 │
│ index 0000000..c2292bf │
│ --- /dev/null │
│ +++ b/ooknet-design/src/pages/docs/[...slug].astro │
│ @@ -0,0 +1,73 @@ │
│ +--- │
│ +import { getCollection, render, type CollectionEntry } from "astro:content"; │
│ +import ArticleLayout from "../../layouts/ArticleLayout/ArticleLayout.astro"; │
│ +import TopHeader from "../../components/TopHeader/TopHeader.astro"; │
│ +import Frame from "../../components/Frame/Frame.astro"; │
│ +import Pre from "../../components/Pre/Pre.astro"; │
│ +import Breadcrumbs from "../../components/Breadcrumbs/Breadcrumbs.astro"; │
│ +import TableOfContents from "../../components/TableOfContents/TableOfContents.ast │
│ ro"; │
│ +import Prose from "../../components/Prose/Prose.astro"; │
│ +import ArticlePagination from "../../components/ArticlePagination/ArticlePaginati │
│ on.astro"; │
│ +import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati │
│ on"; │
│ +import { box, center, len, rule, ruleD, row2, wrap } from "../../lib/ascii"; │
│ +import { published } from "../../lib/notes"; │
│ +import { docsOrder } from "../../lib/docs"; │
│ + │
│ +export async function getStaticPaths() { │
│ + const docs = await getCollection("docs", published); │
│ + const ordered = docsOrder( │
│ + docs.map((d) => ({ id: d.id, title: d.data.title, order: d.data.order })), │
│ + ); │
│ + const byId = new Map(docs.map((d) => [d.id, d])); │
│ + │
│ + // Pagination labels are titles, truncated to keep the buttons on the │
│ + // narrow frame. │
│ + const link = (id?: string): PageLink | null => { │
│ + if (!id) return null; │
│ + const title = byId.get(id)!.data.title.toUpperCase(); │
│ + const note = len(title) > 18 ? [...title].slice(0, 17).join("") + "..." : tit │
│ le; │
│ + return { id, note }; │
│ + }; │
│ + │
│ + return ordered.map((d, i) => ({ │
│ + params: { slug: d.id }, │
│ + props: { │
│ + doc: byId.get(d.id)!, │
│ + prev: link(ordered[i - 1]?.id), │
│ + next: link(ordered[i + 1]?.id), │
│ + }, │
│ + })); │
│ +} │
│ + │
│ +interface Props { │
│ + doc: CollectionEntry<"docs">; │
│ + prev: PageLink | null; │
│ + next: PageLink | null; │
│ +} │
│ +const { doc, prev, next } = Astro.props as Props; │
│ +const { Content, headings } = await render(doc); │
│ + │
│ +const buildTitleCard = (w: number) => { │
│ + const cardW = Math.min(46, w - 8); │
│ + const card = box({ │
│ + width: cardW, │
│ + lines: ["OOKNET DOCS", ...wrap(doc.data.title.toUpperCase(), cardW - 4).split │
│ ("\n")], │
│ + align: "center", │
│ + }); │
│ + return card.split("\n").map((line) => center(line, w)).join("\n"); │
│ +}; │
│ +--- │
│ +<ArticleLayout title={`DOCS - ${doc.data.title}`}> │
│ + <TopHeader /> │
│ + <Frame build={(w) => [row2("OOKNET DOCUMENTATION", doc.id.toUpperCase(), w), ru │
│ le(w)].join("\n")} /> │
│ + <Pre>{" "}</Pre> │
│ + <Frame build={buildTitleCard} /> │
│ + <Pre>{" "}</Pre> │
│ + <Breadcrumbs filed={`/DOCS/${doc.id.toUpperCase()}`} /> │
│ + <Frame build={(w) => ruleD(w)} /> │
│ + {headings.length >= 3 && <TableOfContents headings={headings} />} │
│ + <Prose><Content /></Prose> │
│ + <Pre>{" "}</Pre> │
│ + <ArticlePagination prev={prev} next={next} /> │
│ + <Frame build={(w) => rule(w)} /> │
│ +</ArticleLayout> │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...rc/pages/docs/[...slug].astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/[. │
│ ..slug].astro b/ooknet-design/src/pages/docs │
│ /[...slug].astro │
│ new file mode 100644 │
│ index 0000000..c2292bf │
│ --- /dev/null │
│ +++ b/ooknet-design/src/pages/docs/[...slug] │
│ .astro │
│ @@ -0,0 +1,73 @@ │
│ +--- │
│ +import { getCollection, render, type Collec │
│ tionEntry } from "astro:content"; │
│ +import ArticleLayout from "../../layouts/Ar │
│ ticleLayout/ArticleLayout.astro"; │
│ +import TopHeader from "../../components/Top │
│ Header/TopHeader.astro"; │
│ +import Frame from "../../components/Frame/F │
│ rame.astro"; │
│ +import Pre from "../../components/Pre/Pre.a │
│ stro"; │
│ +import Breadcrumbs from "../../components/B │
│ readcrumbs/Breadcrumbs.astro"; │
│ +import TableOfContents from "../../componen │
│ ts/TableOfContents/TableOfContents.astro"; │
│ +import Prose from "../../components/Prose/P │
│ rose.astro"; │
│ +import ArticlePagination from "../../compon │
│ ents/ArticlePagination/ArticlePagination.ast │
│ ro"; │
│ +import type { PageLink } from "../../compon │
│ ents/ArticlePagination/ArticlePagination"; │
│ +import { box, center, len, rule, ruleD, row │
│ 2, wrap } from "../../lib/ascii"; │
│ +import { published } from "../../lib/notes" │
│ ; │
│ +import { docsOrder } from "../../lib/docs"; │
│ + │
│ +export async function getStaticPaths() { │
│ + const docs = await getCollection("docs", │
│ published); │
│ + const ordered = docsOrder( │
│ + docs.map((d) => ({ id: d.id, title: d.d │
│ ata.title, order: d.data.order })), │
│ + ); │
│ + const byId = new Map(docs.map((d) => [d.i │
│ d, d])); │
│ + │
│ + // Pagination labels are titles, truncate │
│ d to keep the buttons on the │
│ + // narrow frame. │
│ + const link = (id?: string): PageLink | nu │
│ ll => { │
│ + if (!id) return null; │
│ + const title = byId.get(id)!.data.title. │
│ toUpperCase(); │
│ + const note = len(title) > 18 ? [...titl │
│ e].slice(0, 17).join("") + "..." : title; │
│ + return { id, note }; │
│ + }; │
│ + │
│ + return ordered.map((d, i) => ({ │
│ + params: { slug: d.id }, │
│ + props: { │
│ + doc: byId.get(d.id)!, │
│ + prev: link(ordered[i - 1]?.id), │
│ + next: link(ordered[i + 1]?.id), │
│ + }, │
│ + })); │
│ +} │
│ + │
│ +interface Props { │
│ + doc: CollectionEntry<"docs">; │
│ + prev: PageLink | null; │
│ + next: PageLink | null; │
│ +} │
│ +const { doc, prev, next } = Astro.props as │
│ Props; │
│ +const { Content, headings } = await render( │
│ doc); │
│ + │
│ +const buildTitleCard = (w: number) => { │
│ + const cardW = Math.min(46, w - 8); │
│ + const card = box({ │
│ + width: cardW, │
│ + lines: ["OOKNET DOCS", ...wrap(doc.data │
│ .title.toUpperCase(), cardW - 4).split("\n") │
│ ], │
│ + align: "center", │
│ + }); │
│ + return card.split("\n").map((line) => cen │
│ ter(line, w)).join("\n"); │
│ +}; │
│ +--- │
│ +<ArticleLayout title={`DOCS - ${doc.data.ti │
│ tle}`}> │
│ + <TopHeader /> │
│ + <Frame build={(w) => [row2("OOKNET DOCUME │
│ NTATION", doc.id.toUpperCase(), w), rule(w)] │
│ .join("\n")} /> │
│ + <Pre>{" "}</Pre> │
│ + <Frame build={buildTitleCard} /> │
│ + <Pre>{" "}</Pre> │
│ + <Breadcrumbs filed={`/DOCS/${doc.id.toUpp │
│ erCase()}`} /> │
│ + <Frame build={(w) => ruleD(w)} /> │
│ + {headings.length >= 3 && <TableOfContents │
│ headings={headings} />} │
│ + <Prose><Content /></Prose> │
│ + <Pre>{" "}</Pre> │
│ + <ArticlePagination prev={prev} next={next │
│ } /> │
│ + <Frame build={(w) => rule(w)} /> │
│ +</ArticleLayout> │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/pages/docs/index.astro ──────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/index.astro b/ooknet-design/src/pages/do │
│ cs/index.astro │
│ new file mode 100644 │
│ index 0000000..fae3fd1 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/pages/docs/index.astro │
│ @@ -0,0 +1,26 @@ │
│ +--- │
│ +import { getCollection } from "astro:content"; │
│ +import ArticleLayout from "../../layouts/ArticleLayout/ArticleLayout.astro"; │
│ +import TopHeader from "../../components/TopHeader/TopHeader.astro"; │
│ +import Frame from "../../components/Frame/Frame.astro"; │
│ +import Pre from "../../components/Pre/Pre.astro"; │
│ +import Tree from "../../components/Tree/Tree.astro"; │
│ +import { rule, ruleD, row2 } from "../../lib/ascii"; │
│ +import { published } from "../../lib/notes"; │
│ +import { docsTree } from "../../lib/docs"; │
│ + │
│ +const docs = (await getCollection("docs", published)).map((d) => ({ │
│ + id: d.id, │
│ + title: d.data.title, │
│ + order: d.data.order, │
│ +})); │
│ +const tree = docsTree(docs); │
│ +--- │
│ +<ArticleLayout title="OOKNET KB - Docs"> │
│ + <TopHeader /> │
│ + <Frame build={(w) => [row2("DOCUMENTATION", `${docs.length} pages`, w), ruleD(w │
│ )].join("\n")} /> │
│ + <Pre>{" "}</Pre> │
│ + <Tree nodes={tree} /> │
│ + <Pre>{" "}</Pre> │
│ + <Frame build={(w) => rule(w)} /> │
│ +</ArticleLayout> │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...gn/src/pages/docs/index.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/in │
│ dex.astro b/ooknet-design/src/pages/docs/ind │
│ ex.astro │
│ new file mode 100644 │
│ index 0000000..fae3fd1 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/pages/docs/index.ast │
│ ro │
│ @@ -0,0 +1,26 @@ │
│ +--- │
│ +import { getCollection } from "astro:conten │
│ t"; │
│ +import ArticleLayout from "../../layouts/Ar │
│ ticleLayout/ArticleLayout.astro"; │
│ +import TopHeader from "../../components/Top │
│ Header/TopHeader.astro"; │
│ +import Frame from "../../components/Frame/F │
│ rame.astro"; │
│ +import Pre from "../../components/Pre/Pre.a │
│ stro"; │
│ +import Tree from "../../components/Tree/Tre │
│ e.astro"; │
│ +import { rule, ruleD, row2 } from "../../li │
│ b/ascii"; │
│ +import { published } from "../../lib/notes" │
│ ; │
│ +import { docsTree } from "../../lib/docs"; │
│ + │
│ +const docs = (await getCollection("docs", p │
│ ublished)).map((d) => ({ │
│ + id: d.id, │
│ + title: d.data.title, │
│ + order: d.data.order, │
│ +})); │
│ +const tree = docsTree(docs); │
│ +--- │
│ +<ArticleLayout title="OOKNET KB - Docs"> │
│ + <TopHeader /> │
│ + <Frame build={(w) => [row2("DOCUMENTATION │
│ ", `${docs.length} pages`, w), ruleD(w)].joi │
│ n("\n")} /> │
│ + <Pre>{" "}</Pre> │
│ + <Tree nodes={tree} /> │
│ + <Pre>{" "}</Pre> │
│ + <Frame build={(w) => rule(w)} /> │
│ +</ArticleLayout> │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/pages/search.json.ts ────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/search.json.ts b/ooknet-design/src/pages/sear │
│ ch.json.ts │
│ index c4bec1c..cc5aeed 100644 │
│ --- a/ooknet-design/src/pages/search.json.ts │
│ +++ b/ooknet-design/src/pages/search.json.ts │
│ @@ -5,14 +5,25 @@ import { published, byNoteNum } from "../lib/notes"; │
│ │
│ export const GET: APIRoute = async () => { │
│ const notes = (await getCollection("notes", published)).sort(byNoteNum).reverse │
│ (); │
│ - const data = notes.map((n) => ({ │
│ - id: n.id, │
│ - note: n.data.note, │
│ - title: n.data.title, │
│ - category: n.data.meta.category, │
│ - tags: n.data.meta.tags, │
│ - filed: n.data.filed, │
│ - })); │
│ + const docs = await getCollection("docs", published); │
│ + const data = [ │
│ + ...notes.map((n) => ({ │
│ + note: n.data.note, │
│ + title: n.data.title, │
│ + category: n.data.meta.category, │
│ + tags: n.data.meta.tags, │
│ + filed: n.data.filed, │
│ + url: `/notes/${n.id}/`, │
│ + })), │
│ + ...docs.map((d) => ({ │
│ + note: "DOC", │
│ + title: d.data.title, │
│ + category: "docs", │
│ + tags: [], │
│ + filed: `/DOCS/${d.id.toUpperCase()}`, │
│ + url: `/docs/${d.id}/`, │
│ + })), │
│ + ]; │
│ return new Response(JSON.stringify(data), { │
│ headers: { "Content-Type": "application/json" }, │
│ }); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...sign/src/pages/search.json.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/search. │
│ json.ts b/ooknet-design/src/pages/search.jso │
│ n.ts │
│ index c4bec1c..cc5aeed 100644 │
│ --- a/ooknet-design/src/pages/search.json.ts │
│ +++ b/ooknet-design/src/pages/search.json.ts │
│ @@ -5,14 +5,25 @@ import { published, byNote │
│ Num } from "../lib/notes"; │
│ │
│ export const GET: APIRoute = async () => { │
│ const notes = (await getCollection("notes │
│ ", published)).sort(byNoteNum).reverse(); │
│ - const data = notes.map((n) => ({ │
│ - id: n.id, │
│ - note: n.data.note, │
│ - title: n.data.title, │
│ - category: n.data.meta.category, │
│ - tags: n.data.meta.tags, │
│ - filed: n.data.filed, │
│ - })); │
│ + const docs = await getCollection("docs", │
│ published); │
│ + const data = [ │
│ + ...notes.map((n) => ({ │
│ + note: n.data.note, │
│ + title: n.data.title, │
│ + category: n.data.meta.category, │
│ + tags: n.data.meta.tags, │
│ + filed: n.data.filed, │
│ + url: `/notes/${n.id}/`, │
│ + })), │
│ + ...docs.map((d) => ({ │
│ + note: "DOC", │
│ + title: d.data.title, │
│ + category: "docs", │
│ + tags: [], │
│ + filed: `/DOCS/${d.id.toUpperCase()}`, │
│ + url: `/docs/${d.id}/`, │
│ + })), │
│ + ]; │
│ return new Response(JSON.stringify(data), │
│ { │
│ headers: { "Content-Type": "application │
│ /json" }, │
│ }); │
└──────────────────────────────────────────────┘ [ BACK TO LOG ] ──────────────────────────────────────────────────────────────────────────────────────
OOKNET ────────────────────────────────────────────────
OOKNET