237 lines
No EOL
6.7 KiB
JavaScript
237 lines
No EOL
6.7 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 { 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 `<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;
|
|
});
|
|
|
|
eleventyConfig.addPassthroughCopy("css");
|
|
eleventyConfig.addPassthroughCopy("robots.txt");
|
|
|
|
eleventyConfig.ignores.add("README.md");
|
|
|
|
return {
|
|
dir: {
|
|
input: ".",
|
|
includes: "_includes",
|
|
output: "_site"
|
|
},
|
|
markdownTemplateEngine: "njk",
|
|
htmlTemplateEngine: "njk"
|
|
};
|
|
}; |