const HTMLMinifier = require("html-minifier"); const CleanCSS = require("clean-css"); const UglifyES = require("uglify-es"); const MarkdownIt = require("markdown-it"); const JSYaml = require("js-yaml"); require('intl'); const markdownIt = MarkdownIt({ html: true, breaks: true, linkify: true }); module.exports = function(eleventyConfig) { eleventyConfig.setLibrary("md", markdownIt); eleventyConfig.addPassthroughCopy({ "static/favicon": ".", "static/img": "img" }); eleventyConfig.addFilter("monthAndYear", function(date) { return date.toLocaleDateString("en-US", { year: "numeric", month: "short" }); }); // Creates a child object with a new or overridden property without affecting // the original. function inheritAndAdd(base, newProperties) { return Object.assign(Object.create(base), newProperties); } // Identifies highlights in this array. // // itemList is an array of objects with "id" (string) properties // filter is a specification for how to filter this list, which can be: // * true or "all", in which case all items are highlights // * some falsy value, in which case no items are highlights // * an array containing item IDs which are highlights and others are // lowlights // * an array containing "all" and item IDs each prepended by "-", where // negated IDs are lowlights and others are highlights // * an object, in which case its keys are used as an array (as above)m // regardless of their values // highlightsFirst is an optional boolean; if true, highlights are listed // first, then lowlights, though order is preserved within those groups. If // false or not specified, order is the same as the input. // Returns an array of objects, each of which has a one-to-one correspondence // with an input item, using that object as its prototype and adding a // boolean "highlight" property. function identifyHighlights(itemList, filter) { if (!Array.isArray(itemList)) { return itemList; } if (!Array.isArray(filter)) { if (filter === true || filter === "all") { filter = ["all"] } else if (typeof filter === "object") { filter = Object.keys(filter); } else { filter = []; } } return itemList.map(function(item) { if ((filter.includes("all") && !filter.includes("-" + item.id)) || filter.includes(item.id)) { return inheritAndAdd(item, {highlight: true}); } else { return inheritAndAdd(item, {highlight: false}); } }); } eleventyConfig.addFilter("identifyHighlights", identifyHighlights); // Identifies highlights in this and each of the child layers. // // itemList is an array of objects with // "id" (string) and headAttribute (list of object) properties. // filter is a specification for how to filter this list and its children: // * true or "all", in which case all items on all layers are highlights // * some falsy value, in which case all items on all layers are lowlights // * an array containing item IDs, in which case all matching items on this // layer are highlights, as well as all of their descendants, and all // non-matching items on this layer and all of their descendants are // lowlights // * an array containing "all" and item IDs preceded by "-", in which case all // negated IDs and their descendants are lowlights, and all others and // their descendants are highlights // * an object whose keys are used as an array (as above) to match this layer, // but whose values are used as the value for filter when recursively // calling on the value of headAttribute. // If one of these keys exists, the child will be called with its value as // the filter argument, in this order: // * child.id // * "-" + child.id // * "all" // * "-all" // Otherwise, the child will be called with false. // headAttribute, nextAttribute, and tailAttributes are strings; nextAttribute // and tailAttributes are optional. // Returns an array of objects, each of which has a one-to-one correspondence // with an input item, using that object as its prototype and adding a // boolean "highlight" property and a list of objects headAttribute property // fulfilling the same type of contract as described here. Order is // preserved within highlights and lowlights, but highlights are first in // the list. function identifyHighlightsRecursive(itemList, filter, headAttribute, nextAttribute, ...tailAttributes) { if (Array.isArray(filter)) { const newFilter = {}; filter.forEach(function(item) { if (item.startsWith("-")) { newFilter[item] = false; } else { newFilter[item] = true; } }); filter = newFilter; } else if (typeof filter !== "object"){ if (filter === true || filter === "all") { filter = { "all": true }; } else { filter = {}; } } const highlightList = identifyHighlights(itemList, filter); if (!headAttribute || !Array.isArray(highlightList)) { return highlightList; } highlightList.forEach(function(item) { if (!(headAttribute in item)) { // can't recurse into an item which doesn't have our recursive property return; } const children = item[headAttribute]; const match = [item.id, "-" + item.id, "all", "-all"].find((key) => key in filter); const childFilter = match ? filter[match] : false; // safe to modify in-place because we got this result back from // identifyHighlights, which inherits from its input item[headAttribute] = identifyHighlightsRecursive(children, childFilter, nextAttribute, ...tailAttributes); }); return highlightList; } eleventyConfig.addFilter("identifyHighlightsRecursive", identifyHighlightsRecursive); function sortHighlightsFirst(itemList) { return itemList.filter((it) => it.highlight).concat(itemList.filter((it) => !it.highlight)); } eleventyConfig.addFilter("sortHighlightsFirst", sortHighlightsFirst); function identifyExperienceHighlights(roleList, filter) { const expandedRoleList = roleList.map(function(role) { const children = role.achievements ? role.achievements.slice() : []; const fullDescription = role.description || role.shortDescription; const shortDescription = role.description && role.shortDescription; const descriptionObject = { id: "description", full: fullDescription, short: shortDescription, highlight: false }; children.push(descriptionObject); return inheritAndAdd(role, { description: descriptionObject, shortDescription: null, _children: children }); }); const highlightedRoleList = identifyHighlightsRecursive(expandedRoleList, filter, "_children"); highlightedRoleList.forEach(function(role) { role.description = role._children[role._children.length - 1]; role.achievements = role._children.slice(0, role._children.length - 1); }); return highlightedRoleList; } eleventyConfig.addFilter("identifyExperienceHighlights", identifyExperienceHighlights); // Checks if there are any items with a truthy highlight property in itemList. function hasHighlights(itemList) { if (!Array.isArray(itemList)) { return false; } return itemList.some(function(item) { return item.highlight; }); } eleventyConfig.addFilter("hasHighlights", hasHighlights); // Checks if there are any items with a falsy highlight property in itemList. function hasLowlights(itemList) { if (!Array.isArray(itemList)) { return false; } return itemList.some(function(item) { return !item.highlight; }); } eleventyConfig.addFilter("hasLowlights", hasLowlights); // Checks if there are any items within this list or its children with a // truthy highlight property. function hasHighlightsRecursive(itemList, headAttribute, nextAttribute, ...tailAttributes) { if (!Array.isArray(itemList)) { return false; } return hasHighlights(itemList) || itemList.some( (child) => headAttribute in child && hasHighlightsRecursive( child[headAttribute], nextAttribute, ...tailAttributes)); } eleventyConfig.addFilter("hasHighlightsRecursive", hasHighlightsRecursive); // Checks if there are any items within this list or its children with a // falsy highlight property. function hasLowlightsRecursive(itemList, headAttribute, nextAttribute, ...tailAttributes) { if (!Array.isArray(itemList)) { return false; } return hasLowlights(itemList) || itemList.some( (child) => headAttribute in child && hasLowlightsRecursive( child[headAttribute], nextAttribute, ...tailAttributes)); } eleventyConfig.addFilter("hasLowlightsRecursive", hasLowlightsRecursive); eleventyConfig.addFilter("md", function(content) { return markdownIt.render(content); }); eleventyConfig.addTransform("minifyHTML", function(html, path) { if(path && path.endsWith(".html")) { return HTMLMinifier.minify(html, { collapseWhitespace: true, removeComments: true, useShortDoctype: true, }); } return html; }); eleventyConfig.addNunjucksAsyncFilter("minifyCSS", function(code, callback) { return new CleanCSS({ level: 2, inline: ['all'] }).minify(code, (err, result) => { if (err) { callback(err, null); } else { callback(null, result.styles); } }); }); eleventyConfig.addFilter("minifyJS", function(code) { let minified = UglifyES.minify(code); if(minified.error) { console.log("UglifyES failed with an error: ", minified.error); throw new Error("Javascript minification failure"); } return minified.code; }); eleventyConfig.addDataExtension("yaml", contents => JSYaml.safeLoad(contents)); return { templateFormats: [ "html", "md", "njk" ], pathPrefix: "", markdownTemplateEngine: "njk", htmlTemplateEngine: "njk", dataTemplateEngine: "njk", passthroughFileCopy: true, dir: { input: ".", includes: "_includes", data: "_data", output: "_site" } }; }