Un-gulpify build: articles

This commit is contained in:
Thibault “Adædra” Hamel 2024-06-26 04:21:31 +02:00
parent 8ba4adf199
commit 0405a90907
7 changed files with 133 additions and 73 deletions

3
bin/build.ts Normal file
View File

@ -0,0 +1,3 @@
import { articles } from "../lib/tasks/articles.js";
articles().then(() => console.log("Done."));

42
lib/rx-utils.ts Normal file
View File

@ -0,0 +1,42 @@
import { Observable, from, map, mergeAll } from "rxjs";
import File from "vinyl";
import { Glob } from "glob";
import { readFile } from "node:fs/promises";
export function src(glob: string | string[]): Observable<File> {
return from(new Glob(glob, {})).pipe(
map(async (path) => new File({ path, contents: await readFile(path) })),
map(from),
mergeAll(),
);
}
export function synchronise<T>() {
return (observable: Observable<Promise<T>>): Observable<T> =>
new Observable<T>((subscriber) => {
const promiseArray = [];
let done = false;
observable.subscribe({
next(value) {
const promise = value.then((v) => {
const i = promiseArray.findIndex((i) => i == promise);
promiseArray.splice(i, 1);
subscriber.next(v);
if (promiseArray.length === 0 && done) {
subscriber.complete();
}
});
promiseArray.push(promise);
},
complete() {
done = true;
if (promiseArray.length === 0) {
console.log("[synchronise] complete (from complete)");
subscriber.complete();
}
},
});
});
}

View File

@ -1,7 +1,6 @@
import { readFileSync } from "node:fs"; import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join, basename, dirname } from "node:path"; import { join, basename, dirname } from "node:path";
import { Transform } from "node:stream"; import asciidoctor from "asciidoctor";
import asciidoctor, { Document } from "asciidoctor";
import File from "vinyl"; import File from "vinyl";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { unified } from "unified"; import { unified } from "unified";
@ -12,7 +11,6 @@ import rehypePrism from "@mapbox/rehype-prism";
import { PRODUCTION, DEFAULT_DATE } from "../environment.js"; import { PRODUCTION, DEFAULT_DATE } from "../environment.js";
import rehypeExternalLinks from "rehype-external-links"; import rehypeExternalLinks from "rehype-external-links";
import visit from "unist-util-visit"; import visit from "unist-util-visit";
import { src, dest } from "gulp";
import renderLayout from "../views/layout.js"; import renderLayout from "../views/layout.js";
import renderArticleLayout from "../views/article.js"; import renderArticleLayout from "../views/article.js";
import renderIndex from "../views/index.js"; import renderIndex from "../views/index.js";
@ -20,6 +18,8 @@ import { renderToStaticMarkup } from "preact-render-to-string";
import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js"; import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js";
import { JSX } from "preact/jsx-runtime"; import { JSX } from "preact/jsx-runtime";
import { reloadAssets } from "../assets.js"; import { reloadAssets } from "../assets.js";
import { map, toArray, lastValueFrom } from "rxjs";
import { src, synchronise } from "../rx-utils.js";
const Asciidoctor = asciidoctor(); const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create(); const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
@ -33,7 +33,7 @@ EXTENSION_REGISTRY.inlineMacro("abbr", function () {
}); });
}); });
function extractImages(gulp: Transform, file: File) { function extractImages(array: File[], articlePath: string) {
return function () { return function () {
return function (tree) { return function (tree) {
visit(tree, "element", function (node: any) { visit(tree, "element", function (node: any) {
@ -41,13 +41,13 @@ function extractImages(gulp: Transform, file: File) {
return; return;
} }
const path = join(dirname(file.path)); const imagePath = join(dirname(articlePath));
const image = new File({ const image = new File({
path: join(basename(file.path, ".asciidoc"), node.properties.src), path: join(basename(articlePath, ".asciidoc"), node.properties.src),
contents: readFileSync(path), contents: readFileSync(imagePath),
}); });
gulp.push(image); array.push(image);
}); });
}; };
}; };
@ -57,67 +57,77 @@ function renderDocument(root: JSX.Element): Buffer {
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]); return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
} }
function renderArticle() { const output = (prefix: string) => (file: File) => {
const allArticles: [string, DateTime, Document][] = []; const realPath = join(prefix, file.path);
reloadAssets(); if (!existsSync(dirname(realPath))) {
mkdirSync(dirname(realPath), { recursive: true });
}
return new Transform({ console.log("[-] Outputting", join(prefix, file.path));
objectMode: true, writeFileSync(realPath, file.contents as Buffer);
async transform(file: File, _, callback) { };
try {
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 slug = basename(file.path, ".asciidoc");
const article = Asciidoctor.load(file.contents.toString(), { const document = Asciidoctor.load(file.contents.toString(), {
extension_registry: EXTENSION_REGISTRY, extension_registry: EXTENSION_REGISTRY,
}); });
const date = DateTime.fromISO(document.getAttribute("docdate", { zone: "UTC" }));
const article = { path: file.path, slug, date, document };
const date = DateTime.fromISO(article.getAttribute("docdate"), { zone: "UTC" });
if (PRODUCTION && date.equals(DEFAULT_DATE)) { if (PRODUCTION && date.equals(DEFAULT_DATE)) {
callback(null); return null;
return;
} }
allArticles.push([slug, date, article]);
const vfile = await unified() const vfile = await unified()
.use(rehypeParse) .use(rehypeParse)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" }) .use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(this, file)) .use(extractImages(images, file.path))
.use(rehypePrism) .use(rehypePrism)
.use(rehypePresetMinify) .use(rehypePresetMinify)
.use(rehypeStringify) .use(rehypeStringify)
.process(article.convert()); .process(document.convert());
const content = renderLayout({ const content = renderLayout({
title: article.getDoctitle({}) as string, title: document.getDoctitle({}) as string,
meta: { meta: {
description: article.getAttribute("description"), description: document.getAttribute("description"),
keywords: article.getAttribute("keywords"), keywords: document.getAttribute("keywords"),
"og:title": article.getDoctitle({}) as string, "og:title": document.getDoctitle({}) as string,
"og:type": "article", "og:type": "article",
"og:article:published_time": article.getAttribute("docdate"), "og:article:published_time": document.getAttribute("docdate"),
"og:url": `https://adaedra.eu/${slug}/`, "og:url": `https://adaedra.eu/${slug}/`,
"og:description": article.getAttribute("description"), "og:description": document.getAttribute("description"),
"og:site_name": SITE_TITLE, "og:site_name": SITE_TITLE,
}, },
Content: () => Content: () =>
renderArticleLayout({ renderArticleLayout({
article, article,
date,
body: vfile.toString(), body: vfile.toString(),
}), }),
}); });
file.contents = renderDocument(content);
file.path = join(slug, "index.html"); file.path = join(slug, "index.html");
file.base = null; file.contents = renderDocument(content);
callback(null, file); _output(file);
} catch (e) {
console.error(e); return article;
callback(e); }),
} synchronise(),
}, toArray(),
final(callback) { ),
try { );
allArticles.sort(([, a], [, b]) => b.diff(a).toMillis());
images.forEach(_output);
articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis());
const contents = renderLayout({ const contents = renderLayout({
meta: { meta: {
description: SITE_DESCRIPTION, description: SITE_DESCRIPTION,
@ -125,23 +135,13 @@ function renderArticle() {
"og:type": "website", "og:type": "website",
"og:url": `https://adaedra.eu`, "og:url": `https://adaedra.eu`,
}, },
Content: () => renderIndex({ articles: allArticles }), Content: () => renderIndex({ articles }),
}); });
this.push( _output(
new File({ new File({
path: "index.html", path: "index.html",
contents: renderDocument(contents), contents: renderDocument(contents),
}), }),
); );
callback(null);
} catch (e) {
console.error(e);
callback(e);
}
},
});
} }
export const articles = () =>
src("articles/**/*.asciidoc").pipe(renderArticle()).pipe(dest("dist/"));

