1// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13// limitations under the License.
14
15(function(scope, testing) {
16
17  // Evaluates a calc expression.
18  // https://drafts.csswg.org/css-values-3/#calc-notation
19  function calculate(expression) {
20    // In calc expressions, white space is required on both sides of the
21    // + and - operators. https://drafts.csswg.org/css-values-3/#calc-notation
22    // Thus any + or - immediately adjacent to . or 0..9 is part of the number,
23    // e.g. -1.23e+45
24    // This regular expression matches ( ) * / + - and numbers.
25    var tokenRegularExpression = /([\+\-\w\.]+|[\(\)\*\/])/g;
26    var currentToken;
27    function consume() {
28      var matchResult = tokenRegularExpression.exec(expression);
29      if (matchResult)
30        currentToken = matchResult[0];
31      else
32        currentToken = undefined;
33    }
34    consume(); // Read the initial token.
35
36    function calcNumber() {
37      // https://drafts.csswg.org/css-values-3/#number-value
38      var result = Number(currentToken);
39      consume();
40      return result;
41    }
42
43    function calcValue() {
44      // <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
45      if (currentToken !== '(')
46        return calcNumber();
47      consume();
48      var result = calcSum();
49      if (currentToken !== ')')
50        return NaN;
51      consume();
52      return result;
53    }
54
55    function calcProduct() {
56      // <calc-product> = <calc-value> [ '*' <calc-value> | '/' <calc-number-value> ]*
57      var left = calcValue();
58      while (currentToken === '*' || currentToken === '/') {
59        var operator = currentToken;
60        consume();
61        var right = calcValue();
62        if (operator === '*')
63          left *= right;
64        else
65          left /= right;
66      }
67      return left;
68    }
69
70    function calcSum() {
71      // <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
72      var left = calcProduct();
73      while (currentToken === '+' || currentToken === '-') {
74        var operator = currentToken;
75        consume();
76        var right = calcProduct();
77        if (operator === '+')
78          left += right;
79        else
80          left -= right;
81      }
82      return left;
83    }
84
85    // <calc()> = calc( <calc-sum> )
86    return calcSum();
87  }
88
89  function parseDimension(unitRegExp, string) {
90    string = string.trim().toLowerCase();
91
92    if (string == '0' && 'px'.search(unitRegExp) >= 0)
93      return {px: 0};
94
95    // If we have parenthesis, we're a calc and need to start with 'calc'.
96    if (!/^[^(]*$|^calc/.test(string))
97      return;
98    string = string.replace(/calc\(/g, '(');
99
100    // We tag units by prefixing them with 'U' (note that we are already
101    // lowercase) to prevent problems with types which are substrings of
102    // each other (although prefixes may be problematic!)
103    var matchedUnits = {};
104    string = string.replace(unitRegExp, function(match) {
105      matchedUnits[match] = null;
106      return 'U' + match;
107    });
108    var taggedUnitRegExp = 'U(' + unitRegExp.source + ')';
109
110    // Validating input is simply applying as many reductions as we can.
111    var typeCheck = string.replace(/[-+]?(\d*\.)?\d+([Ee][-+]?\d+)?/g, 'N')
112        .replace(new RegExp('N' + taggedUnitRegExp, 'g'), 'D')
113        .replace(/\s[+-]\s/g, 'O')
114        .replace(/\s/g, '');
115    var reductions = [/N\*(D)/g, /(N|D)[*/]N/g, /(N|D)O\1/g, /\((N|D)\)/g];
116    var i = 0;
117    while (i < reductions.length) {
118      if (reductions[i].test(typeCheck)) {
119        typeCheck = typeCheck.replace(reductions[i], '$1');
120        i = 0;
121      } else {
122        i++;
123      }
124    }
125    if (typeCheck != 'D')
126      return;
127
128    for (var unit in matchedUnits) {
129      var result = calculate(string.replace(new RegExp('U' + unit, 'g'), '').replace(new RegExp(taggedUnitRegExp, 'g'), '*0'));
130      if (!isFinite(result))
131        return;
132      matchedUnits[unit] = result;
133    }
134    return matchedUnits;
135  }
136
137  function mergeDimensionsNonNegative(left, right) {
138    return mergeDimensions(left, right, true);
139  }
140
141  function mergeDimensions(left, right, nonNegative) {
142    var units = [], unit;
143    for (unit in left)
144      units.push(unit);
145    for (unit in right) {
146      if (units.indexOf(unit) < 0)
147        units.push(unit);
148    }
149
150    left = units.map(function(unit) { return left[unit] || 0; });
151    right = units.map(function(unit) { return right[unit] || 0; });
152    return [left, right, function(values) {
153      var result = values.map(function(value, i) {
154        if (values.length == 1 && nonNegative) {
155          value = Math.max(value, 0);
156        }
157        // Scientific notation (e.g. 1e2) is not yet widely supported by browser vendors.
158        return scope.numberToString(value) + units[i];
159      }).join(' + ');
160      return values.length > 1 ? 'calc(' + result + ')' : result;
161    }];
162  }
163
164  var lengthUnits = 'px|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc';
165  var parseLength = parseDimension.bind(null, new RegExp(lengthUnits, 'g'));
166  var parseLengthOrPercent = parseDimension.bind(null, new RegExp(lengthUnits + '|%', 'g'));
167  var parseAngle = parseDimension.bind(null, /deg|rad|grad|turn/g);
168
169  scope.parseLength = parseLength;
170  scope.parseLengthOrPercent = parseLengthOrPercent;
171  scope.consumeLengthOrPercent = scope.consumeParenthesised.bind(null, parseLengthOrPercent);
172  scope.parseAngle = parseAngle;
173  scope.mergeDimensions = mergeDimensions;
174
175  var consumeLength = scope.consumeParenthesised.bind(null, parseLength);
176  var consumeSizePair = scope.consumeRepeated.bind(undefined, consumeLength, /^/);
177  var consumeSizePairList = scope.consumeRepeated.bind(undefined, consumeSizePair, /^,/);
178  scope.consumeSizePairList = consumeSizePairList;
179
180  var parseSizePairList = function(input) {
181    var result = consumeSizePairList(input);
182    if (result && result[1] == '') {
183      return result[0];
184    }
185  };
186
187  var mergeNonNegativeSizePair = scope.mergeNestedRepeated.bind(undefined, mergeDimensionsNonNegative, ' ');
188  var mergeNonNegativeSizePairList = scope.mergeNestedRepeated.bind(undefined, mergeNonNegativeSizePair, ',');
189  scope.mergeNonNegativeSizePair = mergeNonNegativeSizePair;
190
191  scope.addPropertiesHandler(parseSizePairList, mergeNonNegativeSizePairList, [
192    'background-size'
193  ]);
194
195  scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensionsNonNegative, [
196    'border-bottom-width',
197    'border-image-width',
198    'border-left-width',
199    'border-right-width',
200    'border-top-width',
201    'flex-basis',
202    'font-size',
203    'height',
204    'line-height',
205    'max-height',
206    'max-width',
207    'outline-width',
208    'width',
209  ]);
210
211  scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensions, [
212    'border-bottom-left-radius',
213    'border-bottom-right-radius',
214    'border-top-left-radius',
215    'border-top-right-radius',
216    'bottom',
217    'left',
218    'letter-spacing',
219    'margin-bottom',
220    'margin-left',
221    'margin-right',
222    'margin-top',
223    'min-height',
224    'min-width',
225    'outline-offset',
226    'padding-bottom',
227    'padding-left',
228    'padding-right',
229    'padding-top',
230    'perspective',
231    'right',
232    'shape-margin',
233    'stroke-dashoffset',
234    'text-indent',
235    'top',
236    'vertical-align',
237    'word-spacing',
238  ]);
239
240})(webAnimations1, webAnimationsTesting);
241