wiki/build.ts
2026-01-23 22:30:50 +01:00

137 lines
5.0 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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, "Annwans wikis 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("<head>.*?</head>", "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<void>[] = [];
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<IndexEntry> {
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<IndexEntry>[] = [];
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");
}