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 { DateTime } = require("luxon"); 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.unreleased !== true; } 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; }); eleventyConfig.addPassthroughCopy("css"); return { dir: { input: ".", includes: "_includes", output: "_site" }, markdownTemplateEngine: "njk", htmlTemplateEngine: "njk" }; };