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:
@@ -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(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/"/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(/</g, "<");
|
||||
text = text.replace(/>/g, ">");
|
||||
text = text.replace(/&/g, "&");
|
||||
text = text.replace(/ /g, " ");
|
||||
text = text.replace(/"/g, '"');
|
||||
text = text.replace(/'/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);
|
||||
});
|
||||
Reference in New Issue
Block a user