import { basename, dirname, join } from "node:path"; import asciidoctor from "asciidoctor"; 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 "rehype-prism"; import { DEFAULT_DATE, PRODUCTION } from "../environment.ts"; import rehypeExternalLinks from "rehype-external-links"; import { visit } from "unist-util-visit"; import renderLayout from "../views/layout.tsx"; import renderArticleLayout, { Article } from "../views/article.tsx"; import renderIndex from "../views/index.tsx"; import { renderToStaticMarkup } from "preact-render-to-string"; import { SITE_DESCRIPTION, SITE_TITLE } from "../constants.ts"; import { JSX } from "preact/jsx-runtime"; import { reloadAssets } from "../assets.ts"; import { lastValueFrom, mergeMap, Observable, Subscriber } from "rxjs"; import { dest, fromGlob, onComplete } from "../rx-utils.ts"; import { VFile } from "vfile"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const DOCTYPE = encoder.encode(""); 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(sink: Subscriber, articlePath: string) { return function () { return function (tree: any) { visit(tree, "element", function (node: any) { if (node.tagName !== "img") { return; } const imagePath = join(dirname(articlePath)); const image = new VFile({ path: join(basename(articlePath, ".asciidoc"), node.properties.src), value: Deno.readFileSync(imagePath), }); sink.next(image); }); }; }; } function renderDocument(root: JSX.Element): Uint8Array { const result = encoder.encode(renderToStaticMarkup(root)); const dest = new Uint8Array(DOCTYPE.length + result.length); dest.set(DOCTYPE); dest.set(result, DOCTYPE.length); return dest; } const transformArticle = (sink: Subscriber, articles: Article[]) => async (file: VFile) => { const slug = basename(file.path, ".asciidoc"); const document = Asciidoctor.load(decoder.decode(file.value as Uint8Array), { 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; } const vfile = await unified() .use(rehypeParse as any) .use(rehypeExternalLinks, { rel: "noopener", target: "_blank" }) .use(extractImages(sink, file.path)) .use(rehypePrism) .use(rehypePresetMinify as any) .use(rehypeStringify as any) .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.value = renderDocument(content); articles.push(article); sink.next(file); }; const finalizeArticles = (articles: Article[]) => (sink: Subscriber) => { 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 }), }); sink.next(new VFile({ path: "index.html", value: renderDocument(contents) })); return Promise.resolve(); }; export const articles = async () => { reloadAssets(); const articles: Article[] = []; await lastValueFrom( fromGlob("articles/**/*.asciidoc").pipe( mergeMap( (file) => new Observable((subscriber) => { transformArticle( subscriber, articles, )(file).then(() => subscriber.complete()); }), ), onComplete(finalizeArticles(articles)), mergeMap(dest("dist")), ), ); };