4-restructShorthand.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. var List = require('../../utils/list.js');
  2. var translate = require('../../utils/translate.js');
  3. var walkRulesRight = require('../../utils/walk.js').rulesRight;
  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.info = 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(value, count) {
  72. var values = [];
  73. var iehack = '';
  74. var hasBadValues = value.sequence.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. special = child.name;
  112. break;
  113. case 'Space':
  114. return false; // ignore space
  115. default:
  116. return true; // bad value
  117. }
  118. values.push({
  119. node: child,
  120. special: special,
  121. important: value.important
  122. });
  123. });
  124. if (hasBadValues || values.length > count) {
  125. return false;
  126. }
  127. if (typeof this.iehack === 'string' && this.iehack !== iehack) {
  128. return false;
  129. }
  130. this.iehack = iehack; // move outside
  131. return values;
  132. };
  133. TRBL.prototype.canOverride = function(side, value) {
  134. var currentValue = this.sides[side];
  135. return !currentValue || (value.important && !currentValue.important);
  136. };
  137. TRBL.prototype.add = function(name, value, info) {
  138. function attemptToAdd() {
  139. var sides = this.sides;
  140. var side = SIDE[name];
  141. if (side) {
  142. if (side in sides === false) {
  143. return false;
  144. }
  145. var values = this.getValueSequence(value, 1);
  146. if (!values || !values.length) {
  147. return false;
  148. }
  149. // can mix only if specials are equal
  150. for (var key in sides) {
  151. if (sides[key] !== null && sides[key].special !== values[0].special) {
  152. return false;
  153. }
  154. }
  155. if (!this.canOverride(side, values[0])) {
  156. return true;
  157. }
  158. sides[side] = values[0];
  159. return true;
  160. } else if (name === this.name) {
  161. var values = this.getValueSequence(value, 4);
  162. if (!values || !values.length) {
  163. return false;
  164. }
  165. switch (values.length) {
  166. case 1:
  167. values[RIGHT] = values[TOP];
  168. values[BOTTOM] = values[TOP];
  169. values[LEFT] = values[TOP];
  170. break;
  171. case 2:
  172. values[BOTTOM] = values[TOP];
  173. values[LEFT] = values[RIGHT];
  174. break;
  175. case 3:
  176. values[LEFT] = values[RIGHT];
  177. break;
  178. }
  179. // can mix only if specials are equal
  180. for (var i = 0; i < 4; i++) {
  181. for (var key in sides) {
  182. if (sides[key] !== null && sides[key].special !== values[i].special) {
  183. return false;
  184. }
  185. }
  186. }
  187. for (var i = 0; i < 4; i++) {
  188. if (this.canOverride(SIDES[i], values[i])) {
  189. sides[SIDES[i]] = values[i];
  190. }
  191. }
  192. return true;
  193. }
  194. }
  195. if (!attemptToAdd.call(this)) {
  196. return false;
  197. }
  198. if (this.info) {
  199. this.info = {
  200. primary: this.info,
  201. merged: info
  202. };
  203. } else {
  204. this.info = info;
  205. }
  206. return true;
  207. };
  208. TRBL.prototype.isOkToMinimize = function() {
  209. var top = this.sides.top;
  210. var right = this.sides.right;
  211. var bottom = this.sides.bottom;
  212. var left = this.sides.left;
  213. if (top && right && bottom && left) {
  214. var important =
  215. top.important +
  216. right.important +
  217. bottom.important +
  218. left.important;
  219. return important === 0 || important === 4;
  220. }
  221. return false;
  222. };
  223. TRBL.prototype.getValue = function() {
  224. var result = [];
  225. var sides = this.sides;
  226. var values = [
  227. sides.top,
  228. sides.right,
  229. sides.bottom,
  230. sides.left
  231. ];
  232. var stringValues = [
  233. translate(sides.top.node),
  234. translate(sides.right.node),
  235. translate(sides.bottom.node),
  236. translate(sides.left.node)
  237. ];
  238. if (stringValues[LEFT] === stringValues[RIGHT]) {
  239. values.pop();
  240. if (stringValues[BOTTOM] === stringValues[TOP]) {
  241. values.pop();
  242. if (stringValues[RIGHT] === stringValues[TOP]) {
  243. values.pop();
  244. }
  245. }
  246. }
  247. for (var i = 0; i < values.length; i++) {
  248. if (i) {
  249. result.push({ type: 'Space' });
  250. }
  251. result.push(values[i].node);
  252. }
  253. if (this.iehack) {
  254. result.push({ type: 'Space' }, {
  255. type: 'Identifier',
  256. info: {},
  257. name: this.iehack
  258. });
  259. }
  260. return {
  261. type: 'Value',
  262. info: {},
  263. important: sides.top.important,
  264. sequence: new List(result)
  265. };
  266. };
  267. TRBL.prototype.getProperty = function() {
  268. return {
  269. type: 'Property',
  270. info: {},
  271. name: this.name
  272. };
  273. };
  274. function processRuleset(ruleset, shorts, shortDeclarations, lastShortSelector) {
  275. var declarations = ruleset.block.declarations;
  276. var selector = ruleset.selector.selectors.first().id;
  277. ruleset.block.declarations.eachRight(function(declaration, item) {
  278. var property = declaration.property.name;
  279. if (!MAIN_PROPERTY.hasOwnProperty(property)) {
  280. return;
  281. }
  282. var key = MAIN_PROPERTY[property];
  283. var shorthand;
  284. var operation;
  285. if (!lastShortSelector || selector === lastShortSelector) {
  286. if (key in shorts) {
  287. operation = REMOVE;
  288. shorthand = shorts[key];
  289. }
  290. }
  291. if (!shorthand || !shorthand.add(property, declaration.value, declaration.info)) {
  292. operation = REPLACE;
  293. shorthand = new TRBL(key);
  294. // if can't parse value ignore it and break shorthand sequence
  295. if (!shorthand.add(property, declaration.value, declaration.info)) {
  296. lastShortSelector = null;
  297. return;
  298. }
  299. }
  300. shorts[key] = shorthand;
  301. shortDeclarations.push({
  302. operation: operation,
  303. block: declarations,
  304. item: item,
  305. shorthand: shorthand
  306. });
  307. lastShortSelector = selector;
  308. });
  309. return lastShortSelector;
  310. };
  311. function processShorthands(shortDeclarations, markDeclaration) {
  312. shortDeclarations.forEach(function(item) {
  313. var shorthand = item.shorthand;
  314. if (!shorthand.isOkToMinimize()) {
  315. return;
  316. }
  317. if (item.operation === REPLACE) {
  318. item.item.data = markDeclaration({
  319. type: 'Declaration',
  320. info: shorthand.info,
  321. property: shorthand.getProperty(),
  322. value: shorthand.getValue(),
  323. id: 0,
  324. length: 0,
  325. fingerprint: null
  326. });
  327. } else {
  328. item.block.remove(item.item);
  329. }
  330. });
  331. };
  332. module.exports = function restructBlock(ast, indexer) {
  333. var stylesheetMap = {};
  334. var shortDeclarations = [];
  335. walkRulesRight(ast, function(node) {
  336. if (node.type !== 'Ruleset') {
  337. return;
  338. }
  339. var stylesheet = this.stylesheet;
  340. var rulesetId = (node.pseudoSignature || '') + '|' + node.selector.selectors.first().id;
  341. var rulesetMap;
  342. var shorts;
  343. if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
  344. rulesetMap = {
  345. lastShortSelector: null
  346. };
  347. stylesheetMap[stylesheet.id] = rulesetMap;
  348. } else {
  349. rulesetMap = stylesheetMap[stylesheet.id];
  350. }
  351. if (rulesetMap.hasOwnProperty(rulesetId)) {
  352. shorts = rulesetMap[rulesetId];
  353. } else {
  354. shorts = {};
  355. rulesetMap[rulesetId] = shorts;
  356. }
  357. rulesetMap.lastShortSelector = processRuleset.call(this, node, shorts, shortDeclarations, rulesetMap.lastShortSelector);
  358. });
  359. processShorthands(shortDeclarations, indexer.declaration);
  360. };