286 lines
No EOL
9.6 KiB
JavaScript
286 lines
No EOL
9.6 KiB
JavaScript
const markdownIt = require("markdown-it");
|
|
const markdownItFootnote = require("markdown-it-footnote");
|
|
const markdownItMermaid = require('markdown-it-mermaid').default
|
|
const markdownItTaskLists = require('markdown-it-task-lists');
|
|
const markdownItMeasurements = require('./lib/measurements/plugin');
|
|
const markdownItHashtag = require('./lib/hashtags/plugin');
|
|
const markdownItStripTrailingHashtags = require('./lib/strip-trailing-hashtags/plugin');
|
|
const markdownItDetails = require('./lib/details/plugin');
|
|
const { extractTags, expandHierarchicalTags, getPostTags, getRecipeTags } = require('./lib/tags/plugin');
|
|
const { cacheBustingPlugin } = require('./lib/cache-busting/plugin');
|
|
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
|
const fs = require("fs");
|
|
const { DateTime } = require("luxon");
|
|
const siteConfig = require("./_data/site.js");
|
|
|
|
const isReleased = (post) => {
|
|
return post.data.released !== false;
|
|
}
|
|
|
|
const sharedPlugins = [
|
|
markdownItFootnote,
|
|
markdownItHashtag,
|
|
markdownItMermaid,
|
|
[markdownItTaskLists, { enabled: true, label: true, labelAfter: false }],
|
|
markdownItDetails,
|
|
markdownItMeasurements,
|
|
];
|
|
|
|
const applyPlugins = (md, plugins) =>
|
|
plugins.reduce((instance, plugin) => {
|
|
const [pluginFn, options] = Array.isArray(plugin) ? plugin : [plugin];
|
|
return instance.use(pluginFn, options), instance;
|
|
}, md);
|
|
|
|
const createMarkdownInstance = (extraPlugins = []) => {
|
|
const md = markdownIt({ html: true, breaks: true, linkify: true });
|
|
applyPlugins(md, [...sharedPlugins, ...extraPlugins]);
|
|
return md;
|
|
};
|
|
|
|
const md = createMarkdownInstance([markdownItStripTrailingHashtags]);
|
|
|
|
// Customize footnote rendering to remove brackets
|
|
md.renderer.rules.footnote_ref = (tokens, idx, options, env, slf) => {
|
|
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
|
const n = Number(tokens[idx].meta.id + 1).toString();
|
|
let refid = id;
|
|
|
|
if (tokens[idx].meta.subId > 0) {
|
|
refid += ':' + tokens[idx].meta.subId;
|
|
}
|
|
|
|
return '<sup class="footnote-ref"><a href="#fn' + id + '" id="fnref' + refid + '">' + n + '</a></sup>';
|
|
};
|
|
|
|
const tagExtractorMd = createMarkdownInstance();
|
|
|
|
module.exports = (eleventyConfig) => {
|
|
eleventyConfig.addPlugin(syntaxHighlight);
|
|
eleventyConfig.addPlugin(cacheBustingPlugin, { rootDir: __dirname });
|
|
|
|
eleventyConfig.addFilter("extractTags", (content) => extractTags(content, tagExtractorMd));
|
|
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
return extractTags(content, tagExtractorMd);
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
eleventyConfig.setLibrary("md", md);
|
|
|
|
eleventyConfig.addFilter("dateTimeFormat", (date) => {
|
|
const dt = date instanceof Date
|
|
? DateTime.fromJSDate(date, { zone: 'utc' })
|
|
: DateTime.fromISO(date, { zone: 'utc' });
|
|
// Convert to site timezone for display
|
|
const displayDt = dt.setZone(siteConfig.timezone);
|
|
return displayDt.toFormat('MMMM d, yyyy h:mm a ZZZZ');
|
|
});
|
|
|
|
eleventyConfig.addFilter("isoDateTime", (date) => {
|
|
const dt = date instanceof Date
|
|
? DateTime.fromJSDate(date, { zone: 'utc' })
|
|
: DateTime.fromISO(date, { zone: 'utc' });
|
|
return dt.toISO();
|
|
});
|
|
|
|
eleventyConfig.addFilter("isoDate", (date) => {
|
|
const dt = date instanceof Date
|
|
? DateTime.fromJSDate(date, { zone: 'utc' })
|
|
: DateTime.fromISO(date, { zone: 'utc' });
|
|
return dt.toISODate();
|
|
});
|
|
|
|
eleventyConfig.addFilter("isMoreThanHourAfter", (date1, date2) => {
|
|
if (!date1 || !date2) return false;
|
|
const toDateTime = (d) => {
|
|
if (DateTime.isDateTime(d)) return d;
|
|
if (d instanceof Date) return DateTime.fromJSDate(d, { zone: 'utc' });
|
|
return DateTime.fromISO(d, { zone: 'utc' });
|
|
};
|
|
const dt1 = toDateTime(date1);
|
|
const dt2 = toDateTime(date2);
|
|
const diff = dt1.diff(dt2, 'hours').hours;
|
|
return Math.abs(diff) > 1;
|
|
});
|
|
|
|
eleventyConfig.addCollection("posts", (collectionApi) => {
|
|
return collectionApi.getFilteredByGlob("posts/**/*.md")
|
|
.filter(isReleased)
|
|
.sort((a, b) => {
|
|
const aDate = a.data.createdAt || a.date;
|
|
const bDate = b.data.createdAt || b.date;
|
|
return aDate - bDate;
|
|
});
|
|
});
|
|
|
|
eleventyConfig.addCollection("allPosts", (collectionApi) => {
|
|
return collectionApi.getFilteredByGlob("posts/**/*.md").sort((a, b) => {
|
|
const aDate = a.data.createdAt || a.date;
|
|
const bDate = b.data.createdAt || b.date;
|
|
return aDate - bDate;
|
|
});
|
|
});
|
|
|
|
eleventyConfig.addCollection("postsBySlug", (collectionApi) => {
|
|
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
|
const slugify = eleventyConfig.getFilter("slugify");
|
|
const grouped = {};
|
|
|
|
posts.forEach(post => {
|
|
const slug = slugify(post.data.title);
|
|
if (!grouped[slug]) {
|
|
grouped[slug] = [];
|
|
}
|
|
grouped[slug].push(post);
|
|
});
|
|
|
|
Object.values(grouped).forEach(postList => {
|
|
postList.sort((a, b) => {
|
|
const aDate = a.data.createdAt || a.date;
|
|
const bDate = b.data.createdAt || b.date;
|
|
return aDate - bDate;
|
|
});
|
|
});
|
|
|
|
return grouped;
|
|
});
|
|
|
|
eleventyConfig.addCollection("postsByTag", (collectionApi) => {
|
|
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
|
const tagMap = {};
|
|
|
|
posts.forEach(post => {
|
|
const rawTags = getPostTags(post, tagExtractorMd);
|
|
const tags = expandHierarchicalTags(rawTags);
|
|
tags.forEach((tag) => {
|
|
tagMap[tag] = {
|
|
name: tag,
|
|
posts: [post, ...(tagMap[tag]?.posts ?? [])],
|
|
recipes: tagMap[tag]?.recipes ?? [],
|
|
}
|
|
})
|
|
});
|
|
|
|
Object.values(tagMap).forEach(tagData => {
|
|
tagData.posts = [...new Set(tagData.posts)].sort((a, b) => {
|
|
const aDate = a.data.createdAt || a.date;
|
|
const bDate = b.data.createdAt || b.date;
|
|
return aDate - bDate;
|
|
});
|
|
});
|
|
|
|
return tagMap;
|
|
});
|
|
|
|
// Recipe collections
|
|
eleventyConfig.addCollection("recipes", (collectionApi) => {
|
|
return collectionApi.getFilteredByGlob("recipes/**/*.md")
|
|
.filter(recipe => recipe.data.draft !== true);
|
|
});
|
|
|
|
eleventyConfig.addCollection("allRecipes", (collectionApi) => {
|
|
return collectionApi.getFilteredByGlob("recipes/**/*.md");
|
|
});
|
|
|
|
eleventyConfig.addCollection("recipesBySlug", (collectionApi) => {
|
|
const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md");
|
|
|
|
// Group recipes by slug using reduce
|
|
const grouped = recipes.reduce((acc, recipe) => {
|
|
const slug = recipe.data.recipeSlug;
|
|
return {
|
|
...acc,
|
|
[slug]: [...(acc[slug] || []), recipe],
|
|
};
|
|
}, {});
|
|
|
|
// Transform grouped recipes into final structure with sorted versions and newest non-draft
|
|
return Object.entries(grouped).reduce((acc, [slug, recipeList]) => {
|
|
const versions = [...recipeList].sort((a, b) => a.data.recipeVersion - b.data.recipeVersion);
|
|
const newest = [...versions].reverse().find(r => r.data.draft !== true) || null;
|
|
return {
|
|
...acc,
|
|
[slug]: { versions, newest },
|
|
};
|
|
}, {});
|
|
});
|
|
|
|
eleventyConfig.addCollection("contentTags", (collectionApi) => {
|
|
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
|
const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md")
|
|
.filter(r => r.data.isNewestVersion && r.data.draft !== true);
|
|
|
|
const postTags = posts.flatMap(post => getPostTags(post, tagExtractorMd));
|
|
const recipeTags = recipes.flatMap(recipe => getRecipeTags(recipe, tagExtractorMd));
|
|
|
|
const allTags = expandHierarchicalTags([...postTags, ...recipeTags]);
|
|
|
|
return [...new Set(allTags)].sort();
|
|
});
|
|
|
|
eleventyConfig.addCollection("contentByTag", (collectionApi) => {
|
|
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
|
const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md")
|
|
.filter(r => r.data.isNewestVersion && r.data.draft !== true);
|
|
|
|
const sortByDate = (a, b) => {
|
|
const aDate = a.data.createdAt || a.date;
|
|
const bDate = b.data.createdAt || b.date;
|
|
return aDate - bDate;
|
|
};
|
|
|
|
const postTagMap = posts.reduce((acc, post) => {
|
|
const rawTags = getPostTags(post, tagExtractorMd);
|
|
const tags = expandHierarchicalTags(rawTags);
|
|
return tags.reduce((innerAcc, tag) => ({
|
|
...innerAcc,
|
|
[tag]: {
|
|
name: tag,
|
|
posts: [...(innerAcc[tag]?.posts || []), post],
|
|
recipes: innerAcc[tag]?.recipes || [],
|
|
},
|
|
}), acc);
|
|
}, {});
|
|
|
|
const tagMap = recipes.reduce((acc, recipe) => {
|
|
const rawTags = getRecipeTags(recipe, tagExtractorMd);
|
|
const tags = expandHierarchicalTags(rawTags);
|
|
return tags.reduce((innerAcc, tag) => ({
|
|
...innerAcc,
|
|
[tag]: {
|
|
name: tag,
|
|
posts: innerAcc[tag]?.posts || [],
|
|
recipes: [...(innerAcc[tag]?.recipes || []), recipe],
|
|
},
|
|
}), acc);
|
|
}, postTagMap);
|
|
|
|
return Object.entries(tagMap).reduce((acc, [tag, tagData]) => ({
|
|
...acc,
|
|
[tag]: {
|
|
...tagData,
|
|
posts: [...new Set(tagData.posts)].sort(sortByDate),
|
|
recipes: [...new Set(tagData.recipes)],
|
|
},
|
|
}), {});
|
|
});
|
|
|
|
eleventyConfig.addPassthroughCopy("robots.txt");
|
|
eleventyConfig.addPassthroughCopy("simulations");
|
|
eleventyConfig.addPassthroughCopy("js");
|
|
|
|
eleventyConfig.ignores.add("README.md");
|
|
|
|
return {
|
|
dir: {
|
|
includes: "_includes",
|
|
output: "_site"
|
|
},
|
|
markdownTemplateEngine: "njk",
|
|
htmlTemplateEngine: "njk"
|
|
};
|
|
}; |