Skip to content

Commit

Permalink
Parse and evaluate calc expressions
Browse files Browse the repository at this point in the history
We no longer rely on 'eval'. Instead, we parse the calc expression
and compute the value.

We take advantage of the requirement that + and - operators must
be surrounded by whitespace. This lets us detect each of the five tokens
in -1.23e+45 + -1.23e+45 - -1.23e+45 using a simple regular expression.

Spec:
https://www.w3.org/TR/css3-values/#calc-syntax

resolves #74
  • Loading branch information
ericwilligers committed Jul 19, 2017
1 parent ec226e6 commit a081deb
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 2 deletions.
76 changes: 74 additions & 2 deletions src/dimension-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,78 @@

(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();

Expand All @@ -36,7 +108,7 @@
var taggedUnitRegExp = 'U(' + unitRegExp.source + ')';

// Validating input is simply applying as many reductions as we can.
var typeCheck = string.replace(/[-+]?(\d*\.)?\d+/g, 'N')
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, '');
Expand All @@ -54,7 +126,7 @@
return;

for (var unit in matchedUnits) {
var result = eval(string.replace(new RegExp('U' + unit, 'g'), '').replace(new RegExp(taggedUnitRegExp, 'g'), '*0'));
var result = calculate(string.replace(new RegExp('U' + unit, 'g'), '').replace(new RegExp(taggedUnitRegExp, 'g'), '*0'));
if (!isFinite(result))
return;
matchedUnits[unit] = result;
Expand Down
2 changes: 2 additions & 0 deletions test/js/dimension-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ suite('dimension-handler', function() {
{px: 50.0, em: -6.5});
assert.deepEqual(webAnimations1.parseLength('calc((5px + 2px)*(1 + 2*(4 + 2*-5)) + 7px - (5em + 6vw/2)*4)'),
{px: -70, em: -20, vw: -12});
assert.deepEqual(webAnimations1.parseLength('calc(-13.2E+1rem/+12e-1/(+1 + +2 - -2 * 2 - -3))'),
{rem: -11});
assert.deepEqual(webAnimations1.parseLength('calc(calc(5px) + calc(((3))) *calc(calc(10px)))'),
{px: 35});
});
Expand Down

0 comments on commit a081deb

Please sign in to comment.