css-tools.js 6.4 KB

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