UNPKG

9.15 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * @typedef {import('css-tree').Rule} CsstreeRule
5 * @typedef {import('./types').Specificity} Specificity
6 * @typedef {import('./types').Stylesheet} Stylesheet
7 * @typedef {import('./types').StylesheetRule} StylesheetRule
8 * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration
9 * @typedef {import('./types').ComputedStyles} ComputedStyles
10 * @typedef {import('./types').XastRoot} XastRoot
11 * @typedef {import('./types').XastElement} XastElement
12 * @typedef {import('./types').XastParent} XastParent
13 * @typedef {import('./types').XastChild} XastChild
14 */
15
16const csstree = require('css-tree');
17const csswhat = require('css-what');
18const {
19 syntax: { specificity },
20} = require('csso');
21const { visit, matches } = require('./xast.js');
22const {
23 attrsGroups,
24 inheritableAttrs,
25 presentationNonInheritableGroupAttrs,
26} = require('../plugins/_collections.js');
27
28// @ts-ignore not defined in @types/csstree
29const csstreeWalkSkip = csstree.walk.skip;
30
31/**
32 * @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule[]}
33 */
34const parseRule = (ruleNode, dynamic) => {
35 /**
36 * @type {StylesheetDeclaration[]}
37 */
38 const declarations = [];
39 // collect declarations
40 ruleNode.block.children.forEach((cssNode) => {
41 if (cssNode.type === 'Declaration') {
42 declarations.push({
43 name: cssNode.property,
44 value: csstree.generate(cssNode.value),
45 important: cssNode.important === true,
46 });
47 }
48 });
49
50 /** @type {StylesheetRule[]} */
51 const rules = [];
52 csstree.walk(ruleNode.prelude, (node) => {
53 if (node.type === 'Selector') {
54 const newNode = csstree.clone(node);
55 let hasPseudoClasses = false;
56 csstree.walk(newNode, (pseudoClassNode, item, list) => {
57 if (pseudoClassNode.type === 'PseudoClassSelector') {
58 hasPseudoClasses = true;
59 list.remove(item);
60 }
61 });
62 rules.push({
63 specificity: specificity(node),
64 dynamic: hasPseudoClasses || dynamic,
65 // compute specificity from original node to consider pseudo classes
66 selector: csstree.generate(newNode),
67 declarations,
68 });
69 }
70 });
71
72 return rules;
73};
74
75/**
76 * @type {(css: string, dynamic: boolean) => StylesheetRule[]}
77 */
78const parseStylesheet = (css, dynamic) => {
79 /** @type {StylesheetRule[]} */
80 const rules = [];
81 const ast = csstree.parse(css, {
82 parseValue: false,
83 parseAtrulePrelude: false,
84 });
85 csstree.walk(ast, (cssNode) => {
86 if (cssNode.type === 'Rule') {
87 rules.push(...parseRule(cssNode, dynamic || false));
88 return csstreeWalkSkip;
89 }
90 if (cssNode.type === 'Atrule') {
91 if (
92 cssNode.name === 'keyframes' ||
93 cssNode.name === '-webkit-keyframes'
94 ) {
95 return csstreeWalkSkip;
96 }
97 csstree.walk(cssNode, (ruleNode) => {
98 if (ruleNode.type === 'Rule') {
99 rules.push(...parseRule(ruleNode, dynamic || true));
100 return csstreeWalkSkip;
101 }
102 });
103 return csstreeWalkSkip;
104 }
105 });
106 return rules;
107};
108
109/**
110 * @type {(css: string) => StylesheetDeclaration[]}
111 */
112const parseStyleDeclarations = (css) => {
113 /** @type {StylesheetDeclaration[]} */
114 const declarations = [];
115 const ast = csstree.parse(css, {
116 context: 'declarationList',
117 parseValue: false,
118 });
119 csstree.walk(ast, (cssNode) => {
120 if (cssNode.type === 'Declaration') {
121 declarations.push({
122 name: cssNode.property,
123 value: csstree.generate(cssNode.value),
124 important: cssNode.important === true,
125 });
126 }
127 });
128 return declarations;
129};
130
131/**
132 * @param {Stylesheet} stylesheet
133 * @param {XastElement} node
134 * @returns {ComputedStyles}
135 */
136const computeOwnStyle = (stylesheet, node) => {
137 /** @type {ComputedStyles} */
138 const computedStyle = {};
139 const importantStyles = new Map();
140
141 // collect attributes
142 for (const [name, value] of Object.entries(node.attributes)) {
143 if (attrsGroups.presentation.has(name)) {
144 computedStyle[name] = { type: 'static', inherited: false, value };
145 importantStyles.set(name, false);
146 }
147 }
148
149 // collect matching rules
150 for (const { selector, declarations, dynamic } of stylesheet.rules) {
151 if (matches(node, selector)) {
152 for (const { name, value, important } of declarations) {
153 const computed = computedStyle[name];
154 if (computed && computed.type === 'dynamic') {
155 continue;
156 }
157 if (dynamic) {
158 computedStyle[name] = { type: 'dynamic', inherited: false };
159 continue;
160 }
161 if (
162 computed == null ||
163 important === true ||
164 importantStyles.get(name) === false
165 ) {
166 computedStyle[name] = { type: 'static', inherited: false, value };
167 importantStyles.set(name, important);
168 }
169 }
170 }
171 }
172
173 // collect inline styles
174 const styleDeclarations =
175 node.attributes.style == null
176 ? []
177 : parseStyleDeclarations(node.attributes.style);
178 for (const { name, value, important } of styleDeclarations) {
179 const computed = computedStyle[name];
180 if (computed && computed.type === 'dynamic') {
181 continue;
182 }
183 if (
184 computed == null ||
185 important === true ||
186 importantStyles.get(name) === false
187 ) {
188 computedStyle[name] = { type: 'static', inherited: false, value };
189 importantStyles.set(name, important);
190 }
191 }
192
193 return computedStyle;
194};
195
196/**
197 * Compares selector specificities.
198 * Derived from https://github.com/keeganstreet/specificity/blob/8757133ddd2ed0163f120900047ff0f92760b536/specificity.js#L207
199 *
200 * @param {Specificity} a
201 * @param {Specificity} b
202 * @returns {number}
203 */
204const compareSpecificity = (a, b) => {
205 for (let i = 0; i < 4; i += 1) {
206 if (a[i] < b[i]) {
207 return -1;
208 } else if (a[i] > b[i]) {
209 return 1;
210 }
211 }
212
213 return 0;
214};
215exports.compareSpecificity = compareSpecificity;
216
217/**
218 * @type {(root: XastRoot) => Stylesheet}
219 */
220const collectStylesheet = (root) => {
221 /** @type {StylesheetRule[]} */
222 const rules = [];
223 /** @type {Map<XastElement, XastParent>} */
224 const parents = new Map();
225
226 visit(root, {
227 element: {
228 enter: (node, parentNode) => {
229 parents.set(node, parentNode);
230
231 if (node.name !== 'style') {
232 return;
233 }
234
235 if (
236 node.attributes.type == null ||
237 node.attributes.type === '' ||
238 node.attributes.type === 'text/css'
239 ) {
240 const dynamic =
241 node.attributes.media != null && node.attributes.media !== 'all';
242
243 for (const child of node.children) {
244 if (child.type === 'text' || child.type === 'cdata') {
245 rules.push(...parseStylesheet(child.value, dynamic));
246 }
247 }
248 }
249 },
250 },
251 });
252 // sort by selectors specificity
253 rules.sort((a, b) => compareSpecificity(a.specificity, b.specificity));
254 return { rules, parents };
255};
256exports.collectStylesheet = collectStylesheet;
257
258/**
259 * @param {Stylesheet} stylesheet
260 * @param {XastElement} node
261 * @returns {ComputedStyles}
262 */
263const computeStyle = (stylesheet, node) => {
264 const { parents } = stylesheet;
265 const computedStyles = computeOwnStyle(stylesheet, node);
266 let parent = parents.get(node);
267 while (parent != null && parent.type !== 'root') {
268 const inheritedStyles = computeOwnStyle(stylesheet, parent);
269 for (const [name, computed] of Object.entries(inheritedStyles)) {
270 if (
271 computedStyles[name] == null &&
272 inheritableAttrs.has(name) &&
273 !presentationNonInheritableGroupAttrs.has(name)
274 ) {
275 computedStyles[name] = { ...computed, inherited: true };
276 }
277 }
278 parent = parents.get(parent);
279 }
280 return computedStyles;
281};
282exports.computeStyle = computeStyle;
283
284/**
285 * Determines if the CSS selector includes or traverses the given attribute.
286 *
287 * Classes and IDs are generated as attribute selectors, so you can check for
288 * if a `.class` or `#id` is included by passing `name=class` or `name=id`
289 * respectively.
290 *
291 * @param {csstree.ListItem<csstree.CssNode>|string} selector
292 * @param {string} name
293 * @param {?string} value
294 * @param {boolean} traversed
295 * @returns {boolean}
296 */
297const includesAttrSelector = (
298 selector,
299 name,
300 value = null,
301 traversed = false,
302) => {
303 const selectors =
304 typeof selector === 'string'
305 ? csswhat.parse(selector)
306 : csswhat.parse(csstree.generate(selector.data));
307
308 for (const subselector of selectors) {
309 const hasAttrSelector = subselector.some((segment, index) => {
310 if (traversed) {
311 if (index === subselector.length - 1) {
312 return false;
313 }
314
315 const isNextTraversal = csswhat.isTraversal(subselector[index + 1]);
316
317 if (!isNextTraversal) {
318 return false;
319 }
320 }
321
322 if (segment.type !== 'attribute' || segment.name !== name) {
323 return false;
324 }
325
326 return value == null ? true : segment.value === value;
327 });
328
329 if (hasAttrSelector) {
330 return true;
331 }
332 }
333
334 return false;
335};
336exports.includesAttrSelector = includesAttrSelector;