source: trip-planner-front/node_modules/svgo/plugins/inlineStyles.js@ 6fe77af

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

primeNG components

  • Property mode set to 100644
File size: 11.9 KB
Line 
1'use strict';
2
3/**
4 * @typedef {import('../lib/types').Specificity} Specificity
5 * @typedef {import('../lib/types').XastElement} XastElement
6 * @typedef {import('../lib/types').XastParent} XastParent
7 */
8
9const csstree = require('css-tree');
10// @ts-ignore not defined in @types/csso
11const specificity = require('csso/lib/restructure/prepare/specificity');
12const stable = require('stable');
13const {
14 visitSkip,
15 querySelectorAll,
16 detachNodeFromParent,
17} = require('../lib/xast.js');
18
19exports.type = 'visitor';
20exports.name = 'inlineStyles';
21exports.active = true;
22exports.description = 'inline styles (additional options)';
23
24/**
25 * Compares two selector specificities.
26 * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
27 *
28 * @type {(a: Specificity, b: Specificity) => number}
29 */
30const compareSpecificity = (a, b) => {
31 for (var i = 0; i < 4; i += 1) {
32 if (a[i] < b[i]) {
33 return -1;
34 } else if (a[i] > b[i]) {
35 return 1;
36 }
37 }
38 return 0;
39};
40
41/**
42 * Moves + merges styles from style elements to element styles
43 *
44 * Options
45 * onlyMatchedOnce (default: true)
46 * inline only selectors that match once
47 *
48 * removeMatchedSelectors (default: true)
49 * clean up matched selectors,
50 * leave selectors that hadn't matched
51 *
52 * useMqs (default: ['', 'screen'])
53 * what media queries to be used
54 * empty string element for styles outside media queries
55 *
56 * usePseudos (default: [''])
57 * what pseudo-classes/-elements to be used
58 * empty string element for all non-pseudo-classes and/or -elements
59 *
60 * @author strarsis <strarsis@gmail.com>
61 *
62 * @type {import('../lib/types').Plugin<{
63 * onlyMatchedOnce?: boolean,
64 * removeMatchedSelectors?: boolean,
65 * useMqs?: Array<string>,
66 * usePseudos?: Array<string>
67 * }>}
68 */
69exports.fn = (root, params) => {
70 const {
71 onlyMatchedOnce = true,
72 removeMatchedSelectors = true,
73 useMqs = ['', 'screen'],
74 usePseudos = [''],
75 } = params;
76
77 /**
78 * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
79 */
80 const styles = [];
81 /**
82 * @type {Array<{
83 * node: csstree.Selector,
84 * item: csstree.ListItem<csstree.CssNode>,
85 * rule: csstree.Rule,
86 * matchedElements?: Array<XastElement>
87 * }>}
88 */
89 let selectors = [];
90
91 return {
92 element: {
93 enter: (node, parentNode) => {
94 // skip <foreignObject /> content
95 if (node.name === 'foreignObject') {
96 return visitSkip;
97 }
98 // collect only non-empty <style /> elements
99 if (node.name !== 'style' || node.children.length === 0) {
100 return;
101 }
102 // values other than the empty string or text/css are not used
103 if (
104 node.attributes.type != null &&
105 node.attributes.type !== '' &&
106 node.attributes.type !== 'text/css'
107 ) {
108 return;
109 }
110 // parse css in style element
111 let cssText = '';
112 for (const child of node.children) {
113 if (child.type === 'text' || child.type === 'cdata') {
114 cssText += child.value;
115 }
116 }
117 /**
118 * @type {null | csstree.CssNode}
119 */
120 let cssAst = null;
121 try {
122 cssAst = csstree.parse(cssText, {
123 parseValue: false,
124 parseCustomProperty: false,
125 });
126 } catch {
127 return;
128 }
129 if (cssAst.type === 'StyleSheet') {
130 styles.push({ node, parentNode, cssAst });
131 }
132
133 // collect selectors
134 csstree.walk(cssAst, {
135 visit: 'Selector',
136 enter(node, item) {
137 const atrule = this.atrule;
138 const rule = this.rule;
139 if (rule == null) {
140 return;
141 }
142
143 // skip media queries not included into useMqs param
144 let mq = '';
145 if (atrule != null) {
146 mq = atrule.name;
147 if (atrule.prelude != null) {
148 mq += ` ${csstree.generate(atrule.prelude)}`;
149 }
150 }
151 if (useMqs.includes(mq) === false) {
152 return;
153 }
154
155 /**
156 * @type {Array<{
157 * item: csstree.ListItem<csstree.CssNode>,
158 * list: csstree.List<csstree.CssNode>
159 * }>}
160 */
161 const pseudos = [];
162 if (node.type === 'Selector') {
163 node.children.each((childNode, childItem, childList) => {
164 if (
165 childNode.type === 'PseudoClassSelector' ||
166 childNode.type === 'PseudoElementSelector'
167 ) {
168 pseudos.push({ item: childItem, list: childList });
169 }
170 });
171 }
172
173 // skip pseudo classes and pseudo elements not includes into usePseudos param
174 const pseudoSelectors = csstree.generate({
175 type: 'Selector',
176 children: new csstree.List().fromArray(
177 pseudos.map((pseudo) => pseudo.item.data)
178 ),
179 });
180 if (usePseudos.includes(pseudoSelectors) === false) {
181 return;
182 }
183
184 // remove pseudo classes and elements to allow querySelector match elements
185 // TODO this is not very accurate since some pseudo classes like first-child
186 // are used for selection
187 for (const pseudo of pseudos) {
188 pseudo.list.remove(pseudo.item);
189 }
190
191 selectors.push({ node, item, rule });
192 },
193 });
194 },
195 },
196
197 root: {
198 exit: () => {
199 if (styles.length === 0) {
200 return;
201 }
202 // stable sort selectors
203 const sortedSelectors = stable(selectors, (a, b) => {
204 const aSpecificity = specificity(a.item.data);
205 const bSpecificity = specificity(b.item.data);
206 return compareSpecificity(aSpecificity, bSpecificity);
207 }).reverse();
208
209 for (const selector of sortedSelectors) {
210 // match selectors
211 const selectorText = csstree.generate(selector.item.data);
212 /**
213 * @type {Array<XastElement>}
214 */
215 const matchedElements = [];
216 try {
217 for (const node of querySelectorAll(root, selectorText)) {
218 if (node.type === 'element') {
219 matchedElements.push(node);
220 }
221 }
222 } catch (selectError) {
223 continue;
224 }
225 // nothing selected
226 if (matchedElements.length === 0) {
227 continue;
228 }
229
230 // apply styles to matched elements
231 // skip selectors that match more than once if option onlyMatchedOnce is enabled
232 if (onlyMatchedOnce && matchedElements.length > 1) {
233 continue;
234 }
235
236 // apply <style/> to matched elements
237 for (const selectedEl of matchedElements) {
238 const styleDeclarationList = csstree.parse(
239 selectedEl.attributes.style == null
240 ? ''
241 : selectedEl.attributes.style,
242 {
243 context: 'declarationList',
244 parseValue: false,
245 }
246 );
247 if (styleDeclarationList.type !== 'DeclarationList') {
248 continue;
249 }
250 const styleDeclarationItems = new Map();
251 csstree.walk(styleDeclarationList, {
252 visit: 'Declaration',
253 enter(node, item) {
254 styleDeclarationItems.set(node.property, item);
255 },
256 });
257 // merge declarations
258 csstree.walk(selector.rule, {
259 visit: 'Declaration',
260 enter(ruleDeclaration) {
261 // existing inline styles have higher priority
262 // no inline styles, external styles, external styles used
263 // inline styles, external styles same priority as inline styles, inline styles used
264 // inline styles, external styles higher priority than inline styles, external styles used
265 const matchedItem = styleDeclarationItems.get(
266 ruleDeclaration.property
267 );
268 const ruleDeclarationItem =
269 styleDeclarationList.children.createItem(ruleDeclaration);
270 if (matchedItem == null) {
271 styleDeclarationList.children.append(ruleDeclarationItem);
272 } else if (
273 matchedItem.data.important !== true &&
274 ruleDeclaration.important === true
275 ) {
276 styleDeclarationList.children.replace(
277 matchedItem,
278 ruleDeclarationItem
279 );
280 styleDeclarationItems.set(
281 ruleDeclaration.property,
282 ruleDeclarationItem
283 );
284 }
285 },
286 });
287 selectedEl.attributes.style =
288 csstree.generate(styleDeclarationList);
289 }
290
291 if (
292 removeMatchedSelectors &&
293 matchedElements.length !== 0 &&
294 selector.rule.prelude.type === 'SelectorList'
295 ) {
296 // clean up matching simple selectors if option removeMatchedSelectors is enabled
297 selector.rule.prelude.children.remove(selector.item);
298 }
299 selector.matchedElements = matchedElements;
300 }
301
302 // no further processing required
303 if (removeMatchedSelectors === false) {
304 return;
305 }
306
307 // clean up matched class + ID attribute values
308 for (const selector of sortedSelectors) {
309 if (selector.matchedElements == null) {
310 continue;
311 }
312
313 if (onlyMatchedOnce && selector.matchedElements.length > 1) {
314 // skip selectors that match more than once if option onlyMatchedOnce is enabled
315 continue;
316 }
317
318 for (const selectedEl of selector.matchedElements) {
319 // class
320 const classList = new Set(
321 selectedEl.attributes.class == null
322 ? null
323 : selectedEl.attributes.class.split(' ')
324 );
325 const firstSubSelector = selector.node.children.first();
326 if (
327 firstSubSelector != null &&
328 firstSubSelector.type === 'ClassSelector'
329 ) {
330 classList.delete(firstSubSelector.name);
331 }
332 if (classList.size === 0) {
333 delete selectedEl.attributes.class;
334 } else {
335 selectedEl.attributes.class = Array.from(classList).join(' ');
336 }
337
338 // ID
339 if (
340 firstSubSelector != null &&
341 firstSubSelector.type === 'IdSelector'
342 ) {
343 if (selectedEl.attributes.id === firstSubSelector.name) {
344 delete selectedEl.attributes.id;
345 }
346 }
347 }
348 }
349
350 for (const style of styles) {
351 csstree.walk(style.cssAst, {
352 visit: 'Rule',
353 enter: function (node, item, list) {
354 // clean up <style/> rulesets without any css selectors left
355 if (
356 node.type === 'Rule' &&
357 node.prelude.type === 'SelectorList' &&
358 node.prelude.children.isEmpty()
359 ) {
360 list.remove(item);
361 }
362 },
363 });
364
365 if (style.cssAst.children.isEmpty()) {
366 // remove emtpy style element
367 detachNodeFromParent(style.node, style.parentNode);
368 } else {
369 // update style element if any styles left
370 const firstChild = style.node.children[0];
371 if (firstChild.type === 'text' || firstChild.type === 'cdata') {
372 firstChild.value = csstree.generate(style.cssAst);
373 }
374 }
375 }
376 },
377 },
378 };
379};
Note: See TracBrowser for help on using the repository browser.