// Fragile, hacky script that finds exercises in chapters, extracts // their starting code, and collects it into a big JSON object // together with the solution code. import * as PJSON from "./pseudo_json.mjs" import * as fs from "fs" import * as path from "path" import jszip from "jszip" let output = [], failed = false; for (let file of fs.readdirSync(".").sort()) { let match = /^((\d+).*).md$/.exec(file), chapNum = match && match[2]; if (!match || chapNum == 22) continue; let text = fs.readFileSync(file, "utf8"); let meta = (/{{meta (.*)}}/.exec(text) || {1: "{}"})[1] let includes = /\bload_files: (\[.*?\])/.exec(meta) if (includes) includes = JSON.parse(includes[1]); let chapter = {number: +chapNum, id: match[1], title: text.match(/(?:^|\n)# (.*?)\n/)[1], start_code: getStartCode(text, includes), exercises: [], include: includes}; let zip = chapterZipFile(meta, chapter); let extraLinks = meta.match(/\bcode_links: (\[.*?\])/); if (extraLinks) extraLinks = JSON.parse(extraLinks[1]); if (extraLinks || zip) chapter.links = (zip ? [zip] : []).concat(extraLinks || []); function addSolution(name, file, type, num, startCode) { let solution, extra try { solution = fs.readFileSync("code/solutions/" + file, "utf8"); extra = /^\s*\s*(= 0 ? text.slice(exerciseSection) : ""; let header = /\n### (.*?)\n/g, nextHeader = /\n##+ \w/g; let num = 1; while (match = header.exec(exerciseBlock)) { nextHeader.lastIndex = header.lastIndex let foundNext = nextHeader.exec(exerciseBlock) let nextsection = foundNext ? foundNext.index : -1 for (let pos = header.lastIndex;;) { let ifdef = exerciseBlock.indexOf("{{if interactive", pos); if (ifdef == -1 || nextsection > 0 && nextsection < ifdef) break; let indef = exerciseBlock.slice(pos = ifdef + 15, exerciseBlock.indexOf("if}}", ifdef)); let sourceBlock = indef.match(/```(.*)\n([^]+?)\n```/); if (!sourceBlock || sourceBlock[1].indexOf("null") > -1) continue; let type = sourceBlock[1].indexOf("html") > -1 ? "html" : "js"; let file = chapNum + "_" + num + "_" + match[1].toLowerCase().replace(/[^\-\s\w]/g, "").replace(/\s/g, "_") + "." + type; addSolution(match[1], file, type, num, sourceBlock[2]); } ++num; } let nodeInfo = "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n"; if (chapter.number == 6) chapter.exercises.push({ name: "Borrowing a method [3rd ed]", file: "code/solutions/06_4_borrowing_a_method.js", number: "4[3]", type: "js", code: "let map = {one: true, two: true, hasOwnProperty: true};\n\n// Fix this call\nconsole.log(map.hasOwnProperty(\"one\"));\n// → true", solution: "let map = {one: true, two: true, hasOwnProperty: true};\n\nconsole.log(Object.prototype.hasOwnProperty.call(map, \"one\"));\n// → true" }) if (chapter.number == 11) chapter.exercises.push({ name: "Tracking the scalpel [3rd ed]", file: "code/solutions/11_1_tracking_the_scalpel.js", number: "1[3]", type: "js", code: "async function locateScalpel(nest) {\n // Your code here.\n}\n\nfunction locateScalpel2(nest) {\n // Your code here.\n}\n\nlocateScalpel(bigOak).then(console.log);\n// → Butcher Shop", solution: "async function locateScalpel(nest) {\n let current = nest.name;\n for (;;) {\n let next = await anyStorage(nest, current, \"scalpel\");\n if (next == current) return current;\n current = next;\n }\n}\n\nfunction locateScalpel2(nest) {\n function loop(current) {\n return anyStorage(nest, current, \"scalpel\").then(next => {\n if (next == current) return current;\n else return loop(next);\n });\n }\n return loop(nest.name);\n}\n\nlocateScalpel(bigOak).then(console.log);\n// → Butcher's Shop\nlocateScalpel2(bigOak).then(console.log);\n// → Butcher's Shop", goto: "https://eloquentjavascript.net/3rd_edition/code/#11.1" }) if (chapter.number == 20) chapter.exercises = [ {name: "Search tool", file: "code/solutions/20_1_search_tool.mjs", number: 1, type: "js", code: nodeInfo, solution: fs.readFileSync("code/solutions/20_1_search_tool.mjs", "utf8") }, {name: "Directory creation", file: "code/solutions/20_2_directory_creation.mjs", number: 2, type: "js", code: nodeInfo, solution: fs.readFileSync("code/solutions/20_2_directory_creation.mjs", "utf8") }, {name: "A public space on the web", file: "code/solutions/20_3_a_public_space_on_the_web.zip", number: 3, type: "js", code: nodeInfo, solution: "// This solutions consists of multiple files. Download it\n// though the link below.\n" } ]; if (chapter.number == 21) chapter.exercises = [ {name: "Disk persistence", file: "code/solutions/21_1_disk_persistence.mjs", number: 1, type: "js", code: nodeInfo, solution: fs.readFileSync("code/solutions/21_1_disk_persistence.mjs", "utf8") }, {name: "Comment field resets", file: "code/solutions/21_2_comment_field_resets.mjs", number: 2, type: "js", code: nodeInfo, solution: fs.readFileSync("code/solutions/21_2_comment_field_resets.mjs", "utf8") } ]; output.push(chapter); } output.push({ title: "JavaScript and Performance", number: 22, start_code: "\n\n\n", include: ["code/draw_layout.js", "code/chapter/22_fast.js"], exercises: [ {name: "Prime numbers", file: "code/solutions/22_1_prime_numbers.js", number: 1, type: "js", code: "function* primes() {\n for (let n = 2;; n++) {\n // ...\n }\n}\n\nfunction measurePrimes() {\n // ...\n}\n\nmeasurePrimes();\n", solution: fs.readFileSync("code/solutions/22_1_prime_numbers.js", "utf8") }, {name: "Faster prime numbers", file: "code/solutions/22_2_faster_prime_numbers.js", number: 2, type: "js", code: "function* primes() {\n for (let n = 2;; n++) {\n // ...\n }\n}\n\nfunction measurePrimes() {\n // ...\n}\n\nmeasurePrimes();\n", solution: fs.readFileSync("code/solutions/22_2_faster_prime_numbers.js", "utf8"), }, {name: "Pathfinding [3rd ed]", file: "code/solutions/22_1_pathfinding.js", number: "1[3]", type: "js", code: "function findPath(a, b) {\n // Your code here...\n}\n\nlet graph = treeGraph(4, 4);\nlet root = graph[0], leaf = graph[graph.length - 1];\nconsole.log(findPath(root, leaf).length);\n// → 4\n\nleaf.connect(root);\nconsole.log(findPath(root, leaf).length);\n// → 2\n", solution: fs.readFileSync("code/solutions/22_1_pathfinding.js", "utf8"), goto: "https://eloquentjavascript.net/3rd_edition/code/#22.1" }, {name: "Timing [3rd ed]", file: "code/solutions/22_2_timing.js", number: "2[3]", type: "js", code: "", solution: fs.readFileSync("code/solutions/22_2_timing.js", "utf8"), goto: "https://eloquentjavascript.net/3rd_edition/code/#22.2" }, {name: "Optimizing [3rd ed]", file: "code/solutions/22_3_optimizing.js", number: "3[3]", type: "js", code: "", solution: fs.readFileSync("code/solutions/22_3_optimizing.js", "utf8"), goto: "https://eloquentjavascript.net/3rd_edition/code/#22.3" } ] }); let usedSolutions = new Set() for (let ch of output) for (let ex of ch.exercises) usedSolutions.add(path.basename(ex.file).replace(/\..*/, "")) for (let file of fs.readdirSync("code/solutions/")) if (!usedSolutions.has(file.replace(/\..*/, ""))) { console.error("Solution file " + file + " was not used."); failed = true; } if (!failed) console.log("var chapterData = " + JSON.stringify(output, null, 2) + ";"); else process.exit(1); function prepareHTML(code, include) { return "\n" + (include || []).map(s => "\n").join("") + "\n" + code; } function guessType(code) { return /^[\s\w\n:]* !/(^|\/)_/.test(f)); let exists = fs.existsSync(name) && fs.statSync(name).mtime; if (exists && files.every(file => fs.statSync("html/" + file).mtime < exists)) return name; let zip = new jszip; for (let file of files) { zip.file(chapter.id + "/" + file, fs.readFileSync("html/" + file)); } if (data[1].indexOf("html") != -1) { let html = chapter.start_code; if (guessType(html) != "html") html = prepareHTML("", chapter.include); zip.file(chapter.id + "/index.html", html); } if (data[1].indexOf("node") != -1) { zip.file(chapter.id + "/code/load.js", fs.readFileSync("code/load.js", "utf8")); let js = chapter.start_code; if (chapter.include) js = "// load dependencies\nrequire(\"./code/load\")(" + chapter.include.map(JSON.stringify).join(", ") + ");\n\n" + js; zip.file(chapter.id + "/run_with_node.js", js); } zip.generateAsync({type: "uint8array"}).then(content => fs.writeFileSync(name, content)); return name; }