Eleventy config

I use Eleventy for this site. The documents I write in Djot are Syncthing’d to my fileserver and the webserver this site is hosted on, which runs a simple SystemD service that just starts up 11ty’s dev server. It’s reverse-proxied via Nginx. I tried to set it up on a subdirectory of my main site, but getting the websockets required by the auto-reload script to work was just too much of a pain to be worth it.

Here’s my Eleventy config. It’s largely inherited from Una’s.

import handlebarsPlugin from '@11ty/eleventy-plugin-handlebars';
import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight';
import fs from 'node:fs/promises';
import process from 'node:process';
import path from 'node:path';
import djot from '@djot/djot';
import {parse as jik} from '@bgotink/kdl/json';
import { DateTime } from 'luxon';

export default function (cfg) {
	// plugins
	cfg.addPlugin(syntaxHighlight);
	cfg.addPlugin(handlebarsPlugin);

	cfg.setInputDirectory("/opt/garden/source/");
	// Relative to input directory
	cfg.setIncludesDirectory("_includes");
	// Keys are relative to project root; values are relative to the output folder
	cfg.addPassthroughCopy({'source/static': 'static'});

	// Additional things to watch, relative to project root
	cfg.addWatchTarget("source/_include/");
	cfg.addWatchTarget("source/eleventy.config.js");

	// Additional template format for djot
	cfg.addTemplateFormats('dj');

	// experiment
	cfg.setFrontMatterParsingOptions({
		engines: {
			kdl: (str) => jik('- {\n'+str+'\n}'),
		}
	});

	function renderDjot(c, highlight) {
		let general = (node, renderer) => {
			if (node.attributes && node.attributes.class === 'noscript') {
				return '<noscript>'+renderer.renderChildren(node)+'</noscript>';
			}
			if (node.attributes && node.attributes.datetime) {
				return '<time datetime="'+node.attributes.datetime+'">'+renderer.renderChildren(node)+'</time>';
			}
			return renderer.renderAstNodeDefault(node);
		};
		return djot.renderHTML(djot.parse(c), {
			overrides: {
				div: (node, renderer) => {
					let clz = node.attributes ? node.attributes.class : '';
					if (clz === "figure" || clz === "figcaption" || clz == "details" || clz == "summary") {
						return `<${clz}>${renderer.renderChildren(node)}</${clz}>`;
					}
					return general(node, renderer);
				},
				span: general,
				code_block: (node, renderer) => {
					if (node.lang) {
						return highlight(node.lang, node.text);
					}
					return renderer.renderAstNodeDefault(node);
				},
				verbatim: (node, renderer) => {
					if (node.attributes && node.attributes.lang) {
						return highlight(node.attributes.lang, node.text).replace(/<pre class="[^"]+">/, '').replace(/<\/pre>$/, '');
					}
					return renderer.renderAstNodeDefault(node);
				},
				symb: (node, renderer) => {
					return '<span class="symbol symbol-'+node.alias+'"></span>';
				},
				link: (node, renderer) => {
					let clz = node.attributes?.class;
					let dest;
					if (node.reference && !node.destination) {
						dest = renderer.references[node.reference]?.destination;
					} else {
						dest = node.destination;
					}
					if (clz === "pronounce") {
						return `<a class="pronounce" href="#"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"/></svg>${renderer.renderChildren(node)} <audio src="${dest}"/></a>`;
					}
					if (dest?.indexOf('data:image/svg+xml;utf8,') === 0) {
						return decodeURIComponent(dest.substring(24));
					}
					return renderer.renderAstNodeDefault(node);
				}
			}
		});
	}
	
	cfg.addExtension('dj', {
		key: 'njk',
		compile: async function(inputContent, inputPath) {
			return async (data) => {
				let highlight = this.config.javascriptFunctions.highlight;
				let noscript = (node, renderer) => {
					if (node.attributes && node.attributes.class === 'noscript') {
						return '<noscript>'+renderer.renderChildren(node)+'</noscript>';
					}
					return renderer.renderAstNodeDefault(node);
				};
				return renderDjot(await this.defaultRenderer(data), this.config.javascriptFunctions.highlight);
			};
		}
	});
	
	cfg.addExtension('djraw', {
		compile: async function(inputContent, inputPath) {
			return async (data) => {
				return renderDjot(inputContent, this.config.javascriptFunctions.highlight);
			};
		}
	});

	cfg.addFilter('keys', function(value) {
		return Object.keys(value);
	});
	cfg.addFilter('keysExceptAll', function(value) {
		return Object.keys(value).filter(k => k !== 'all');
	});
	cfg.addFilter('sortByActualModifiedTime', function(value) {
		let arr = [...value];
		arr.sort((a, b) => {
			return b.data.mtime - a.data.mtime;
		});
		return arr;
	});
	cfg.addFilter('toISO', async function(value) {
		while (typeof value === 'function') {
			value = await value(this);
		}
		return value ? new Date(value).toISOString() : '??';
	});
	cfg.addFilter('mtime', async function(value) {
		return value.eleventyComputed.mtime(value);
	});
	cfg.addFilter('templateSyntaxToHighlight', function(value) {
		if (value === 'dj') return 'markdown'; // close enough!
		if (value === 'njk,md') return 'markdown';
		if (value === 'liquid,md') return 'markdown';
		if (value === 'njk') return 'jinja2';
		return value;
	});
	cfg.addFilter('basename', function(value) {
		return path.basename(value);
	});
	cfg.addFilter("postDate", dateObj => {
		var dateTimeObj;
		if (typeof(dateObj) === "object") {
			dateTimeObj = DateTime.fromJSDate(dateObj);
		} else if (typeof(dateObj) === "number") {
			dateTimeObj = DateTime.fromMillis(dateObj);
		} else {
			return "";
		}
		dateTimeObj.setZone("America/Chicago");
  		return dateTimeObj.toLocaleString({
			month: "short",
			day: "2-digit",
			year: "numeric"
		});
	})
	cfg.addGlobalData("layout", "default.njk");
	cfg.addGlobalData("eleventyComputed.mtime", () => { return async (data) => {
		if (!data.page || !data.page.inputPath) return 0;
		let s = await fs.stat(data.page.inputPath);
		return new Date(s.mtimeMs);
	}});
	cfg.addGlobalData("eleventyComputed.srcCode", () => { return async (data) => {
		return (await fs.readFile(data.page.inputPath)).toString('utf8').replace(/data:([^\/]+)\/[^;]+;(base64,[a-zA-Z0-9+/=]+|utf8,[^\s]+)/g, '8< snip: raw $1 data');
	}});
	cfg.addGlobalData("eleventyComputed.wip", () => { return async (data) => {
		return data.classes && (data.classes === "wip" || data.classes.indexOf('wip') >= 0)
	}});
	cfg.addGlobalData("eleventyComputed.blog", () => { return async (data) => {
		return data.classes && (data.classes === "blog" || data.classes.indexOf('blog') >= 0)
	}});

	return {
		markdownTemplateEngine: 'njk',
		htmlTemplateEngine: 'njk',
	};
}