6-restructBlock.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. var resolveProperty = require('css-tree').property;
  2. var resolveKeyword = require('css-tree').keyword;
  3. var walk = require('css-tree').walk;
  4. var generate = require('css-tree').generate;
  5. var fingerprintId = 1;
  6. var dontRestructure = {
  7. 'src': 1 // https://github.com/afelix/csso/issues/50
  8. };
  9. var DONT_MIX_VALUE = {
  10. // https://developer.mozilla.org/en-US/docs/Web/CSS/display#Browser_compatibility
  11. 'display': /table|ruby|flex|-(flex)?box$|grid|contents|run-in/i,
  12. // https://developer.mozilla.org/en/docs/Web/CSS/text-align
  13. 'text-align': /^(start|end|match-parent|justify-all)$/i
  14. };
  15. var SAFE_VALUES = {
  16. cursor: [
  17. 'auto', 'crosshair', 'default', 'move', 'text', 'wait', 'help',
  18. 'n-resize', 'e-resize', 's-resize', 'w-resize',
  19. 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize',
  20. 'pointer', 'progress', 'not-allowed', 'no-drop', 'vertical-text', 'all-scroll',
  21. 'col-resize', 'row-resize'
  22. ],
  23. overflow: [
  24. 'hidden', 'visible', 'scroll', 'auto'
  25. ],
  26. position: [
  27. 'static', 'relative', 'absolute', 'fixed'
  28. ]
  29. };
  30. var NEEDLESS_TABLE = {
  31. 'border-width': ['border'],
  32. 'border-style': ['border'],
  33. 'border-color': ['border'],
  34. 'border-top': ['border'],
  35. 'border-right': ['border'],
  36. 'border-bottom': ['border'],
  37. 'border-left': ['border'],
  38. 'border-top-width': ['border-top', 'border-width', 'border'],
  39. 'border-right-width': ['border-right', 'border-width', 'border'],
  40. 'border-bottom-width': ['border-bottom', 'border-width', 'border'],
  41. 'border-left-width': ['border-left', 'border-width', 'border'],
  42. 'border-top-style': ['border-top', 'border-style', 'border'],
  43. 'border-right-style': ['border-right', 'border-style', 'border'],
  44. 'border-bottom-style': ['border-bottom', 'border-style', 'border'],
  45. 'border-left-style': ['border-left', 'border-style', 'border'],
  46. 'border-top-color': ['border-top', 'border-color', 'border'],
  47. 'border-right-color': ['border-right', 'border-color', 'border'],
  48. 'border-bottom-color': ['border-bottom', 'border-color', 'border'],
  49. 'border-left-color': ['border-left', 'border-color', 'border'],
  50. 'margin-top': ['margin'],
  51. 'margin-right': ['margin'],
  52. 'margin-bottom': ['margin'],
  53. 'margin-left': ['margin'],
  54. 'padding-top': ['padding'],
  55. 'padding-right': ['padding'],
  56. 'padding-bottom': ['padding'],
  57. 'padding-left': ['padding'],
  58. 'font-style': ['font'],
  59. 'font-variant': ['font'],
  60. 'font-weight': ['font'],
  61. 'font-size': ['font'],
  62. 'font-family': ['font'],
  63. 'list-style-type': ['list-style'],
  64. 'list-style-position': ['list-style'],
  65. 'list-style-image': ['list-style']
  66. };
  67. function getPropertyFingerprint(propertyName, declaration, fingerprints) {
  68. var realName = resolveProperty(propertyName).basename;
  69. if (realName === 'background') {
  70. return propertyName + ':' + generate(declaration.value);
  71. }
  72. var declarationId = declaration.id;
  73. var fingerprint = fingerprints[declarationId];
  74. if (!fingerprint) {
  75. switch (declaration.value.type) {
  76. case 'Value':
  77. var vendorId = '';
  78. var iehack = '';
  79. var special = {};
  80. var raw = false;
  81. declaration.value.children.each(function walk(node) {
  82. switch (node.type) {
  83. case 'Value':
  84. case 'Brackets':
  85. case 'Parentheses':
  86. node.children.each(walk);
  87. break;
  88. case 'Raw':
  89. raw = true;
  90. break;
  91. case 'Identifier':
  92. var name = node.name;
  93. if (!vendorId) {
  94. vendorId = resolveKeyword(name).vendor;
  95. }
  96. if (/\\[09]/.test(name)) {
  97. iehack = RegExp.lastMatch;
  98. }
  99. if (SAFE_VALUES.hasOwnProperty(realName)) {
  100. if (SAFE_VALUES[realName].indexOf(name) === -1) {
  101. special[name] = true;
  102. }
  103. } else if (DONT_MIX_VALUE.hasOwnProperty(realName)) {
  104. if (DONT_MIX_VALUE[realName].test(name)) {
  105. special[name] = true;
  106. }
  107. }
  108. break;
  109. case 'Function':
  110. var name = node.name;
  111. if (!vendorId) {
  112. vendorId = resolveKeyword(name).vendor;
  113. }
  114. if (name === 'rect') {
  115. // there are 2 forms of rect:
  116. // rect(<top>, <right>, <bottom>, <left>) - standart
  117. // rect(<top> <right> <bottom> <left>) – backwards compatible syntax
  118. // only the same form values can be merged
  119. var hasComma = node.children.some(function(node) {
  120. return node.type === 'Operator' && node.value === ',';
  121. });
  122. if (!hasComma) {
  123. name = 'rect-backward';
  124. }
  125. }
  126. special[name + '()'] = true;
  127. // check nested tokens too
  128. node.children.each(walk);
  129. break;
  130. case 'Dimension':
  131. var unit = node.unit;
  132. if (/\\[09]/.test(unit)) {
  133. iehack = RegExp.lastMatch;
  134. }
  135. switch (unit) {
  136. // is not supported until IE11
  137. case 'rem':
  138. // v* units is too buggy across browsers and better
  139. // don't merge values with those units
  140. case 'vw':
  141. case 'vh':
  142. case 'vmin':
  143. case 'vmax':
  144. case 'vm': // IE9 supporting "vm" instead of "vmin".
  145. special[unit] = true;
  146. break;
  147. }
  148. break;
  149. }
  150. });
  151. fingerprint = raw
  152. ? '!' + fingerprintId++
  153. : '!' + Object.keys(special).sort() + '|' + iehack + vendorId;
  154. break;
  155. case 'Raw':
  156. fingerprint = '!' + declaration.value.value;
  157. break;
  158. default:
  159. fingerprint = generate(declaration.value);
  160. }
  161. fingerprints[declarationId] = fingerprint;
  162. }
  163. return propertyName + fingerprint;
  164. }
  165. function needless(props, declaration, fingerprints) {
  166. var property = resolveProperty(declaration.property);
  167. if (NEEDLESS_TABLE.hasOwnProperty(property.basename)) {
  168. var table = NEEDLESS_TABLE[property.basename];
  169. for (var i = 0; i < table.length; i++) {
  170. var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints);
  171. var prev = props.hasOwnProperty(ppre) ? props[ppre] : null;
  172. if (prev && (!declaration.important || prev.item.data.important)) {
  173. return prev;
  174. }
  175. }
  176. }
  177. }
  178. function processRule(rule, item, list, props, fingerprints) {
  179. var declarations = rule.block.children;
  180. declarations.eachRight(function(declaration, declarationItem) {
  181. var property = declaration.property;
  182. var fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
  183. var prev = props[fingerprint];
  184. if (prev && !dontRestructure.hasOwnProperty(property)) {
  185. if (declaration.important && !prev.item.data.important) {
  186. props[fingerprint] = {
  187. block: declarations,
  188. item: declarationItem
  189. };
  190. prev.block.remove(prev.item);
  191. // TODO: use it when we can refer to several points in source
  192. // declaration.loc = {
  193. // primary: declaration.loc,
  194. // merged: prev.item.data.loc
  195. // };
  196. } else {
  197. declarations.remove(declarationItem);
  198. // TODO: use it when we can refer to several points in source
  199. // prev.item.data.loc = {
  200. // primary: prev.item.data.loc,
  201. // merged: declaration.loc
  202. // };
  203. }
  204. } else {
  205. var prev = needless(props, declaration, fingerprints);
  206. if (prev) {
  207. declarations.remove(declarationItem);
  208. // TODO: use it when we can refer to several points in source
  209. // prev.item.data.loc = {
  210. // primary: prev.item.data.loc,
  211. // merged: declaration.loc
  212. // };
  213. } else {
  214. declaration.fingerprint = fingerprint;
  215. props[fingerprint] = {
  216. block: declarations,
  217. item: declarationItem
  218. };
  219. }
  220. }
  221. });
  222. if (declarations.isEmpty()) {
  223. list.remove(item);
  224. }
  225. }
  226. module.exports = function restructBlock(ast) {
  227. var stylesheetMap = {};
  228. var fingerprints = Object.create(null);
  229. walk(ast, {
  230. visit: 'Rule',
  231. reverse: true,
  232. enter: function(node, item, list) {
  233. var stylesheet = this.block || this.stylesheet;
  234. var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
  235. var ruleMap;
  236. var props;
  237. if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
  238. ruleMap = {};
  239. stylesheetMap[stylesheet.id] = ruleMap;
  240. } else {
  241. ruleMap = stylesheetMap[stylesheet.id];
  242. }
  243. if (ruleMap.hasOwnProperty(ruleId)) {
  244. props = ruleMap[ruleId];
  245. } else {
  246. props = {};
  247. ruleMap[ruleId] = props;
  248. }
  249. processRule.call(this, node, item, list, props, fingerprints);
  250. }
  251. });
  252. };