mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
chore: clean up content phase organization
This commit is contained in:
1 parent
403a2d7f23
commit
bf0ac34a43
138 files changed
+1436
-368
No files matched your search
+112
-39
@@ -17,11 +17,14 @@
|
||||
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
|
||||
* plan (warm-ups) has no topic, so it groups under its category instead.
|
||||
*
|
||||
* Topic folders are real route segments (`/tree-dfs/invert-binary-tree`), not
|
||||
* `(group)` folders, so a hand-written `index.mdx` in one is the topic's
|
||||
* landing page at `/tree-dfs` and Blume links the sidebar group row to it.
|
||||
* Sync never writes or moves an `index.mdx`, and never removes a folder that
|
||||
* still holds one.
|
||||
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
||||
* The phase folder is a Blume `(group)` — a sidebar section with its own
|
||||
* script-owned `meta.ts`, adding no route segment — while the topic folder is
|
||||
* a real segment (`/tree-dfs/invert-binary-tree`), so a hand-written
|
||||
* `index.mdx` in it is the topic's landing page at `/tree-dfs` and Blume links
|
||||
* the sidebar group row to it. Sync never writes or moves an `index.mdx`, and
|
||||
* never removes a folder that still holds one. An unplanned category group has
|
||||
* no phase and sits at the content root.
|
||||
*
|
||||
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
||||
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
||||
@@ -171,12 +174,29 @@ function parseHeader(header: string): Header {
|
||||
|
||||
// ── README topic map ─────────────────────────────────────────────
|
||||
|
||||
/** A campaign phase: `## Phase N — Name · Weeks …` in the README. */
|
||||
interface Phase {
|
||||
order: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** A campaign topic: `<strong>NN. Name</strong>` in the README table. */
|
||||
interface Topic {
|
||||
order?: number; // README topic number; absent for an unplanned problem's category group
|
||||
name: string;
|
||||
phase?: Phase; // absent for an unplanned category group
|
||||
}
|
||||
|
||||
/** README phase names → the short labels the overview page uses. */
|
||||
const PHASE_ALIASES: Record<string, string> = {
|
||||
"Nodal & Grid Structures": "Nodal & Grid",
|
||||
"Hierarchical Structures": "Hierarchical",
|
||||
"Relational Structures": "Relational",
|
||||
"Decision-Space Exploration": "Decision Space",
|
||||
};
|
||||
|
||||
const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
|
||||
|
||||
const TOPIC_ALIASES: Record<string, string> = {
|
||||
"Hash-Based Lookup": "Hash Table",
|
||||
};
|
||||
@@ -224,10 +244,16 @@ const TOPIC_ICONS: Record<string, string> = {
|
||||
async function readmeTopics(): Promise<Map<number, Topic>> {
|
||||
const md = await Bun.file(join(ROOT, "README.md")).text();
|
||||
const map = new Map<number, Topic>();
|
||||
let phase: Phase | undefined;
|
||||
let topic: Topic | null = null;
|
||||
for (const line of md.split("\n")) {
|
||||
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
|
||||
if (p) {
|
||||
const name = p[2]!.replaceAll("&", "&");
|
||||
phase = { order: ROMAN.indexOf(p[1]!) + 1, name: PHASE_ALIASES[name] ?? name };
|
||||
}
|
||||
const t = line.match(/<strong>(\d+)\.\s*([^<]+)<\/strong>/);
|
||||
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim() };
|
||||
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(), phase };
|
||||
if (!topic) continue;
|
||||
const n = line.match(/<td align="center">(\d+)<\/td>/);
|
||||
if (n && !map.has(Number(n[1]))) map.set(Number(n[1]), topic);
|
||||
@@ -236,9 +262,43 @@ async function readmeTopics(): Promise<Map<number, Topic>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Blume folder meta: exact topic label (the folder slug loses `/` and case),
|
||||
* curriculum order, and the group's sidebar icon. An unplanned category group
|
||||
* (warm-ups) has no README number and sorts at 0, ahead of topic 1, so the
|
||||
* Folder slug for a topic or phase name. Blume reads a leading `<digits>-` on a
|
||||
* folder as a sidebar-order prefix and strips it from the route (`1-d-…` →
|
||||
* `/d-…`), so a slug that would start that way keeps the digits glued to the
|
||||
* next token (`1d-dynamic-programming`).
|
||||
*/
|
||||
function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.replace(/^(\d+)-/, "$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Content folder for a topic, relative to `DOCS`: `(0N-<phase>)/<topic>` for a
|
||||
* planned topic (the phase is a `(group)`, so the route is still `/<topic>`),
|
||||
* bare `<topic>` for an unplanned category group.
|
||||
*/
|
||||
function topicDir(topic: Topic): string {
|
||||
const slug = slugify(topic.name);
|
||||
return topic.phase ? `${phaseDir(topic.phase)}/${slug}` : slug;
|
||||
}
|
||||
|
||||
function phaseDir(phase: Phase): string {
|
||||
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
|
||||
}
|
||||
|
||||
/** Blume `(group)` meta for a phase: the overview page's heading, in phase order. */
|
||||
function phaseMeta(phase: Phase): string {
|
||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(`Phase ${phase.order}: ${phase.name}`)}, order: ${phase.order} });\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blume folder meta for a topic: exact label (the folder slug loses `/` and
|
||||
* case), curriculum order, the sidebar icon, and `display: "group"` so the
|
||||
* topic collapses under its phase heading. An unplanned category group
|
||||
* (warm-ups) has no README number and sorts at 0, ahead of phase 1, so the
|
||||
* sidebar opens on the basics.
|
||||
*/
|
||||
function groupMeta(topic: Topic): string {
|
||||
@@ -246,6 +306,7 @@ function groupMeta(topic: Topic): string {
|
||||
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
|
||||
const fields = [`title: ${JSON.stringify(topic.name)}`, `order: ${topic.order ?? 0}`];
|
||||
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
|
||||
fields.push(`display: "group"`);
|
||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
||||
}
|
||||
|
||||
@@ -427,6 +488,11 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
|
||||
}
|
||||
|
||||
const topics = await readmeTopics();
|
||||
// An unplanned problem whose work/ category is also a README topic name (a
|
||||
// `Hash Table` warm-up) files under that topic, in its phase, not a second
|
||||
// root-level `hash-table/`.
|
||||
const topicByName = new Map<string, Topic>();
|
||||
for (const topic of topics.values()) topicByName.set(topic.name, topic);
|
||||
|
||||
/** Insert a number-free `sidebar.label` into existing frontmatter when missing. */
|
||||
function ensureLabel(page: string, h: Header): string {
|
||||
@@ -464,29 +530,23 @@ for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).s
|
||||
|
||||
const report: [string, string, string][] = [];
|
||||
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
|
||||
const groups = new Map<string, Topic>(); // folder -> topic, for meta.ts
|
||||
const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
|
||||
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
|
||||
|
||||
for (const sol of ordered) {
|
||||
const langs = Object.keys(sol.code).sort().join("+");
|
||||
const workRef = `${sol.num}.${sol.slug} (${langs})`;
|
||||
// <topic>/ folder (a real route segment, so its index.mdx is the topic landing
|
||||
// page) + leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
||||
// An unplanned problem's category may share a folder with a numbered topic
|
||||
// (`Hash Table` warm-ups under topic 1); the numbered one owns the meta.
|
||||
const topic = topics.get(sol.num) ?? { name: sol.category };
|
||||
// Blume reads a leading `<digits>-` on a folder as a sidebar-order prefix and
|
||||
// strips it from the route (`1-d-…` → `/d-…`), so a slug that would start that
|
||||
// way keeps the digits glued to the next token (`1d-dynamic-programming`).
|
||||
const group = topic.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.replace(/^(\d+)-/, "$1");
|
||||
if (groups.get(group)?.order === undefined) groups.set(group, topic);
|
||||
const name = `${group}/${sol.num}-${sol.slug}.mdx`;
|
||||
// (0N-<phase>)/<topic>/ folder (the topic is a real route segment, so its
|
||||
// index.mdx is the landing page) + leetcode-number prefix: numeric sidebar
|
||||
// order, prefix stripped from the URL.
|
||||
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
|
||||
const dir = topicDir(topic);
|
||||
groups.set(dir, topic);
|
||||
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
|
||||
const name = `${dir}/${sol.num}-${sol.slug}.mdx`;
|
||||
const existing = pages.get(sol.num);
|
||||
await mkdir(join(DOCS, group), { recursive: true });
|
||||
await mkdir(join(DOCS, dir), { recursive: true });
|
||||
|
||||
if (existing) {
|
||||
const page = await Bun.file(join(DOCS, existing)).text();
|
||||
@@ -515,23 +575,36 @@ for (const sol of ordered) {
|
||||
}
|
||||
}
|
||||
|
||||
// Script-owned folder meta: one per live group, rewritten only when it differs.
|
||||
for (const [group, topic] of groups) {
|
||||
const path = join(DOCS, group, "meta.ts");
|
||||
const meta = groupMeta(topic);
|
||||
// Script-owned folder metas — one per live phase and topic folder — rewritten
|
||||
// only when they differ.
|
||||
const metas: [label: string, dir: string, meta: string][] = [
|
||||
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
|
||||
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
|
||||
];
|
||||
for (const [label, dir, meta] of metas) {
|
||||
const path = join(DOCS, dir, "meta.ts");
|
||||
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;
|
||||
await Bun.write(path, meta);
|
||||
report.push([`topic ${topic.order ?? "-"}`, `${group}/meta.ts`, "written"]);
|
||||
report.push([label, `${dir}/meta.ts`, "written"]);
|
||||
}
|
||||
|
||||
// A group every page left behind is dead: drop its meta and the folder itself.
|
||||
for (const dir of vacated) {
|
||||
if (groups.has(dir)) continue;
|
||||
const left = await readdir(join(DOCS, dir));
|
||||
if (left.some((f) => f !== "meta.ts")) continue;
|
||||
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
|
||||
await rmdir(join(DOCS, dir));
|
||||
report.push(["", dir, "removed (empty)"]);
|
||||
// A folder every page left behind is dead: drop its meta and the folder itself,
|
||||
// then the phase folder above it if that emptied too. A folder that still holds
|
||||
// anything else (a hand-written index.mdx, another topic) stops the climb.
|
||||
for (const start of vacated) {
|
||||
for (let dir = start; dir !== "."; dir = dirname(dir)) {
|
||||
if (groups.has(dir) || phases.has(dir)) break;
|
||||
let left: string[];
|
||||
try {
|
||||
left = await readdir(join(DOCS, dir));
|
||||
} catch {
|
||||
break; // already removed via a sibling's climb
|
||||
}
|
||||
if (left.some((f) => f !== "meta.ts")) break;
|
||||
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
|
||||
await rmdir(join(DOCS, dir));
|
||||
report.push(["", dir, "removed (empty)"]);
|
||||
}
|
||||
}
|
||||
|
||||
const w0 = Math.max(...report.map((r) => r[0].length), 9);
|
||||
|
||||
Reference in new issue
Block a user