volpe/eleventy.config.js

284 lines
No EOL
8.1 KiB
JavaScript

const markdownIt = require("markdown-it");
const markdownItContainer = require("markdown-it-container");
const markdownItFootnote = require("markdown-it-footnote");
const markdownItMermaid = require('markdown-it-mermaid').default
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
const { DateTime } = require("luxon");
const cssHashCache = {};
const getCssHash = (cssFile) => {
if (cssHashCache[cssFile]) return cssHashCache[cssFile];
const cssPath = path.join(__dirname, "css", cssFile);
try {
const content = fs.readFileSync(cssPath, "utf-8");
const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
cssHashCache[cssFile] = hash;
return hash;
} catch (e) {
console.warn(`Could not hash CSS file: ${cssFile}`);
return null;
}
};
const extractTags = (content, mdInstance) => {
if (!content) return [];
const collectHashtags = (tokens) =>
tokens.flatMap(token => [
...(token.type === 'hashtag' ? [token.content] : []),
...(token.children ? collectHashtags(token.children) : [])
]);
const tokens = mdInstance.parse(content, {});
const tags = collectHashtags(tokens);
return [...new Set(tags)];
}
const getPostTags = (post, mdInstance) => {
const filePath = post.inputPath;
try {
const content = fs.readFileSync(filePath, 'utf-8');
const tags = extractTags(content, mdInstance);
return tags.map(tag => {
const normalizedTag = tag.toLowerCase();
return normalizedTag
});
} catch (e) {
// Skip if file can't be read
return []
}
}
const isReleased = (post) => {
return post.data.released !== false;
}
const markdownItHashtag = (md) => {
const hashtagRegex = /^#([a-zA-Z][a-zA-Z0-9_]*)(?![a-zA-Z0-9_-])/;
const HASH_CODE = '#'.charCodeAt(0);
const SPACE_CODE = ' '.charCodeAt(0);
const TAB_CODE = '\t'.charCodeAt(0);
const NEWLINE_CODE = '\n'.charCodeAt(0);
const CARRIAGE_RETURN_CODE = '\r'.charCodeAt(0);
md.inline.ruler.push('hashtag', function(state, silent) {
const pos = state.pos;
const ch = state.src.charCodeAt(pos);
if (ch !== HASH_CODE) return false;
if (pos > 0) {
const prevCh = state.src.charCodeAt(pos - 1);
if (prevCh !== SPACE_CODE && prevCh !== TAB_CODE && prevCh !== NEWLINE_CODE && prevCh !== CARRIAGE_RETURN_CODE) {
return false;
}
}
const match = state.src.slice(pos).match(hashtagRegex);
if (!match) return false;
if (!silent) {
const token = state.push('hashtag', 'a', 0);
token.content = match[1];
token.markup = '#';
}
state.pos += match[0].length;
return true;
});
md.renderer.rules.hashtag = function(tokens, idx) {
const tagName = tokens[idx].content;
const slug = tagName.toLowerCase();
return `<a href="/tags/${slug}/" class="inline-tag">#${md.utils.escapeHtml(tagName)}</a>`;
};
}
const md = markdownIt({
html: true,
breaks: true,
linkify: true
});
md.use(markdownItContainer, 'details', {
validate: function (params) {
return params.trim().match(/^(.*)$/);
},
render: function (tokens, idx) {
const m = tokens[idx].info.trim().match(/^(.*)$/);
if (tokens[idx].nesting === 1) {
const title = md.utils.escapeHtml(m[1]);
return `<details class="expandable">\n<summary>${title}</summary>\n`;
} else {
return '</details>\n';
}
}
});
md.use(markdownItFootnote);
md.use(markdownItHashtag);
md.use(markdownItMermaid);
module.exports = (eleventyConfig) => {
eleventyConfig.addPlugin(syntaxHighlight);
eleventyConfig.addFilter("extractTags", (content) => extractTags(content, md));
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return extractTags(content, md);
} catch (e) {
return [];
}
});
eleventyConfig.setLibrary("md", md);
eleventyConfig.addFilter("dateTimeFormat", (date) => {
const dt = date instanceof Date
? DateTime.fromJSDate(date)
: DateTime.fromISO(date);
return dt.toFormat('MMMM d, yyyy h:mm a');
});
eleventyConfig.addFilter("isoDate", (date) => {
const dt = date instanceof Date
? DateTime.fromJSDate(date)
: DateTime.fromISO(date);
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);
return DateTime.fromISO(d);
};
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("contentTags", (collectionApi) => {
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
return [...new Set(posts.flatMap(post => getPostTags(post, md)))].sort();
});
eleventyConfig.addCollection("postsByTag", (collectionApi) => {
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
const tagMap = {};
posts.forEach(post => {
const tags = getPostTags(post, md)
tags.forEach((tag) => {
tagMap[tag] = {
name: tag,
posts: [post, ...(tagMap[tag]?.posts ?? [])],
}
})
});
Object.values(tagMap).forEach(tagData => {
tagData.posts.sort((a, b) => {
const aDate = a.data.createdAt || a.date;
const bDate = b.data.createdAt || b.date;
return aDate - bDate;
});
});
return tagMap;
});
// Cache busting filter: returns hashed CSS filename
eleventyConfig.addFilter("cssHash", (cssFile) => {
const hash = getCssHash(cssFile);
const ext = path.extname(cssFile);
const base = path.basename(cssFile, ext);
return `/css/${base}.${hash}${ext}`;
});
eleventyConfig.on("eleventy.before", async () => {
const cssDir = path.join(__dirname, "css");
const outputCssDir = path.join(__dirname, "_site", "css");
if (!fs.existsSync(outputCssDir)) {
fs.mkdirSync(outputCssDir, { recursive: true });
}
const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith(".css"));
for (const cssFile of cssFiles) {
const hash = getCssHash(cssFile);
const ext = path.extname(cssFile);
const base = path.basename(cssFile, ext);
const hashedName = `${base}${hash == null ? '' : `.${hash}`}${ext}`;
fs.copyFileSync(
path.join(cssDir, cssFile),
path.join(outputCssDir, hashedName)
);
}
});
eleventyConfig.addPassthroughCopy("robots.txt");
eleventyConfig.ignores.add("README.md");
return {
dir: {
input: ".",
includes: "_includes",
output: "_site"
},
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk"
};
};