'use strict'; /** * @typedef {import('../lib/types').XastElement} XastElement * @typedef {import('../lib/types').XastParent} XastParent */ const csstree = require('css-tree'); const { // @ts-ignore internal api syntax: { specificity }, } = require('csso'); const { visitSkip, querySelectorAll, detachNodeFromParent, } = require('../lib/xast.js'); const { compareSpecificity, includesAttrSelector } = require('../lib/style'); const { attrsGroups } = require('./_collections'); exports.name = 'inlineStyles'; exports.description = 'inline styles (additional options)'; /** * Merges styles from style nodes into inline styles. * * @type {import('./plugins-types').Plugin<'inlineStyles'>} * @author strarsis */ 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, * rule: csstree.Rule, * matchedElements?: Array * }>} */ let selectors = []; return { element: { enter: (node, parentNode) => { if (node.name === 'foreignObject') { return visitSkip; } if (node.name !== 'style' || node.children.length === 0) { return; } if ( node.attributes.type != null && node.attributes.type !== '' && node.attributes.type !== 'text/css' ) { return; } const cssText = node.children .filter((child) => child.type === 'text' || child.type === 'cdata') // @ts-ignore .map((child) => child.value) .join(''); /** @type {?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 mediaQuery = ''; if (atrule != null) { mediaQuery = atrule.name; if (atrule.prelude != null) { mediaQuery += ` ${csstree.generate(atrule.prelude)}`; } } if (!useMqs.includes(mediaQuery)) { return; } /** * @type {Array<{ * item: csstree.ListItem, * list: csstree.List * }>} */ 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)) { 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; } const sortedSelectors = selectors .slice() .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} */ 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