Fixes #933 - smarter longhand into shorthand merging.
[clean-css.git] / lib / optimizer / level-2 / properties / override-properties.js
1 var hasInherit = require('./has-inherit');
2 var everyValuesPair = require('./every-values-pair');
3 var findComponentIn = require('./find-component-in');
4 var isComponentOf = require('./is-component-of');
5 var isMergeableShorthand = require('./is-mergeable-shorthand');
6 var overridesNonComponentShorthand = require('./overrides-non-component-shorthand');
7 var sameVendorPrefixesIn = require('./vendor-prefixes').same;
8
9 var compactable = require('../compactable');
10 var deepClone = require('../clone').deep;
11 var deepClone = require('../clone').deep;
12 var restoreWithComponents = require('../restore-with-components');
13 var shallowClone = require('../clone').shallow;
14
15 var restoreFromOptimizing = require('../../restore-from-optimizing');
16
17 var Token = require('../../../tokenizer/token');
18 var Marker = require('../../../tokenizer/marker');
19
20 var serializeProperty = require('../../../writer/one-time').property;
21
22 function wouldBreakCompatibility(property, validator) {
23   for (var i = 0; i < property.components.length; i++) {
24     var component = property.components[i];
25     var descriptor = compactable[component.name];
26     var canOverride = descriptor && descriptor.canOverride || canOverride.sameValue;
27
28     var _component = shallowClone(component);
29     _component.value = [[Token.PROPERTY_VALUE, descriptor.defaultValue]];
30
31     if (!everyValuesPair(canOverride.bind(null, validator), _component, component)) {
32       return true;
33     }
34   }
35
36   return false;
37 }
38
39 function overrideIntoMultiplex(property, by) {
40   by.unused = true;
41
42   turnIntoMultiplex(by, multiplexSize(property));
43   property.value = by.value;
44 }
45
46 function overrideByMultiplex(property, by) {
47   by.unused = true;
48   property.multiplex = true;
49   property.value = by.value;
50 }
51
52 function overrideSimple(property, by) {
53   by.unused = true;
54   property.value = by.value;
55 }
56
57 function override(property, by) {
58   if (by.multiplex)
59     overrideByMultiplex(property, by);
60   else if (property.multiplex)
61     overrideIntoMultiplex(property, by);
62   else
63     overrideSimple(property, by);
64 }
65
66 function overrideShorthand(property, by) {
67   by.unused = true;
68
69   for (var i = 0, l = property.components.length; i < l; i++) {
70     override(property.components[i], by.components[i], property.multiplex);
71   }
72 }
73
74 function turnIntoMultiplex(property, size) {
75   property.multiplex = true;
76
77   if (compactable[property.name].shorthand) {
78     turnShorthandValueIntoMultiplex(property, size);
79   } else {
80     turnLonghandValueIntoMultiplex(property, size);
81   }
82 }
83
84 function turnShorthandValueIntoMultiplex(property, size) {
85   var component;
86   var i, l;
87
88   for (i = 0, l = property.components.length; i < l; i++) {
89     component = property.components[i];
90
91     if (!component.multiplex) {
92       turnLonghandValueIntoMultiplex(component, size);
93     }
94   }
95 }
96
97 function turnLonghandValueIntoMultiplex(property, size) {
98   var withRealValue = compactable[property.name].intoMultiplexMode == 'real';
99   var withValue = withRealValue ?
100     property.value.slice(0) :
101     compactable[property.name].defaultValue;
102   var i = multiplexSize(property);
103   var j;
104   var m = withValue.length;
105
106   for (; i < size; i++) {
107     property.value.push([Token.PROPERTY_VALUE, Marker.COMMA]);
108
109     if (Array.isArray(withValue)) {
110       for (j = 0; j < m; j++) {
111         property.value.push(withRealValue ? withValue[j] : [Token.PROPERTY_VALUE, withValue[j]]);
112       }
113     } else {
114       property.value.push(withRealValue ? withValue : [Token.PROPERTY_VALUE, withValue]);
115     }
116   }
117 }
118
119 function multiplexSize(component) {
120   var size = 0;
121
122   for (var i = 0, l = component.value.length; i < l; i++) {
123     if (component.value[i][1] == Marker.COMMA)
124       size++;
125   }
126
127   return size + 1;
128 }
129
130 function lengthOf(property) {
131   var fakeAsArray = [
132     Token.PROPERTY,
133     [Token.PROPERTY_NAME, property.name]
134   ].concat(property.value);
135   return serializeProperty([fakeAsArray], 0).length;
136 }
137
138 function moreSameShorthands(properties, startAt, name) {
139   // Since we run the main loop in `compactOverrides` backwards, at this point some
140   // properties may not be marked as unused.
141   // We should consider reverting the order if possible
142   var count = 0;
143
144   for (var i = startAt; i >= 0; i--) {
145     if (properties[i].name == name && !properties[i].unused)
146       count++;
147     if (count > 1)
148       break;
149   }
150
151   return count > 1;
152 }
153
154 function overridingFunction(shorthand, validator) {
155   for (var i = 0, l = shorthand.components.length; i < l; i++) {
156     if (!anyValue(validator.isUrl, shorthand.components[i]) && anyValue(validator.isFunction, shorthand.components[i])) {
157       return true;
158     }
159   }
160
161   return false;
162 }
163
164 function anyValue(fn, property) {
165   for (var i = 0, l = property.value.length; i < l; i++) {
166     if (property.value[i][1] == Marker.COMMA)
167       continue;
168
169     if (fn(property.value[i][1]))
170       return true;
171   }
172
173   return false;
174 }
175
176 function wouldResultInLongerValue(left, right) {
177   if (!left.multiplex && !right.multiplex || left.multiplex && right.multiplex)
178     return false;
179
180   var multiplex = left.multiplex ? left : right;
181   var simple = left.multiplex ? right : left;
182   var component;
183
184   var multiplexClone = deepClone(multiplex);
185   restoreFromOptimizing([multiplexClone], restoreWithComponents);
186
187   var simpleClone = deepClone(simple);
188   restoreFromOptimizing([simpleClone], restoreWithComponents);
189
190   var lengthBefore = lengthOf(multiplexClone) + 1 + lengthOf(simpleClone);
191
192   if (left.multiplex) {
193     component = findComponentIn(multiplexClone, simpleClone);
194     overrideIntoMultiplex(component, simpleClone);
195   } else {
196     component = findComponentIn(simpleClone, multiplexClone);
197     turnIntoMultiplex(simpleClone, multiplexSize(multiplexClone));
198     overrideByMultiplex(component, multiplexClone);
199   }
200
201   restoreFromOptimizing([simpleClone], restoreWithComponents);
202
203   var lengthAfter = lengthOf(simpleClone);
204
205   return lengthBefore <= lengthAfter;
206 }
207
208 function isCompactable(property) {
209   return property.name in compactable;
210 }
211
212 function noneOverrideHack(left, right) {
213   return !left.multiplex &&
214     (left.name == 'background' || left.name == 'background-image') &&
215     right.multiplex &&
216     (right.name == 'background' || right.name == 'background-image') &&
217     anyLayerIsNone(right.value);
218 }
219
220 function anyLayerIsNone(values) {
221   var layers = intoLayers(values);
222
223   for (var i = 0, l = layers.length; i < l; i++) {
224     if (layers[i].length == 1 && layers[i][0][1] == 'none')
225       return true;
226   }
227
228   return false;
229 }
230
231 function intoLayers(values) {
232   var layers = [];
233
234   for (var i = 0, layer = [], l = values.length; i < l; i++) {
235     var value = values[i];
236     if (value[1] == Marker.COMMA) {
237       layers.push(layer);
238       layer = [];
239     } else {
240       layer.push(value);
241     }
242   }
243
244   layers.push(layer);
245   return layers;
246 }
247
248 function overrideProperties(properties, withMerging, compatibility, validator) {
249   var mayOverride, right, left, component;
250   var overriddenComponents;
251   var overriddenComponent;
252   var overridingComponent;
253   var overridable;
254   var i, j, k;
255
256   propertyLoop:
257   for (i = properties.length - 1; i >= 0; i--) {
258     right = properties[i];
259
260     if (!isCompactable(right))
261       continue;
262
263     if (right.block)
264       continue;
265
266     mayOverride = compactable[right.name].canOverride;
267
268     traverseLoop:
269     for (j = i - 1; j >= 0; j--) {
270       left = properties[j];
271
272       if (!isCompactable(left))
273         continue;
274
275       if (left.block)
276         continue;
277
278       if (left.unused || right.unused)
279         continue;
280
281       if (left.hack && !right.hack && !right.important || !left.hack && !left.important && right.hack)
282         continue;
283
284       if (left.important == right.important && left.hack[0] != right.hack[0])
285         continue;
286
287       if (left.important == right.important && (left.hack[0] != right.hack[0] || (left.hack[1] && left.hack[1] != right.hack[1])))
288         continue;
289
290       if (hasInherit(right))
291         continue;
292
293       if (noneOverrideHack(left, right))
294         continue;
295
296       if (right.shorthand && isComponentOf(right, left)) {
297         // maybe `left` can be overridden by `right` which is a shorthand?
298         if (!right.important && left.important)
299           continue;
300
301         if (!sameVendorPrefixesIn([left], right.components))
302           continue;
303
304         if (!anyValue(validator.isFunction, left) && overridingFunction(right, validator))
305           continue;
306
307         if (!isMergeableShorthand(right)) {
308           left.unused = true;
309           continue;
310         }
311
312         component = findComponentIn(right, left);
313         mayOverride = compactable[left.name].canOverride;
314         if (everyValuesPair(mayOverride.bind(null, validator), left, component)) {
315           left.unused = true;
316         }
317       } else if (right.shorthand && overridesNonComponentShorthand(right, left)) {
318         // `right` is a shorthand while `left` can be overriden by it, think `border` and `border-top`
319         if (!right.important && left.important) {
320           continue;
321         }
322
323         if (!sameVendorPrefixesIn([left], right.components)) {
324           continue;
325         }
326
327         if (!anyValue(validator.isFunction, left) && overridingFunction(right, validator)) {
328           continue;
329         }
330
331         overriddenComponents = left.shorthand ?
332           left.components:
333           [left];
334
335         for (k = overriddenComponents.length - 1; k >= 0; k--) {
336           overriddenComponent = overriddenComponents[k];
337           overridingComponent = findComponentIn(right, overriddenComponent);
338           mayOverride = compactable[overriddenComponent.name].canOverride;
339
340           if (!everyValuesPair(mayOverride.bind(null, validator), left, overridingComponent)) {
341             continue traverseLoop;
342           }
343         }
344
345         left.unused = true;
346       } else if (withMerging && left.shorthand && !right.shorthand && isComponentOf(left, right, true)) {
347         // maybe `right` can be pulled into `left` which is a shorthand?
348         if (right.important && !left.important)
349           continue;
350
351         if (!right.important && left.important) {
352           right.unused = true;
353           continue;
354         }
355
356         // Pending more clever algorithm in #527
357         if (moreSameShorthands(properties, i - 1, left.name))
358           continue;
359
360         if (overridingFunction(left, validator))
361           continue;
362
363         if (!isMergeableShorthand(left))
364           continue;
365
366         component = findComponentIn(left, right);
367         if (everyValuesPair(mayOverride.bind(null, validator), component, right)) {
368           var disabledBackgroundMerging =
369             !compatibility.properties.backgroundClipMerging && component.name.indexOf('background-clip') > -1 ||
370             !compatibility.properties.backgroundOriginMerging && component.name.indexOf('background-origin') > -1 ||
371             !compatibility.properties.backgroundSizeMerging && component.name.indexOf('background-size') > -1;
372           var nonMergeableValue = compactable[right.name].nonMergeableValue === right.value[0][1];
373
374           if (disabledBackgroundMerging || nonMergeableValue)
375             continue;
376
377           if (!compatibility.properties.merging && wouldBreakCompatibility(left, validator))
378             continue;
379
380           if (component.value[0][1] != right.value[0][1] && (hasInherit(left) || hasInherit(right)))
381             continue;
382
383           if (wouldResultInLongerValue(left, right))
384             continue;
385
386           if (!left.multiplex && right.multiplex)
387             turnIntoMultiplex(left, multiplexSize(right));
388
389           override(component, right);
390           left.dirty = true;
391         }
392       } else if (withMerging && left.shorthand && right.shorthand && left.name == right.name) {
393         // merge if all components can be merged
394
395         if (!left.multiplex && right.multiplex)
396           continue;
397
398         if (!right.important && left.important) {
399           right.unused = true;
400           continue propertyLoop;
401         }
402
403         if (right.important && !left.important) {
404           left.unused = true;
405           continue;
406         }
407
408         if (!isMergeableShorthand(right)) {
409           left.unused = true;
410           continue;
411         }
412
413         for (k = left.components.length - 1; k >= 0; k--) {
414           var leftComponent = left.components[k];
415           var rightComponent = right.components[k];
416
417           mayOverride = compactable[leftComponent.name].canOverride;
418           if (!everyValuesPair(mayOverride.bind(null, validator), leftComponent, rightComponent))
419             continue propertyLoop;
420         }
421
422         overrideShorthand(left, right);
423         left.dirty = true;
424       } else if (withMerging && left.shorthand && right.shorthand && isComponentOf(left, right)) {
425         // border is a shorthand but any of its components is a shorthand too
426
427         if (!left.important && right.important)
428           continue;
429
430         component = findComponentIn(left, right);
431         mayOverride = compactable[right.name].canOverride;
432         if (!everyValuesPair(mayOverride.bind(null, validator), component, right))
433           continue;
434
435         if (left.important && !right.important) {
436           right.unused = true;
437           continue;
438         }
439
440         var rightRestored = compactable[right.name].restore(right, compactable);
441         if (rightRestored.length > 1)
442           continue;
443
444         component = findComponentIn(left, right);
445         override(component, right);
446         right.dirty = true;
447       } else if (left.name == right.name) {
448         // two non-shorthands should be merged based on understandability
449         overridable = true;
450
451         if (right.shorthand) {
452           for (k = right.components.length - 1; k >= 0 && overridable; k--) {
453             overriddenComponent = left.components[k];
454             overridingComponent = right.components[k];
455             mayOverride = compactable[overridingComponent.name].canOverride;
456
457             overridable = overridable && everyValuesPair(mayOverride.bind(null, validator), overriddenComponent, overridingComponent);
458           }
459         } else {
460           mayOverride = compactable[right.name].canOverride;
461           overridable = everyValuesPair(mayOverride.bind(null, validator), left, right);
462         }
463
464         if (left.important && !right.important && overridable) {
465           right.unused = true;
466           continue;
467         }
468
469         if (!left.important && right.important && overridable) {
470           left.unused = true;
471           continue;
472         }
473
474         if (!overridable) {
475           continue;
476         }
477
478         left.unused = true;
479       }
480     }
481   }
482 }
483
484 module.exports = overrideProperties;