blob: 9e487f972a8691ae1760457a57841e0b3b8edd40 [file] [log] [blame]
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(scope, testing) {
// Evaluates a calc expression.
// https://drafts.csswg.org/css-values-3/#calc-notation
function calculate(expression) {
// In calc expressions, white space is required on both sides of the
// + and - operators. https://drafts.csswg.org/css-values-3/#calc-notation
// Thus any + or - immediately adjacent to . or 0..9 is part of the number,
// e.g. -1.23e+45
// This regular expression matches ( ) * / + - and numbers.
var tokenRegularExpression = /([\+\-\w\.]+|[\(\)\*\/])/g;
var currentToken;
function consume() {
var matchResult = tokenRegularExpression.exec(expression);
if (matchResult)
currentToken = matchResult[0];
else
currentToken = undefined;
}
consume(); // Read the initial token.
function calcNumber() {
// https://drafts.csswg.org/css-values-3/#number-value
var result = Number(currentToken);
consume();
return result;
}
function calcValue() {
// <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
if (currentToken !== '(')
return calcNumber();
consume();
var result = calcSum();
if (currentToken !== ')')
return NaN;
consume();
return result;
}
function calcProduct() {
// <calc-product> = <calc-value> [ '*' <calc-value> | '/' <calc-number-value> ]*
var left = calcValue();
while (currentToken === '*' || currentToken === '/') {
var operator = currentToken;
consume();
var right = calcValue();
if (operator === '*')
left *= right;
else
left /= right;
}
return left;
}
function calcSum() {
// <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
var left = calcProduct();
while (currentToken === '+' || currentToken === '-') {
var operator = currentToken;
consume();
var right = calcProduct();
if (operator === '+')
left += right;
else
left -= right;
}
return left;
}
// <calc()> = calc( <calc-sum> )
return calcSum();
}
function parseDimension(unitRegExp, string) {
string = string.trim().toLowerCase();
if (string == '0' && 'px'.search(unitRegExp) >= 0)
return {px: 0};
// If we have parenthesis, we're a calc and need to start with 'calc'.
if (!/^[^(]*$|^calc/.test(string))
return;
string = string.replace(/calc\(/g, '(');
// We tag units by prefixing them with 'U' (note that we are already
// lowercase) to prevent problems with types which are substrings of
// each other (although prefixes may be problematic!)
var matchedUnits = {};
string = string.replace(unitRegExp, function(match) {
matchedUnits[match] = null;
return 'U' + match;
});
var taggedUnitRegExp = 'U(' + unitRegExp.source + ')';
// Validating input is simply applying as many reductions as we can.
var typeCheck = string.replace(/[-+]?(\d*\.)?\d+([Ee][-+]?\d+)?/g, 'N')
.replace(new RegExp('N' + taggedUnitRegExp, 'g'), 'D')
.replace(/\s[+-]\s/g, 'O')
.replace(/\s/g, '');
var reductions = [/N\*(D)/g, /(N|D)[*/]N/g, /(N|D)O\1/g, /\((N|D)\)/g];
var i = 0;
while (i < reductions.length) {
if (reductions[i].test(typeCheck)) {
typeCheck = typeCheck.replace(reductions[i], '$1');
i = 0;
} else {
i++;
}
}
if (typeCheck != 'D')
return;
for (var unit in matchedUnits) {
var result = calculate(string.replace(new RegExp('U' + unit, 'g'), '').replace(new RegExp(taggedUnitRegExp, 'g'), '*0'));
if (!isFinite(result))
return;
matchedUnits[unit] = result;
}
return matchedUnits;
}
function mergeDimensionsNonNegative(left, right) {
return mergeDimensions(left, right, true);
}
function mergeDimensions(left, right, nonNegative) {
var units = [], unit;
for (unit in left)
units.push(unit);
for (unit in right) {
if (units.indexOf(unit) < 0)
units.push(unit);
}
left = units.map(function(unit) { return left[unit] || 0; });
right = units.map(function(unit) { return right[unit] || 0; });
return [left, right, function(values) {
var result = values.map(function(value, i) {
if (values.length == 1 && nonNegative) {
value = Math.max(value, 0);
}
// Scientific notation (e.g. 1e2) is not yet widely supported by browser vendors.
return scope.numberToString(value) + units[i];
}).join(' + ');
return values.length > 1 ? 'calc(' + result + ')' : result;
}];
}
var lengthUnits = 'px|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc';
var parseLength = parseDimension.bind(null, new RegExp(lengthUnits, 'g'));
var parseLengthOrPercent = parseDimension.bind(null, new RegExp(lengthUnits + '|%', 'g'));
var parseAngle = parseDimension.bind(null, /deg|rad|grad|turn/g);
scope.parseLength = parseLength;
scope.parseLengthOrPercent = parseLengthOrPercent;
scope.consumeLengthOrPercent = scope.consumeParenthesised.bind(null, parseLengthOrPercent);
scope.parseAngle = parseAngle;
scope.mergeDimensions = mergeDimensions;
var consumeLength = scope.consumeParenthesised.bind(null, parseLength);
var consumeSizePair = scope.consumeRepeated.bind(undefined, consumeLength, /^/);
var consumeSizePairList = scope.consumeRepeated.bind(undefined, consumeSizePair, /^,/);
scope.consumeSizePairList = consumeSizePairList;
var parseSizePairList = function(input) {
var result = consumeSizePairList(input);
if (result && result[1] == '') {
return result[0];
}
};
var mergeNonNegativeSizePair = scope.mergeNestedRepeated.bind(undefined, mergeDimensionsNonNegative, ' ');
var mergeNonNegativeSizePairList = scope.mergeNestedRepeated.bind(undefined, mergeNonNegativeSizePair, ',');
scope.mergeNonNegativeSizePair = mergeNonNegativeSizePair;
scope.addPropertiesHandler(parseSizePairList, mergeNonNegativeSizePairList, [
'background-size'
]);
scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensionsNonNegative, [
'border-bottom-width',
'border-image-width',
'border-left-width',
'border-right-width',
'border-top-width',
'flex-basis',
'font-size',
'height',
'line-height',
'max-height',
'max-width',
'outline-width',
'width',
]);
scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensions, [
'border-bottom-left-radius',
'border-bottom-right-radius',
'border-top-left-radius',
'border-top-right-radius',
'bottom',
'left',
'letter-spacing',
'margin-bottom',
'margin-left',
'margin-right',
'margin-top',
'min-height',
'min-width',
'outline-offset',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'perspective',
'right',
'shape-margin',
'stroke-dashoffset',
'text-indent',
'top',
'vertical-align',
'word-spacing',
]);
})(webAnimations1, webAnimationsTesting);