Skip to content

Commit

Permalink
build(catalog): implement the eleventy config
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 534959163
  • Loading branch information
Elliott Marquez authored and copybara-github committed May 24, 2023
1 parent c87d732 commit a7a061f
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 0 deletions.
68 changes: 68 additions & 0 deletions catalog/eleventy-helpers/filters/filter-sort.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* A filter that sorts and filters an array based on truthyness and sorts the
* filtered array.
*
* This filter takes the following arguments:
* - arr: (required) The array to filter-sort.
* - attr: (required) The attribute to filter and sort by.
*
* @example
* ```html
* <!--
* Will generate an array of anchor tags based on the array of entries in the
* "component" 11ty collection. The anchor tags are sorted alphabetically by
* `data.name` and will not be rendered if `data.name` is not defined.
* -->
* {% for component in collections.component|filtersort('data.name') %}
* <a href={{ component.url }}>{{ component.data.name }}</a>
* {% endfor %}
* ```
*
* @param eleventyConfig The 11ty config in which to attach this filter.
*/
function filterSort (eleventyConfig) {
eleventyConfig.addFilter("filtersort", function(arr, attr) {
// get the parts of the attribute to look up
const attrParts = attr.split(".");

const array = arr.filter(item => {
let value = item;

// get the deep attribute
for (const part of attrParts) {
value = value[part];
}

return !!value;
});

array.sort((a, b) => {
let aVal = a;
let bVal = b;

// get the deep attributes of each a and b
for (const part of attrParts) {
aVal = aVal[part];
bVal = bVal[part];
}

if (aVal < bVal) {
return -1;
} else if (aVal > bVal) {
return 1;
}

return 0;
});

return array;
});
};

module.exports = filterSort;
81 changes: 81 additions & 0 deletions catalog/eleventy-helpers/plugins/permalinks.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const markdownIt = require('markdown-it');
const markdownItAnchor = require('markdown-it-anchor');
const slugifyLib = require('slugify');

/**
* An 11ty plugin that integrates `markdown-it-anchor` to 11ty's markdown
* engine. This allows us to inject an <a> around our <h*> elements.
*
* @param eleventyConfig The 11ty config in which to attach this plugin.
*/
function permalinks(eleventyConfig) {
// Use the same slugify as 11ty for markdownItAnchor.
const slugify = (s) => slugifyLib(s, { lower: true });

const linkAfterHeaderBase = markdownItAnchor.permalink.linkAfterHeader({
style: 'visually-hidden',
class: 'anchor',
visuallyHiddenClass: 'offscreen',
assistiveText: (title) => `Link to “${title}”`,
});

/**
* Wraps the link with a div so that it's more accessible. Implementation
* taken from lit.dev
*
* https://github.com/lit/lit.dev/blob/18d86901c2814913a35b201d78e95ba8735c42e7/packages/lit-dev-content/.eleventy.js#L105-L134
*/
const linkAfterHeaderWithWrapper = (slug, opts, state, idx) => {
const headingTag = state.tokens[idx].tag;
if (!headingTag.match(/^h[123456]$/)) {
throw new Error(`Expected token to be a h1-6: ${headingTag}`);
}

// Using markdownit's token system to inject a div wrapper so that we can
// have:
// <div class="heading h2">
// <h2 id="interactive-demo">Interactive Demo<h2>
// <a class="anchor" href="#interactive-demo">
// <span class="offscreen">Permalink to "Interactive Demo"</span>
// </a>
// </div>
state.tokens.splice(
idx,
0,
Object.assign(new state.Token('div_open', 'div', 1), {
attrs: [['class', `heading ${headingTag}`]],
block: true,
})
);
state.tokens.splice(
idx + 4,
0,
Object.assign(new state.Token('div_close', 'div', -1), {
block: true,
})
);
linkAfterHeaderBase(slug, opts, state, idx + 1);
};

// Apply the anchor plugin to markdownit
const md = markdownIt({
html: true,
breaks: false, // 2 newlines for paragraph break instead of 1
linkify: true,
}).use(markdownItAnchor, {
slugify,
permalink: linkAfterHeaderWithWrapper,
permalinkClass: 'anchor',
permalinkSymbol: '#',
level: [2, 3, 4], // only apply to h2 h3 and h4
});
eleventyConfig.setLibrary('md', md);
}

module.exports = permalinks;
53 changes: 53 additions & 0 deletions catalog/eleventy-helpers/shortcodes/inline-css.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const CleanCSS = require('clean-css');

/**
* Bundle, minify, and inline a CSS file. Path is relative to ./site/css/.
*
* In dev mode, instead import the CSS file directly.
*
* This filter takes the following arguments:
* - path: (required) The path of the file to minify and inject relative to
* /site/css
*
* @example
* ```html
* <!--
* In prod will minify and inline the file at site/css/global.css into the
* page to prevent a new network request. In dev will inject a <link> tag for
* a faster build.
* -->
* <head>
* {% inlinecss "global.css" %}
* </head>
* ```
*
* @param eleventyConfig The 11ty config in which to attach this shortcode.
* @param isDev {boolean} Whether or not the build is in development mode.
*/
function inlineCSS(eleventyConfig, isDev) {
eleventyConfig.addShortcode('inlinecss', (path) => {
if (isDev) {
return `<link rel="stylesheet" href="/css/${path}">`;
}
const result = new CleanCSS({ inline: ['local'] }).minify([
`./site/css/${path}`,
]);
if (result.errors.length > 0 || result.warnings.length > 0) {
throw new Error(
`CleanCSS errors/warnings on file ${path}:\n\n${[
...result.errors,
...result.warnings,
].join('\n')}`
);
}
return `<style>${result.styles}</style>`;
});
}

