Compare commits

..

No commits in common. "4bfecd462e20b27223618669b2e2c93a49d321a0" and "6dc157e55a6ea19c3fc8f2c2ebab5efef90c9a47" have entirely different histories.

7 changed files with 138 additions and 110 deletions

View File

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

View File

@ -1,10 +1,21 @@
import { Observable, Subscriber, from, mergeMap } from "rxjs";
import { Observable, from, map } from "rxjs";
import File from "vinyl";
import { Glob } from "glob";
import { readFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { existsSync } from "node:fs";
import { writeFile, mkdir } from "node:fs/promises";
import { Glob } from "glob";
export function src(glob: string | string[]): Observable<Promise<File>> {
return from(new Glob(glob, {})).pipe(
map(async (path) => new File({ path, contents: await readFile(path) })),
);
}
export function then<T, U>(f: (v: T) => Promise<U>) {
return (observable: Observable<Promise<T>>): Observable<Promise<U>> =>
observable.pipe(map((v) => v.then(f)));
}
export function dest(prefix: string) {
return async (file: File) => {
@ -19,26 +30,30 @@ export function dest(prefix: string) {
};
}
export function onComplete<T>(f: (sink: Subscriber<T>) => Promise<void>) {
return (observable: Observable<T>) =>
new Observable<T>((subscriber) =>
export function synchronise<T>() {
return (observable: Observable<Promise<T>>): Observable<T> =>
new Observable<T>((subscriber) => {
const runningPromises = new Set<Promise<void>>();
let done = false;
observable.subscribe({
next(value) {
subscriber.next(value);
},
error(err) {
subscriber.error(err);
const promise = value.then((v) => {
runningPromises.delete(promise);
subscriber.next(v);
if (runningPromises.size === 0 && done) {
subscriber.complete();
}
});
runningPromises.add(promise);
},
complete() {
f(subscriber);
done = true;
if (runningPromises.size === 0) {
subscriber.complete();
}
},
}),
);
});
});
}
const loadFile = async (path: string): Promise<File> =>
new File({ path, contents: await readFile(path) });
export const fromGlob = (paths: string | string[]): Observable<File> =>
from(new Glob(paths, {})).pipe(mergeMap(loadFile));

View File

@ -1,4 +1,4 @@
import { readFileSync } from "node:fs";
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import asciidoctor from "asciidoctor";
import File from "vinyl";
@ -18,8 +18,8 @@ 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 { mergeMap, Observable, Subscriber } from "rxjs";
import { dest, fromGlob, onComplete } from "../rx-utils.js";
import { lastValueFrom } from "rxjs";
import { src, synchronise, dest, then } from "../rx-utils.js";
const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
@ -33,7 +33,7 @@ EXTENSION_REGISTRY.inlineMacro("abbr", function () {
});
});
function extractImages(sink: (image: File) => void, articlePath: string) {
function extractImages(array: File[], articlePath: string) {
return function () {
return function (tree) {
visit(tree, "element", function (node: any) {
@ -47,7 +47,7 @@ function extractImages(sink: (image: File) => void, articlePath: string) {
contents: readFileSync(imagePath),
});
sink(image);
array.push(image);
});
};
};
@ -57,8 +57,25 @@ function renderDocument(root: JSX.Element): Buffer {
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
}
const transformArticle =
(sink: (file: File) => void, articles: Article[]) => async (file: File) => {
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 articles: Article[] = [];
const images: File[] = [];
await lastValueFrom(
src("articles/**/*.asciidoc").pipe(
then(async (file) => {
const slug = basename(file.path, ".asciidoc");
const document = Asciidoctor.load(file.contents.toString(), {
extension_registry: EXTENSION_REGISTRY,
@ -67,13 +84,13 @@ const transformArticle =
const article = { path: file.path, slug, date, document };
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
return;
return null;
}
const vfile = await unified()
.use(rehypeParse)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(sink, file.path))
.use(extractImages(images, file.path))
.use(rehypePrism)
.use(rehypePresetMinify)
.use(rehypeStringify)
@ -90,17 +107,25 @@ const transformArticle =
"og:description": document.getAttribute("description"),
"og:site_name": SITE_TITLE,
},
Content: () => renderArticleLayout({ article, body: vfile.toString() }),
Content: () =>
renderArticleLayout({
article,
body: vfile.toString(),
}),
});
file.path = join(slug, "index.html");
file.contents = renderDocument(content);
articles.push(article);
sink(file);
};
return file;
}),
then(dest("dist")),
synchronise(),
),
);
images.forEach(_output);
const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>) => {
articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis());
const contents = renderLayout({
meta: {
@ -112,25 +137,10 @@ const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>)
Content: () => renderIndex({ articles }),
});
sink.next(new File({ path: "index.html", contents: renderDocument(contents) }));
};
export const articles = () => {
reloadAssets();
const articles = [];
return fromGlob("articles/**/*.asciidoc")
.pipe(
mergeMap(
(file) =>
new Observable<File>((subscriber) => {
transformArticle(
(f) => subscriber.next(f),
articles,
)(file).then(() => subscriber.complete());
_output(
new File({
path: "index.html",
contents: renderDocument(contents),
}),
),
onComplete(finalizeArticles(articles)),
)
.forEach(dest("dist"));
};
);
}

View File

@ -1,13 +1,13 @@
import postcss, { Result } from "postcss";
import config from "../../postcss.config.js";
import { dest, fromGlob } from "../rx-utils.js";
import { lastValueFrom, mergeMap } from "rxjs";
import { src, then, synchronise, dest } from "../rx-utils.js";
import { map, lastValueFrom } from "rxjs";
import hashPaths from "../hash.js";
export const css = () =>
lastValueFrom(
fromGlob("src/index.css").pipe(
mergeMap(
src("src/index.css").pipe(
then(
(file) =>
new Promise((resolve) =>
postcss(config.plugins)
@ -20,7 +20,8 @@ export const css = () =>
}),
),
),
synchronise(),
hashPaths("css.manifest"),
mergeMap(dest("dist/_assets")),
map(dest("dist/_assets")),
),
);

View File

@ -3,8 +3,8 @@ import tmp from "tmp";
import { execFile } from "node:child_process";
import { readFileSync, unlinkSync } from "node:fs";
import hashPaths from "../hash.js";
import { dest, fromGlob } from "../rx-utils.js";
import { mergeMap } from "rxjs";
import { src, then, synchronise, dest } from "../rx-utils.js";
import { map } from "rxjs";
const FONT_PRESETS = {
mono: { ranges: ["20-7F", "2205", "2E22-2E25", "2713", "2717"] },
@ -34,6 +34,9 @@ function compileFont(font: File): Promise<File> {
}
export const fonts = () =>
fromGlob("vendor/*.ttf")
.pipe(mergeMap(compileFont), hashPaths("fonts.manifest"))
.forEach(dest("dist/_assets"));
src("vendor/*.ttf").pipe(
then(compileFont),
synchronise(),
hashPaths("fonts.manifest"),
map(dest("dist/_assets")),
);

View File

@ -1,11 +1,14 @@
import hashPaths from "../hash.js";
import { dest, fromGlob } from "../rx-utils.js";
import { mergeMap, tap } from "rxjs";
import { src, then, synchronise, dest } from "../rx-utils.js";
import { map } from "rxjs";
export const images = () =>
fromGlob("src/*.avif")
.pipe(
tap((f) => (f.path = f.path.substring(4))),
src("src/*.avif").pipe(
then(async (file) => {
file.path = file.path.substring(4);
return file;
}),
synchronise(),
hashPaths("images.manifest"),
)
.forEach(dest("dist/_assets"));
map(dest("dist/_assets")),
);

View File

@ -1,8 +1,4 @@
import { tap } from "rxjs";
import { dest, fromGlob } from "../rx-utils.js";
import { src, dest } from "gulp";
// SVG `use` has no way of allowing cross-origin, so we need to keep them with the HTML files.
export const svg = () =>
fromGlob("src/*.svg")
.pipe(tap((f) => (f.path = f.path.substring(4))))
.forEach(dest("dist"));
export const svg = () => src("src/*.svg").pipe(dest("dist/"));