Skip to content

Commit a7a061f

Browse files
Elliott Marquezcopybara-github
authored andcommitted
build(catalog): implement the eleventy config
PiperOrigin-RevId: 534959163
1 parent c87d732 commit a7a061f

File tree

7 files changed

+436
-0
lines changed

7 files changed

+436
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* A filter that sorts and filters an array based on truthyness and sorts the
9+
* filtered array.
10+
*
11+
* This filter takes the following arguments:
12+
* - arr: (required) The array to filter-sort.
13+
* - attr: (required) The attribute to filter and sort by.
14+
*
15+
* @example
16+
* ```html
17+
* <!--
18+
* Will generate an array of anchor tags based on the array of entries in the
19+
* "component" 11ty collection. The anchor tags are sorted alphabetically by
20+
* `data.name` and will not be rendered if `data.name` is not defined.
21+
* -->
22+
* {% for component in collections.component|filtersort('data.name') %}
23+
* <a href={{ component.url }}>{{ component.data.name }}</a>
24+
* {% endfor %}
25+
* ```
26+
*
27+
* @param eleventyConfig The 11ty config in which to attach this filter.
28+
*/
29+
function filterSort (eleventyConfig) {
30+
eleventyConfig.addFilter("filtersort", function(arr, attr) {
31+
// get the parts of the attribute to look up
32+
const attrParts = attr.split(".");
33+
34+
const array = arr.filter(item => {
35+
let value = item;
36+
37+
// get the deep attribute
38+
for (const part of attrParts) {
39+
value = value[part];
40+
}
41+
42+
return !!value;
43+
});
44+
45+
array.sort((a, b) => {
46+
let aVal = a;
47+
let bVal = b;
48+
49+
// get the deep attributes of each a and b
50+
for (const part of attrParts) {
51+
aVal = aVal[part];
52+
bVal = bVal[part];
53+
}
54+
55+
if (aVal < bVal) {
56+
return -1;
57+
} else if (aVal > bVal) {
58+
return 1;
59+
}
60+
61+
return 0;
62+
});
63+
64+
return array;
65+
});
66+
};
67+
68+
module.exports = filterSort;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
const markdownIt = require('markdown-it');
8+
const markdownItAnchor = require('markdown-it-anchor');
9+
const slugifyLib = require('slugify');
10+
11+
/**
12+
* An 11ty plugin that integrates `markdown-it-anchor` to 11ty's markdown
13+
* engine. This allows us to inject an <a> around our <h*> elements.
14+
*
15+
* @param eleventyConfig The 11ty config in which to attach this plugin.
16+
*/
17+
function permalinks(eleventyConfig) {
18+
// Use the same slugify as 11ty for markdownItAnchor.
19+
const slugify = (s) => slugifyLib(s, { lower: true });
20+
21+
const linkAfterHeaderBase = markdownItAnchor.permalink.linkAfterHeader({
22+
style: 'visually-hidden',
23+
class: 'anchor',
24+
visuallyHiddenClass: 'offscreen',
25+
assistiveText: (title) => `Link to “${title}”`,
26+
});
27+
28+
/**
29+
* Wraps the link with a div so that it's more accessible. Implementation
30+
* taken from lit.dev
31+
*
32+
* https://github.com/lit/lit.dev/blob/18d86901c2814913a35b201d78e95ba8735c42e7/packages/lit-dev-content/.eleventy.js#L105-L134
33+
*/
34+
const linkAfterHeaderWithWrapper = (slug, opts, state, idx) => {
35+
const headingTag = state.tokens[idx].tag;
36+
if (!headingTag.match(/^h[123456]$/)) {
37+
throw new Error(`Expected token to be a h1-6: ${headingTag}`);
38+
}
39+
40+
// Using markdownit's token system to inject a div wrapper so that we can
41+
// have:
42+
// <div class="heading h2">
43+
// <h2 id="interactive-demo">Interactive Demo<h2>
44+
// <a class="anchor" href="#interactive-demo">
45+
// <span class="offscreen">Permalink to "Interactive Demo"</span>
46+
// </a>
47+
// </div>
48+
state.tokens.splice(
49+
idx,
50+
0,
51+
Object.assign(new state.Token('div_open', 'div', 1), {
52+
attrs: [['class', `heading ${headingTag}`]],
53+
block: true,
54+
})
55+
);
56+
state.tokens.splice(
57+
idx + 4,
58+
0,
59+
Object.assign(new state.Token('div_close', 'div', -1), {
60+
block: true,
61+
})
62+
);
63+
linkAfterHeaderBase(slug, opts, state, idx + 1);
64+
};
65+
66+
// Apply the anchor plugin to markdownit
67+
const md = markdownIt({
68+
html: true,
69+
breaks: false, // 2 newlines for paragraph break instead of 1
70+
linkify: true,
71+
}).use(markdownItAnchor, {
72+
slugify,
73+
permalink: linkAfterHeaderWithWrapper,
74+
permalinkClass: 'anchor',
75+
permalinkSymbol: '#',
76+
level: [2, 3, 4], // only apply to h2 h3 and h4
77+
});
78+
eleventyConfig.setLibrary('md', md);
79+
}
80+
81+
module.exports = permalinks;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
const CleanCSS = require('clean-css');
8+
9+
/**
10+
* Bundle, minify, and inline a CSS file. Path is relative to ./site/css/.
11+
*
12+
* In dev mode, instead import the CSS file directly.
13+
*
14+
* This filter takes the following arguments:
15+
* - path: (required) The path of the file to minify and inject relative to
16+
* /site/css
17+
*
18+
* @example
19+
* ```html
20+
* <!--
21+
* In prod will minify and inline the file at site/css/global.css into the
22+
* page to prevent a new network request. In dev will inject a <link> tag for
23+
* a faster build.
24+
* -->
25+
* <head>
26+
* {% inlinecss "global.css" %}
27+
* </head>
28+
* ```
29+
*
30+
* @param eleventyConfig The 11ty config in which to attach this shortcode.
31+
* @param isDev {boolean} Whether or not the build is in development mode.
32+
*/
33+
function inlineCSS(eleventyConfig, isDev) {
34+
eleventyConfig.addShortcode('inlinecss', (path) => {
35+
if (isDev) {
36+
return `<link rel="stylesheet" href="/css/${path}">`;
37+
}
38+
const result = new CleanCSS({ inline: ['local'] }).minify([
39+
`./site/css/${path}`,
40+
]);
41+
if (result.errors.length > 0 || result.warnings.length > 0) {
42+
throw new Error(
43+
`CleanCSS errors/warnings on file ${path}:\n\n${[
44+
...result.errors,
45+
...result.warnings,
46+
].join('\n')}`
47+
);
48+
}
49+
return `<style>${result.styles}</style>`;
50+
});
51+
}
52+
53+
module.exports = inlineCSS;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
const fsSync = require('fs');
8+
9+
/**
10+
* Inline the Rollup-bundled version of a JavaScript module. Path is relative
11+
* to ./lib or ./build aliased to /js by 11ty
12+
*
13+
* In dev mode, instead directly import the module in a
14+
* script[type=module][src=/js/...], which has already been symlinked directly
15+
* to the 11ty JS output directory.
16+
*
17+
* This filter takes the following arguments:
18+
* - path: (required) The path of the file to minify and inject relative to
19+
* ./lib, ./build, or ./js folders depending on dev mode.
20+
*
21+
* @example
22+
* ```html
23+
* <!--
24+
* In prod will inline the file at /build/ssr-utils/dsd-polyfill in a
25+
* synchronous script tag. In dev it will externally load the file in a
26+
* module script for faster build.
27+
* -->
28+
* <body dsd-pending>
29+
* {% inlinejs "ssr-utils/dsd-polyfill.js" %}
30+
* </body>
31+
* ```
32+
*
33+
* @param eleventyConfig The 11ty config in which to attach this shortcode.
34+
* @param isDev {boolean} Whether or not the build is in development mode.
35+
* @param config {{jsdir: string}} Configuration options to set the JS directory
36+
*/
37+
function inlineJS(eleventyConfig, isDev, {jsDir}) {
38+
eleventyConfig.addShortcode('inlinejs', (path) => {
39+
// script type module
40+
if (isDev) {
41+
return `<script type="module" src="/js/${path}"></script>`;
42+
}
43+
const script = fsSync.readFileSync(`${jsDir}/${path}`, 'utf8').trim();
44+
return `<script>${script}</script>`;
45+
});
46+
}
47+
48+
module.exports = inlineJS;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* Will render a playground example with a project.json in the
9+
* `/catalog/stories/${dirname}/` directory.
10+
*
11+
* This shorcode takes the following arguments:
12+
* - dirname: (required) The name of the directory where the project.json is
13+
* located
14+
* - id: (optional) The id of the project. This is used to identify the project on pages
15+
* with multiple playground examples.
16+
* - previewHeight: (optional) The height of the preview window. Defaults to `400`.
17+
* - editorHeight: (optional) The height of the editor window. Defaults to `500`.
18+
*
19+
* @example
20+
* ```html
21+
* <!--
22+
* Will generate a playground example located at
23+
* /catalog/stories/checkbox/project.json
24+
* and give the project the id "example1"
25+
* -->
26+
* {% playgroundexample dirname="checkbox", id="example2", previewHeight="400", editorHeight="500" %}
27+
* ```
28+
*
29+
* @param eleventyConfig The 11ty config in which to attach this shortcode.
30+
*/
31+
function playgroundExample(eleventyConfig) {
32+
eleventyConfig.addShortcode('playgroundexample', (config) => {
33+
let { id, dirname } = config;
34+
if (!dirname) {
35+
throw new Error('No dirname provided to playgroundexample shortcode');
36+
}
37+
38+
id ||= 'project';
39+
40+
const previewHeight = config.previewHeight
41+
? `height: ${config.previewHeight}px`
42+
: 'height: 400px;';
43+
const editorHeight = config.editorHeight
44+
? `height: ${config.editorHeight}px`
45+
: 'height: 500px;';
46+
47+
return `
48+
<details>
49+
<summary>
50+
<md-outlined-icon-button toggle tabindex="-1" aria-hidden="true">
51+
<md-icon aria-hidden="true">expand_more</md-icon>
52+
<md-icon aria-hidden="true" slot="selectedIcon">expand_less</md-icon>
53+
</md-outlined-icon-button>
54+
Expand interactive demo.
55+
</summary>
56+
<lit-island on:visible import="/material-web/js/hydration-entrypoints/playground-elements.js" class="example" aria-hidden="true">
57+
<playground-project
58+
id="${id}" project-src="/material-web/assets/stories/${dirname}/project.json">
59+
<playground-preview
60+
style="${previewHeight}"
61+
project="${id}"
62+
><md-circular-progress indeterminate></md-circular-progress></playground-preview>
63+
<playground-file-editor
64+
style="${editorHeight}"
65+
project="${id}"
66+
filename="stories.ts"
67+
line-numbers
68+
><md-circular-progress indeterminate></md-circular-progress></playground-file-editor>
69+
</lit-island>
70+
</details>
71+
`;
72+
});
73+
}
74+
75+
module.exports = playgroundExample;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
const htmlMinifier = require('html-minifier');
8+
9+
/**
10+
* Minifies HTML in production mode. Does nothing in dev mode for a faster build
11+
* and debuggability
12+
*
13+
* @param eleventyConfig The 11ty config in which to attach this transform.
14+
* @param isDev {boolean} Whether or not the build is in development mode.
15+
*/
16+
function minifyHTML(eleventyConfig, isDev) {
17+
eleventyConfig.addTransform('htmlMinify', function (content, outputPath) {
18+
// return the normal content in dev moe.
19+
if (isDev || !outputPath.endsWith('.html')) {
20+
return content;
21+
}
22+
// minify the html in Prod mode
23+
const minified = htmlMinifier.minify(content, {
24+
useShortDoctype: true,
25+
removeComments: true,
26+
collapseWhitespace: true,
27+
});
28+
return minified;
29+
});
30+
}
31+
32+
module.exports = minifyHTML;

0 commit comments

Comments
 (0)