Skip to content

Commit a81830d

Browse files
committed
Safer evaluation for nested expressions
Ref postcss/postcss-calc#2
1 parent 774f3f0 commit a81830d

File tree

3 files changed

+57
-20
lines changed

3 files changed

+57
-20
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 1.1.2 - 2014-08-10
2+
3+
* Prevent infinite loop by adding a `Call stack overflow`
4+
* Correctly ignore unrecognized values (safer evaluation for nested expressions, see [postcss/postcss-calc#2](https://github.com/postcss/postcss-calc/issues/2))
5+
* Handle rounding issues (eg: 10% * 20% now give 2%, not 2.0000000000000004%)
6+
17
# 1.1.1 - 2014-08-06
28

39
* Fix issue when using mutiples differents prefixes in the same function

index.js

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var reduceFunctionCall = require("reduce-function-call")
99
*/
1010
var MAX_STACK = 100 // should be enough for a single calc()...
1111
var DECIMAL_PRECISION = 10000 // 5 decimals
12+
var NESTED_CALC_RE = /(\+|\-|\*|\\|[^a-z]|)(\s*)(\()/g
1213

1314
/**
1415
* Global variables
@@ -37,9 +38,7 @@ function reduceCSSCalc(value) {
3738
*
3839
* @param {String} expression
3940
* @returns {String}
40-
* @api private
4141
*/
42-
4342
function evaluateExpression (expression, functionIdentifier, call) {
4443
if (stack++ > MAX_STACK) {
4544
stack = 0
@@ -50,23 +49,7 @@ function evaluateExpression (expression, functionIdentifier, call) {
5049
throw new Error(functionIdentifier + "(): '" + call + "' must contain a non-whitespace string")
5150
}
5251

53-
var balancedExpr = balanced("(", ")", expression)
54-
while (balancedExpr) {
55-
if (balancedExpr.body === "") {
56-
throw new Error(functionIdentifier + "(): '" + expression + "' must contain a non-whitespace string")
57-
}
58-
59-
var evaluated = evaluateExpression(balancedExpr.body, functionIdentifier, call)
60-
61-
// if result didn't change since the last try, we consider it won't change anymore
62-
if (evaluated === balancedExpr.body) {
63-
balancedExpr = false
64-
}
65-
else {
66-
expression = balancedExpr.pre + evaluated + balancedExpr.post
67-
balancedExpr = balanced("(", ")", expression)
68-
}
69-
}
52+
expression = evaluateNestedExpression(expression, call)
7053

7154
var units = getUnitsInExpression(expression)
7255

@@ -112,12 +95,41 @@ function evaluateExpression (expression, functionIdentifier, call) {
11295
return result
11396
}
11497

98+
/**
99+
* Evaluates nested expressions
100+
*
101+
* @param {String} expression
102+
* @returns {String}
103+
*/
104+
function evaluateNestedExpression(expression, call) {
105+
var evaluatedPart = ""
106+
var nonEvaluatedPart = expression
107+
var matches
108+
while ((matches = NESTED_CALC_RE.exec(nonEvaluatedPart))) {
109+
if (matches[0].index > 0) {
110+
evaluatedPart += nonEvaluatedPart.substring(0, matches[0].index)
111+
}
112+
113+
var balancedExpr = balanced("(", ")", nonEvaluatedPart.substring([0].index))
114+
if (balancedExpr.body === "") {
115+
throw new Error("'" + expression + "' must contain a non-whitespace string")
116+
}
117+
118+
var evaluated = evaluateExpression(balancedExpr.body, "", call)
119+
120+
evaluatedPart += balancedExpr.pre + evaluated
121+
nonEvaluatedPart = balancedExpr.post
122+
}
123+
124+
return evaluatedPart + nonEvaluatedPart
125+
}
126+
127+
115128
/**
116129
* Checks what units are used in an expression
117130
*
118131
* @param {String} expression
119132
* @returns {Array}
120-
* @api private
121133
*/
122134

123135
function getUnitsInExpression(expression) {

test/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,24 @@ test("handle rounding issues", function(t) {
7979

8080
test("ignore unrecognized values", function(t) {
8181
t.equal(reduceCSSCalc("calc((4px * 2) + 4.2 + a1 + (2rem * .4))"), "calc(8px + 4.2 + a1 + 0.8rem)", "ignore when eval fail")
82+
83+
t.equal(reduceCSSCalc("calc(z)"), "calc(z)", "ignore when there is something unknow")
84+
85+
t.equal(reduceCSSCalc("calc((a) + 1)"), "calc((a) + 1)", "ignore when there is something unknow in ( )")
86+
87+
t.equal(reduceCSSCalc("calc(1 (a))"), "calc(1 (a))", "ignore when there is something unknow in ( ) after something else")
88+
89+
t.equal(reduceCSSCalc("calc(b(a) + 1)"), "calc(b(a) + 1)", "ignore when there is unknown function used")
90+
91+
t.equal(reduceCSSCalc("calc(var(--foo) + 10px)"), "calc(var(--foo) + 10px)", "ignore when there is css var() at the beginning")
92+
t.equal(reduceCSSCalc("calc(10px + var(--foo))"), "calc(10px + var(--foo))", "ignore when there is css var() at the end")
93+
t.equal(reduceCSSCalc("calc(10px + var(--foo) + 2)"), "calc(10px + var(--foo) + 2)", "ignore when there is css var() in the middle")
94+
95+
t.equal(reduceCSSCalc("calc((4px + 8px) + --foo + (10% * 20%))"), "calc(12px + --foo + 2%)", "ignore unrecognized part")
96+
t.equal(reduceCSSCalc("calc((4px + 8px) + (--foo) + (10% * 20%))"), "calc(12px + (--foo) + 2%)", "ignore unrecognized part between parenthesis")
97+
t.equal(reduceCSSCalc("calc((4px + 8px) + var(--foo) + (10% * 20%))"), "calc(12px + var(--foo) + 2%)", "ignore unrecognized function")
98+
99+
t.equal(reduceCSSCalc("calc(calc(4px + 8px) + calc(var(--foo) + 10px) + calc(10% * 20%))"), "calc(12px + calc(var(--foo) + 10px) + 2%)", "ignore unrecognized nested call")
100+
82101
t.end()
83102
})

0 commit comments

Comments
 (0)