feat(sync): organize solution docs by category and add sidebar labels

This commit is contained in:
Prad Nukala
2026-08-20 13:47:54 -04:00
parent a990d912d8
commit 1b4501e419
2 changed files with 26 additions and 13 deletions
+5 -4
View File
@@ -2,7 +2,7 @@
description: Sync work/ leetcode solutions into docs/solutions/ pages (gold standard format) description: Sync work/ leetcode solutions into docs/solutions/ pages (gold standard format)
--- ---
Sync every leetcode solution under `work/` into a docs page under `docs/solutions/`, formatted like the gold standard `docs/solutions/344-reverse-string.mdx`. Extra focus (optional): $@ Sync every leetcode solution under `work/` into a docs page under `docs/solutions/`, formatted like the gold standard `docs/solutions/(two-pointers)/344-reverse-string.mdx`. Extra focus (optional): $@
## Run the script ## Run the script
@@ -16,12 +16,13 @@ bun run sync
- Globs `work/**/*.{js,py}` (`<number>.<kebab-slug>.{js,py}` under `work/<Difficulty>/<Category>/`) and groups the two language variants of a problem by leetcode number. - Globs `work/**/*.{js,py}` (`<number>.<kebab-slug>.{js,py}` under `work/<Difficulty>/<Category>/`) and groups the two language variants of a problem by leetcode number.
- Parses each file's header comment (js block comment / py docstring) into title, difficulty, statement, examples, constraints, and follow-up. - Parses each file's header comment (js block comment / py docstring) into title, difficulty, statement, examples, constraints, and follow-up.
- New problems get a full page at `docs/solutions/<leetcode-number>-<kebab-slug>.mdx`: frontmatter, `<Badge>` topic (README curriculum table, falling back to the `work/` subdirectory), `::::warning` for follow-ups/special requirements, examples, constraints, and the solution code verbatim. - New problems get a full page at `docs/solutions/(<category>)/<leetcode-number>-<kebab-slug>.mdx`: frontmatter, `<Badge>` topic (README curriculum table, falling back to the `work/` subdirectory), `::::warning` for follow-ups/special requirements, examples, constraints, and the solution code verbatim.
- The filename prefix is the leetcode problem number — it orders the sidebar numerically and is stripped from the URL (`344-reverse-string.mdx``/solutions/reverse-string`). The script renames any page whose filename drifts from `<leetcode-number>-<kebab-slug>.mdx`. - `(<category>)` is the kebab-cased `work/` subdirectory (`(array)`, `(hash-table)`, `(two-pointers)`, …) and becomes the sidebar group; the leetcode-number filename prefix orders pages numerically. Both are stripped from the URL (`(two-pointers)/344-reverse-string.mdx``/solutions/reverse-string`). The script moves any page whose path drifts from this scheme.
- Frontmatter `title` keeps the number (`'344. Reverse String'`); `sidebar.label` is the number-free problem name. The script backfills a missing `sidebar.label` on existing pages but never overwrites one.
- Already-ported pages (frontmatter title starts with the leetcode number) only get their `## Solution` section regenerated — curated prose is never touched. - Already-ported pages (frontmatter title starts with the leetcode number) only get their `## Solution` section regenerated — curated prose is never touched.
- Problems solved in both languages render as a `<CodeGroup>` (Python first, then JavaScript); single-language solutions render as a plain fence. - Problems solved in both languages render as a `<CodeGroup>` (Python first, then JavaScript); single-language solutions render as a plain fence.
The script prints the report table: work file → docs page → created / renamed / updated / skipped (already in sync). The script prints the report table: work file → docs page → created / moved / updated / skipped (already in sync).
## Review ## Review
+21 -9
View File
@@ -10,7 +10,7 @@
* renders as a <CodeGroup> (Python first); single-language solutions * renders as a <CodeGroup> (Python first); single-language solutions
* render as a plain fence. * render as a plain fence.
*/ */
import { readdir, rename } from "node:fs/promises"; import { mkdir, rename } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
const ROOT = join(import.meta.dir, ".."); const ROOT = join(import.meta.dir, "..");
@@ -284,6 +284,7 @@ function renderPage(sol: Solution, topic: string): string {
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`, `title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`, /[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
"sidebar:", "sidebar:",
` label: '${h.title.replaceAll("'", "''")}'`,
` badge: '${h.difficulty}'`, ` badge: '${h.difficulty}'`,
"---", "---",
"", "",
@@ -346,12 +347,20 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
const topics = await readmeTopics(); const topics = await readmeTopics();
/** Insert a number-free `sidebar.label` into existing frontmatter when missing. */
function ensureLabel(page: string, h: Header): string {
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm || /^ {2}label: /m.test(fm[0])) return page;
const label = ` label: '${h.title.replaceAll("'", "''")}'`;
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
}
// Existing docs pages, keyed by leetcode number from the frontmatter title. // Existing docs pages, keyed by leetcode number from the frontmatter title.
const pages = new Map<number, string>(); // num -> filename const pages = new Map<number, string>(); // num -> path relative to DOCS
for (const f of (await readdir(DOCS)).filter((f) => f.endsWith(".mdx")).sort()) { for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
const text = await Bun.file(join(DOCS, f)).text(); const text = await Bun.file(join(DOCS, rel)).text();
const t = text.match(/^title: '(\d+)\./m); const t = text.match(/^title: '(\d+)\./m);
if (t) pages.set(Number(t[1]), f); if (t) pages.set(Number(t[1]), rel);
} }
const report: [string, string, string][] = []; const report: [string, string, string][] = [];
@@ -360,8 +369,11 @@ const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
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("+");
const workRef = `${sol.num}.${sol.slug} (${langs})`; const workRef = `${sol.num}.${sol.slug} (${langs})`;
const name = `${sol.num}-${sol.slug}.mdx`; // prefix = leetcode number (numeric sidebar order, stripped from the URL) // (category) group folder + leetcode-number prefix: numeric sidebar order, both stripped from the URL.
const group = `(${sol.category.toLowerCase().replaceAll(" ", "-")})`;
const name = `${group}/${sol.num}-${sol.slug}.mdx`;
const existing = pages.get(sol.num); const existing = pages.get(sol.num);
await mkdir(join(DOCS, group), { recursive: true });
if (existing) { if (existing) {
const page = await Bun.file(join(DOCS, existing)).text(); const page = await Bun.file(join(DOCS, existing)).text();
@@ -370,15 +382,15 @@ for (const sol of ordered) {
report.push([workRef, existing, "ERROR: no '## Solution' heading"]); report.push([workRef, existing, "ERROR: no '## Solution' heading"]);
continue; continue;
} }
const next = page.slice(0, at) + solutionSection(sol); const next = ensureLabel(page.slice(0, at) + solutionSection(sol), sol.header);
const actions: string[] = []; const actions: string[] = [];
if (existing !== name) { if (existing !== name) {
await rename(join(DOCS, existing), join(DOCS, name)); await rename(join(DOCS, existing), join(DOCS, name));
actions.push("renamed"); actions.push("moved");
} }
if (next !== page) { if (next !== page) {
await Bun.write(join(DOCS, name), next); await Bun.write(join(DOCS, name), next);
actions.push("updated (solution)"); actions.push("updated");
} }
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]); report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
} else { } else {