| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 | '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');// @ts-ignore not defined in @types/cssoconst specificity = require('csso/lib/restructure/prepare/specificity');const stable = require('stable');const {  visitSkip,  querySelectorAll,  detachNodeFromParent,} = require('../lib/xast.js');exports.type = 'visitor';exports.name = 'inlineStyles';exports.active = true;exports.description = 'inline styles (additional options)';/** * Compares two selector specificities. * extracted from https://github.com/keeganstreet/specificity/blob/master/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;};/** * 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('../lib/types').Plugin<{ *   onlyMatchedOnce?: boolean, *   removeMatchedSelectors?: boolean, *   useMqs?: Array<string>, *   usePseudos?: Array<string> * }>} */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.each((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 = stable(selectors, (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(' ')            );            const firstSubSelector = 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' &&                node.prelude.children.isEmpty()              ) {                list.remove(item);              }            },          });          if (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);            }          }        }      },    },  };};
 |