1dec88aaf2
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.
278 lines
7.1 KiB
JavaScript
278 lines
7.1 KiB
JavaScript
#!/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);
|
|
});
|