#!/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( /
([\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(/([\s\S]*?)<\/code>/g, "~$1~");
// Bold
text = text.replace(/]*>([\s\S]*?)<\/strong>/g, "*$1*");
// Italic
text = text.replace(/([\s\S]*?)<\/em>/g, "/$1/");
// Line breaks and paragraphs
text = text.replace(/
/g, "\n");
text = text.replace(/<\/p>/g, "\n");
text = text.replace(/]*>/g, "");
// Lists
text = text.replace(/
]*>/g, "- ");
text = text.replace(/<\/li>/g, "\n");
text = text.replace(/<\/?[ou]l[^>]*>/g, "");
// Sup/sub
text = text.replace(/([\s\S]*?)<\/sup>/g, "^{$1}");
text = text.replace(/([\s\S]*?)<\/sub>/g, "_{$1}");
// Example blocks — flatten to plain text
text = text.replace(
/([\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);
});