#!/usr/bin/env -S bun run import { $ } from "bun"; import { readdir } from "node:fs/promises" import { parseArgs } from "util"; import { JSDOM } from "jsdom"; import { Dirent } from "node:fs"; import { mkdir, stat } from "node:fs/promises"; enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 } let {DEBUG, INFO, WARN, ERROR, FATAL} = LogLevel let minLogLevel : LogLevel = INFO function log(level: LogLevel, message: any) { const p = Bun.argv[1]?.split("/").slice(-1)[0] ?? "build script"; switch (level) { case DEBUG: if (minLogLevel <= DEBUG) console.log(`\x1b[90m${p}: \x1b[1;45;97m [DEBUG] \x1b[0m ${message}`); break; case INFO: if (minLogLevel <= INFO) console.log(`\x1b[90m${p}: \x1b[1;44;97m [INFO] \x1b[0m ${message}`); break; case WARN: if (minLogLevel <= WARN) console.log(`\x1b[90m${p}: \x1b[1;48;5;208;97m [WARN] \x1b[0m ${message}`); break; case ERROR: if (minLogLevel <= ERROR) console.log(`\x1b[90m${p}: \x1b[1;41;97m [ERROR] \x1b[0m ${message}`); break; case FATAL: /*Cannot suppress fatal*/console.log(`\x1b[90m${p}: \x1b[1;41;97m [FATAL] ${message} \x1b[0m`); process.exit(); break; default: log(FATAL, `${level} isn't a valid log level`); } } log(INFO, "Annwan’s wiki’s custom build script v2") async function build_typst_file(file: Dirent) { let path = file.parentPath + "/" + file.name let dir = "public_html" + file.parentPath.slice(3) let slug = path.slice(4, -4) let outfile = `public_html/${slug}.html` log(INFO, `Compiling ${slug}`) await mkdir(dir, {recursive: true}) await $`typst c --features html --format html --root . ${path} ${outfile}` log(DEBUG, `Checking if ${slug} needs post processing`) let html = await Bun.file(outfile).text(); let heads = html.match(new RegExp(".*?", "sg")); if (heads != null && heads.length >= 2) { log(DEBUG, `Postprocessing ${slug}`) html = html.replace(heads[1], ""); html = html.replace(heads[0], heads[1]); await Bun.write(outfile, html); } log(INFO, `${slug} done`) } async function build_all_typst() { let files = await readdir("src", {withFileTypes: true, recursive: true}); let promises: Promise[] = []; for (let f of files) { if (f.isFile() && f.name.endsWith(".typ")) promises.push(build_typst_file(f)) } await Promise.all(promises) } async function copy_assets() { log(INFO, "Updating assets") await $`rm -rf ${minLogLevel == DEBUG ? "-v" : ""} public_html/assets/` await $`cp -r ${minLogLevel == DEBUG ? "-v" : ""} assets/ public_html/` await $`cp ${minLogLevel == DEBUG ? "-v" : ""} public_html/assets/favicon.ico public_html/favicon.ico` } type IndexEntry = { id: string; title: string; body: string; tags: string;} async function collect_data(f:string) : Promise { let id = f.slice(12, -5) log(INFO, `collecting metadata for ${id}`) let dom = await JSDOM.fromFile(f) log(DEBUG, `${id}: read`) let things = dom.window.document.getElementsByTagName("main") let body = "" for (let thing of things) body += thing.textContent; log(DEBUG, `${id}: body extracted`) let title = dom.window.document.getElementById("search-title")?.getAttribute("content") ?? "" log(DEBUG, `${id} title extracted: ${title}`) let tags = dom.window.document.getElementById("search-tags")?.getAttribute("content") ?? "" log(DEBUG, `${id}: tags extracted: ${tags}`) return {id, title, body, tags} } async function gen_index() { let files = await readdir("public_html", {withFileTypes: true, recursive: true}) log(INFO, "Generating the search index"); let promises : Promise[] = []; for (let f of files) { if (f.isFile() && f.name.endsWith(".html")) { promises.push(collect_data(f.parentPath + "/" + f.name)); } } let data : IndexEntry[] = await Promise.all(promises) log(INFO, `Writing index`) await Bun.write("public_html/index.json", JSON.stringify(data), {mode: 0o644, createPath: true}) } const args = parseArgs({ args: Bun.argv, options: { "build": {type: "boolean", short: "b"}, "assets": {type: "boolean", short: "a"}, "index": {short: "i", type: "boolean" }, "verbose": {short: "v", type: "string"}, "help": {type: "boolean", short: "h"} }, allowPositionals: true }).values if (args.verbose) { try { minLogLevel = parseInt(args.verbose); } catch { log(FATAL, `'${args.verbose}' isn't a number`); } } if (args.help) { log(INFO, `USAGE:`) log(INFO, `\t${Bun.argv[1]} [-b|--build] [-a|--assets] [-i|--index] [-v ⟨n⟩|--verbose ⟨n⟩]`) log(INFO, `\t${Bun.argv[1]} -h|--help`) } else { if (!args.build && !args.assets && !args.index) { await build_all_typst() await copy_assets() await gen_index() } if (args.build) await build_all_typst(); if (args.assets) await copy_assets(); if (args.index) await gen_index(); log(INFO, "DONE"); }