1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const csstree = require('css-tree');
|
17 | const csswhat = require('css-what');
|
18 | const {
|
19 | syntax: { specificity },
|
20 | } = require('csso');
|
21 | const { visit, matches } = require('./xast.js');
|
22 | const {
|
23 | attrsGroups,
|
24 | inheritableAttrs,
|
25 | presentationNonInheritableGroupAttrs,
|
26 | } = require('../plugins/_collections.js');
|
27 |
|
28 |
|
29 | const csstreeWalkSkip = csstree.walk.skip;
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | const parseRule = (ruleNode, dynamic) => {
|
35 | |
36 |
|
37 |
|
38 | const declarations = [];
|
39 |
|
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 |
|
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 |
|
66 | selector: csstree.generate(newNode),
|
67 | declarations,
|
68 | });
|
69 | }
|
70 | });
|
71 |
|
72 | return rules;
|
73 | };
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | const parseStylesheet = (css, dynamic) => {
|
79 |
|
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 |
|
111 |
|
112 | const parseStyleDeclarations = (css) => {
|
113 |
|
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 |
|
133 |
|
134 |
|
135 |
|
136 | const computeOwnStyle = (stylesheet, node) => {
|
137 |
|
138 | const computedStyle = {};
|
139 | const importantStyles = new Map();
|
140 |
|
141 |
|
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 |
|
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 |
|
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 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | const 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 | };
|
215 | exports.compareSpecificity = compareSpecificity;
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | const collectStylesheet = (root) => {
|
221 |
|
222 | const rules = [];
|
223 |
|
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 |
|
253 | rules.sort((a, b) => compareSpecificity(a.specificity, b.specificity));
|
254 | return { rules, parents };
|
255 | };
|
256 | exports.collectStylesheet = collectStylesheet;
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 | const 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 | };
|
282 | exports.computeStyle = computeStyle;
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 | const 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 | };
|
336 | exports.includesAttrSelector = includesAttrSelector;
|