inlineStyles.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. 'use strict';
  2. exports.type = 'full';
  3. exports.active = true;
  4. exports.params = {
  5. onlyMatchedOnce: true,
  6. removeMatchedSelectors: true,
  7. useMqs: ['', 'screen'],
  8. usePseudos: ['']
  9. };
  10. exports.description = 'inline styles (additional options)';
  11. var csstree = require('css-tree'),
  12. cssTools = require('../lib/css-tools');
  13. /**
  14. * Moves + merges styles from style elements to element styles
  15. *
  16. * Options
  17. * onlyMatchedOnce (default: true)
  18. * inline only selectors that match once
  19. *
  20. * removeMatchedSelectors (default: true)
  21. * clean up matched selectors,
  22. * leave selectors that hadn't matched
  23. *
  24. * useMqs (default: ['', 'screen'])
  25. * what media queries to be used
  26. * empty string element for styles outside media queries
  27. *
  28. * usePseudos (default: [''])
  29. * what pseudo-classes/-elements to be used
  30. * empty string element for all non-pseudo-classes and/or -elements
  31. *
  32. * @param {Object} document document element
  33. * @param {Object} opts plugin params
  34. *
  35. * @author strarsis <strarsis@gmail.com>
  36. */
  37. exports.fn = function(document, opts) {
  38. // collect <style/>s
  39. var styleEls = document.querySelectorAll('style');
  40. //no <styles/>s, nothing to do
  41. if (styleEls === null) {
  42. return document;
  43. }
  44. var styles = [],
  45. selectors = [];
  46. for (var styleEl of styleEls) {
  47. if (styleEl.isEmpty() || styleEl.closestElem('foreignObject')) {
  48. // skip empty <style/>s or <foreignObject> content.
  49. continue;
  50. }
  51. var cssStr = cssTools.getCssStr(styleEl);
  52. // collect <style/>s and their css ast
  53. var cssAst = {};
  54. try {
  55. cssAst = csstree.parse(cssStr, {
  56. parseValue: false,
  57. parseCustomProperty: false
  58. });
  59. } catch (parseError) {
  60. // console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError);
  61. continue;
  62. }
  63. styles.push({
  64. styleEl: styleEl,
  65. cssAst: cssAst
  66. });
  67. selectors = selectors.concat(cssTools.flattenToSelectors(cssAst));
  68. }
  69. // filter for mediaqueries to be used or without any mediaquery
  70. var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs);
  71. // filter for pseudo elements to be used
  72. var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos);
  73. // remove PseudoClass from its SimpleSelector for proper matching
  74. cssTools.cleanPseudos(selectorsPseudo);
  75. // stable sort selectors
  76. var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse();
  77. var selector,
  78. selectedEl;
  79. // match selectors
  80. for (selector of sortedSelectors) {
  81. var selectorStr = csstree.generate(selector.item.data),
  82. selectedEls = null;
  83. try {
  84. selectedEls = document.querySelectorAll(selectorStr);
  85. } catch (selectError) {
  86. if (selectError.constructor === SyntaxError) {
  87. // console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError);
  88. continue;
  89. }
  90. throw selectError;
  91. }
  92. if (selectedEls === null) {
  93. // nothing selected
  94. continue;
  95. }
  96. selector.selectedEls = selectedEls;
  97. }
  98. // apply <style/> styles to matched elements
  99. for (selector of sortedSelectors) {
  100. if(!selector.selectedEls) {
  101. continue;
  102. }
  103. if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
  104. // skip selectors that match more than once if option onlyMatchedOnce is enabled
  105. continue;
  106. }
  107. // apply <style/> to matched elements
  108. for (selectedEl of selector.selectedEls) {
  109. if (selector.rule === null) {
  110. continue;
  111. }
  112. // merge declarations
  113. csstree.walk(selector.rule, {visit: 'Declaration', enter: function(styleCsstreeDeclaration) {
  114. // existing inline styles have higher priority
  115. // no inline styles, external styles, external styles used
  116. // inline styles, external styles same priority as inline styles, inline styles used
  117. // inline styles, external styles higher priority than inline styles, external styles used
  118. var styleDeclaration = cssTools.csstreeToStyleDeclaration(styleCsstreeDeclaration);
  119. if (selectedEl.style.getPropertyValue(styleDeclaration.name) !== null &&
  120. selectedEl.style.getPropertyPriority(styleDeclaration.name) >= styleDeclaration.priority) {
  121. return;
  122. }
  123. selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
  124. }});
  125. }
  126. if (opts.removeMatchedSelectors && selector.selectedEls !== null && selector.selectedEls.length > 0) {
  127. // clean up matching simple selectors if option removeMatchedSelectors is enabled
  128. selector.rule.prelude.children.remove(selector.item);
  129. }
  130. }
  131. if (!opts.removeMatchedSelectors) {
  132. return document; // no further processing required
  133. }
  134. // clean up matched class + ID attribute values
  135. for (selector of sortedSelectors) {
  136. if(!selector.selectedEls) {
  137. continue;
  138. }
  139. if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
  140. // skip selectors that match more than once if option onlyMatchedOnce is enabled
  141. continue;
  142. }
  143. for (selectedEl of selector.selectedEls) {
  144. // class
  145. var firstSubSelector = selector.item.data.children.first();
  146. if(firstSubSelector.type === 'ClassSelector') {
  147. selectedEl.class.remove(firstSubSelector.name);
  148. }
  149. // clean up now empty class attributes
  150. if(typeof selectedEl.class.item(0) === 'undefined') {
  151. selectedEl.removeAttr('class');
  152. }
  153. // ID
  154. if(firstSubSelector.type === 'IdSelector') {
  155. selectedEl.removeAttr('id', firstSubSelector.name);
  156. }
  157. }
  158. }
  159. // clean up now empty elements
  160. for (var style of styles) {
  161. csstree.walk(style.cssAst, {visit: 'Rule', enter: function(node, item, list) {
  162. // clean up <style/> atrules without any rulesets left
  163. if (node.type === 'Atrule' &&
  164. // only Atrules containing rulesets
  165. node.block !== null &&
  166. node.block.children.isEmpty()) {
  167. list.remove(item);
  168. return;
  169. }
  170. // clean up <style/> rulesets without any css selectors left
  171. if (node.type === 'Rule' &&
  172. node.prelude.children.isEmpty()) {
  173. list.remove(item);
  174. }
  175. }});
  176. if (style.cssAst.children.isEmpty()) {
  177. // clean up now emtpy <style/>s
  178. var styleParentEl = style.styleEl.parentNode;
  179. styleParentEl.spliceContent(styleParentEl.content.indexOf(style.styleEl), 1);
  180. if (styleParentEl.elem === 'defs' &&
  181. styleParentEl.content.length === 0) {
  182. // also clean up now empty <def/>s
  183. var defsParentEl = styleParentEl.parentNode;
  184. defsParentEl.spliceContent(defsParentEl.content.indexOf(styleParentEl), 1);
  185. }
  186. continue;
  187. }
  188. // update existing, left over <style>s
  189. cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst));
  190. }
  191. return document;
  192. };