UNPKG

4.76 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.default = buildSelectorProfile;
7
8var _cssTree = _interopRequireDefault(require("css-tree"));
9
10var _debug = _interopRequireDefault(require("debug"));
11
12function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14const debuglog = (0, _debug.default)('penthouse:preformatting:selectors-profile');
15var pseudoSelectorsToKeep = [':before', ':after', ':visited', ':first-letter', ':first-line']; // detect these selectors regardless of whether one or two semicolons are used
16
17var pseudoSelectorsToKeepRegex = pseudoSelectorsToKeep.map(function (s) {
18 return ':?' + s;
19}).join('|'); // separate in regular expression
20// we will replace all instances of these pseudo selectors; hence global flag
21
22var PSUEDO_SELECTOR_REGEXP = new RegExp(pseudoSelectorsToKeepRegex, 'g');
23
24function matchesSelectors(selector, selectors) {
25 return selectors.some(function (toMatchSelector) {
26 if (toMatchSelector.type === 'RegExp') {
27 const {
28 source,
29 flags
30 } = toMatchSelector;
31 const re = new RegExp(source, flags);
32 return re.test(selector);
33 }
34
35 return toMatchSelector.value === selector;
36 });
37} // returns:
38// true, if selector should be force kept
39// false, if selector should be force removed
40// otherwise the selector string to look for in the critical viewport
41
42
43function normalizeSelector(selectorNode, forceInclude, forceExclude) {
44 const selector = _cssTree.default.generate(selectorNode); // some selectors can't be matched on page.
45 // In these cases we test a slightly modified selector instead
46
47
48 let modifiedSelector = selector.trim();
49
50 if (forceInclude && matchesSelectors(modifiedSelector, forceInclude)) {
51 debuglog('forceInclude', modifiedSelector);
52 return true;
53 }
54
55 if (forceExclude && matchesSelectors(modifiedSelector, forceExclude)) {
56 debuglog('forceExclude', modifiedSelector);
57 return false;
58 }
59
60 if (modifiedSelector.indexOf(':') > -1) {
61 // handle special case selectors, the ones that contain a semicolon (:)
62 // many of these selectors can't be matched to anything on page via JS,
63 // but that still might affect the above the fold styling
64 // ::selection we just remove
65 if (/:?:(-moz-)?selection/.test(modifiedSelector)) {
66 return false;
67 } // for the pseudo selectors that depend on an element, test for presence
68 // of the element (in the critical viewport) instead
69 // (:hover, :focus, :active would be treated same
70 // IF we wanted to keep them for critical path css, but we don’t)
71
72
73 modifiedSelector = modifiedSelector.replace(PSUEDO_SELECTOR_REGEXP, ''); // if selector is purely pseudo (f.e. ::-moz-placeholder), just keep as is.
74 // we can't match it to anything on page, but it can impact above the fold styles
75
76 if (modifiedSelector.replace(/:[:]?([a-zA-Z0-9\-_])*/g, '').trim().length === 0) {
77 return true;
78 } // handle browser specific pseudo selectors bound to elements,
79 // Example, button::-moz-focus-inner, input[type=number]::-webkit-inner-spin-button
80 // remove browser specific pseudo and test for element
81
82
83 modifiedSelector = modifiedSelector.replace(/:?:-[a-z-]*/g, '');
84 }
85
86 return modifiedSelector;
87}
88
89async function buildSelectorProfile(ast, forceInclude, forceExclude) {
90 debuglog('buildSelectorProfile START');
91 const selectors = new Set();
92 const selectorNodeMap = new WeakMap();
93
94 _cssTree.default.walk(ast, {
95 visit: 'Rule',
96 enter: function (rule, item, list) {
97 // ignore rules inside @keyframes at-rule
98 if (this.atrule && _cssTree.default.keyword(this.atrule.name).basename === 'keyframes') {
99 return;
100 } // ignore a rule with a bad selector
101
102
103 if (rule.prelude.type !== 'SelectorList') {
104 return;
105 }
106
107 const addedRule = rule.block.children.some(declarationNode => {
108 if (declarationNode.property === 'grid-area') {
109 const ruleSelectorList = _cssTree.default.generate(rule.prelude);
110
111 debuglog('rule contains grid-area, keeping: ', ruleSelectorList);
112 selectors.add(ruleSelectorList);
113 selectorNodeMap.set(rule.prelude, ruleSelectorList);
114 return true;
115 }
116 });
117 if (addedRule) return; // collect selectors and build a map
118
119 rule.prelude.children.each(selectorNode => {
120 const selector = normalizeSelector(selectorNode, forceInclude, forceExclude);
121
122 if (typeof selector === 'string') {
123 selectors.add(selector);
124 }
125
126 selectorNodeMap.set(selectorNode, selector);
127 });
128 }
129 });
130
131 debuglog('buildSelectorProfile DONE');
132 return {
133 selectorNodeMap,
134 selectors: Array.from(selectors)
135 };
136}
\No newline at end of file