source: trip-planner-front/node_modules/critters/dist/critters.cjs.js@ 571e0df

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

primeNG components

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