source: trip-planner-front/node_modules/critters/dist/critters.js@ bdd6491

Last change on this file since bdd6491 was e29cc2e, checked in by Ema <ema_spirova@…>, 3 years ago

primeNG components

  • Property mode set to 100644
File size: 33.8 KB
RevLine 
[6a3a178]1function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
2
3var path = _interopDefault(require('path'));
4var prettyBytes = _interopDefault(require('pretty-bytes'));
5var parse5 = _interopDefault(require('parse5'));
[e29cc2e]6var cssSelect = require('css-select');
7var treeAdapter = _interopDefault(require('parse5-htmlparser2-tree-adapter'));
8var postcss = require('postcss');
[6a3a178]9var chalk = _interopDefault(require('chalk'));
10
11/**
12 * Copyright 2018 Google LLC
13 *
14 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
15 * use this file except in compliance with the License. You may obtain a copy of
16 * the License at
17 *
18 * http://www.apache.org/licenses/LICENSE-2.0
19 *
20 * Unless required by applicable law or agreed to in writing, software
21 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
22 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
23 * License for the specific language governing permissions and limitations under
24 * the License.
25 */
26
27const PARSE5_OPTS = {
28 treeAdapter
29};
30/**
31 * Parse HTML into a mutable, serializable DOM Document.
32 * The DOM implementation is an htmlparser2 DOM enhanced with basic DOM mutation methods.
33 * @param {String} html HTML to parse into a Document instance
34 */
35
36function createDocument(html) {
[e29cc2e]37 const document =
38 /** @type {HTMLDocument} */
39 parse5.parse(html, PARSE5_OPTS);
[6a3a178]40 defineProperties(document, DocumentExtensions); // Extend Element.prototype with DOM manipulation methods.
41
42 const scratch = document.createElement('div'); // Get a reference to the base Node class - used by createTextNode()
43
44 document.$$Node = scratch.constructor;
45 const elementProto = Object.getPrototypeOf(scratch);
46 defineProperties(elementProto, ElementExtensions);
47 elementProto.ownerDocument = document;
48 return document;
49}
50/**
51 * Serialize a Document to an HTML String
[e29cc2e]52 * @param {HTMLDocument} document A Document, such as one created via `createDocument()`
[6a3a178]53 */
54
55function serializeDocument(document) {
56 return parse5.serialize(document, PARSE5_OPTS);
57}
[e29cc2e]58/** @typedef {treeAdapter.Document & typeof ElementExtensions} HTMLDocument */
59
[6a3a178]60/**
61 * Methods and descriptors to mix into Element.prototype
[e29cc2e]62 * @private
[6a3a178]63 */
64
65const ElementExtensions = {
[e29cc2e]66 /** @extends treeAdapter.Element.prototype */
[6a3a178]67 nodeName: {
68 get() {
69 return this.tagName.toUpperCase();
70 }
71
72 },
73 id: reflectedProperty('id'),
74 className: reflectedProperty('class'),
75
76 insertBefore(child, referenceNode) {
77 if (!referenceNode) return this.appendChild(child);
78 treeAdapter.insertBefore(this, child, referenceNode);
79 return child;
80 },
81
82 appendChild(child) {
83 treeAdapter.appendChild(this, child);
84 return child;
85 },
86
87 removeChild(child) {
88 treeAdapter.detachNode(child);
89 },
90
91 remove() {
92 treeAdapter.detachNode(this);
93 },
94
95 textContent: {
96 get() {
[e29cc2e]97 return getText(this);
[6a3a178]98 },
99
100 set(text) {
101 this.children = [];
102 treeAdapter.insertText(this, text);
103 }
104
105 },
106
107 setAttribute(name, value) {
108 if (this.attribs == null) this.attribs = {};
109 if (value == null) value = '';
110 this.attribs[name] = value;
111 },
112
113 removeAttribute(name) {
114 if (this.attribs != null) {
115 delete this.attribs[name];
116 }
117 },
118
119 getAttribute(name) {
120 return this.attribs != null && this.attribs[name];
121 },
122
123 hasAttribute(name) {
124 return this.attribs != null && this.attribs[name] != null;
125 },
126
127 getAttributeNode(name) {
128 const value = this.getAttribute(name);
129 if (value != null) return {
130 specified: true,
131 value
132 };
133 }
134
135};
136/**
137 * Methods and descriptors to mix into the global document instance
138 * @private
139 */
140
141const DocumentExtensions = {
[e29cc2e]142 /** @extends treeAdapter.Document.prototype */
[6a3a178]143 // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
144 // TODO: verify if these are needed for css-select
145 nodeType: {
146 get() {
147 return 9;
148 }
149
150 },
151 contentType: {
152 get() {
153 return 'text/html';
154 }
155
156 },
157 nodeName: {
158 get() {
159 return '#document';
160 }
161
162 },
163 documentElement: {
164 get() {
165 // Find the first <html> element within the document
166 return this.childNodes.filter(child => String(child.tagName).toLowerCase() === 'html');
167 }
168
169 },
170 compatMode: {
171 get() {
172 const compatMode = {
173 'no-quirks': 'CSS1Compat',
174 quirks: 'BackCompat',
175 'limited-quirks': 'CSS1Compat'
176 };
177 return compatMode[treeAdapter.getDocumentMode(this)];
178 }
179
180 },
181 body: {
182 get() {
183 return this.querySelector('body');
184 }
185
186 },
187
188 createElement(name) {
189 return treeAdapter.createElement(name, null, []);
190 },
191
192 createTextNode(text) {
193 // there is no dedicated createTextNode equivalent exposed in htmlparser2's DOM
194 const Node = this.$$Node;
195 return new Node({
196 type: 'text',
197 data: text,
198 parent: null,
199 prev: null,
200 next: null
201 });
202 },
203
204 querySelector(sel) {
205 return cssSelect.selectOne(sel, this.documentElement);
206 },
207
208 querySelectorAll(sel) {
209 if (sel === ':root') {
210 return this;
211 }
212
[e29cc2e]213 return cssSelect.selectAll(sel, this.documentElement);
[6a3a178]214 }
215
216};
217/**
218 * Essentially `Object.defineProperties()`, except function values are assigned as value descriptors for convenience.
219 * @private
220 */
221
222function defineProperties(obj, properties) {
223 for (const i in properties) {
224 const value = properties[i];
225 Object.defineProperty(obj, i, typeof value === 'function' ? {
226 value
227 } : value);
228 }
229}
230/**
231 * Create a property descriptor defining a getter/setter pair alias for a named attribute.
232 * @private
233 */
234
235
236function reflectedProperty(attributeName) {
237 return {
238 get() {
239 return this.getAttribute(attributeName);
240 },
241
242 set(value) {
243 this.setAttribute(attributeName, value);
244 }
245
246 };
247}
248/**
249 * Helper to get the text content of a node
250 * https://github.com/fb55/domutils/blob/master/src/stringify.ts#L21
251 * @private
252 */
253
254
[e29cc2e]255function getText(node) {
256 if (Array.isArray(node)) return node.map(getText).join('');
257 if (treeAdapter.isElementNode(node)) return node.name === 'br' ? '\n' : getText(node.children);
[6a3a178]258 if (treeAdapter.isTextNode(node)) return node.data;
259 return '';
260}
261
262/**
263 * Copyright 2018 Google LLC
264 *
265 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
266 * use this file except in compliance with the License. You may obtain a copy of
267 * the License at
268 *
269 * http://www.apache.org/licenses/LICENSE-2.0
270 *
271 * Unless required by applicable law or agreed to in writing, software
272 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
273 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
274 * License for the specific language governing permissions and limitations under
275 * the License.
276 */
277/**
278 * Parse a textual CSS Stylesheet into a Stylesheet instance.
[e29cc2e]279 * Stylesheet is a mutable postcss AST with format similar to CSSOM.
280 * @see https://github.com/postcss/postcss/
[6a3a178]281 * @private
282 * @param {String} stylesheet
283 * @returns {css.Stylesheet} ast
284 */
285
286function parseStylesheet(stylesheet) {
[e29cc2e]287 return postcss.parse(stylesheet);
[6a3a178]288}
289/**
[e29cc2e]290 * Serialize a postcss Stylesheet to a String of CSS.
[6a3a178]291 * @private
292 * @param {css.Stylesheet} ast A Stylesheet to serialize, such as one returned from `parseStylesheet()`
[e29cc2e]293 * @param {Object} options Options used by the stringify logic
[6a3a178]294 * @param {Boolean} [options.compress] Compress CSS output (removes comments, whitespace, etc)
295 */
296
297function serializeStylesheet(ast, options) {
[e29cc2e]298 let cssStr = '';
299 postcss.stringify(ast, (result, node, type) => {
300 var _node$raws;
301
302 if (!options.compress) {
303 cssStr += result;
304 return;
305 } // Simple minification logic
306
307
308 if ((node == null ? void 0 : node.type) === 'comment') return;
309
310 if ((node == null ? void 0 : node.type) === 'decl') {
311 const prefix = node.prop + node.raws.between;
312 cssStr += result.replace(prefix, prefix.trim());
313 return;
314 }
315
316 if (type === 'start') {
317 if (node.type === 'rule' && node.selectors) {
318 cssStr += node.selectors.join(',') + '{';
319 } else {
320 cssStr += result.replace(/\s\{$/, '{');
321 }
322
323 return;
324 }
325
326 if (type === 'end' && result === '}' && node != null && (_node$raws = node.raws) != null && _node$raws.semicolon) {
327 cssStr = cssStr.slice(0, -1);
328 }
329
330 cssStr += result.trim();
331 });
332 return cssStr;
[6a3a178]333}
334/**
335 * Converts a walkStyleRules() iterator to mark nodes with `.$$remove=true` instead of actually removing them.
336 * This means they can be removed in a second pass, allowing the first pass to be nondestructive (eg: to preserve mirrored sheets).
337 * @private
338 * @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node.
339 * @returns {(rule) => void} nonDestructiveIterator
340 */
341
342function markOnly(predicate) {
343 return rule => {
344 const sel = rule.selectors;
345
346 if (predicate(rule) === false) {
347 rule.$$remove = true;
348 }
349
350 rule.$$markedSelectors = rule.selectors;
351
352 if (rule._other) {
353 rule._other.$$markedSelectors = rule._other.selectors;
354 }
355
356 rule.selectors = sel;
357 };
358}
359/**
360 * Apply filtered selectors to a rule from a previous markOnly run.
361 * @private
362 * @param {css.Rule} rule The Rule to apply marked selectors to (if they exist).
[e29cc2e]363 */
[6a3a178]364
365function applyMarkedSelectors(rule) {
366 if (rule.$$markedSelectors) {
367 rule.selectors = rule.$$markedSelectors;
368 }
369
370 if (rule._other) {
371 applyMarkedSelectors(rule._other);
372 }
373}
374/**
375 * Recursively walk all rules in a stylesheet.
376 * @private
377 * @param {css.Rule} node A Stylesheet or Rule to descend into.
378 * @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node.
379 */
380
381function walkStyleRules(node, iterator) {
[e29cc2e]382 node.nodes = node.nodes.filter(rule => {
383 if (hasNestedRules(rule)) {
[6a3a178]384 walkStyleRules(rule, iterator);
385 }
386
387 rule._other = undefined;
388 rule.filterSelectors = filterSelectors;
389 return iterator(rule) !== false;
390 });
391}
392/**
393 * Recursively walk all rules in two identical stylesheets, filtering nodes into one or the other based on a predicate.
394 * @private
395 * @param {css.Rule} node A Stylesheet or Rule to descend into.
396 * @param {css.Rule} node2 A second tree identical to `node`
397 * @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node from the first tree, true to remove it from the second.
398 */
399
400function walkStyleRulesWithReverseMirror(node, node2, iterator) {
401 if (node2 === null) return walkStyleRules(node, iterator);
[e29cc2e]402 [node.nodes, node2.nodes] = splitFilter(node.nodes, node2.nodes, (rule, index, rules, rules2) => {
[6a3a178]403 const rule2 = rules2[index];
404
[e29cc2e]405 if (hasNestedRules(rule)) {
[6a3a178]406 walkStyleRulesWithReverseMirror(rule, rule2, iterator);
407 }
408
409 rule._other = rule2;
410 rule.filterSelectors = filterSelectors;
411 return iterator(rule) !== false;
412 });
[e29cc2e]413} // Checks if a node has nested rules, like @media
414// @keyframes are an exception since they are evaluated as a whole
415
416function hasNestedRules(rule) {
417 return rule.nodes && rule.nodes.length && rule.nodes.some(n => n.type === 'rule' || n.type === 'atrule') && rule.name !== 'keyframes';
[6a3a178]418} // Like [].filter(), but applies the opposite filtering result to a second copy of the Array without a second pass.
419// This is just a quicker version of generating the compliment of the set returned from a filter operation.
420
[e29cc2e]421
[6a3a178]422function splitFilter(a, b, predicate) {
423 const aOut = [];
424 const bOut = [];
425
426 for (let index = 0; index < a.length; index++) {
427 if (predicate(a[index], index, a, b)) {
428 aOut.push(a[index]);
429 } else {
430 bOut.push(a[index]);
431 }
432 }
433
434 return [aOut, bOut];
435} // can be invoked on a style rule to subset its selectors (with reverse mirroring)
436
437
438function filterSelectors(predicate) {
439 if (this._other) {
440 const [a, b] = splitFilter(this.selectors, this._other.selectors, predicate);
441 this.selectors = a;
442 this._other.selectors = b;
443 } else {
444 this.selectors = this.selectors.filter(predicate);
445 }
446}
447
448const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'silent'];
449const defaultLogger = {
450 trace(msg) {
451 console.trace(msg);
452 },
453
454 debug(msg) {
455 console.debug(msg);
456 },
457
458 warn(msg) {
459 console.warn(chalk.yellow(msg));
460 },
461
462 error(msg) {
463 console.error(chalk.bold.red(msg));
464 },
465
466 info(msg) {
467 console.info(chalk.bold.blue(msg));
468 },
469
470 silent() {}
471
472};
473function createLogger(logLevel) {
474 const logLevelIdx = LOG_LEVELS.indexOf(logLevel);
475 return LOG_LEVELS.reduce((logger, type, index) => {
476 if (index >= logLevelIdx) {
477 logger[type] = defaultLogger[type];
478 } else {
479 logger[type] = defaultLogger.silent;
480 }
481
482 return logger;
483 }, {});
484}
485
486/**
487 * Copyright 2018 Google LLC
488 *
489 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
490 * use this file except in compliance with the License. You may obtain a copy of
491 * the License at
492 *
493 * http://www.apache.org/licenses/LICENSE-2.0
494 *
495 * Unless required by applicable law or agreed to in writing, software
496 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
497 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
498 * License for the specific language governing permissions and limitations under
499 * the License.
500 */
501/**
502 * The mechanism to use for lazy-loading stylesheets.
[e29cc2e]503 *
[6a3a178]504 * Note: <kbd>JS</kbd> indicates a strategy requiring JavaScript (falls back to `<noscript>` unless disabled).
505 *
506 * - **default:** Move stylesheet links to the end of the document and insert preload meta tags in their place.
507 * - **"body":** Move all external stylesheet links to the end of the document.
508 * - **"media":** Load stylesheets asynchronously by adding `media="not x"` and removing once loaded. <kbd>JS</kbd>
509 * - **"swap":** Convert stylesheet links to preloads that swap to `rel="stylesheet"` once loaded ([details](https://www.filamentgroup.com/lab/load-css-simpler/#the-code)). <kbd>JS</kbd>
510 * - **"swap-high":** Use `<link rel="alternate stylesheet preload">` and swap to `rel="stylesheet"` once loaded ([details](http://filamentgroup.github.io/loadCSS/test/new-high.html)). <kbd>JS</kbd>
511 * - **"js":** Inject an asynchronous CSS loader similar to [LoadCSS](https://github.com/filamentgroup/loadCSS) and use it to load stylesheets. <kbd>JS</kbd>
512 * - **"js-lazy":** Like `"js"`, but the stylesheet is disabled until fully loaded.
513 * @typedef {(default|'body'|'media'|'swap'|'swap-high'|'js'|'js-lazy')} PreloadStrategy
514 * @public
515 */
516
517/**
518 * Controls which keyframes rules are inlined.
519 *
520 * - **"critical":** _(default)_ inline keyframes rules that are used by the critical CSS.
521 * - **"all":** Inline all keyframes rules.
522 * - **"none":** Remove all keyframes rules.
523 * @typedef {('critical'|'all'|'none')} KeyframeStrategy
524 * @private
525 * @property {String} keyframes Which {@link KeyframeStrategy keyframe strategy} to use (default: `critical`)_
526 */
527
528/**
529 * Controls log level of the plugin. Specifies the level the logger should use. A logger will
530 * not produce output for any log level beneath the specified level. Available levels and order
531 * are:
532 *
533 * - **"info"** _(default)_
534 * - **"warn"**
535 * - **"error"**
536 * - **"trace"**
537 * - **"debug"**
538 * - **"silent"**
539 * @typedef {('info'|'warn'|'error'|'trace'|'debug'|'silent')} LogLevel
540 * @public
541 */
542
543/**
544 * Custom logger interface:
545 * @typedef {object} Logger
546 * @public
547 * @property {function(String)} trace - Prints a trace message
548 * @property {function(String)} debug - Prints a debug message
549 * @property {function(String)} info - Prints an information message
550 * @property {function(String)} warn - Prints a warning message
551 * @property {function(String)} error - Prints an error message
552 */
553
554/**
555 * All optional. Pass them to `new Critters({ ... })`.
556 * @public
557 * @typedef Options
558 * @property {String} path Base path location of the CSS files _(default: `''`)_
559 * @property {String} publicPath Public path of the CSS resources. This prefix is removed from the href _(default: `''`)_
560 * @property {Boolean} external Inline styles from external stylesheets _(default: `true`)_
561 * @property {Number} inlineThreshold Inline external stylesheets smaller than a given size _(default: `0`)_
562 * @property {Number} minimumExternalSize If the non-critical external stylesheet would be below this size, just inline it _(default: `0`)_
563 * @property {Boolean} pruneSource Remove inlined rules from the external stylesheet _(default: `false`)_
564 * @property {Boolean} mergeStylesheets Merged inlined stylesheets into a single <style> tag _(default: `true`)_
565 * @property {String[]} additionalStylesheets Glob for matching other stylesheets to be used while looking for critical CSS _(default: ``)_.
566 * @property {String} preload Which {@link PreloadStrategy preload strategy} to use
567 * @property {Boolean} noscriptFallback Add `<noscript>` fallback to JS-based strategies
568 * @property {Boolean} inlineFonts Inline critical font-face rules _(default: `false`)_
569 * @property {Boolean} preloadFonts Preloads critical fonts _(default: `true`)_
570 * @property {Boolean} fonts Shorthand for setting `inlineFonts`+`preloadFonts`
571 * - Values:
572 * - `true` to inline critical font-face rules and preload the fonts
573 * - `false` to don't inline any font-face rules and don't preload fonts
574 * @property {String} keyframes Controls which keyframes rules are inlined.
575 * - Values:
576 * - `"critical"`: _(default)_ inline keyframes rules used by the critical CSS
577 * - `"all"` inline all keyframes rules
578 * - `"none"` remove all keyframes rules
579 * @property {Boolean} compress Compress resulting critical CSS _(default: `true`)_
580 * @property {String} logLevel Controls {@link LogLevel log level} of the plugin _(default: `"info"`)_
581 * @property {object} logger Provide a custom logger interface {@link Logger logger}
582 */
583
584class Critters {
585 /** @private */
586 constructor(options) {
587 this.options = Object.assign({
588 logLevel: 'info',
589 path: '',
590 publicPath: '',
591 reduceInlineStyles: true,
592 pruneSource: false,
593 additionalStylesheets: []
594 }, options || {});
595 this.urlFilter = this.options.filter;
596
597 if (this.urlFilter instanceof RegExp) {
598 this.urlFilter = this.urlFilter.test.bind(this.urlFilter);
599 }
600
601 this.logger = this.options.logger || createLogger(this.options.logLevel);
602 }
603 /**
604 * Read the contents of a file from the specified filesystem or disk
605 */
606
607
608 readFile(filename) {
609 const fs = this.fs;
610 return new Promise((resolve, reject) => {
611 const callback = (err, data) => {
612 if (err) reject(err);else resolve(data);
613 };
614
615 if (fs && fs.readFile) {
616 fs.readFile(filename, callback);
617 } else {
618 require('fs').readFile(filename, 'utf8', callback);
619 }
620 });
621 }
622 /**
623 * Apply critical CSS processing to the html
624 */
625
626
[e29cc2e]627 async process(html) {
628 const start = process.hrtime.bigint(); // Parse the generated HTML in a DOM we can mutate
[6a3a178]629
[e29cc2e]630 const document = createDocument(html);
[6a3a178]631
[e29cc2e]632 if (this.options.additionalStylesheets.length > 0) {
633 this.embedAdditionalStylesheet(document);
634 } // `external:false` skips processing of external sheets
[6a3a178]635
636
[e29cc2e]637 if (this.options.external !== false) {
638 const externalSheets = [].slice.call(document.querySelectorAll('link[rel="stylesheet"]'));
639 await Promise.all(externalSheets.map(link => this.embedLinkedStylesheet(link, document)));
640 } // go through all the style tags in the document and reduce them to only critical CSS
[6a3a178]641
642
[e29cc2e]643 const styles = this.getAffectedStyleTags(document);
644 await Promise.all(styles.map(style => this.processStyle(style, document)));
[6a3a178]645
[e29cc2e]646 if (this.options.mergeStylesheets !== false && styles.length !== 0) {
647 await this.mergeStylesheets(document);
648 } // serialize the document back to HTML and we're done
[6a3a178]649
650
[e29cc2e]651 const output = serializeDocument(document);
652 const end = process.hrtime.bigint();
653 this.logger.info('Time ' + parseFloat(end - start) / 1000000.0);
654 return output;
[6a3a178]655 }
656 /**
657 * Get the style tags that need processing
658 */
659
660
661 getAffectedStyleTags(document) {
662 const styles = [].slice.call(document.querySelectorAll('style')); // `inline:false` skips processing of inline stylesheets
663
664 if (this.options.reduceInlineStyles === false) {
665 return styles.filter(style => style.$$external);
666 }
667
668 return styles;
669 }
670
[e29cc2e]671 async mergeStylesheets(document) {
672 const styles = this.getAffectedStyleTags(document);
[6a3a178]673
[e29cc2e]674 if (styles.length === 0) {
675 this.logger.warn('Merging inline stylesheets into a single <style> tag skipped, no inline stylesheets to merge');
676 return;
677 }
[6a3a178]678
[e29cc2e]679 const first = styles[0];
680 let sheet = first.textContent;
[6a3a178]681
[e29cc2e]682 for (let i = 1; i < styles.length; i++) {
683 const node = styles[i];
684 sheet += node.textContent;
685 node.remove();
[6a3a178]686 }
[e29cc2e]687
688 first.textContent = sheet;
[6a3a178]689 }
690 /**
691 * Given href, find the corresponding CSS asset
692 */
693
694
[e29cc2e]695 async getCssAsset(href) {
696 const outputPath = this.options.path;
697 const publicPath = this.options.publicPath; // CHECK - the output path
698 // path on disk (with output.publicPath removed)
[6a3a178]699
[e29cc2e]700 let normalizedPath = href.replace(/^\//, '');
701 const pathPrefix = (publicPath || '').replace(/(^\/|\/$)/g, '') + '/';
[6a3a178]702
[e29cc2e]703 if (normalizedPath.indexOf(pathPrefix) === 0) {
704 normalizedPath = normalizedPath.substring(pathPrefix.length).replace(/^\//, '');
705 } // Ignore remote stylesheets
[6a3a178]706
707
[e29cc2e]708 if (/^https:\/\//.test(normalizedPath) || href.startsWith('//')) {
709 return undefined;
710 }
[6a3a178]711
[e29cc2e]712 const filename = path.resolve(outputPath, normalizedPath);
713 let sheet;
[6a3a178]714
[e29cc2e]715 try {
716 sheet = await this.readFile(filename);
[6a3a178]717 } catch (e) {
[e29cc2e]718 this.logger.warn(`Unable to locate stylesheet: ${filename}`);
[6a3a178]719 }
[e29cc2e]720
721 return sheet;
[6a3a178]722 }
723
724 checkInlineThreshold(link, style, sheet) {
725 if (this.options.inlineThreshold && sheet.length < this.options.inlineThreshold) {
726 const href = style.$$name;
727 style.$$reduce = false;
728 this.logger.info(`\u001b[32mInlined all of ${href} (${sheet.length} was below the threshold of ${this.options.inlineThreshold})\u001b[39m`);
729 link.remove();
730 return true;
731 }
732
733 return false;
734 }
735 /**
736 * Inline the stylesheets from options.additionalStylesheets (assuming it passes `options.filter`)
737 */
738
739
[e29cc2e]740 async embedAdditionalStylesheet(document) {
741 const styleSheetsIncluded = [];
742 const sources = await Promise.all(this.options.additionalStylesheets.map(cssFile => {
743 if (styleSheetsIncluded.includes(cssFile)) {
744 return;
745 }
[6a3a178]746
[e29cc2e]747 styleSheetsIncluded.push(cssFile);
748 const style = document.createElement('style');
749 style.$$external = true;
750 return this.getCssAsset(cssFile, style).then(sheet => [sheet, style]);
751 }));
752 sources.forEach(([sheet, style]) => {
753 if (!sheet) return;
754 style.textContent = sheet;
755 document.head.appendChild(style);
756 });
[6a3a178]757 }
758 /**
759 * Inline the target stylesheet referred to by a <link rel="stylesheet"> (assuming it passes `options.filter`)
760 */
761
762
[e29cc2e]763 async embedLinkedStylesheet(link, document) {
764 const href = link.getAttribute('href');
765 const media = link.getAttribute('media');
766 const preloadMode = this.options.preload; // skip filtered resources, or network resources if no filter is provided
[6a3a178]767
[e29cc2e]768 if (this.urlFilter ? this.urlFilter(href) : !(href || '').match(/\.css$/)) {
769 return Promise.resolve();
770 } // the reduced critical CSS gets injected into a new <style> tag
[6a3a178]771
772
[e29cc2e]773 const style = document.createElement('style');
774 style.$$external = true;
775 const sheet = await this.getCssAsset(href, style);
[6a3a178]776
[e29cc2e]777 if (!sheet) {
778 return;
779 }
[6a3a178]780
[e29cc2e]781 style.textContent = sheet;
782 style.$$name = href;
783 style.$$links = [link];
784 link.parentNode.insertBefore(style, link);
[6a3a178]785
[e29cc2e]786 if (this.checkInlineThreshold(link, style, sheet)) {
787 return;
788 } // CSS loader is only injected for the first sheet, then this becomes an empty string
789
790
791 let cssLoaderPreamble = "function $loadcss(u,m,l){(l=document.createElement('link')).rel='stylesheet';l.href=u;document.head.appendChild(l)}";
792 const lazy = preloadMode === 'js-lazy';
793
794 if (lazy) {
795 cssLoaderPreamble = cssLoaderPreamble.replace('l.href', "l.media='print';l.onload=function(){l.media=m};l.href");
796 } // Allow disabling any mutation of the stylesheet link:
797
798
799 if (preloadMode === false) return;
800 let noscriptFallback = false;
801
802 if (preloadMode === 'body') {
803 document.body.appendChild(link);
804 } else {
805 link.setAttribute('rel', 'preload');
806 link.setAttribute('as', 'style');
807
808 if (preloadMode === 'js' || preloadMode === 'js-lazy') {
809 const script = document.createElement('script');
810 const js = `${cssLoaderPreamble}$loadcss(${JSON.stringify(href)}${lazy ? ',' + JSON.stringify(media || 'all') : ''})`; // script.appendChild(document.createTextNode(js));
811
812 script.textContent = js;
813 link.parentNode.insertBefore(script, link.nextSibling);
814 style.$$links.push(script);
815 cssLoaderPreamble = '';
816 noscriptFallback = true;
817 } else if (preloadMode === 'media') {
818 // @see https://github.com/filamentgroup/loadCSS/blob/af1106cfe0bf70147e22185afa7ead96c01dec48/src/loadCSS.js#L26
819 link.setAttribute('rel', 'stylesheet');
820 link.removeAttribute('as');
821 link.setAttribute('media', 'print');
822 link.setAttribute('onload', `this.media='${media || 'all'}'`);
823 noscriptFallback = true;
824 } else if (preloadMode === 'swap-high') {
825 // @see http://filamentgroup.github.io/loadCSS/test/new-high.html
826 link.setAttribute('rel', 'alternate stylesheet preload');
827 link.setAttribute('title', 'styles');
828 link.setAttribute('onload', `this.title='';this.rel='stylesheet'`);
829 noscriptFallback = true;
830 } else if (preloadMode === 'swap') {
831 link.setAttribute('onload', "this.rel='stylesheet'");
832 noscriptFallback = true;
833 } else {
834 const bodyLink = document.createElement('link');
835 bodyLink.setAttribute('rel', 'stylesheet');
836 if (media) bodyLink.setAttribute('media', media);
837 bodyLink.setAttribute('href', href);
838 document.body.appendChild(bodyLink);
839 style.$$links.push(bodyLink);
840 }
841 }
842
843 if (this.options.noscriptFallback !== false && noscriptFallback) {
844 const noscript = document.createElement('noscript');
845 const noscriptLink = document.createElement('link');
846 noscriptLink.setAttribute('rel', 'stylesheet');
847 noscriptLink.setAttribute('href', href);
848 if (media) noscriptLink.setAttribute('media', media);
849 noscript.appendChild(noscriptLink);
850 link.parentNode.insertBefore(noscript, link.nextSibling);
851 style.$$links.push(noscript);
[6a3a178]852 }
853 }
854 /**
855 * Prune the source CSS files
856 */
857
858
859 pruneSource(style, before, sheetInverse) {
860 // if external stylesheet would be below minimum size, just inline everything
861 const minSize = this.options.minimumExternalSize;
862 const name = style.$$name;
863
864 if (minSize && sheetInverse.length < minSize) {
865 this.logger.info(`\u001b[32mInlined all of ${name} (non-critical external stylesheet would have been ${sheetInverse.length}b, which was below the threshold of ${minSize})\u001b[39m`);
866 style.textContent = before; // remove any associated external resources/loaders:
867
868 if (style.$$links) {
869 for (const link of style.$$links) {
870 const parent = link.parentNode;
871 if (parent) parent.removeChild(link);
872 }
873 }
874
875 return true;
876 }
877
878 return false;
879 }
880 /**
881 * Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document.
882 */
883
884
[e29cc2e]885 async processStyle(style, document) {
886 if (style.$$reduce === false) return;
887 const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS';
888 const options = this.options; // const document = style.ownerDocument;
[6a3a178]889
[e29cc2e]890 const head = document.querySelector('head');
891 let keyframesMode = options.keyframes || 'critical'; // we also accept a boolean value for options.keyframes
[6a3a178]892
[e29cc2e]893 if (keyframesMode === true) keyframesMode = 'all';
894 if (keyframesMode === false) keyframesMode = 'none';
895 let sheet = style.textContent; // store a reference to the previous serialized stylesheet for reporting stats
896
897 const before = sheet; // Skip empty stylesheets
898
899 if (!sheet) return;
900 const ast = parseStylesheet(sheet);
901 const astInverse = options.pruneSource ? parseStylesheet(sheet) : null; // a string to search for font names (very loose)
902
903 let criticalFonts = '';
904 const failedSelectors = [];
905 const criticalKeyframeNames = []; // Walk all CSS rules, marking unused rules with `.$$remove=true` for removal in the second pass.
906 // This first pass is also used to collect font and keyframe usage used in the second pass.
907
908 walkStyleRules(ast, markOnly(rule => {
909 if (rule.type === 'rule') {
910 // Filter the selector list down to only those match
911 rule.filterSelectors(sel => {
912 // Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
913 // This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
914 if (sel === ':root' || sel.match(/^::?(before|after)$/)) {
915 return true;
916 }
917
918 sel = sel.replace(/(?<!\\)::?[a-z-]+(?![a-z-(])/gi, '').replace(/::?not\(\s*\)/g, '').trim();
919 if (!sel) return false;
[6a3a178]920
[e29cc2e]921 try {
922 return document.querySelector(sel) != null;
923 } catch (e) {
924 failedSelectors.push(sel + ' -> ' + e.message);
[6a3a178]925 return false;
926 }
[e29cc2e]927 }); // If there are no matched selectors, remove the rule:
[6a3a178]928
[e29cc2e]929 if (!rule.selector) {
930 return false;
931 }
932
933 if (rule.nodes) {
934 for (let i = 0; i < rule.nodes.length; i++) {
935 const decl = rule.nodes[i]; // detect used fonts
[6a3a178]936
[e29cc2e]937 if (decl.prop && decl.prop.match(/\bfont(-family)?\b/i)) {
938 criticalFonts += ' ' + decl.value;
939 } // detect used keyframes
[6a3a178]940
941
[e29cc2e]942 if (decl.prop === 'animation' || decl.prop === 'animation-name') {
943 // @todo: parse animation declarations and extract only the name. for now we'll do a lazy match.
944 const names = decl.value.split(/\s+/);
[6a3a178]945
[e29cc2e]946 for (let j = 0; j < names.length; j++) {
947 const name = names[j].trim();
948 if (name) criticalKeyframeNames.push(name);
[6a3a178]949 }
950 }
951 }
[e29cc2e]952 }
953 } // keep font rules, they're handled in the second pass:
[6a3a178]954
955
[e29cc2e]956 if (rule.type === 'atrule' && rule.name === 'font-face') return; // If there are no remaining rules, remove the whole rule:
[6a3a178]957
[e29cc2e]958 const rules = rule.nodes && rule.nodes.filter(rule => !rule.$$remove);
959 return !rules || rules.length !== 0;
960 }));
[6a3a178]961
[e29cc2e]962 if (failedSelectors.length !== 0) {
963 this.logger.warn(`${failedSelectors.length} rules skipped due to selector errors:\n ${failedSelectors.join('\n ')}`);
964 }
[6a3a178]965
[e29cc2e]966 const shouldPreloadFonts = options.fonts === true || options.preloadFonts === true;
967 const shouldInlineFonts = options.fonts !== false && options.inlineFonts === true;
968 const preloadedFonts = []; // Second pass, using data picked up from the first
[6a3a178]969
[e29cc2e]970 walkStyleRulesWithReverseMirror(ast, astInverse, rule => {
971 // remove any rules marked in the first pass
972 if (rule.$$remove === true) return false;
973 applyMarkedSelectors(rule); // prune @keyframes rules
[6a3a178]974
[e29cc2e]975 if (rule.type === 'atrule' && rule.name === 'keyframes') {
976 if (keyframesMode === 'none') return false;
977 if (keyframesMode === 'all') return true;
978 return criticalKeyframeNames.indexOf(rule.params) !== -1;
979 } // prune @font-face rules
[6a3a178]980
981
[e29cc2e]982 if (rule.type === 'atrule' && rule.name === 'font-face') {
983 let family, src;
[6a3a178]984
[e29cc2e]985 for (let i = 0; i < rule.nodes.length; i++) {
986 const decl = rule.nodes[i];
[6a3a178]987
[e29cc2e]988 if (decl.prop === 'src') {
989 // @todo parse this properly and generate multiple preloads with type="font/woff2" etc
990 src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
991 } else if (decl.prop === 'font-family') {
992 family = decl.value;
[6a3a178]993 }
[e29cc2e]994 }
[6a3a178]995
[e29cc2e]996 if (src && shouldPreloadFonts && preloadedFonts.indexOf(src) === -1) {
997 preloadedFonts.push(src);
998 const preload = document.createElement('link');
999 preload.setAttribute('rel', 'preload');
1000 preload.setAttribute('as', 'font');
1001 preload.setAttribute('crossorigin', 'anonymous');
1002 preload.setAttribute('href', src.trim());
1003 head.appendChild(preload);
1004 } // if we're missing info, if the font is unused, or if critical font inlining is disabled, remove the rule:
[6a3a178]1005
1006
[e29cc2e]1007 if (!family || !src || criticalFonts.indexOf(family) === -1 || !shouldInlineFonts) {
1008 return false;
[6a3a178]1009 }
1010 }
[e29cc2e]1011 });
1012 sheet = serializeStylesheet(ast, {
1013 compress: this.options.compress !== false
1014 }); // If all rules were removed, get rid of the style element entirely
[6a3a178]1015
[e29cc2e]1016 if (sheet.trim().length === 0) {
1017 if (style.parentNode) {
1018 style.remove();
1019 }
[6a3a178]1020
[e29cc2e]1021 return;
1022 }
[6a3a178]1023
[e29cc2e]1024 let afterText = '';
1025 let styleInlinedCompletely = false;
[6a3a178]1026
[e29cc2e]1027 if (options.pruneSource) {
1028 const sheetInverse = serializeStylesheet(astInverse, {
1029 compress: this.options.compress !== false
1030 });
1031 styleInlinedCompletely = this.pruneSource(style, before, sheetInverse);
[6a3a178]1032
[e29cc2e]1033 if (styleInlinedCompletely) {
1034 const percent = sheetInverse.length / before.length * 100;
1035 afterText = `, reducing non-inlined size ${percent | 0}% to ${prettyBytes(sheetInverse.length)}`;
1036 }
1037 } // replace the inline stylesheet with its critical'd counterpart
[6a3a178]1038
1039
[e29cc2e]1040 if (!styleInlinedCompletely) {
1041 style.textContent = sheet;
1042 } // output stats
[6a3a178]1043
1044
[e29cc2e]1045 const percent = sheet.length / before.length * 100 | 0;
1046 this.logger.info('\u001b[32mInlined ' + prettyBytes(sheet.length) + ' (' + percent + '% of original ' + prettyBytes(before.length) + ') of ' + name + afterText + '.\u001b[39m');
[6a3a178]1047 }
1048
1049}
1050
1051module.exports = Critters;
Note: See TracBrowser for help on using the repository browser.