refactor(cli): drop guide page creation and streamline appendix term handling in sync script

This commit is contained in:
Prad Nukala committed 2026-10-05 12:34:17 -04:00
1 parent 8c39b3ac23
commit 099123539e
1 file changed
+29 -112
+29 -112
View File
@@ -28,17 +28,12 @@
* page is written without it and the next sync retries. Delete the key to * page is written without it and the next sync retries. Delete the key to
* refetch it. * refetch it.
* - Every tag a page carries that no campaign topic teaches (not a key of * - Every tag a page carries that no campaign topic teaches (not a key of
* `apps/docs/topic-landings.ts`) gets a placeholder guide page in the * `apps/docs/topic-landings.ts`) belongs in the Appendix glossary,
* Appendix, `appendix/<slug>.mdx`, with `list:` pointing at LeetCode's * `apps/docs/appendix.ts`, which is hand-written: sync only checks it,
* problem list for the tag. Created once (a missing or empty file), then * warning about a carried tag with no term (its badge falls back to
* hand-owned except for `sidebar.badge`, which sync keeps at the guide's * LeetCode's list), a term whose tag a topic now teaches, and a term icon
* backlink count (the problem pages carrying the tag; dropped at zero); the * that is missing, a topic's (`TOPIC_ICONS`), or another term's — every
* tag's badge on a problem page links to it. A guide may be filed into a * badge linking to the term shows it.
* category `(group)` folder (e.g. `appendix/(2-algorithms)/`) — no route
* segment, so `/appendix/<slug>` holds — and is found there by slug; a new
* placeholder lands at the `appendix/` root for a human to file (and give
* an `icon:`, which every badge linking to the guide shows; sync warns
* while one is missing or shared).
* *
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`, * The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
* ordered by topic number via a script-owned `meta.ts`). A problem outside the * ordered by topic number via a script-owned `meta.ts`). A problem outside the
@@ -68,6 +63,7 @@
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises"; import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
import { basename, dirname, join } from "node:path"; import { basename, dirname, join } from "node:path";
import { APPENDIX } from "../docs/appendix.ts";
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts"; import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
import { github } from "./github.ts"; import { github } from "./github.ts";
@@ -452,56 +448,6 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
return url; return url;
} }
// ── guides ───────────────────────────────────────────────────────
/**
* Placeholder for a tag no campaign topic teaches: the `/appendix/<slug>` page a
* problem's topic badge links to. `list:` is the tag's LeetCode problem list,
* rendered as a badge by the docs MDX `wrapper`. Written once — the page
* is hand-owned from then on.
*/
function guidePage(tag: TopicTag): string {
return [
"---",
`title: ${JSON.stringify(tag.name)}`,
`list: ${JSON.stringify(`https://leetcode.com/problem-list/${tag.slug}/`)}`,
"---",
"",
":::note",
`This guide has not been written yet. The LeetCode list above has every problem tagged ${tag.name}.`,
":::",
"",
].join("\n");
}
/**
* Folder meta for `appendix/`: a drill-in (`display: "page"`) sidebar row
* sorted ahead of the phase groups, so it sits right under the root pages
* (Introduction, Keywords, Progress).
*/
const APPENDIX_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Appendix", icon: "library", order: 0, display: "page" });\n`;
/**
* A guide's frontmatter with `sidebar.badge` set to its backlink count — the
* one key sync owns on a hand-owned guide — or dropped (with a `sidebar:` left
* empty) at zero, so the sidebar shows a count only where there is something
* to count.
*/
function withBacklinkBadge(page: string, count: number): string {
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm) return page;
let head = fm[0].replace(/^ {2}badge: .*\n/m, "");
if (count > 0) {
const line = ` badge: '${count}'`;
head = /^sidebar:$/m.test(head)
? head.replace(/^sidebar:$/m, `sidebar:\n${line}`)
: head.replace(/\n---$/, `\nsidebar:\n${line}\n---`);
} else {
head = head.replace(/^sidebar:\n(?! {2})/m, "");
}
return head + page.slice(fm[0].length);
}
// ── inline-code formatting heuristics ──────────────────────────── // ── inline-code formatting heuristics ────────────────────────────
/** Identifiers worth backticking in prose, harvested from the code. */ /** Identifiers worth backticking in prose, harvested from the code. */
@@ -756,7 +702,6 @@ const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
const tagged = new Map<string, number>(); // tag slug -> how many problem pages carry it
for (const sol of ordered) { for (const sol of ordered) {
const langs = Object.keys(sol.code).sort().join("+"); const langs = Object.keys(sol.code).sort().join("+");
@@ -801,10 +746,7 @@ for (const sol of ordered) {
report.push([workRef, name, "created"]); report.push([workRef, name, "created"]);
written = page; written = page;
} }
for (const tag of frontmatterTopics(written)) { for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag);
tags.set(tag.slug, tag);
tagged.set(tag.slug, (tagged.get(tag.slug) ?? 0) + 1);
}
// The route drops the phase `(group)` and the page's number prefix. // The route drops the phase `(group)` and the page's number prefix.
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`); redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
} }
@@ -825,48 +767,22 @@ for (const sol of ordered) {
} }
} }
// One guide page per tag that no campaign topic teaches, created when missing // Every carried tag no campaign topic teaches is an Appendix term, filed by
// (an empty file counts as missing) and never rewritten. A guide is found by // hand in apps/docs/appendix.ts: warn about the gaps and the dead weight. A
// slug anywhere under appendix/ — a human files it into a category `(group)`, // term's icon is also the icon on every problem-page badge linking to its
// which leaves its route alone — and a new one lands at the appendix/ root. A // card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge
// guide whose tag no page carries any more is left alone: it is hand-owned. // never reads as a topic's, nor another term's.
const guideTags = [...tags.values()] for (const tag of [...tags.values()].sort((a, b) => a.slug.localeCompare(b.slug))) {
.filter((tag) => !TOPIC_LANDINGS[tag.slug]) if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug])
.sort((a, b) => a.slug.localeCompare(b.slug)); console.warn(`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`);
const guideFiles = new Map<string, string>(); // slug -> path relative to DOCS
for await (const rel of new Bun.Glob("appendix/**/*.mdx").scan(DOCS)) guideFiles.set(basename(rel, ".mdx"), rel);
for (const tag of guideTags) {
const rel = guideFiles.get(tag.slug) ?? `appendix/${tag.slug}.mdx`;
const file = Bun.file(join(DOCS, rel));
if ((await file.exists()) && file.size > 0) continue;
await Bun.write(file, guidePage(tag));
report.push([`tag ${tag.slug}`, rel, "created"]);
} }
// Every guide's sidebar badge is its backlink count: the problem pages whose
// tag badge links to it, which is every page carrying the tag, since a guide's
// tag has no landing (`tagTarget()`). A guide whose tag a landing has since
// claimed has no backlinks left. Counts move with every solved problem, so the
// badge is rewritten whenever it differs; the rest of the guide is untouched.
// The same pass checks each guide's hand-picked `icon:`, which is also the icon
// on every problem-page badge linking to the guide: it must exist, must not be
// a topic group's (`TOPIC_ICONS`), so a guide badge never reads as a topic's,
// and must not be another guide's.
const topicIcons = new Set(Object.values(TOPIC_ICONS)); const topicIcons = new Set(Object.values(TOPIC_ICONS));
const guideIcons = new Map<string, string>(); // icon -> the guide that took it first const termIcons = new Map<string, string>(); // icon -> the term that took it first
for await (const rel of new Bun.Glob("appendix/**/*.mdx").scan(DOCS)) { for (const [slug, term] of Object.entries(APPENDIX)) {
const slug = basename(rel, ".mdx"); if (TOPIC_LANDINGS[slug]) console.warn(`appendix ${slug}: a campaign topic teaches it now; delete its term`);
const count = TOPIC_LANDINGS[slug] ? 0 : (tagged.get(slug) ?? 0); if (topicIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`);
const page = await Bun.file(join(DOCS, rel)).text(); else if (termIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`);
const icon = page.match(/^---\n[\s\S]*?\n---/)?.[0].match(/^icon: (\S+)$/m)?.[1]; else termIcons.set(term.icon, slug);
if (!icon) console.warn(`${rel}: no icon; its badges render without one — file it and add \`icon:\``);
else if (topicIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is a topic's (TOPIC_ICONS); pick another`);
else if (guideIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is ${guideIcons.get(icon)}'s too; pick another`);
else guideIcons.set(icon, rel);
const next = withBacklinkBadge(page, count);
if (next === page) continue;
await Bun.write(join(DOCS, rel), next);
report.push([`${count} backlinks`, rel, "badged"]);
} }
// Phase metas: created when missing (an empty file counts as missing), never // Phase metas: created when missing (an empty file counts as missing), never
@@ -878,12 +794,13 @@ for (const [dir, phase] of phases) {
report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]); report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]);
} }
// Script-owned folder metas — one per live topic folder, plus appendix/ — // Script-owned folder metas — one per live topic folder — rewritten only when
// rewritten only when they differ. // they differ.
const metas: [label: string, dir: string, meta: string][] = [ const metas: [label: string, dir: string, meta: string][] = [...groups].map(([dir, topic]) => [
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]), `topic ${topic.order ?? "-"}`,
...(guideTags.length ? [["appendix", "appendix", APPENDIX_META] as [string, string, string]] : []), dir,
]; groupMeta(topic),
]);
for (const [label, dir, meta] of metas) { for (const [label, dir, meta] of metas) {
const path = join(DOCS, dir, "meta.ts"); const path = join(DOCS, dir, "meta.ts");
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue; if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;