blog/lib/tasks/articles.ts

137 lines
5.0 KiB
TypeScript
Raw Normal View History

2024-06-27 01:28:17 +00:00
import { readFileSync } from "node:fs";
import { join, basename, dirname } from "node:path";
2024-06-26 02:21:31 +00:00
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";
2024-06-24 17:44:08 +00:00
import renderLayout from "../views/layout.js";
2024-06-26 02:38:13 +00:00
import renderArticleLayout, { Article } from "../views/article.js";
2024-06-24 17:44:08 +00:00
import renderIndex from "../views/index.js";
import { renderToStaticMarkup } from "preact-render-to-string";
2024-06-24 17:44:08 +00:00
import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js";
import { JSX } from "preact/jsx-runtime";
2024-06-24 17:31:11 +00:00
import { reloadAssets } from "../assets.js";
2024-06-27 01:36:05 +00:00
import { mergeMap, Observable, Subscriber } from "rxjs";
import { dest, fromGlob, onComplete } 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>`,
);
});
});
2024-06-27 01:28:17 +00:00
function extractImages(sink: (image: File) => void, articlePath: string) {
return function () {
return function (tree) {
visit(tree, "element", function (node: any) {
if (node.tagName !== "img") {
return;
}
2024-06-26 02:21:31 +00:00
const imagePath = join(dirname(articlePath));
const image = new File({
2024-06-26 02:21:31 +00:00
path: join(basename(articlePath, ".asciidoc"), node.properties.src),
contents: readFileSync(imagePath),
});
2024-06-27 01:28:17 +00:00
sink(image);
});
};
};
}
function renderDocument(root: JSX.Element): Buffer {
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
}
2024-06-27 01:28:17 +00:00
const transformArticle =
(sink: (file: File) => void, articles: Article[]) => async (file: 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 };
2024-06-26 02:21:31 +00:00
2024-06-27 01:28:17 +00:00
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
return;
}
2024-06-27 01:28:17 +00:00
const vfile = await unified()
.use(rehypeParse)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(sink, 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() }),
});
2024-06-27 01:28:17 +00:00
file.path = join(slug, "index.html");
file.contents = renderDocument(content);
2024-06-27 01:28:17 +00:00
articles.push(article);
sink(file);
};
2024-06-26 02:21:31 +00:00
2024-06-27 01:28:17 +00:00
const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>) => {
2024-06-26 02:21:31 +00:00
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`,
},
2024-06-26 03:30:43 +00:00
Content: () => renderIndex({ articles }),
});
2024-06-27 01:28:17 +00:00
sink.next(new File({ path: "index.html", contents: renderDocument(contents) }));
};
export const articles = () => {
reloadAssets();
const articles = [];
2024-06-27 01:36:05 +00:00
return fromGlob("articles/**/*.asciidoc")
2024-06-27 01:28:17 +00:00
.pipe(
mergeMap(
(file) =>
new Observable<File>((subscriber) => {
transformArticle(
(f) => subscriber.next(f),
articles,
)(file).then(() => subscriber.complete());
}),
),
onComplete(finalizeArticles(articles)),
)
.forEach(dest("dist"));
};