blog/lib/tasks/articles.ts

148 lines
5.3 KiB
TypeScript

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 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 { map, toArray, lastValueFrom } from "rxjs";
import { src, synchronise } 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",
`<abbr title="${attributes["$positional"]}">${target}</abbr>`,
);
});
});
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("<!doctype html>"), 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<void> {
reloadAssets();
const _output = output("dist");
const images: File[] = [];
const articles = await lastValueFrom(
src("articles/**/*.asciidoc").pipe(
map(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);
_output(file);
return article;
}),
synchronise(),
toArray(),
),
);
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),
}),
);
}