View File

@ -3,12 +3,13 @@ import { DateTime } from "luxon";
import { asset } from "../assets.js"; import { asset } from "../assets.js";
type Props = { type Props = {
article: Document; article: Article;
date: DateTime;
body: string; body: string;
}; };
export default ({ article, date, body }: Props) => ( export type Article = { path: string; slug: string; date: DateTime; document: Document };
export default ({ article: { document, date }, body }: Props) => (
<> <>
<nav> <nav>
<a href="/" title="Back to home page"> <a href="/" title="Back to home page">
@ -19,7 +20,7 @@ export default ({ article, date, body }: Props) => (
<article> <article>
<header class="article-header"> <header class="article-header">
{date.toLocaleString(DateTime.DATE_FULL)} {date.toLocaleString(DateTime.DATE_FULL)}
<h1 dangerouslySetInnerHTML={{ __html: article.getDocumentTitle() as string }} /> <h1 dangerouslySetInnerHTML={{ __html: document.getDocumentTitle() as string }} />
</header> </header>
<a name="content" /> <a name="content" />
<main dangerouslySetInnerHTML={{ __html: body }} /> <main dangerouslySetInnerHTML={{ __html: body }} />

View File

@ -1,6 +1,5 @@
import { Document } from "asciidoctor";
import { DateTime } from "luxon";
import { asset } from "../assets.js"; import { asset } from "../assets.js";
import { Article } from "./article.js";
const Header = () => ( const Header = () => (
<header class="index-header"> <header class="index-header">
@ -17,20 +16,20 @@ const Header = () => (
); );
type Props = { type Props = {
articles: [string, DateTime, Document][]; articles: Article[];
}; };
const Articles = ({ articles }: Props) => ( const Articles = ({ articles }: Props) => (
<> <>
<h2>Latest articles</h2> <h2>Latest articles</h2>
<ul class="index-list"> <ul class="index-list">
{articles.map(([slug, date, article]) => ( {articles.map(({ slug, date, document }) => (
<li> <li>
{date.toISODate()}&nbsp; {date.toISODate()}&nbsp;
<a <a
href={`/${slug}/`} href={`/${slug}/`}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: article.getDocumentTitle() as string, __html: document.getDocumentTitle() as string,
}} }}
/> />
</li> </li>

14
package-lock.json generated
View File

@ -24,6 +24,7 @@
"rehype": "^13.0.1", "rehype": "^13.0.1",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0", "rehype-preset-minify": "^7.0.0",
"rxjs": "^7.8.1",
"tmp": "^0.2.3", "tmp": "^0.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"unified": "^11.0.4" "unified": "^11.0.4"
@ -6031,6 +6032,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -6467,6 +6476,11 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.2", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",

View File

@ -20,6 +20,7 @@
"rehype": "^13.0.1", "rehype": "^13.0.1",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0", "rehype-preset-minify": "^7.0.0",
"rxjs": "^7.8.1",
"tmp": "^0.2.3", "tmp": "^0.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"unified": "^11.0.4" "unified": "^11.0.4"