Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/token-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ TokenStream.prototype.next = function () {
this.isSemicolon() ||
this.isNamedOp() ||
this.isConst() ||
this.isBacktickName() ||
this.isName()
) {
return this.current;
Expand Down Expand Up @@ -454,3 +455,32 @@ TokenStream.prototype.parseError = function (msg) {
var coords = this.getCoordinates();
throw new Error('parse error [' + coords.line + ':' + coords.column + ']: ' + msg);
};

TokenStream.prototype.isBacktickName = function () {
if (this.expression.charAt(this.pos) !== '`') {
return false;
}

var startPos = this.pos;
var i = startPos + 1;
var escaped = false;

for (; i < this.expression.length; i++) {
var c = this.expression.charAt(i);
if (c === '`' && !escaped) {
// Found the closing backtick
var str = this.expression.substring(startPos + 1, i);
this.current = this.newToken(TNAME, str);
this.pos = i + 1;
return true;
}
if (c === '\\' && !escaped) {
escaped = true;
} else {
escaped = false;
}
}

this.parseError('Unterminated backtick string');
return false;
};
108 changes: 78 additions & 30 deletions test/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,18 @@ describe('Expression', function () {
assert.strictEqual(Parser.evaluate('2^(-4)'), 1 / 16);
});

it('\'as\' || \'df\'', function () {
assert.strictEqual(Parser.evaluate('\'as\' || \'df\''), 'asdf');
it("'as' || 'df'", function () {
assert.strictEqual(Parser.evaluate("'as' || 'df'"), 'asdf');
});

it('[1, 2] || [3, 4] || [5, 6]', function () {
assert.deepStrictEqual(Parser.evaluate('[1, 2] || [3, 4] || [5, 6]'), [ 1, 2, 3, 4, 5, 6 ]);
assert.deepStrictEqual(Parser.evaluate('[1, 2] || [3, 4] || [5, 6]'), [1, 2, 3, 4, 5, 6]);
});

it('should fail with undefined variables', function () {
assert.throws(function () { Parser.evaluate('x + 1'); }, Error);
assert.throws(function () {
Parser.evaluate('x + 1');
}, Error);
});

it('x = 3 * 2 + 1', function () {
Expand Down Expand Up @@ -221,7 +223,9 @@ describe('Expression', function () {
});

it('should fail trying to call a non-function', function () {
assert.throws(function () { Parser.evaluate('f()', { f: 2 }); }, Error);
assert.throws(function () {
Parser.evaluate('f()', { f: 2 });
}, Error);
});

it('$x * $y_+$a1*$z - $b2', function () {
Expand Down Expand Up @@ -341,11 +345,36 @@ describe('Expression', function () {
});

it('a[2] + b[3]', function () {
assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '9');
assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ] }).toString(), '(5 + b[3])');
assert.strictEqual(Parser.parse('a[2] + b[5 - 2]').simplify({ b: [ 0, 0, 0, 4, 0 ] }).toString(), '(a[2] + 4)');
assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, 0, 5, 0][two] + 4)');
assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 'New\nLine', 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, "New\\nLine", 5, 0][two] + 4)');
assert.strictEqual(
Parser.parse('a[2] + b[3]')
.simplify({ a: [0, 0, 5, 0], b: [0, 0, 0, 4, 0] })
.toString(),
'9'
);
assert.strictEqual(
Parser.parse('a[2] + b[3]')
.simplify({ a: [0, 0, 5, 0] })
.toString(),
'(5 + b[3])'
);
assert.strictEqual(
Parser.parse('a[2] + b[5 - 2]')
.simplify({ b: [0, 0, 0, 4, 0] })
.toString(),
'(a[2] + 4)'
);
assert.strictEqual(
Parser.parse('a[two] + b[3]')
.simplify({ a: [0, 0, 5, 0], b: [0, 0, 0, 4, 0] })
.toString(),
'([0, 0, 5, 0][two] + 4)'
);
assert.strictEqual(
Parser.parse('a[two] + b[3]')
.simplify({ a: [0, 'New\nLine', 5, 0], b: [0, 0, 0, 4, 0] })
.toString(),
'([0, "New\\nLine", 5, 0][two] + 4)'
);
});
});

Expand Down Expand Up @@ -597,8 +626,10 @@ describe('Expression', function () {
});

it('a < b or c > d and e <= f or g >= h and i == j or k != l', function () {
assert.strictEqual(parser.parse('a < b or c > d and e <= f or g >= h and i == j or k != l').toString(),
'((((a < b) or (((c > d) and ((e <= f))))) or (((g >= h) and ((i == j))))) or ((k != l)))');
assert.strictEqual(
parser.parse('a < b or c > d and e <= f or g >= h and i == j or k != l').toString(),
'((((a < b) or (((c > d) and ((e <= f))))) or (((g >= h) and ((i == j))))) or ((k != l)))'
);
});