module.exports = inlineCSS;
48 changes: 48 additions & 0 deletions catalog/eleventy-helpers/shortcodes/inline-js.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const fsSync = require('fs');

/**
* Inline the Rollup-bundled version of a JavaScript module. Path is relative
* to ./lib or ./build aliased to /js by 11ty
*
* In dev mode, instead directly import the module in a
* script[type=module][src=/js/...], which has already been symlinked directly
* to the 11ty JS output directory.
*
* This filter takes the following arguments:
* - path: (required) The path of the file to minify and inject relative to
* ./lib, ./build, or ./js folders depending on dev mode.
*
* @example
* ```html
* <!--
* In prod will inline the file at /build/ssr-utils/dsd-polyfill in a
* synchronous script tag. In dev it will externally load the file in a
* module script for faster build.
* -->
* <body dsd-pending>
* {% inlinejs "ssr-utils/dsd-polyfill.js" %}
* </body>
* ```
*
* @param eleventyConfig The 11ty config in which to attach this shortcode.
* @param isDev {boolean} Whether or not the build is in development mode.
* @param config {{jsdir: string}} Configuration options to set the JS directory
*/
function inlineJS(eleventyConfig, isDev, {jsDir}) {
eleventyConfig.addShortcode('inlinejs', (path) => {
// script type module
if (isDev) {
return `<script type="module" src="/js/${path}"></script>`;
}
const script = fsSync.readFileSync(`${jsDir}/${path}`, 'utf8').trim();
return `<script>${script}</script>`;
});
}

module.exports = inlineJS;
75 changes: 75 additions & 0 deletions catalog/eleventy-helpers/shortcodes/playground-example.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Will render a playground example with a project.json in the
* `/catalog/stories/${dirname}/` directory.
*
* This shorcode takes the following arguments:
* - dirname: (required) The name of the directory where the project.json is
* located
* - id: (optional) The id of the project. This is used to identify the project on pages
* with multiple playground examples.
* - previewHeight: (optional) The height of the preview window. Defaults to `400`.
* - editorHeight: (optional) The height of the editor window. Defaults to `500`.
*
* @example
* ```html
* <!--
* Will generate a playground example located at
* /catalog/stories/checkbox/project.json
* and give the project the id "example1"
* -->
* {% playgroundexample dirname="checkbox", id="example2", previewHeight="400", editorHeight="500" %}
* ```
*
* @param eleventyConfig The 11ty config in which to attach this shortcode.
*/
function playgroundExample(eleventyConfig) {
eleventyConfig.addShortcode('playgroundexample', (config) => {
let { id, dirname } = config;
if (!dirname) {
throw new Error('No dirname provided to playgroundexample shortcode');
}

id ||= 'project';

const previewHeight = config.previewHeight
? `height: ${config.previewHeight}px`
: 'height: 400px;';
const editorHeight = config.editorHeight
? `height: ${config.editorHeight}px`
: 'height: 500px;';

return `
<details>
<summary>
<md-outlined-icon-button toggle tabindex="-1" aria-hidden="true">
<md-icon aria-hidden="true">expand_more</md-icon>
<md-icon aria-hidden="true" slot="selectedIcon">expand_less</md-icon>
</md-outlined-icon-button>
Expand interactive demo.
</summary>
<lit-island on:visible import="/material-web/js/hydration-entrypoints/playground-elements.js" class="example" aria-hidden="true">
<playground-project
id="${id}" project-src="/material-web/assets/stories/${dirname}/project.json">
<playground-preview
style="${previewHeight}"
project="${id}"
><md-circular-progress indeterminate></md-circular-progress></playground-preview>
<playground-file-editor
style="${editorHeight}"
project="${id}"
filename="stories.ts"
line-numbers
><md-circular-progress indeterminate></md-circular-progress></playground-file-editor>
</lit-island>
</details>
`;
});
}

module.exports = playgroundExample;
32 changes: 32 additions & 0 deletions catalog/eleventy-helpers/transforms/minify-html.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

const htmlMinifier = require('html-minifier');

/**
* Minifies HTML in production mode. Does nothing in dev mode for a faster build
* and debuggability
*
* @param eleventyConfig The 11ty config in which to attach this transform.
* @param isDev {boolean} Whether or not the build is in development mode.
*/
function minifyHTML(eleventyConfig, isDev) {
eleventyConfig.addTransform('htmlMinify', function (content, outputPath) {
// return the normal content in dev moe.
if (isDev || !outputPath.endsWith('.html')) {
return content;
}
// minify the html in Prod mode
const minified = htmlMinifier.minify(content, {
useShortDoctype: true,
removeComments: true,
collapseWhitespace: true,
});
return minified;
});
}

module.exports = minifyHTML;
Loading

0 comments on commit a7a061f

Please sign in to comment.