Un-gulpify build: articles
This commit is contained in:
parent
8ba4adf199
commit
0405a90907
3
bin/build.ts
Normal file
3
bin/build.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { articles } from "../lib/tasks/articles.js";
|
||||||
|
|
||||||
|
articles().then(() => console.log("Done."));
|
42
lib/rx-utils.ts
Normal file
42
lib/rx-utils.ts
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,91 +57,91 @@ 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);
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
final(callback) {
|
|
||||||
try {
|
|
||||||
allArticles.sort(([, a], [, 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: allArticles }),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.push(
|
return article;
|
||||||
new File({
|
}),
|
||||||
path: "index.html",
|
synchronise(),
|
||||||
contents: renderDocument(contents),
|
toArray(),
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
callback(null);
|
|
||||||
} catch (e) {
|
images.forEach(_output);
|
||||||
console.error(e);
|
|
||||||
callback(e);
|
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 }),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export const articles = () =>
|
_output(
|
||||||
src("articles/**/*.asciidoc").pipe(renderArticle()).pipe(dest("dist/"));
|
new File({
|
||||||
|
path: "index.html",
|
||||||
|
contents: renderDocument(contents),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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 }} />
|
||||||
|
|
|
@ -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()}
|
{date.toISODate()}
|
||||||
<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
14
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user