const markdownIt = require("markdown-it");
const markdownItContainer = require("markdown-it-container");
const markdownItFootnote = require("markdown-it-footnote");
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const fs = require("fs");
const tagPattern = /(?<=^|\s)#([a-zA-Z][a-zA-Z0-9_]*)(?![a-zA-Z0-9_-])/g;
// TODO: is there any reasonable way to make this use real markdown parsing because right now this is sketchy
const extractTags = (content) => {
if (!content) return [];
const matches = content.match(tagPattern);
if (!matches) return [];
const tags = [...new Set(matches.map(m => m.slice(1)))];
return tags;
}
const getPostTags = (post) => {
const filePath = post.inputPath;
try {
const content = fs.readFileSync(filePath, 'utf-8');
const tags = extractTags(content);
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_-])/;
md.inline.ruler.push('hashtag', function(state, silent) {
const pos = state.pos;
const ch = state.src.charCodeAt(pos);
if (ch !== '#') return false;
if (pos > 0) {
const prevCh = state.src.charCodeAt(pos - 1);
if (prevCh !== ' ' && prevCh !== '\t' && prevCh !== '\n' && prevCh !== '\r') {
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${title}
\n`;
} else {
return ' \n';
}
}
});
md.use(markdownItFootnote);
md.use(markdownItHashtag);
module.exports = (eleventyConfig) => {
eleventyConfig.addPlugin(syntaxHighlight);
eleventyConfig.addFilter("extractTags", extractTags);
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return extractTags(content);
} catch (e) {
return [];
}
});
eleventyConfig.setLibrary("md", md);
eleventyConfig.addFilter("dateFormat", (date) => {
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC'
});
});
eleventyConfig.addFilter("isoDate", (date) => {
const d = new Date(date);
return d.toISOString().split('T')[0];
});
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(getPostTags))].sort();
});
eleventyConfig.addCollection("postsByTag", (collectionApi) => {
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
const tagMap = {};
posts.forEach(post => {
const tags = getPostTags(post)
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"
};
};