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