4-restructShorthand.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. var List = require('css-tree').List;
  2. var generate = require('css-tree').generate;
  3. var walk = require('css-tree').walk;
  4. var REPLACE = 1;
  5. var REMOVE = 2;
  6. var TOP = 0;
  7. var RIGHT = 1;
  8. var BOTTOM = 2;
  9. var LEFT = 3;
  10. var SIDES = ['top', 'right', 'bottom', 'left'];
  11. var SIDE = {
  12. 'margin-top': 'top',
  13. 'margin-right': 'right',
  14. 'margin-bottom': 'bottom',
  15. 'margin-left': 'left',
  16. 'padding-top': 'top',
  17. 'padding-right': 'right',
  18. 'padding-bottom': 'bottom',
  19. 'padding-left': 'left',
  20. 'border-top-color': 'top',
  21. 'border-right-color': 'right',
  22. 'border-bottom-color': 'bottom',
  23. 'border-left-color': 'left',
  24. 'border-top-width': 'top',
  25. 'border-right-width': 'right',
  26. 'border-bottom-width': 'bottom',
  27. 'border-left-width': 'left',
  28. 'border-top-style': 'top',
  29. 'border-right-style': 'right',
  30. 'border-bottom-style': 'bottom',
  31. 'border-left-style': 'left'
  32. };
  33. var MAIN_PROPERTY = {
  34. 'margin': 'margin',
  35. 'margin-top': 'margin',
  36. 'margin-right': 'margin',
  37. 'margin-bottom': 'margin',
  38. 'margin-left': 'margin',
  39. 'padding': 'padding',
  40. 'padding-top': 'padding',
  41. 'padding-right': 'padding',
  42. 'padding-bottom': 'padding',
  43. 'padding-left': 'padding',
  44. 'border-color': 'border-color',
  45. 'border-top-color': 'border-color',
  46. 'border-right-color': 'border-color',
  47. 'border-bottom-color': 'border-color',
  48. 'border-left-color': 'border-color',
  49. 'border-width': 'border-width',
  50. 'border-top-width': 'border-width',
  51. 'border-right-width': 'border-width',
  52. 'border-bottom-width': 'border-width',
  53. 'border-left-width': 'border-width',
  54. 'border-style': 'border-style',
  55. 'border-top-style': 'border-style',
  56. 'border-right-style': 'border-style',
  57. 'border-bottom-style': 'border-style',
  58. 'border-left-style': 'border-style'
  59. };
  60. function TRBL(name) {
  61. this.name = name;
  62. this.loc = null;
  63. this.iehack = undefined;
  64. this.sides = {
  65. 'top': null,
  66. 'right': null,
  67. 'bottom': null,
  68. 'left': null
  69. };
  70. }
  71. TRBL.prototype.getValueSequence = function(declaration, count) {
  72. var values = [];
  73. var iehack = '';
  74. var hasBadValues = declaration.value.type !== 'Value' || declaration.value.children.some(function(child) {
  75. var special = false;
  76. switch (child.type) {
  77. case 'Identifier':
  78. switch (child.name) {
  79. case '\\0':
  80. case '\\9':
  81. iehack = child.name;
  82. return;
  83. case 'inherit':
  84. case 'initial':
  85. case 'unset':
  86. case 'revert':
  87. special = child.name;
  88. break;
  89. }
  90. break;
  91. case 'Dimension':
  92. switch (child.unit) {
  93. // is not supported until IE11
  94. case 'rem':
  95. // v* units is too buggy across browsers and better
  96. // don't merge values with those units
  97. case 'vw':
  98. case 'vh':
  99. case 'vmin':
  100. case 'vmax':
  101. case 'vm': // IE9 supporting "vm" instead of "vmin".
  102. special = child.unit;
  103. break;
  104. }
  105. break;
  106. case 'Hash': // color
  107. case 'Number':
  108. case 'Percentage':
  109. break;
  110. case 'Function':
  111. if (child.name === 'var') {
  112. return true;
  113. }
  114. special = child.name;
  115. break;
  116. case 'WhiteSpace':
  117. return false; // ignore space
  118. default:
  119. return true; // bad value
  120. }
  121. values.push({
  122. node: child,
  123. special: special,
  124. important: declaration.important
  125. });
  126. });
  127. if (hasBadValues || values.length > count) {
  128. return false;
  129. }
  130. if (typeof this.iehack === 'string' && this.iehack !== iehack) {
  131. return false;
  132. }
  133. this.iehack = iehack; // move outside
  134. return values;
  135. };
  136. TRBL.prototype.canOverride = function(side, value) {
  137. var currentValue = this.sides[side];
  138. return !currentValue || (value.important && !currentValue.important);
  139. };
  140. TRBL.prototype.add = function(name, declaration) {
  141. function attemptToAdd() {
  142. var sides = this.sides;
  143. var side = SIDE[name];
  144. if (side) {
  145. if (side in sides === false) {
  146. return false;
  147. }
  148. var values = this.getValueSequence(declaration, 1);
  149. if (!values || !values.length) {
  150. return false;
  151. }
  152. // can mix only if specials are equal
  153. for (var key in sides) {
  154. if (sides[key] !== null && sides[key].special !== values[0].special) {
  155. return false;
  156. }
  157. }
  158. if (!this.canOverride(side, values[0])) {
  159. return true;
  160. }
  161. sides[side] = values[0];
  162. return true;
  163. } else if (name === this.name) {
  164. var values = this.getValueSequence(declaration, 4);
  165. if (!values || !values.length) {
  166. return false;
  167. }
  168. switch (values.length) {
  169. case 1:
  170. values[RIGHT] = values[TOP];
  171. values[BOTTOM] = values[TOP];
  172. values[LEFT] = values[TOP];
  173. break;
  174. case 2:
  175. values[BOTTOM] = values[TOP];
  176. values[LEFT] = values[RIGHT];
  177. break;
  178. case 3:
  179. values[LEFT] = values[RIGHT];
  180. break;
  181. }
  182. // can mix only if specials are equal
  183. for (var i = 0; i < 4; i++) {
  184. for (var key in sides) {
  185. if (sides[key] !== null && sides[key].special !== values[i].special) {
  186. return false;
  187. }
  188. }
  189. }
  190. for (var i = 0; i < 4; i++) {
  191. if (this.canOverride(SIDES[i], values[i])) {
  192. sides[SIDES[i]] = values[i];
  193. }
  194. }
  195. return true;
  196. }
  197. }
  198. if (!attemptToAdd.call(this)) {
  199. return false;
  200. }
  201. // TODO: use it when we can refer to several points in source
  202. // if (this.loc) {
  203. // this.loc = {
  204. // primary: this.loc,
  205. // merged: declaration.loc
  206. // };
  207. // } else {
  208. // this.loc = declaration.loc;
  209. // }
  210. if (!this.loc) {
  211. this.loc = declaration.loc;
  212. }
  213. return true;
  214. };
  215. TRBL.prototype.isOkToMinimize = function() {
  216. var top = this.sides.top;
  217. var right = this.sides.right;
  218. var bottom = this.sides.bottom;
  219. var left = this.sides.left;
  220. if (top && right && bottom && left) {
  221. var important =
  222. top.important +
  223. right.important +
  224. bottom.important +
  225. left.important;
  226. return important === 0 || important === 4;
  227. }
  228. return false;
  229. };
  230. TRBL.prototype.getValue = function() {
  231. var result = new List();
  232. var sides = this.sides;
  233. var values = [
  234. sides.top,
  235. sides.right,
  236. sides.bottom,
  237. sides.left
  238. ];
  239. var stringValues = [
  240. generate(sides.top.node),
  241. generate(sides.right.node),
  242. generate(sides.bottom.node),
  243. generate(sides.left.node)
  244. ];
  245. if (stringValues[LEFT] === stringValues[RIGHT]) {
  246. values.pop();
  247. if (stringValues[BOTTOM] === stringValues[TOP]) {
  248. values.pop();
  249. if (stringValues[RIGHT] === stringValues[TOP]) {
  250. values.pop();
  251. }
  252. }
  253. }
  254. for (var i = 0; i < values.length; i++) {
  255. if (i) {
  256. result.appendData({ type: 'WhiteSpace', value: ' ' });
  257. }
  258. result.appendData(values[i].node);
  259. }
  260. if (this.iehack) {
  261. result.appendData({ type: 'WhiteSpace', value: ' ' });
  262. result.appendData({
  263. type: 'Identifier',
  264. loc: null,
  265. name: this.iehack
  266. });
  267. }
  268. return {
  269. type: 'Value',
  270. loc: null,
  271. children: result
  272. };
  273. };
  274. TRBL.prototype.getDeclaration = function() {
  275. return {
  276. type: 'Declaration',
  277. loc: this.loc,
  278. important: this.sides.top.important,
  279. property: this.name,
  280. value: this.getValue()
  281. };
  282. };
  283. function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
  284. var declarations = rule.block.children;
  285. var selector = rule.prelude.children.first().id;
  286. rule.block.children.eachRight(function(declaration, item) {
  287. var property = declaration.property;
  288. if (!MAIN_PROPERTY.hasOwnProperty(property)) {
  289. return;
  290. }
  291. var key = MAIN_PROPERTY[property];
  292. var shorthand;
  293. var operation;
  294. if (!lastShortSelector || selector === lastShortSelector) {
  295. if (key in shorts) {
  296. operation = REMOVE;
  297. shorthand = shorts[key];
  298. }
  299. }
  300. if (!shorthand || !shorthand.add(property, declaration)) {
  301. operation = REPLACE;
  302. shorthand = new TRBL(key);
  303. // if can't parse value ignore it and break shorthand children
  304. if (!shorthand.add(property, declaration)) {
  305. lastShortSelector = null;
  306. return;
  307. }
  308. }
  309. shorts[key] = shorthand;
  310. shortDeclarations.push({
  311. operation: operation,
  312. block: declarations,
  313. item: item,
  314. shorthand: shorthand
  315. });
  316. lastShortSelector = selector;
  317. });
  318. return lastShortSelector;
  319. }
  320. function processShorthands(shortDeclarations, markDeclaration) {
  321. shortDeclarations.forEach(function(item) {
  322. var shorthand = item.shorthand;
  323. if (!shorthand.isOkToMinimize()) {
  324. return;
  325. }
  326. if (item.operation === REPLACE) {
  327. item.item.data = markDeclaration(shorthand.getDeclaration());
  328. } else {
  329. item.block.remove(item.item);
  330. }
  331. });
  332. }
  333. module.exports = function restructBlock(ast, indexer) {
  334. var stylesheetMap = {};
  335. var shortDeclarations = [];
  336. walk(ast, {
  337. visit: 'Rule',
  338. reverse: true,
  339. enter: function(node) {
  340. var stylesheet = this.block || this.stylesheet;
  341. var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
  342. var ruleMap;
  343. var shorts;
  344. if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
  345. ruleMap = {
  346. lastShortSelector: null
  347. };
  348. stylesheetMap[stylesheet.id] = ruleMap;
  349. } else {
  350. ruleMap = stylesheetMap[stylesheet.id];
  351. }
  352. if (ruleMap.hasOwnProperty(ruleId)) {
  353. shorts = ruleMap[ruleId];
  354. } else {
  355. shorts = {};
  356. ruleMap[ruleId] = shorts;
  357. }
  358. ruleMap.lastShortSelector = processRule.call(this, node, shorts, shortDeclarations, ruleMap.lastShortSelector);
  359. }
  360. });
  361. processShorthands(shortDeclarations, indexer.declaration);
  362. };