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