148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
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("<!doctype html>");
|
|
|
|
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(sink: Subscriber<VFile>, 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<VFile>, 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,
|
|
attributes: { "docdate": `${DEFAULT_DATE.toISODate()}@` },
|
|
});
|
|
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<VFile>) => {
|
|
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<VFile>((subscriber) => {
|
|
transformArticle(
|
|
subscriber,
|
|
articles,
|
|
)(file).then(() => subscriber.complete());
|
|
}),
|
|
),
|
|
onComplete(finalizeArticles(articles)),
|
|
mergeMap(dest("dist")),
|
|
),
|
|
);
|
|
};
|