You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

428 lines
11 KiB

4 years ago
  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.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 'HexColor': // color
  107. case 'Number':
  108. case 'Percentage':
  109. break;
  110. case 'Function':
  111. special = child.name;
  112. break;
  113. case 'WhiteSpace':
  114. return false; // ignore space
  115. default:
  116. return true; // bad value
  117. }
  118. values.push({
  119. node: child,
  120. special: special,
  121. important: declaration.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, declaration) {
  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(declaration, 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(declaration, 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. // TODO: use it when we can refer to several points in source
  199. // if (this.loc) {
  200. // this.loc = {
  201. // primary: this.loc,
  202. // merged: declaration.loc
  203. // };
  204. // } else {
  205. // this.loc = declaration.loc;
  206. // }
  207. if (!this.loc) {
  208. this.loc = declaration.loc;
  209. }
  210. return true;
  211. };
  212. TRBL.prototype.isOkToMinimize = function() {
  213. var top = this.sides.top;
  214. var right = this.sides.right;
  215. var bottom = this.sides.bottom;
  216. var left = this.sides.left;
  217. if (top && right && bottom && left) {
  218. var important =
  219. top.important +
  220. right.important +
  221. bottom.important +
  222. left.important;
  223. return important === 0 || important === 4;
  224. }
  225. return false;
  226. };
  227. TRBL.prototype.getValue = function() {
  228. var result = new List();
  229. var sides = this.sides;
  230. var values = [
  231. sides.top,
  232. sides.right,
  233. sides.bottom,
  234. sides.left
  235. ];
  236. var stringValues = [
  237. generate(sides.top.node),
  238. generate(sides.right.node),
  239. generate(sides.bottom.node),
  240. generate(sides.left.node)
  241. ];
  242. if (stringValues[LEFT] === stringValues[RIGHT]) {
  243. values.pop();
  244. if (stringValues[BOTTOM] === stringValues[TOP]) {
  245. values.pop();
  246. if (stringValues[RIGHT] === stringValues[TOP]) {
  247. values.pop();
  248. }
  249. }
  250. }
  251. for (var i = 0; i < values.length; i++) {
  252. if (i) {
  253. result.appendData({ type: 'WhiteSpace', value: ' ' });
  254. }
  255. result.appendData(values[i].node);
  256. }
  257. if (this.iehack) {
  258. result.appendData({ type: 'WhiteSpace', value: ' ' });
  259. result.appendData({
  260. type: 'Identifier',
  261. loc: null,
  262. name: this.iehack
  263. });
  264. }
  265. return {
  266. type: 'Value',
  267. loc: null,
  268. children: result
  269. };
  270. };
  271. TRBL.prototype.getDeclaration = function() {
  272. return {
  273. type: 'Declaration',
  274. loc: this.loc,
  275. important: this.sides.top.important,
  276. property: this.name,
  277. value: this.getValue()
  278. };
  279. };
  280. function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
  281. var declarations = rule.block.children;
  282. var selector = rule.prelude.children.first().id;
  283. rule.block.children.eachRight(function(declaration, item) {
  284. var property = declaration.property;
  285. if (!MAIN_PROPERTY.hasOwnProperty(property)) {
  286. return;
  287. }
  288. var key = MAIN_PROPERTY[property];
  289. var shorthand;
  290. var operation;
  291. if (!lastShortSelector || selector === lastShortSelector) {
  292. if (key in shorts) {
  293. operation = REMOVE;
  294. shorthand = shorts[key];
  295. }
  296. }
  297. if (!shorthand || !shorthand.add(property, declaration)) {
  298. operation = REPLACE;
  299. shorthand = new TRBL(key);
  300. // if can't parse value ignore it and break shorthand children
  301. if (!shorthand.add(property, declaration)) {
  302. lastShortSelector = null;
  303. return;
  304. }
  305. }
  306. shorts[key] = shorthand;
  307. shortDeclarations.push({
  308. operation: operation,
  309. block: declarations,
  310. item: item,
  311. shorthand: shorthand
  312. });
  313. lastShortSelector = selector;
  314. });
  315. return lastShortSelector;
  316. }
  317. function processShorthands(shortDeclarations, markDeclaration) {
  318. shortDeclarations.forEach(function(item) {
  319. var shorthand = item.shorthand;
  320. if (!shorthand.isOkToMinimize()) {
  321. return;
  322. }
  323. if (item.operation === REPLACE) {
  324. item.item.data = markDeclaration(shorthand.getDeclaration());
  325. } else {
  326. item.block.remove(item.item);
  327. }
  328. });
  329. }
  330. module.exports = function restructBlock(ast, indexer) {
  331. var stylesheetMap = {};
  332. var shortDeclarations = [];
  333. walk(ast, {
  334. visit: 'Rule',
  335. reverse: true,
  336. enter: function(node) {
  337. var stylesheet = this.block || this.stylesheet;
  338. var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
  339. var ruleMap;
  340. var shorts;
  341. if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
  342. ruleMap = {
  343. lastShortSelector: null
  344. };
  345. stylesheetMap[stylesheet.id] = ruleMap;
  346. } else {
  347. ruleMap = stylesheetMap[stylesheet.id];
  348. }
  349. if (ruleMap.hasOwnProperty(ruleId)) {
  350. shorts = ruleMap[ruleId];
  351. } else {
  352. shorts = {};
  353. ruleMap[ruleId] = shorts;
  354. }
  355. ruleMap.lastShortSelector = processRule.call(this, node, shorts, shortDeclarations, ruleMap.lastShortSelector);
  356. }
  357. });
  358. processShorthands(shortDeclarations, indexer.declaration);
  359. };