feat: populate note files with problem descriptions and code stubs

Add populate-notes.mjs that fetches problem descriptions and
Python/C++ code stubs from LeetCode's GraphQL API. Populated
all 197 NeetCode 150 note files with:
- Problem description (examples, constraints)
- Python code stub (function signature)
- C++ code stub (function signature + includes)

API responses cached in leetcode/.cache/leetcode/ for instant re-runs.
This commit is contained in:
2026-06-01 17:22:07 +08:00
parent e798e449bd
commit 1dec88aaf2
198 changed files with 10459 additions and 534 deletions
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env node
/**
* Populate DSA note files with problem descriptions and code stubs
* from LeetCode's GraphQL API.
*
* Usage:
* node populate-notes.mjs # populate all missing
* node populate-notes.mjs --force # overwrite existing content
* node populate-notes.mjs --dry-run # show what would change
*/
import {
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const force = args.includes("--force");
const dryRun = args.includes("--dry-run");
const roadmap = JSON.parse(
readFileSync(join(__dirname, "out/roadmap.json"), "utf8")
);
const dsaDir = join(__dirname, "../org/study_deck_02/dsa");
const cacheDir = join(__dirname, ".cache/leetcode");
mkdirSync(cacheDir, { recursive: true });
const API = "https://leetcode.com/graphql";
const DELAY_MS = 300;
const topicSlug = (name) =>
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function fetchProblem(titleSlug) {
const cachePath = join(cacheDir, `${titleSlug}.json`);
if (existsSync(cachePath)) {
return JSON.parse(readFileSync(cachePath, "utf8"));
}
const res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `query {
question(titleSlug: "${titleSlug}") {
title
content
difficulty
codeSnippets { lang langSlug code }
}
}`,
}),
});
if (!res.ok) {
console.error(` HTTP ${res.status} for ${titleSlug}`);
return null;
}
const data = await res.json();
const q = data?.data?.question;
if (!q) {
console.error(` No data for ${titleSlug}`);
return null;
}
writeFileSync(cachePath, JSON.stringify(q, null, 2), "utf8");
return q;
}
function htmlToOrg(html) {
if (!html) return "";
let text = html;
// Preserve code blocks
const codeBlocks = [];
text = text.replace(
/<pre>([\s\S]*?)<\/pre>/g,
(_, code) => {
const cleaned = code
.replace(/<[^>]+>/g, "")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&nbsp;/g, " ")
.replace(/&quot;/g, '"')
.trim();
codeBlocks.push(cleaned);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
}
);
// Inline code
text = text.replace(/<code>([\s\S]*?)<\/code>/g, "~$1~");
// Bold
text = text.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/g, "*$1*");
// Italic
text = text.replace(/<em>([\s\S]*?)<\/em>/g, "/$1/");
// Line breaks and paragraphs
text = text.replace(/<br\s*\/?>/g, "\n");
text = text.replace(/<\/p>/g, "\n");
text = text.replace(/<p[^>]*>/g, "");
// Lists
text = text.replace(/<li[^>]*>/g, "- ");
text = text.replace(/<\/li>/g, "\n");
text = text.replace(/<\/?[ou]l[^>]*>/g, "");
// Sup/sub
text = text.replace(/<sup>([\s\S]*?)<\/sup>/g, "^{$1}");
text = text.replace(/<sub>([\s\S]*?)<\/sub>/g, "_{$1}");
// Example blocks — flatten to plain text
text = text.replace(
/<div class="example-block">([\s\S]*?)<\/div>/g,
(_, inner) => inner.replace(/<[^>]+>/g, "").trim() + "\n"
);
// Strip remaining HTML tags
text = text.replace(/<[^>]+>/g, "");
// Decode HTML entities
text = text.replace(/&lt;/g, "<");
text = text.replace(/&gt;/g, ">");
text = text.replace(/&amp;/g, "&");
text = text.replace(/&nbsp;/g, " ");
text = text.replace(/&quot;/g, '"');
text = text.replace(/&#39;/g, "'");
// Collapse whitespace but preserve intentional newlines
text = text.replace(/[ \t]+/g, " ");
text = text.replace(/\n\s*\n\s*\n+/g, "\n\n");
text = text.trim();
// Restore code blocks
for (let i = 0; i < codeBlocks.length; i++) {
text = text.replace(
`__CODE_BLOCK_${i}__`,
`\n#+begin_src\n${codeBlocks[i]}\n#+end_src\n`
);
}
return text;
}
function buildNoteContent(num, name, diff, description, stubs, relPath) {
const py = stubs.find((s) => s.langSlug === "python3");
const cpp = stubs.find((s) => s.langSlug === "cpp");
let out = `#+PROPERTY: STUDY_DECK_02
* TODO ${num}. ${name} :${diff}:
:PROPERTIES:
:NEETCODE: [[file:${relPath}][${num}. ${name}]]
:END:
`;
if (description) {
out += `\n${description}\n`;
}
out += `
** TODO Approach
Write your approach here.
** TODO Python
#+begin_src python
${py ? py.code.trim() : ""}
#+end_src
** TODO C++
#+begin_src cpp
${cpp ? cpp.code.trim() : ""}
#+end_src
`;
return out;
}
// ── Main ────────────────────────────────────────────────────────────────
async function main() {
const allProblems = [];
for (const [topic, problems] of Object.entries(roadmap.problemsByTopic)) {
for (const p of problems) {
if (p.neetcode150) {
allProblems.push({ ...p, topicSlug: topicSlug(topic) });
}
}
}
console.log(`Processing ${allProblems.length} NeetCode 150 problems...`);
if (dryRun) console.log("(dry run — no files written)\n");
let populated = 0;
let skipped = 0;
let errors = 0;
for (let i = 0; i < allProblems.length; i++) {
const p = allProblems[i];
const titleSlug = p.link.replace(/\/$/, "");
const filePath = join(dsaDir, p.topicSlug, `${p.code}.org`);
process.stdout.write(`[${i + 1}/${allProblems.length}] ${p.code}...`);
if (!existsSync(filePath)) {
console.log(" SKIP (no note file)");
skipped++;
continue;
}
const existing = readFileSync(filePath, "utf8");
const hasDescription = existing.includes("** TODO Approach\nWrite your approach here.") === false ||
existing.split("\n").filter(l => l.trim() && !l.startsWith("#") && !l.startsWith(":") && !l.startsWith("*") && !l.startsWith("**") && !l.startsWith("-") && !l.startsWith("Write your approach")).length > 10;
if (!force && hasDescription && !existing.includes("Write your approach here.")) {
console.log(" SKIP (already populated)");
skipped++;
continue;
}
const question = await fetchProblem(titleSlug);
if (!question) {
console.log(" ERROR");
errors++;
await sleep(DELAY_MS);
continue;
}
const description = htmlToOrg(question.content);
const stubs = question.codeSnippets || [];
const num = p.code.split("-")[0];
const relPath = `../../roadmap.org::*${num}. ${p.name}`;
const content = buildNoteContent(
num,
p.name,
p.difficulty.toLowerCase(),
description,
stubs,
relPath
);
if (!dryRun) {
writeFileSync(filePath, content, "utf8");
}
console.log(` OK (${description.length} chars)`);
populated++;
await sleep(DELAY_MS);
}
console.log(`\nDone: ${populated} populated, ${skipped} skipped, ${errors} errors`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});