2024-06-11 23:57:32 +00:00
|
|
|
import { src, dest, parallel, watch as _watch } from "gulp";
|
|
|
|
import postcss from "gulp-postcss";
|
|
|
|
import { Transform } from "node:stream";
|
|
|
|
import pug from "pug";
|
|
|
|
import asciidoctor from "asciidoctor";
|
|
|
|
import { basename, dirname, join } from "node:path";
|
|
|
|
import Vinyl from "vinyl";
|
|
|
|
import { unified } from "unified";
|
|
|
|
import rehypeParse from "rehype-parse";
|
|
|
|
import rehypeStringify from "rehype-stringify";
|
|
|
|
import rehypePresetMinify from "rehype-preset-minify";
|
|
|
|
import rehypeExternalLinks from "rehype-external-links";
|
|
|
|
import rehypePrism from "@mapbox/rehype-prism";
|
|
|
|
import { DateTime } from "luxon";
|
2024-06-13 17:01:31 +00:00
|
|
|
import visit from "unist-util-visit";
|
2024-06-11 23:57:32 +00:00
|
|
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
|
|
import { env } from "node:process";
|
|
|
|
import tmp from "tmp";
|
|
|
|
import { execFile } from "node:child_process";
|
|
|
|
|
|
|
|
const PRODUCTION = env.NODE_ENV === "production";
|
|
|
|
const DEFAULT_DATE = DateTime.fromSeconds(Number(env.SOURCE_DATE_EPOCH)).toUTC();
|
|
|
|
const FONT_PRESETS = {
|
|
|
|
mono: { ranges: ["20-7F", "2E22-2E25", "2713", "2717"] },
|
|
|
|
text: { ranges: ["20-7F", "A0-FF", "2000-206F", "20AC"] },
|
|
|
|
};
|
2024-06-14 01:50:53 +00:00
|
|
|
const SITE_TITLE = "Ad\xE6dra's blog";
|
2024-06-12 19:51:03 +00:00
|
|
|
const SITE_DESCRIPTION = [
|
|
|
|
"Ad\xE6dra",
|
|
|
|
"Software Developper in Paris, France",
|
|
|
|
"Rust, Ruby, Typescript, Linux",
|
|
|
|
].join(" \u2022 ");
|
2024-06-11 23:57:32 +00:00
|
|
|
|
|
|
|
const Asciidoctor = asciidoctor();
|
|
|
|
|
|
|
|
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
|
|
|
|
EXTENSION_REGISTRY.inlineMacro("abbr", function () {
|
|
|
|
this.process((parent, target, attributes) => {
|
|
|
|
return this.createInline(
|
|
|
|
parent,
|
|
|
|
"quoted",
|
|
|
|
`<abbr title="${attributes["$positional"]}">${target}</abbr>`,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const extractImages = (gulp, file) => () => (tree) => {
|
|
|
|
visit(tree, "element", (node) => {
|
|
|
|
if (node.tagName !== "img") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const path = join(dirname(file.path), node.properties.src);
|
|
|
|
const image = new Vinyl({
|
|
|
|
path: join(basename(file.path, ".asciidoc"), node.properties.src),
|
|
|
|
contents: readFileSync(path),
|
|
|
|
});
|
|
|
|
|
|
|
|
gulp.push(image);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2024-06-12 15:58:42 +00:00
|
|
|
const asset = env.ASSETS_HOSTS
|
|
|
|
? (path) => new URL(path, env.ASSETS_HOSTS).toString()
|
2024-06-11 23:57:32 +00:00
|
|
|
: (path) => join("/_assets/", path);
|
|
|
|
|
|
|
|
const renderArticle = () => {
|
|
|
|
const allArticles = [];
|
|
|
|
const renderLayout = pug.compileFile("src/layout.pug");
|
|
|
|
const renderArticleLayout = pug.compileFile("src/article.pug");
|
|
|
|
|
|
|
|
return new Transform({
|
|
|
|
readableObjectMode: true,
|
|
|
|
writableObjectMode: true,
|
|
|
|
async transform(file, _, callback) {
|
|
|
|
try {
|
|
|
|
const slug = basename(file.path, ".asciidoc");
|
|
|
|
const article = Asciidoctor.load(file.contents, {
|
|
|
|
extension_registry: EXTENSION_REGISTRY,
|
|
|
|
});
|
|
|
|
|
|
|
|
const date = DateTime.fromISO(article.getAttribute("docdate"), {
|
|
|
|
zone: "UTC",
|
|
|
|
});
|
|
|
|
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
|
|
|
|
callback(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
allArticles.push([slug, date, article]);
|
|
|
|
|
|
|
|
const vfile = await unified()
|
|
|
|
.use(rehypeParse)
|
|
|
|
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
|
|
|
|
.use(extractImages(this, file))
|
|
|
|
.use(rehypePrism)
|
|
|
|
.use(rehypePresetMinify)
|
|
|
|
.use(rehypeStringify)
|
|
|
|
.process(article.convert());
|
|
|
|
const content = renderLayout({
|
2024-06-14 01:50:53 +00:00
|
|
|
SITE_TITLE,
|
2024-06-11 23:57:32 +00:00
|
|
|
asset,
|
|
|
|
title: article.getDoctitle(),
|
2024-06-12 19:51:03 +00:00
|
|
|
meta: {
|
|
|
|
description: article.getAttribute("description"),
|
|
|
|
keywords: article.getAttribute("keywords"),
|
|
|
|
"og:title": article.getDoctitle(),
|
|
|
|
"og:type": "article",
|
|
|
|
"og:article:published_time": article.getAttribute("docdate"),
|
|
|
|
"og:url": `https://adaedra.eu/${slug}/`,
|
|
|
|
"og:image":
|
|
|
|
"https://adaedra.blob.core.windows.net/blog-assets/cariboudev.avif",
|
|
|
|
"og:image:alt": "Ad\xE6dra's mascot",
|
|
|
|
"og:description": article.getAttribute("description"),
|
|
|
|
"og:locale": "en_GB",
|
2024-06-14 01:50:53 +00:00
|
|
|
"og:site_name": SITE_TITLE,
|
2024-06-12 19:51:03 +00:00
|
|
|
},
|
2024-06-11 23:57:32 +00:00
|
|
|
render() {
|
|
|
|
return renderArticleLayout({
|
|
|
|
asset,
|
|
|
|
article,
|
|
|
|
date,
|
|
|
|
DateTime,
|
|
|
|
body: vfile.toString(),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
file.contents = Buffer.from(content);
|
|
|
|
file.path = join(slug, "index.html");
|
|
|
|
file.base = null;
|
|
|
|
|
|
|
|
callback(null, file);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
callback(e);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
final(callback) {
|
|
|
|
try {
|
|
|
|
allArticles.sort(([, a], [, b]) => b.diff(a).toMillis());
|
|
|
|
const renderIndex = pug.compileFile("src/index.pug");
|
|
|
|
const contents = renderLayout({
|
2024-06-14 02:14:43 +00:00
|
|
|
SITE_TITLE,
|
2024-06-11 23:57:32 +00:00
|
|
|
asset,
|
2024-06-12 19:51:03 +00:00
|
|
|
meta: {
|
|
|
|
description: SITE_DESCRIPTION,
|
2024-06-14 01:50:53 +00:00
|
|
|
"og:title": SITE_TITLE,
|
2024-06-12 19:51:03 +00:00
|
|
|
"og:type": "website",
|
|
|
|
"og:url": `https://adaedra.eu/`,
|
|
|
|
"og:image":
|
|
|
|
"https://adaedra.blob.core.windows.net/blog-assets/cariboudev.avif",
|
|
|
|
"og:image:alt": "Ad\xE6dra's mascot",
|
|
|
|
"og:description": SITE_DESCRIPTION,
|
|
|
|
"og:locale": "en_GB",
|
|
|
|
},
|
2024-06-11 23:57:32 +00:00
|
|
|
render() {
|
|
|
|
return renderIndex({ articles: allArticles, asset });
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.push(
|
|
|
|
new Vinyl({
|
|
|
|
path: "index.html",
|
|
|
|
contents: Buffer.from(contents),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
callback(e);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const articles = () =>
|
|
|
|
src("articles/**/*.asciidoc").pipe(renderArticle()).pipe(dest("dist/"));
|
|
|
|
|
|
|
|
export const images = () => src("src/*.avif", { encoding: false }).pipe(dest("dist/_assets/"));
|
|
|
|
|
2024-06-12 18:51:19 +00:00
|
|
|
// SVG use has no way of allowing cross-origin, so we need to keep them with the HTML files.
|
|
|
|
export const svg = () => src("src/*.svg").pipe(dest("dist/"));
|
2024-06-11 23:57:32 +00:00
|
|
|
|
|
|
|
export const css = () => src("src/index.css").pipe(postcss()).pipe(dest("dist/_assets/"));
|
|
|
|
|
|
|
|
const compileFont = () =>
|
|
|
|
new Transform({
|
|
|
|
readableObjectMode: true,
|
|
|
|
writableObjectMode: true,
|
|
|
|
transform(chunk, _, callback) {
|
|
|
|
const [, variant, weight] = /([A-Z][a-z]+)-(\w+)\.ttf$/.exec(chunk.basename);
|
|
|
|
const tmpOutput = tmp.fileSync({ discardDescriptor: true });
|
|
|
|
const unicodes = FONT_PRESETS[variant.toLowerCase()].ranges;
|
|
|
|
|
|
|
|
execFile("pyftsubset", [
|
|
|
|
chunk.path,
|
|
|
|
`--unicodes=${unicodes.join(",")}`,
|
|
|
|
`--output-file=${tmpOutput.name}`,
|
|
|
|
"--layout-features=*",
|
|
|
|
"--flavor=woff2",
|
|
|
|
]).once("exit", () => {
|
|
|
|
const file = new Vinyl({
|
|
|
|
path: `iosevka-adaedra-${variant.toLowerCase()}-${weight.toLowerCase()}.woff2`,
|
|
|
|
contents: readFileSync(tmpOutput.name),
|
|
|
|
});
|
|
|
|
unlinkSync(tmpOutput.name);
|
|
|
|
|
|
|
|
callback(null, file);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
export const fonts = () => src("vendor/*.ttf").pipe(compileFont()).pipe(dest("dist/_assets/"));
|
|
|
|
|
|
|
|
export default parallel(fonts, articles, images, svg, css);
|
|
|
|
|
|
|
|
export const watch = () => {
|
|
|
|
_watch(["src/*.pug", "articles/**/*.asciidoc"], articles);
|
|
|
|
|
|
|
|
_watch("src/*.css", css);
|
|
|
|
_watch("src/*.avif", images);
|
|
|
|
_watch("src/*.svg", svg);
|
|
|
|
};
|