it('x = x + 1', function () {
Expand Down Expand Up @@ -639,12 +670,12 @@ describe('Expression', function () {
assert.strictEqual(Parser.parse('["a", ["b", ["c"]], true, 1 + 2 + 3]').toString(), '["a", ["b", ["c"]], true, ((1 + 2) + 3)]');
});

it('\'as\' || \'df\'', function () {
assert.strictEqual(parser.parse('\'as\' || \'df\'').toString(), '("as" || "df")');
it("'as' || 'df'", function () {
assert.strictEqual(parser.parse("'as' || 'df'").toString(), '("as" || "df")');
});

it('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'', function () {
assert.strictEqual(parser.parse('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'').toString(), '"A\\bB\\tC\\nD\\fE\\r\'F\\\\G"');
it("'A\\bB\\tC\\nD\\fE\\r\\'F\\\\G'", function () {
assert.strictEqual(parser.parse("'A\\bB\\tC\\nD\\fE\\r\\'F\\\\G'").toString(), '"A\\bB\\tC\\nD\\fE\\r\'F\\\\G"');
});

it('negative numbers are parenthesized', function () {
Expand Down Expand Up @@ -689,7 +720,7 @@ describe('Expression', function () {
it('[4, 3] || [1, 2]', function () {
var expr = parser.parse('x || y');
var f = expr.toJSFunction('x, y');
assert.deepStrictEqual(f([ 4, 3 ], [ 1, 2 ]), [ 4, 3, 1, 2 ]);
assert.deepStrictEqual(f([4, 3], [1, 2]), [4, 3, 1, 2]);
});

it('x = x + 1', function () {
Expand Down Expand Up @@ -717,7 +748,9 @@ describe('Expression', function () {
assert.strictEqual(f(2, 4), 6.283185307179586);

f = expr.toJSFunction(['y']);
assert.throws(function () { return f(4); }, Error);
assert.throws(function () {
return f(4);
}, Error);
});

it('should simplify first', function () {
Expand Down Expand Up @@ -819,7 +852,16 @@ describe('Expression', function () {
});

it('hypot(f(), max(2, x, y))', function () {
assert.strictEqual(parser.parse('hypot(f(), max(2, x, y))').toJSFunction('f, x, y')(function () { return 3; }, 4, 1), 5);
assert.strictEqual(
parser.parse('hypot(f(), max(2, x, y))').toJSFunction('f, x, y')(
function () {
return 3;
},
4,
1
),
5
);
});

it('not x or y and z', function () {
Expand Down Expand Up @@ -863,17 +905,17 @@ describe('Expression', function () {
assert.strictEqual(parser.parse('a or fail()').toJSFunction('a')(true), true);
});

it('\'as\' || s', function () {
assert.strictEqual(parser.parse('\'as\' || s').toJSFunction('s')('df'), 'asdf');
assert.strictEqual(parser.parse('\'as\' || s').toJSFunction('s')(4), 'as4');
it("'as' || s", function () {
assert.strictEqual(parser.parse("'as' || s").toJSFunction('s')('df'), 'asdf');
assert.strictEqual(parser.parse("'as' || s").toJSFunction('s')(4), 'as4');
});

it('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'', function () {
assert.strictEqual(parser.parse('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'').toJSFunction()(), 'A\bB\tC\nD\fE\r\'F\\G');
it("'A\\bB\\tC\\nD\\fE\\r\\'F\\\\G'", function () {
assert.strictEqual(parser.parse("'A\\bB\\tC\\nD\\fE\\r\\'F\\\\G'").toJSFunction()(), "A\bB\tC\nD\fE\r'F\\G");
});

it('"A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G"', function () {
assert.strictEqual(parser.parse('"A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G"').toJSFunction()(), 'A\bB\tC\nD\fE\r\'F\\G');
assert.strictEqual(parser.parse('"A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G"').toJSFunction()(), "A\bB\tC\nD\fE\r'F\\G");
});

it('"\\u2028 and \\u2029"', function () {
Expand Down Expand Up @@ -904,23 +946,29 @@ describe('Expression', function () {
});

it('a[2]', function () {
assert.strictEqual(parser.parse('a[2]').toJSFunction('a')([ 1, 2, 3 ]), 3);
assert.strictEqual(parser.parse('a[2]').toJSFunction('a')([1, 2, 3]), 3);
});

it('a[2.9]', function () {
assert.strictEqual(parser.parse('a[2.9]').toJSFunction('a')([ 1, 2, 3, 4, 5 ]), 3);
assert.strictEqual(parser.parse('a[2.9]').toJSFunction('a')([1, 2, 3, 4, 5]), 3);
});

it('a[n]', function () {
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 0), 1);
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 1), 2);
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 2), 3);
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 0), 1);
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 1), 2);
assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 2), 3);
});

it('a["foo"]', function () {
assert.strictEqual(parser.parse('a["foo"]').toJSFunction('a')({ foo: 42 }), undefined);
});

it('backtick-escaped variable names', function () {
assert.strictEqual(Parser.evaluate('`bfs-id` + 23', { 'bfs-id': 7 }), 30);
assert.strictEqual(Parser.evaluate('`my.complex-var` * 2', { 'my.complex-var': 5 }), 10);
assert.strictEqual(Parser.evaluate('`special!@#$%^&*()` + 10', { 'special!@#$%^&*()': 5 }), 15);
});

it('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]', function () {
var exp = parser.parse('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]');
assert.strictEqual(JSON.stringify(exp.toJSFunction()()), JSON.stringify([1, 5, 20, 6 / 7, [8, 9, 10], '11']));
Expand Down