#!/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); });