"use strict"; const eleventyNavigationPlugin = require("@11ty/eleventy-navigation"); const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); const pluginRss = require("@11ty/eleventy-plugin-rss"); const pluginTOC = require("eleventy-plugin-nesting-toc"); const markdownItAnchor = require("markdown-it-anchor"); const markdownItContainer = require("markdown-it-container"); const Image = require("@11ty/eleventy-img"); const path = require("path"); const { slug } = require("github-slugger"); const yaml = require("js-yaml"); const { highlighter, lineNumberPlugin } = require("./src/_plugins/md-syntax-highlighter"); const { DateTime } = require("luxon"); module.exports = function(eleventyConfig) { /* * The docs stored in the eslint repo are loaded through eslint.org at * at /docs/head to show the most recent version of the documentation * based on the HEAD commit. This gives users a preview of what's coming * in the next release. This is the way that the site works locally so * it's easier to see if URLs are broken. * * When a release is published, HEAD is pushed to the "latest" branch. * When a pre-release is published, HEAD is pushed to the "next" branch. * Netlify deploys those branches as well, and in that case, we want the * docs to be loaded from /docs/latest or /docs/next on eslint.org. * * The path prefix is turned off for deploy previews so we can properly * see changes before deployed. */ let pathPrefix = "/docs/head/"; if (process.env.CONTEXT === "deploy-preview") { pathPrefix = "/"; } else if (process.env.BRANCH === "latest") { pathPrefix = "/docs/latest/"; } else if (process.env.BRANCH === "next") { pathPrefix = "/docs/next/"; } //------------------------------------------------------------------------------ // Data //------------------------------------------------------------------------------ // Load site-specific data const siteName = process.env.ESLINT_SITE_NAME || "en"; eleventyConfig.addGlobalData("site_name", siteName); eleventyConfig.addGlobalData("GIT_BRANCH", process.env.BRANCH); eleventyConfig.addGlobalData("HEAD", process.env.BRANCH === "main"); eleventyConfig.addGlobalData("NOINDEX", process.env.BRANCH !== "latest"); eleventyConfig.addDataExtension("yml", contents => yaml.load(contents)); //------------------------------------------------------------------------------ // Filters //------------------------------------------------------------------------------ eleventyConfig.addFilter("limitTo", (arr, limit) => arr.slice(0, limit)); eleventyConfig.addFilter("jsonify", variable => JSON.stringify(variable)); eleventyConfig.addFilter("slugify", str => { if (!str) { return ""; } return slug(str); }); eleventyConfig.addFilter("URIencode", str => { if (!str) { return ""; } return encodeURI(str); }); /* order collection by the order specified in the front matter */ eleventyConfig.addFilter("sortByPageOrder", values => values.slice().sort((a, b) => a.data.order - b.data.order)); eleventyConfig.addFilter("readableDate", dateObj => { // turn it into a JS Date string const date = new Date(dateObj); // pass it to luxon for formatting return DateTime.fromJSDate(date).toFormat("dd MMM, yyyy"); }); eleventyConfig.addFilter("blogPermalinkDate", dateObj => { // turn it into a JS Date string const date = new Date(dateObj); // pass it to luxon for formatting return DateTime.fromJSDate(date).toFormat("yyyy/MM"); }); eleventyConfig.addFilter("readableDateFromISO", ISODate => DateTime.fromISO(ISODate).toUTC().toLocaleString(DateTime.DATE_FULL)); eleventyConfig.addFilter("dollars", value => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(value)); /* * parse markdown from includes, used for author bios * Source: https://github.com/11ty/eleventy/issues/658 */ eleventyConfig.addFilter("markdown", value => { const markdown = require("markdown-it")({ html: true }); return markdown.render(value); }); /* * Removes `.html` suffix from the given url. * `page.url` will include the `.html` suffix for all documents * except for those written as `index.html` (their `page.url` ends with a `/`). */ eleventyConfig.addFilter("prettyURL", url => { if (url.endsWith(".html")) { return url.slice(0, -".html".length); } return url; }); //------------------------------------------------------------------------------ // Plugins //------------------------------------------------------------------------------ eleventyConfig.addPlugin(eleventyNavigationPlugin); eleventyConfig.addPlugin(syntaxHighlight, { alwaysWrapLineHighlights: true, templateFormats: ["liquid", "njk"] }); eleventyConfig.addPlugin(pluginRss); eleventyConfig.addPlugin(pluginTOC, { tags: ["h2", "h3", "h4"], wrapper: "nav", // Element to put around the root `ol` wrapperClass: "c-toc", // Class for the element around the root `ol` headingText: "", // Optional text to show in heading above the wrapper element headingTag: "h2" // Heading tag when showing heading above the wrapper element }); /** @typedef {import("markdown-it/lib/token")} MarkdownItToken A MarkdownIt token. */ /** * Generates HTML markup for an inline alert. * @param {"warning"|"tip"|"important"} type The type of alert to create. * @param {Array} tokens Array of MarkdownIt tokens to use. * @param {number} index The index of the current token in the tokens array. * @returns {string} The markup for the alert. */ function generateAlertMarkup(type, tokens, index) { if (tokens[index].nesting === 1) { return ` `.trim(); } const markdownIt = require("markdown-it"); const md = markdownIt({ html: true, linkify: true, typographer: true, highlight: (str, lang) => highlighter(md, str, lang) }) .use(markdownItAnchor, { slugify: s => slug(s) }) .use(markdownItContainer, "img-container", {}) .use(markdownItContainer, "correct", {}) .use(markdownItContainer, "incorrect", {}) .use(markdownItContainer, "warning", { render(tokens, idx) { return generateAlertMarkup("warning", tokens, idx); } }) .use(markdownItContainer, "tip", { render(tokens, idx) { return generateAlertMarkup("tip", tokens, idx); } }) .use(markdownItContainer, "important", { render(tokens, idx) { return generateAlertMarkup("important", tokens, idx); } }) .use(lineNumberPlugin) .disable("code"); eleventyConfig.setLibrary("md", md); //------------------------------------------------------------------------------ // Shortcodes //------------------------------------------------------------------------------ eleventyConfig.addNunjucksShortcode("link", function(url) { // eslint-disable-next-line no-invalid-this -- Eleventy API const urlData = this.ctx.further_reading_links[url]; if (!urlData) { throw new Error(`Data missing for ${url}`); } const { domain, title, logo } = urlData; return `
Avatar image for ${domain}
${title}
${domain}
`; }); eleventyConfig.addShortcode("fixable", () => `
🔧 Fixable

if some problems reported by the rule are automatically fixable by the --fix command line option

`); eleventyConfig.addShortcode("recommended", () => `
✅ Recommended

if the "extends": "eslint:recommended" property in a configuration file enables the rule.

`); eleventyConfig.addShortcode("hasSuggestions", () => `
💡 hasSuggestions

if some problems reported by the rule are manually fixable by editor suggestions

`); eleventyConfig.addShortcode("related_rules", arr => { const rules = arr; let items = ""; rules.forEach(rule => { const listItem = ``; items += listItem; }); return ` `; }); eleventyConfig.addShortcode("important", (text, url) => `
Important
${text}
Learn more
`); eleventyConfig.addShortcode("warning", (text, url) => `
Warning
${text}
Learn more
`); eleventyConfig.addShortcode("tip", (text, url) => `
Tip
${text}
Learn more
`); eleventyConfig.addWatchTarget("./src/assets/"); //------------------------------------------------------------------------------ // File PassThroughs //------------------------------------------------------------------------------ eleventyConfig.addPassthroughCopy({ "./src/static": "/" }); eleventyConfig.addPassthroughCopy("./src/assets/"); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.png": "/assets/images" }); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.jpg": "/assets/images" }); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.jpeg": "/assets/images" }); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.svg": "/assets/images" }); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.mp4": "/assets/videos" }); eleventyConfig.addPassthroughCopy({ "./src/content/**/*.pdf": "/assets/documents" }); eleventyConfig.addPassthroughCopy({ "./node_modules/algoliasearch/dist/algoliasearch-lite.esm.browser.js": "/assets/js/algoliasearch.js" }); //------------------------------------------------------------------------------ // Collections //------------------------------------------------------------------------------ eleventyConfig.addCollection("docs", collection => collection.getFilteredByGlob("./src/**/**/*.md")); eleventyConfig.addCollection("library", collection => collection.getFilteredByGlob("./src/library/**/*.md")); // START, eleventy-img (https://www.11ty.dev/docs/plugins/image/) /* eslint-disable-next-line jsdoc/require-jsdoc -- This shortcode is currently unused. If we are going to use it, add JSDoc and describe what exactly is this doing. */ function imageShortcode(source, alt, cls, sizes = "(max-width: 768px) 100vw, 50vw") { const options = { widths: [600, 900, 1500], formats: ["webp", "jpeg"], urlPath: "/assets/images/", outputDir: "./_site/assets/images/", filenameFormat(id, src, width, format) { const extension = path.extname(src); const name = path.basename(src, extension); return `${name}-${width}w.${format}`; } }; /** * Resolves source * @returns {string} URL or a local file path */ function getSRC() { if (source.startsWith("http://") || source.startsWith("https://")) { return source; } /* * for convenience, you only need to use the image's name in the shortcode, * and this will handle appending the full path to it */ return path.join("./src/assets/images/", source); } const fullSrc = getSRC(); // generate images Image(fullSrc, options); // eslint-disable-line new-cap -- `Image` is a function const imageAttributes = { alt, class: cls, sizes, loading: "lazy", decoding: "async" }; // get metadata const metadata = Image.statsSync(fullSrc, options); return Image.generateHTML(metadata, imageAttributes); } eleventyConfig.addShortcode("image", imageShortcode); // END, eleventy-img //------------------------------------------------------------------------------ // Settings //------------------------------------------------------------------------------ /* * When we run `eleventy --serve`, Eleventy 1.x uses browser-sync to serve the content. * By default, browser-sync (more precisely, underlying serve-static) will not serve * `foo/bar.html` when we request `foo/bar`. Thus, we need to rewrite URLs to append `.html` * so that pretty links without `.html` can work in a local development environment. * * There's no need to rewrite URLs that end with `/`, because that already works well * (server will return the content of `index.html` in the directory). * URLs with a file extension, like main.css, main.js, sitemap.xml, etc. should not be rewritten */ eleventyConfig.setBrowserSyncConfig({ middleware(req, res, next) { if (!/(?:\.[a-zA-Z][^/]*|\/)$/u.test(req.url)) { req.url += ".html"; } return next(); } }); /* * Generate the sitemap only in certain contexts to prevent unwanted discovery of sitemaps that * contain URLs we'd prefer not to appear in search results (URLs in sitemaps are considered important). * In particular, we don't want to deploy https://eslint.org/docs/head/sitemap.xml * We want to generate the sitemap for: * - Local previews * - Netlify deploy previews * - Netlify production deploy of the `latest` branch (https://eslint.org/docs/latest/sitemap.xml) * * Netlify always sets `CONTEXT` environment variable. If it isn't set, we assume this is a local build. */ if ( process.env.CONTEXT && // if this is a build on Netlify ... process.env.CONTEXT !== "deploy-preview" && // ... and not for a deploy preview ... process.env.BRANCH !== "latest" // .. and not of the `latest` branch ... ) { eleventyConfig.ignores.add("src/static/sitemap.njk"); // ... then don't generate the sitemap. } return { passthroughFileCopy: true, pathPrefix, markdownTemplateEngine: "njk", dataTemplateEngine: "njk", htmlTemplateEngine: "njk", dir: { input: "src", includes: "_includes", layouts: "_includes/layouts", data: "_data", output: "_site" } }; };