import * as csstree from 'css-tree'; import { syntax } from 'csso'; import { attrsGroups, pseudoClasses } from './_collections.js'; import { visitSkip, querySelectorAll, detachNodeFromParent, } from '../lib/xast.js'; import { compareSpecificity, includesAttrSelector } from '../lib/style.js'; /** * @typedef {import('../lib/types.js').XastElement} XastElement * @typedef {import('../lib/types.js').XastParent} XastParent */ export const name = 'inlineStyles'; export const description = 'inline styles (additional options)'; /** * Some pseudo-classes can only be calculated by clients, like :visited, * :future, or :hover, but there are other pseudo-classes that we can evaluate * during optimization. * * Pseudo-classes that we can evaluate during optimization, and shouldn't be * toggled conditionally through the `usePseudos` parameter. * * @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes */ const preservedPseudos = [ ...pseudoClasses.functional, ...pseudoClasses.treeStructural, ]; /** * Merges styles from style nodes into inline styles. * * @type {import('./plugins-types.js').Plugin<'inlineStyles'>} * @author strarsis */ export const fn = (root, params) => { const { onlyMatchedOnce = true, removeMatchedSelectors = true, useMqs = ['', 'screen'], usePseudos = [''], } = params; /** * @type {{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }[]} */ const styles = []; /** * @type {{ * node: csstree.Selector, * item: csstree.ListItem, * rule: csstree.Rule, * matchedElements?: XastElement[] * }[]} */ 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-expect-error .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: 'Rule', enter(node) { const atrule = this.atrule; // 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; } if (node.prelude.type === 'SelectorList') { node.prelude.children.forEach((childNode, item) => { if (childNode.type === 'Selector') { /** * @type {{ * item: csstree.ListItem, * list: csstree.List * }[]} */ const pseudos = []; childNode.children.forEach( (grandchildNode, grandchildItem, grandchildList) => { const isPseudo = grandchildNode.type === 'PseudoClassSelector' || grandchildNode.type === 'PseudoElementSelector'; if ( isPseudo && !preservedPseudos.includes(grandchildNode.name) ) { pseudos.push({ item: grandchildItem, list: grandchildList, }); } }, ); const pseudoSelectors = csstree.generate({ type: 'Selector', children: new csstree.List().fromArray( pseudos.map((pseudo) => pseudo.item.data), ), }); if (usePseudos.includes(pseudoSelectors)) { for (const pseudo of pseudos) { pseudo.list.remove(pseudo.item); } } selectors.push({ node: childNode, rule: node, item: item }); } }); } }, }); }, }, root: { exit: () => { if (styles.length === 0) { return; } const sortedSelectors = selectors .slice() .sort((a, b) => { const aSpecificity = syntax.specificity(a.item.data); const bSpecificity = syntax.specificity(b.item.data); return compareSpecificity(aSpecificity, bSpecificity); }) .reverse(); for (const selector of sortedSelectors) { // match selectors const selectorText = csstree.generate(selector.item.data); /** @type {XastElement[]} */ const matchedElements = []; try { for (const node of querySelectorAll(root, selectorText)) { if (node.type === 'element') { matchedElements.push(node); } } } catch { 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