import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join, basename, dirname } from "node:path"; import asciidoctor from "asciidoctor"; import File from "vinyl"; import { DateTime } from "luxon"; import { unified } from "unified"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; import rehypePresetMinify from "rehype-preset-minify"; import rehypePrism from "@mapbox/rehype-prism"; import { PRODUCTION, DEFAULT_DATE } from "../environment.js"; import rehypeExternalLinks from "rehype-external-links"; import visit from "unist-util-visit"; import renderLayout from "../views/layout.js"; import renderArticleLayout, { Article } from "../views/article.js"; import renderIndex from "../views/index.js"; import { renderToStaticMarkup } from "preact-render-to-string"; import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js"; import { JSX } from "preact/jsx-runtime"; import { reloadAssets } from "../assets.js"; import { lastValueFrom } from "rxjs"; import { src, synchronise, dest, then } from "../rx-utils.js"; const Asciidoctor = asciidoctor(); const EXTENSION_REGISTRY = Asciidoctor.Extensions.create(); EXTENSION_REGISTRY.inlineMacro("abbr", function () { this.process(function (parent, target, attributes) { return this.createInline( parent, "quoted", `${target}`, ); }); }); function extractImages(array: File[], articlePath: string) { return function () { return function (tree) { visit(tree, "element", function (node: any) { if (node.tagName !== "img") { return; } const imagePath = join(dirname(articlePath)); const image = new File({ path: join(basename(articlePath, ".asciidoc"), node.properties.src), contents: readFileSync(imagePath), }); array.push(image); }); }; }; } function renderDocument(root: JSX.Element): Buffer { return Buffer.concat([Buffer.from(""), Buffer.from(renderToStaticMarkup(root))]); } const output = (prefix: string) => (file: File) => { const realPath = join(prefix, file.path); if (!existsSync(dirname(realPath))) { mkdirSync(dirname(realPath), { recursive: true }); } console.log("[-] Outputting", join(prefix, file.path)); writeFileSync(realPath, file.contents as Buffer); }; export async function articles(): Promise { reloadAssets(); const _output = output("dist"); const articles: Article[] = []; const images: File[] = []; await lastValueFrom( src("articles/**/*.asciidoc").pipe( then(async (file) => { const slug = basename(file.path, ".asciidoc"); const document = Asciidoctor.load(file.contents.toString(), { extension_registry: EXTENSION_REGISTRY, }); const date = DateTime.fromISO(document.getAttribute("docdate", { zone: "UTC" })); const article = { path: file.path, slug, date, document }; if (PRODUCTION && date.equals(DEFAULT_DATE)) { return null; } const vfile = await unified() .use(rehypeParse) .use(rehypeExternalLinks, { rel: "noopener", target: "_blank" }) .use(extractImages(images, file.path)) .use(rehypePrism) .use(rehypePresetMinify) .use(rehypeStringify) .process(document.convert()); const content = renderLayout({ title: document.getDoctitle({}) as string, meta: { description: document.getAttribute("description"), keywords: document.getAttribute("keywords"), "og:title": document.getDoctitle({}) as string, "og:type": "article", "og:article:published_time": document.getAttribute("docdate"), "og:url": `https://adaedra.eu/${slug}/`, "og:description": document.getAttribute("description"), "og:site_name": SITE_TITLE, }, Content: () => renderArticleLayout({ article, body: vfile.toString(), }), }); file.path = join(slug, "index.html"); file.contents = renderDocument(content); articles.push(article); return file; }), then(dest("dist")), synchronise(), ), ); images.forEach(_output); articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis()); const contents = renderLayout({ meta: { description: SITE_DESCRIPTION, "og:title": SITE_TITLE, "og:type": "website", "og:url": `https://adaedra.eu`, }, Content: () => renderIndex({ articles }), }); _output( new File({ path: "index.html", contents: renderDocument(contents), }), ); }