]> Witch of Git - web/blog/blob - eleventy.config.js
Add the 31 duovigintillion
[web/blog] / eleventy.config.js
1 import pluginRss from "@11ty/eleventy-plugin-rss";
2 import pluginSyntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
3 import uslug from "uslug";
4 import anchor from "markdown-it-anchor";
5 import markdownIt from "markdown-it";
6 import markdownItFootnote from "markdown-it-footnote";
7 import CleanCSS from "clean-css";
8 import htmlMinifier from "html-minifier-terser";
9 import util from "util";
10 import { DateTime } from "luxon";
11
12 const md = markdownIt({
13 html: true,
14 typographer: true,
15 }).use(anchor, {
16 slugify: s => uslug(s),
17 permalink: true,
18 permalinkBefore: true,
19 permalinkClass: "header-anchor c-sun dec-none",
20 }).use(markdownItFootnote);
21
22 export default async function (eleventyConfig) {
23 eleventyConfig.addDateParsing(parseDate);
24 // @TODO: Update to the new virtual template setup?
25 eleventyConfig.addPlugin(pluginRss);
26 eleventyConfig.addPlugin(pluginSyntaxHighlight, {
27 errorOnInvalidLanguage: true,
28 });
29
30 eleventyConfig.setLibrary("md", md);
31
32 eleventyConfig.addTransform("html-minifier", htmlMinifierTransform);
33
34 eleventyConfig.addPassthroughCopy("img");
35 eleventyConfig.addPassthroughCopy("static");
36 eleventyConfig.addPassthroughCopy("stamps");
37 eleventyConfig.addPassthroughCopy({ "favicon": "/" });
38
39 eleventyConfig.addNunjucksShortcode("youtube", youtubeShortcode);
40 eleventyConfig.addPairedNunjucksShortcode("tweet", tweetShortcode);
41 eleventyConfig.addPairedShortcode("aside", asideShortcode);
42 eleventyConfig.addPairedShortcode("figure", figureShortcode);
43
44 eleventyConfig.addFilter("date", dateFilter);
45 eleventyConfig.addFilter("markdown", value => md.renderInline(value));
46 eleventyConfig.addFilter("groupby", groupbyFilter);
47 eleventyConfig.addFilter("cssmin", css =>
48 new CleanCSS({}).minify(css).styles);
49 eleventyConfig.addFilter("debug", util.inspect);
50 eleventyConfig.addFilter("toRfc3339", toRfc3339Filter);
51 eleventyConfig.addFilter("hasTime", hasTimeFilter);
52
53 eleventyConfig.setDataDeepMerge(true);
54
55 eleventyConfig.addCollection("years", collection => {
56 const posts = collection.getFilteredByTag("posts");
57 const items = groupby(posts, item => item.date.getFullYear());
58 return items.reduce((obj, [k, v]) => (obj[k] = v, obj), {});
59 });
60
61 return {
62 markdownTemplateEngine: "njk",
63 };
64 };
65
66 /**
67 * @param {{string | Date | DateTime}} anyDate
68 * @returns {DateTime}
69 */
70 function dateTime(anyDate) {
71 if (anyDate instanceof Date) {
72 return DateTime.fromJSDate(anyDate);
73 }
74 if (DateTime.isDateTime(anyDate)) {
75 return anyDate;
76 }
77 let date = parseDate(anyDate);
78 if (date == null) { throw Error(`failed to parse date ${anyDate}`); }
79 return date;
80 }
81
82 function dateFilter(date, format) {
83 return dateTime(date).toFormat(format);
84 }
85
86 function toRfc3339Filter(date) {
87 return dateTime(date).toISO({ suppressMilliseconds: true });
88 }
89
90 function hasTimeFilter(date) {
91 date = dateTime(date);
92 return date.hour != 0
93 || date.minute != 0
94 || date.second != 0
95 || date.millisecond != 0;
96 }
97
98 /**
99 * @param {string} dateText
100 * @returns {{DateTime | null}}
101 */
102 function parseDate(dateText) {
103 try {
104 if (!dateText) dateText = "";
105 const formats = [
106 "yyyy-MM-dd HH:mm:ss z",
107 "yyyy-MM-dd z",
108 "yyyy-MM-dd",
109 ];
110 for (const format of formats) {
111 const date = DateTime.fromFormat(dateText, format, { setZone: true });
112 if (date.isValid) { return date; }
113 }
114 } catch {
115 return null;
116 }
117 }
118
119 function access(item, path) {
120 const segments = path.split(".");
121 for (const seg of segments) {
122 if (item === undefined) { return null; }
123 if (seg.endsWith("()")) {
124 const method = item[seg.slice(0, -2)];
125 if (method === undefined) { return null; }
126 item = method.bind(item)();
127 } else {
128 item = item[seg];
129 }
130 }
131 return item;
132 }
133
134 function groupby(items, keyFn) {
135 const results = [];
136 for (const item of items) {
137 const key = keyFn(item);
138 if (results.length == 0 || key != results[results.length-1][0]) {
139 results.push([key, [item]]);
140 } else {
141 results[results.length-1][1].push(item);
142 }
143 }
144 return results;
145 }
146
147 function groupbyFilter(items, path) {
148 return groupby(items, item => access(item, path));
149 }
150
151 function youtubeShortcode(items, inWidth = 560, inHeight = 315) {
152 const width = items.width || inWidth;
153 const height = items.height || inHeight;
154 const allow = items.allow || "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
155 const border = items.border || "0";
156 const video = items.video || items;
157 if (!video) {
158 throw "Required argument 'video'.";
159 }
160 const src = "https://www.youtube.com/embed/" + video;
161 return `<div class="youtube row hcenter">
162 <iframe width="${width}" height="${height}" src="${src}"
163 frameborder="${border}" allow="${allow}" allowfullscreen></iframe>
164 </div>`;
165 }
166
167 function tweetShortcode(content, items) {
168 // @TODO: Handle parsing date
169 return `<div class="row hcenter">
170 <blockquote class="twitter-tweet">
171 <p lang="en" dir="ltr">${content}</p> &mdash; ${items.name} (@${items.at})
172 <a href="${items.link}">${items.date}</a>
173 </blockquote>
174 <script async src="https://platform.twitter.com/widgets.js" charset="utf-8">
175 </script>
176 </div>`;
177 }
178
179 function asideShortcode(content, style='') {
180 const html = md.render(content);
181 if (style) {
182 return `<aside class="${style}">${html}</aside>`;
183 } else {
184 return `<aside>${html}</aside>`;
185 }
186 }
187
188 function figureShortcode(content, { src, alt }) {
189 const captionHtml = md.render(content);
190 return `<figure class="col hcenter">
191 <img src="${src}" alt="${alt}">
192 <figcaption>${captionHtml}</figcaption>
193 </figure>`;
194 }
195
196 function htmlMinifierTransform(content, outputPath) {
197 if (outputPath.endsWith(".html")) {
198 return htmlMinifier.minify(content, {
199 useShortDoctype: true,
200 removeComments: true,
201 collapseWhitespace: true,
202 });
203 }
204 return content;
205 }