css-tools.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. 'use strict';
  2. var csstree = require('css-tree'),
  3. List = csstree.List,
  4. stable = require('stable'),
  5. specificity = require('csso/lib/restructure/prepare/specificity');
  6. /**
  7. * Flatten a CSS AST to a selectors list.
  8. *
  9. * @param {import('css-tree').CssNode} cssAst css-tree AST to flatten
  10. * @return {Array} selectors
  11. */
  12. function flattenToSelectors(cssAst) {
  13. var selectors = [];
  14. csstree.walk(cssAst, {
  15. visit: 'Rule',
  16. enter: function (node) {
  17. if (node.type !== 'Rule') {
  18. return;
  19. }
  20. var atrule = this.atrule;
  21. var rule = node;
  22. node.prelude.children.each(function (selectorNode, selectorItem) {
  23. var selector = {
  24. item: selectorItem,
  25. atrule: atrule,
  26. rule: rule,
  27. pseudos: /** @type {{item: any; list: any[]}[]} */ ([]),
  28. };
  29. selectorNode.children.each(function (
  30. selectorChildNode,
  31. selectorChildItem,
  32. selectorChildList
  33. ) {
  34. if (
  35. selectorChildNode.type === 'PseudoClassSelector' ||
  36. selectorChildNode.type === 'PseudoElementSelector'
  37. ) {
  38. selector.pseudos.push({
  39. item: selectorChildItem,
  40. list: selectorChildList,
  41. });
  42. }
  43. });
  44. selectors.push(selector);
  45. });
  46. },
  47. });
  48. return selectors;
  49. }
  50. /**
  51. * Filter selectors by Media Query.
  52. *
  53. * @param {Array} selectors to filter
  54. * @param {Array} useMqs Array with strings of media queries that should pass (<name> <expression>)
  55. * @return {Array} Filtered selectors that match the passed media queries
  56. */
  57. function filterByMqs(selectors, useMqs) {
  58. return selectors.filter(function (selector) {
  59. if (selector.atrule === null) {
  60. return ~useMqs.indexOf('');
  61. }
  62. var mqName = selector.atrule.name;
  63. var mqStr = mqName;
  64. if (
  65. selector.atrule.expression &&
  66. selector.atrule.expression.children.first().type === 'MediaQueryList'
  67. ) {
  68. var mqExpr = csstree.generate(selector.atrule.expression);
  69. mqStr = [mqName, mqExpr].join(' ');
  70. }
  71. return ~useMqs.indexOf(mqStr);
  72. });
  73. }
  74. /**
  75. * Filter selectors by the pseudo-elements and/or -classes they contain.
  76. *
  77. * @param {Array} selectors to filter
  78. * @param {Array} usePseudos Array with strings of single or sequence of pseudo-elements and/or -classes that should pass
  79. * @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes
  80. */
  81. function filterByPseudos(selectors, usePseudos) {
  82. return selectors.filter(function (selector) {
  83. var pseudoSelectorsStr = csstree.generate({
  84. type: 'Selector',
  85. children: new List().fromArray(
  86. selector.pseudos.map(function (pseudo) {
  87. return pseudo.item.data;
  88. })
  89. ),
  90. });
  91. return ~usePseudos.indexOf(pseudoSelectorsStr);
  92. });
  93. }
  94. /**
  95. * Remove pseudo-elements and/or -classes from the selectors for proper matching.
  96. *
  97. * @param {Array} selectors to clean
  98. * @return {void}
  99. */
  100. function cleanPseudos(selectors) {
  101. selectors.forEach(function (selector) {
  102. selector.pseudos.forEach(function (pseudo) {
  103. pseudo.list.remove(pseudo.item);
  104. });
  105. });
  106. }
  107. /**
  108. * Compares two selector specificities.
  109. * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
  110. *
  111. * @param {Array} aSpecificity Specificity of selector A
  112. * @param {Array} bSpecificity Specificity of selector B
  113. * @return {number} Score of selector specificity A compared to selector specificity B
  114. */
  115. function compareSpecificity(aSpecificity, bSpecificity) {
  116. for (var i = 0; i < 4; i += 1) {
  117. if (aSpecificity[i] < bSpecificity[i]) {
  118. return -1;
  119. } else if (aSpecificity[i] > bSpecificity[i]) {
  120. return 1;
  121. }
  122. }
  123. return 0;
  124. }
  125. /**
  126. * Compare two simple selectors.
  127. *
  128. * @param {Object} aSimpleSelectorNode Simple selector A
  129. * @param {Object} bSimpleSelectorNode Simple selector B
  130. * @return {number} Score of selector A compared to selector B
  131. */
  132. function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) {
  133. var aSpecificity = specificity(aSimpleSelectorNode),
  134. bSpecificity = specificity(bSimpleSelectorNode);
  135. return compareSpecificity(aSpecificity, bSpecificity);
  136. }
  137. function _bySelectorSpecificity(selectorA, selectorB) {
  138. return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data);
  139. }
  140. /**
  141. * Sort selectors stably by their specificity.
  142. *
  143. * @param {Array} selectors to be sorted
  144. * @return {Array} Stable sorted selectors
  145. */
  146. function sortSelectors(selectors) {
  147. return stable(selectors, _bySelectorSpecificity);
  148. }
  149. /**
  150. * Convert a css-tree AST style declaration to CSSStyleDeclaration property.
  151. *
  152. * @param {import('css-tree').CssNode} declaration css-tree style declaration
  153. * @return {Object} CSSStyleDeclaration property
  154. */
  155. function csstreeToStyleDeclaration(declaration) {
  156. var propertyName = declaration.property,
  157. propertyValue = csstree.generate(declaration.value),
  158. propertyPriority = declaration.important ? 'important' : '';
  159. return {
  160. name: propertyName,
  161. value: propertyValue,
  162. priority: propertyPriority,
  163. };
  164. }
  165. /**
  166. * Gets the CSS string of a style element
  167. *
  168. * @param {Object} elem style element
  169. * @return {string} CSS string or empty array if no styles are set
  170. */
  171. function getCssStr(elem) {
  172. if (
  173. elem.children.length > 0 &&
  174. (elem.children[0].type === 'text' || elem.children[0].type === 'cdata')
  175. ) {
  176. return elem.children[0].value;
  177. }
  178. return '';
  179. }
  180. /**
  181. * Sets the CSS string of a style element
  182. *
  183. * @param {Object} elem style element
  184. * @param {string} css string to be set
  185. * @return {string} reference to field with CSS
  186. */
  187. function setCssStr(elem, css) {
  188. if (elem.children.length === 0) {
  189. elem.children.push({
  190. type: 'text',
  191. value: '',
  192. });
  193. }
  194. if (elem.children[0].type !== 'text' && elem.children[0].type !== 'cdata') {
  195. return css;
  196. }
  197. elem.children[0].value = css;
  198. return css;
  199. }
  200. module.exports.flattenToSelectors = flattenToSelectors;
  201. module.exports.filterByMqs = filterByMqs;
  202. module.exports.filterByPseudos = filterByPseudos;
  203. module.exports.cleanPseudos = cleanPseudos;
  204. module.exports.compareSpecificity = compareSpecificity;
  205. module.exports.compareSimpleSelectorNode = compareSimpleSelectorNode;
  206. module.exports.sortSelectors = sortSelectors;
  207. module.exports.csstreeToStyleDeclaration = csstreeToStyleDeclaration;
  208. module.exports.getCssStr = getCssStr;
  209. module.exports.setCssStr = setCssStr;