diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2023-10-14 15:05:55 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2023-10-14 15:05:55 +0800 |
| commit | c06f7ee0a40255214c477e13d9b956a7e07c2900 (patch) | |
| tree | f5982a568ef8fab865d0142e71c7494fea3e4c1b /node_modules/svgo/plugins/inlineStyles.js | |
| parent | 3fd9400d4dccc25aa29b5702216732e0b7542073 (diff) | |
postcss,(plugins): nanocss, postcss-preset-env
Diffstat (limited to 'node_modules/svgo/plugins/inlineStyles.js')
| -rw-r--r-- | node_modules/svgo/plugins/inlineStyles.js | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/node_modules/svgo/plugins/inlineStyles.js b/node_modules/svgo/plugins/inlineStyles.js new file mode 100644 index 00000000..e931b5f6 --- /dev/null +++ b/node_modules/svgo/plugins/inlineStyles.js @@ -0,0 +1,386 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').Specificity} Specificity + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').XastParent} XastParent + */ + +const csstree = require('css-tree'); +const { + // @ts-ignore not defined in @types/csso + syntax: { specificity }, +} = require('csso'); +const { + visitSkip, + querySelectorAll, + detachNodeFromParent, +} = require('../lib/xast.js'); + +exports.name = 'inlineStyles'; +exports.description = 'inline styles (additional options)'; + +/** + * Compares two selector specificities. + * extracted from https://github.com/keeganstreet/specificity/blob/main/specificity.js#L211 + * + * @type {(a: Specificity, b: Specificity) => number} + */ +const compareSpecificity = (a, b) => { + for (var i = 0; i < 4; i += 1) { + if (a[i] < b[i]) { + return -1; + } else if (a[i] > b[i]) { + return 1; + } + } + return 0; +}; + +/** + * @type {(value: any) => any} + */ +const toAny = (value) => value; + +/** + * Moves + merges styles from style elements to element styles + * + * Options + * onlyMatchedOnce (default: true) + * inline only selectors that match once + * + * removeMatchedSelectors (default: true) + * clean up matched selectors, + * leave selectors that hadn't matched + * + * useMqs (default: ['', 'screen']) + * what media queries to be used + * empty string element for styles outside media queries + * + * usePseudos (default: ['']) + * what pseudo-classes/-elements to be used + * empty string element for all non-pseudo-classes and/or -elements + * + * @author strarsis <strarsis@gmail.com> + * + * @type {import('./plugins-types').Plugin<'inlineStyles'>} + */ +exports.fn = (root, params) => { + const { + onlyMatchedOnce = true, + removeMatchedSelectors = true, + useMqs = ['', 'screen'], + usePseudos = [''], + } = params; + + /** + * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>} + */ + const styles = []; + /** + * @type {Array<{ + * node: csstree.Selector, + * item: csstree.ListItem<csstree.CssNode>, + * rule: csstree.Rule, + * matchedElements?: Array<XastElement> + * }>} + */ + let selectors = []; + + return { + element: { + enter: (node, parentNode) => { + // skip <foreignObject /> content + if (node.name === 'foreignObject') { + return visitSkip; + } + // collect only non-empty <style /> elements + if (node.name !== 'style' || node.children.length === 0) { + return; + } + // values other than the empty string or text/css are not used + if ( + node.attributes.type != null && + node.attributes.type !== '' && + node.attributes.type !== 'text/css' + ) { + return; + } + // parse css in style element + let cssText = ''; + for (const child of node.children) { + if (child.type === 'text' || child.type === 'cdata') { + cssText += child.value; + } + } + /** + * @type {null | csstree.CssNode} + */ + let cssAst = null; + try { + cssAst = csstree.parse(cssText, { + parseValue: false, + parseCustomProperty: false, + }); + } catch { + return; + } + if (cssAst.type === 'StyleSheet') { + styles.push({ node, parentNode, cssAst }); + } + + // collect selectors + csstree.walk(cssAst, { + visit: 'Selector', + enter(node, item) { + const atrule = this.atrule; + const rule = this.rule; + if (rule == null) { + return; + } + + // skip media queries not included into useMqs param + let mq = ''; + if (atrule != null) { + mq = atrule.name; + if (atrule.prelude != null) { + mq += ` ${csstree.generate(atrule.prelude)}`; + } + } + if (useMqs.includes(mq) === false) { + return; + } + + /** + * @type {Array<{ + * item: csstree.ListItem<csstree.CssNode>, + * list: csstree.List<csstree.CssNode> + * }>} + */ + const pseudos = []; + if (node.type === 'Selector') { + node.children.forEach((childNode, childItem, childList) => { + if ( + childNode.type === 'PseudoClassSelector' || + childNode.type === 'PseudoElementSelector' + ) { + pseudos.push({ item: childItem, list: childList }); + } + }); + } + + // skip pseudo classes and pseudo elements not includes into usePseudos param + const pseudoSelectors = csstree.generate({ + type: 'Selector', + children: new csstree.List().fromArray( + pseudos.map((pseudo) => pseudo.item.data) + ), + }); + if (usePseudos.includes(pseudoSelectors) === false) { + return; + } + + // remove pseudo classes and elements to allow querySelector match elements + // TODO this is not very accurate since some pseudo classes like first-child + // are used for selection + for (const pseudo of pseudos) { + pseudo.list.remove(pseudo.item); + } + + selectors.push({ node, item, rule }); + }, + }); + }, + }, + + root: { + exit: () => { + if (styles.length === 0) { + return; + } + // stable sort selectors + const sortedSelectors = [...selectors] + .sort((a, b) => { + const aSpecificity = specificity(a.item.data); + const bSpecificity = specificity(b.item.data); + return compareSpecificity(aSpecificity, bSpecificity); + }) + .reverse(); + + for (const selector of sortedSelectors) { + // match selectors + const selectorText = csstree.generate(selector.item.data); + /** + * @type {Array<XastElement>} + */ + const matchedElements = []; + try { + for (const node of querySelectorAll(root, selectorText)) { + if (node.type === 'element') { + matchedElements.push(node); + } + } + } catch (selectError) { + continue; + } + // nothing selected + if (matchedElements.length === 0) { + continue; + } + + // apply styles to matched elements + // skip selectors that match more than once if option onlyMatchedOnce is enabled + if (onlyMatchedOnce && matchedElements.length > 1) { + continue; + } + + // apply <style/> to matched elements + for (const selectedEl of matchedElements) { + const styleDeclarationList = csstree.parse( + selectedEl.attributes.style == null + ? '' + : selectedEl.attributes.style, + { + context: 'declarationList', + parseValue: false, + } + ); + if (styleDeclarationList.type !== 'DeclarationList') { + continue; + } + const styleDeclarationItems = new Map(); + csstree.walk(styleDeclarationList, { + visit: 'Declaration', + enter(node, item) { + styleDeclarationItems.set(node.property, item); + }, + }); + // merge declarations + csstree.walk(selector.rule, { + visit: 'Declaration', + enter(ruleDeclaration) { + // existing inline styles have higher priority + // no inline styles, external styles, external styles used + // inline styles, external styles same priority as inline styles, inline styles used + // inline styles, external styles higher priority than inline styles, external styles used + const matchedItem = styleDeclarationItems.get( + ruleDeclaration.property + ); + const ruleDeclarationItem = + styleDeclarationList.children.createItem(ruleDeclaration); + if (matchedItem == null) { + styleDeclarationList.children.append(ruleDeclarationItem); + } else if ( + matchedItem.data.important !== true && + ruleDeclaration.important === true + ) { + styleDeclarationList.children.replace( + matchedItem, + ruleDeclarationItem + ); + styleDeclarationItems.set( + ruleDeclaration.property, + ruleDeclarationItem + ); + } + }, + }); + selectedEl.attributes.style = + csstree.generate(styleDeclarationList); + } + + if ( + removeMatchedSelectors && + matchedElements.length !== 0 && + selector.rule.prelude.type === 'SelectorList' + ) { + // clean up matching simple selectors if option removeMatchedSelectors is enabled + selector.rule.prelude.children.remove(selector.item); + } + selector.matchedElements = matchedElements; + } + + // no further processing required + if (removeMatchedSelectors === false) { + return; + } + + // clean up matched class + ID attribute values + for (const selector of sortedSelectors) { + if (selector.matchedElements == null) { + continue; + } + + if (onlyMatchedOnce && selector.matchedElements.length > 1) { + // skip selectors that match more than once if option onlyMatchedOnce is enabled + continue; + } + + for (const selectedEl of selector.matchedElements) { + // class + const classList = new Set( + selectedEl.attributes.class == null + ? null + : selectedEl.attributes.class.split(' ') + ); + /** + * csstree v2 changed this type + * @type {csstree.CssNode} + */ + const firstSubSelector = toAny(selector.node.children.first); + if ( + firstSubSelector != null && + firstSubSelector.type === 'ClassSelector' + ) { + classList.delete(firstSubSelector.name); + } + if (classList.size === 0) { + delete selectedEl.attributes.class; + } else { + selectedEl.attributes.class = Array.from(classList).join(' '); + } + + // ID + if ( + firstSubSelector != null && + firstSubSelector.type === 'IdSelector' + ) { + if (selectedEl.attributes.id === firstSubSelector.name) { + delete selectedEl.attributes.id; + } + } + } + } + + for (const style of styles) { + csstree.walk(style.cssAst, { + visit: 'Rule', + enter: function (node, item, list) { + // clean up <style/> rulesets without any css selectors left + if ( + node.type === 'Rule' && + node.prelude.type === 'SelectorList' && + // csstree v2 changed this type + toAny(node.prelude.children.isEmpty) + ) { + list.remove(item); + } + }, + }); + + // csstree v2 changed this type + if (toAny(style.cssAst.children.isEmpty)) { + // remove emtpy style element + detachNodeFromParent(style.node, style.parentNode); + } else { + // update style element if any styles left + const firstChild = style.node.children[0]; + if (firstChild.type === 'text' || firstChild.type === 'cdata') { + firstChild.value = csstree.generate(style.cssAst); + } + } + } + }, + }, + }; +}; |
