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 `#${md.utils.escapeHtml(tagName)}`; }; } 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 `\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" }; };