Lot's of improvements

All string expressions are now parsed using esprima when using the builder API
This commit is contained in:
Patrick Steele-Idem 2015-12-23 16:47:42 -07:00
parent 2655a67b22
commit 1bf6838c2c
101 changed files with 1455 additions and 330 deletions

View File

@ -19,20 +19,40 @@ var HtmlElement = require('./ast/HtmlElement');
var Html = require('./ast/Html');
var Text = require('./ast/Text');
var ForEach = require('./ast/ForEach');
var ForRange = require('./ast/ForRange');
var Slot = require('./ast/Slot');
var HtmlComment = require('./ast/HtmlComment');
var SelfInvokingFunction = require('./ast/SelfInvokingFunction');
var ForStatement = require('./ast/ForStatement');
var BinaryExpression = require('./ast/BinaryExpression');
var UpdateExpression = require('./ast/UpdateExpression');
var Expression = require('./ast/Expression');
var UnaryExpression = require('./ast/UnaryExpression');
var MemberExpression = require('./ast/MemberExpression');
var parseExpression = require('./util/parseExpression');
function makeNode(arg) {
if (typeof arg === 'string') {
return parseExpression(arg);
} else if (arg instanceof Node) {
return arg;
} else {
return undefined;
}
}
class Builder {
assignment(left, right) {
return new Assignment({left, right});
assignment(left, right, operator) {
if (operator == null) {
operator = '=';
}
left = makeNode(left);
right = makeNode(right);
return new Assignment({left, right, operator});
}
binaryExpression(left, operator, right) {
left = makeNode(left);
right = makeNode(right);
return new BinaryExpression({left, operator, right});
}
@ -41,38 +61,66 @@ class Builder {
}
elseIfStatement(test, body, elseStatement) {
test = makeNode(test);
return new ElseIf({
if: new If({test, body, else: elseStatement})
});
}
expression(value) {
return new Expression({value});
return makeNode(value);
}
forEach(varName, target, body) {
if (typeof varName === 'object') {
var options = varName;
return new ForEach(options);
forEach(varName, inExpression, body) {
if (arguments.length === 1) {
var def = arguments[0];
return new ForEach(def);
} else {
return new ForEach({varName, target, body});
varName = makeNode(varName);
inExpression = makeNode(inExpression);
return new ForEach({varName, in: inExpression, body});
}
}
forRange(varName, from, to, step, body) {
if (arguments.length === 1) {
var def = arguments[0];
return new ForRange(def);
} else {
varName = makeNode(varName);
from = makeNode(from);
to = makeNode(to);
step = makeNode(step);
body = makeNode(body);
return new ForRange({varName, from, to, step, body});
}
}
forStatement(init, test, update, body) {
if (typeof init === 'object' && !init.type) {
if (arguments.length === 1) {
var def = arguments[0];
return new ForStatement(def);
} else {
init = makeNode(init);
test = makeNode(test);
update = makeNode(update);
return new ForStatement({init, test, update, body});
}
}
functionCall(callee, args) {
callee = makeNode(callee);
if (args) {
if (!isArray(args)) {
args = [args];
}
for (var i=0; i<args.length; i++) {
args[i] = makeNode(args[i]);
}
} else {
args = [];
}
@ -85,6 +133,8 @@ class Builder {
}
html(argument) {
argument = makeNode(argument);
return new Html({argument});
}
@ -94,14 +144,11 @@ class Builder {
htmlElement(tagName, attributes, body, argument) {
if (typeof tagName === 'object' && !(tagName instanceof Node)) {
let elInfo = tagName;
tagName = elInfo.tagName;
attributes = elInfo.attributes;
body = elInfo.body;
argument = elInfo.argument;
let def = arguments[0];
return new HtmlElement(def);
} else {
return new HtmlElement({tagName, attributes, body, argument});
}
return new HtmlElement({tagName, attributes, body, argument});
}
identifier(name) {
@ -116,6 +163,21 @@ class Builder {
return new Literal({value});
}
memberExpression(object, property, computed) {
object = makeNode(object);
property = makeNode(property);
return new MemberExpression({object, property, computed});
}
negate(argument) {
argument = makeNode(argument);
var operator = '!';
var prefix = true;
return new UnaryExpression({argument, operator, prefix});
}
node(type, generateCode) {
if (typeof type === 'function') {
generateCode = arguments[0];
@ -134,12 +196,16 @@ class Builder {
}
require(path) {
path = makeNode(path);
let callee = 'require';
let args = [ path ];
return new FunctionCall({callee, args});
}
returnStatement(argument) {
argument = makeNode(argument);
return new Return({argument});
}
@ -158,6 +224,9 @@ class Builder {
}
strictEquality(left, right) {
left = makeNode(left);
right = makeNode(right);
var operator = '===';
return new BinaryExpression({left, right, operator});
}
@ -167,14 +236,48 @@ class Builder {
}
text(argument, escape) {
argument = makeNode(argument);
return new Text({argument, escape});
}
unaryExpression(argument, operator, prefix) {
argument = makeNode(argument);
return new UnaryExpression({argument, operator, prefix});
}
updateExpression(argument, operator, prefix) {
argument = makeNode(argument);
return new UpdateExpression({argument, operator, prefix});
}
vars(declarations, kind) {
if (declarations) {
if (Array.isArray(declarations)) {
for (let i=0; i<declarations.length; i++) {
var declaration = declarations[i];
if (typeof declaration === 'string') {
declarations[i] = {
id: makeNode(declaration)
};
} else if (declaration instanceof Identifier) {
declarations[i] = {
id: declaration
};
}
}
} else if (typeof declarations === 'object') {
// Convert the object into an array of variables
declarations = Object.keys(declarations).map((key) => {
let id = new Identifier({name: key});
let init = makeNode(declarations[key]);
return { id, init };
});
}
}
return new Vars({declarations, kind});
}
}

View File

@ -7,15 +7,32 @@ class Assignment extends Node {
super('Assignment');
this.left = def.left;
this.right = def.right;
this.operator = def.operator;
}
generateCode(generator) {
var left = this.left;
var right = this.right;
var operator = this.operator;
generator.generateCode(left);
generator.write(' = ');
generator.write(' ' + (operator || '=') + ' ');
var wrap = right instanceof Assignment;
if (wrap) {
generator.write('(');
}
generator.generateCode(right);
if (wrap) {
generator.write(')');
}
}
isCompoundExpression() {
return true;
}
}

View File

@ -1,6 +1,21 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
function generateCodeForOperand(node, generator) {
var wrap = isCompoundExpression(node);
if (wrap) {
generator.write('(');
}
generator.generateCode(node);
if (wrap) {
generator.write(')');
}
}
class BinaryExpression extends Node {
constructor(def) {
@ -8,35 +23,26 @@ class BinaryExpression extends Node {
this.left = def.left;
this.operator = def.operator;
this.right = def.right;
this.parens = def.parens === true;
}
generateCode(generator) {
var left = this.left;
var operator = this.operator;
var right = this.right;
var parens = this.parens || this.data.isSubExpression;
if (left instanceof Node) {
left.data.isSubExpression = true;
if (!left || !right) {
throw new Error('Invalid BinaryExpression: ' + this);
}
if (right instanceof Node) {
right.data.isSubExpression = true;
}
if (parens) {
generator.write('(');
}
generator.generateCode(this.left);
generateCodeForOperand(left, generator);
generator.write(' ');
generator.generateCode(this.operator);
generator.generateCode(operator);
generator.write(' ');
generator.generateCode(this.right);
generateCodeForOperand(right, generator);
}
if (parens) {
generator.write(')');
}
isCompoundExpression() {
return true;
}
toJSON() {

View File

@ -1,16 +1,22 @@
'use strict';
var Node = require('./Node');
var ok = require('assert').ok;
class Expression extends Node {
constructor(def) {
super('Html');
super('Expression');
this.value = def.value;
ok(this.value != null, 'Invalid expression');
}
generateCode(generator) {
generator.generateCode(this.value);
}
isCompoundExpression() {
return true;
}
}
module.exports = Expression;

View File

@ -6,66 +6,23 @@ class ForEach extends Node {
constructor(def) {
super('ForEach');
this.varName = def.varName;
this.target = def.target;
this.in = def.in;
this.body = this.makeContainer(def.body);
this.separator = def.separator;
this.statusVarName = def.statusVarName;
this.from = def.from;
this.to = def.to;
this.step = def.step;
ok(this.varName, '"varName" is required');
ok(this.target != null || this.from != null, '"target" or "from" is required');
ok(this.in != null, '"in" is required');
}
generateCode(generator) {
var varName = this.varName;
var target = this.target;
var inExpression = this.in;
var separator = this.separator;
var statusVarName = this.statusVarName;
var builder = generator.builder;
if (this.from != null) {
// This is a range loop
var from = this.from;
var to = this.to;
var step = this.step;
var comparison = '<=';
if (typeof step === 'number') {
if (step < 0) {
comparison = '>=';
}
if (step === 1) {
step = varName + '++';
} else if (step === -1) {
step = varName + '--';
} else if (step > 0) {
step = varName + '+=' + step;
} else if (step === 0) {
throw new Error('Invalid step of 0');
} else if (step < 0) {
step = 0-step; // Make the step positive and switch to -=
step = varName + '-=' + step;
}
} else {
step = varName + '+=' + step;
}
return builder.selfInvokingFunction([
builder.forStatement({
init: [
builder.vars([ { id: varName, init: from }])
],
test: varName + comparison + to,
update: step,
body: this.body
})
]);
}
if (separator && !statusVarName) {
statusVarName = '__loop';
}
@ -75,22 +32,28 @@ class ForEach extends Node {
let body = this.body;
if (separator) {
let isNotLastTest = builder.functionCall(
builder.memberExpression(statusVarName, builder.identifier('isLast')),
[]);
isNotLastTest = builder.negate(isNotLastTest);
body = body.items.concat([
builder.ifStatement('!' + statusVarName + '.isLast()', [
builder.ifStatement(isNotLastTest, [
builder.text(separator)
])
]);
}
return builder.functionCall(forEachVarName, [
target,
inExpression,
builder.functionDeclaration(null, [varName, statusVarName], body)
]);
} else {
let forEachVarName = generator.addStaticVar('forEach', '__helpers.f');
return builder.functionCall(forEachVarName, [
target,
inExpression,
builder.functionDeclaration(null, [varName], this.body)
]);
}

97
compiler/ast/ForRange.js Normal file
View File

@ -0,0 +1,97 @@
'use strict';
var ok = require('assert').ok;
var Node = require('./Node');
var Literal = require('./Literal');
var Identifier = require('./Identifier');
class ForRange extends Node {
constructor(def) {
super('ForRange');
this.varName = def.varName;
this.body = this.makeContainer(def.body);
this.from = def.from;
this.to = def.to;
this.step = def.step;
ok(this.varName, '"varName" is required');
ok(this.from != null, '"from" is required');
}
generateCode(generator) {
var varName = this.varName;
var from = this.from;
var to = this.to;
var step = this.step;
var builder = generator.builder;
var comparison = '<=';
if (varName instanceof Identifier) {
varName = varName.name;
}
var updateExpression;
if (step == null) {
let fromLiteral = (from instanceof Literal) && from.value;
let toLiteral = (to instanceof Literal) && to.value;
if (typeof fromLiteral === 'number' && typeof toLiteral === 'number') {
if (fromLiteral > toLiteral) {
updateExpression = varName + '--';
comparison = '>=';
} else {
updateExpression = varName + '++';
}
}
} else {
let stepLiteral;
if (step instanceof Literal) {
stepLiteral = step.value;
} else if (typeof step === 'number') {
stepLiteral = step;
}
if (typeof stepLiteral === 'number') {
if (stepLiteral < 0) {
comparison = '>=';
}
if (stepLiteral === 1) {
updateExpression = varName + '++';
} else if (stepLiteral === -1) {
updateExpression = varName + '--';
} else if (stepLiteral > 0) {
updateExpression = varName + ' += ' + stepLiteral;
} else if (stepLiteral === 0) {
throw new Error('Invalid step of 0');
} else if (stepLiteral < 0) {
stepLiteral = 0-stepLiteral; // Make the step positive and switch to -=
updateExpression = varName + ' -= ' + stepLiteral;
}
} else {
updateExpression = builder.assignment(varName, step, '+=');
}
}
if (updateExpression == null) {
updateExpression = varName + '++';
}
return builder.selfInvokingFunction([
builder.forStatement({
init: [
builder.vars([ { id: varName, init: from }])
],
test: builder.binaryExpression(varName, comparison, to),
update: updateExpression,
body: this.body
})
]);
}
}
module.exports = ForRange;

View File

@ -51,6 +51,10 @@ class FunctionDeclaration extends Node {
generator.write('\n');
}
}
isCompoundExpression() {
return true;
}
}
module.exports = FunctionDeclaration;

View File

@ -14,8 +14,6 @@ class Html extends Node {
generateHtmlCode(generator) {
let argument = this.argument;
console.log(module.id, 'Html', argument);
generator.addWrite(argument);
}
}

View File

@ -0,0 +1,40 @@
'use strict';
var Node = require('./Node');
class MemberExpression extends Node {
constructor(def) {
super('MemberExpression');
this.object = def.object;
this.property = def.property;
this.computed = def.computed;
}
generateCode(generator) {
var object = this.object;
var property = this.property;
var computed = this.computed;
generator.generateCode(object);
if (computed) {
generator.write('[');
generator.generateCode(property);
generator.write(']');
} else {
generator.write('.');
generator.generateCode(property);
}
}
toJSON() {
return {
type: 'MemberExpression',
object: this.object,
property: this.property,
computed: this.computed
};
}
}
module.exports = MemberExpression;

View File

@ -84,6 +84,14 @@ class Node {
this.container.removeChild(this);
}
/**
* Returns true if the current node represents a compound expression (e.g. )
* @return {Boolean} [description]
*/
isCompoundExpression() {
return false;
}
isDetached() {
return this.container == null;
}

View File

@ -0,0 +1,58 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
class UnaryExpression extends Node {
constructor(def) {
super('UnaryExpression');
this.argument = def.argument;
this.operator = def.operator;
this.prefix = def.prefix === true;
}
generateCode(generator) {
var argument = this.argument;
var operator = this.operator;
var prefix = this.prefix;
if (prefix) {
generator.write(operator);
if (operator === 'typeof') {
generator.write(' ');
}
}
var wrap = isCompoundExpression(argument);
if (wrap) {
generator.write('(');
}
generator.generateCode(argument);
if (wrap) {
generator.write(')');
}
if (!prefix) {
generator.write(operator);
}
}
isCompoundExpression() {
return true;
}
toJSON() {
return {
type: 'UnaryExpression',
argument: this.argument,
operator: this.operator,
prefix: this.prefix
};
}
}
module.exports = UnaryExpression;

View File

@ -1,6 +1,7 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
class UpdateExpression extends Node {
constructor(def) {
@ -19,13 +20,27 @@ class UpdateExpression extends Node {
generator.generateCode(operator);
}
var wrap = isCompoundExpression(argument);
if (wrap) {
generator.write('(');
}
generator.generateCode(argument);
if (wrap) {
generator.write(')');
}
if (!prefix) {
generator.generateCode(operator);
}
}
isCompoundExpression() {
return true;
}
toJSON() {
return {
type: 'UpdateExpression',

View File

@ -24,14 +24,6 @@ class Vars extends Node {
return generator.builder.selfInvokingFunction([ this ]);
}
if (declarations && !Array.isArray(declarations) && typeof declarations === 'object') {
// Convert the object into an array of variables
declarations = Object.keys(declarations).map((id) => {
let init = declarations[id];
return { id, init };
});
}
if (!declarations || !declarations.length) {
return;
}
@ -39,12 +31,6 @@ class Vars extends Node {
for (let i=0; i<declarations.length; i++) {
var declaration = declarations[i];
if (typeof declaration === 'string' || declaration instanceof Identifier) {
declaration = {
id: declaration
};
}
if (i === 0) {
generator.write(kind + ' ');
} else {
@ -52,14 +38,14 @@ class Vars extends Node {
generator.writeLineIndent();
}
var varName = declaration.id || declaration.name;
var varId = declaration.id || declaration.name;
if (typeof varName !== 'string') {
throw new Error('Invalid variable name: ' + varName);
if (!(varId instanceof Identifier) && typeof varId !== 'string') {
throw new Error('Invalid variable name: ' + varId);
}
// TODO Validate the variable name
generator.generateCode(varName);
generator.generateCode(varId);
var initValue;
if (declaration.hasOwnProperty('init')) {

View File

@ -124,6 +124,7 @@ exports.compile = compile;
exports.defaultOptions = defaultOptions;
exports.checkUpToDate = checkUpToDate;
exports.createWalker = createWalker;
exports.defaultBuilder = defaultBuilder;
var taglibLookup = require('./taglib-lookup');
taglibLookup.registerTaglib(require.resolve('../taglibs/core/marko-taglib.json'));

View File

@ -0,0 +1,10 @@
function isCompoundExpression(expression) {
if (typeof expression === 'string') {
// TBD: Should we use Esprima to parse the expression string to see if it is a compount expression?
return true;
}
return expression.isCompoundExpression();
}
module.exports = isCompoundExpression;

View File

@ -0,0 +1,88 @@
'use strict';
const esprima = require('esprima');
const Expression = require('../ast/Expression');
var compiler = require('../');
function convert(node) {
var builder = compiler.defaultBuilder;
switch(node.type) {
case 'Program': {
if (node.body && node.body.length === 1) {
return convert(node.body[0]);
}
return null;
}
case 'ExpressionStatement': {
return convert(node.expression);
}
case 'Identifier': {
return builder.identifier(node.name);
}
case 'Literal': {
return builder.literal(node.value);
}
case 'BinaryExpression': {
let left = convert(node.left);
if (!left) {
return null;
}
let right = convert(node.right);
if (!right) {
return null;
}
return builder.binaryExpression(left, node.operator, right);
}
case 'UnaryExpression': {
let argument = convert(node.argument);
if (!argument) {
return null;
}
return builder.unaryExpression(argument, node.operator, node.prefix);
}
case 'AssignmentExpression': {
let left = convert(node.left);
if (!left) {
return null;
}
let right = convert(node.right);
if (!right) {
return null;
}
return builder.assignment(left, right, node.operator);
}
case 'MemberExpression': {
let object = convert(node.object);
if (!object) {
return null;
}
let property = convert(node.property);
if (!property) {
return null;
}
return builder.memberExpression(object, property, node.computed);
}
default:
return null;
}
}
function parseExpression(src) {
let jsAST = esprima.parse(src);
var converted = convert(jsAST);
if (converted == null) {
converted = new Expression({value: src});
}
return converted;
}
module.exports = parseExpression;

View File

@ -182,14 +182,18 @@ forEach(data.colors, function(color) {
});
```
___object properties:___
### forEach(varName, target, body)
TBD
Returns a node that generates a simple `forEach`. See `forEach(def)`.
___range:___
### forRange(def)
Returns a node that generates code to loop over a number range
___array:___
```javascript
builder.forEach({
builder.forRange({
varName: 'i',
from: 0,
to: 'myArray.length',
@ -199,6 +203,7 @@ builder.forEach({
]
})
// Output code:
(function() {
for (var i = 0; i<=myArray.length; i+=2) {
@ -207,9 +212,9 @@ builder.forEach({
}());
```
### forEach(varName, target, body)
### forRange(varName, from, to, step, body)
Returns a node that generates a simple `forEach`. See `forEach(def)`.
Returns a node that generates a simple `forRange`. See `forRange(def)`.
### forStatement(init, test, update, body)
@ -443,6 +448,23 @@ var aString = "abc",
]
```
### negate(argument)
Returns a node that generates the following code:
```javascript
!<argument>
```
For example:
```javascript
builder.negate(builder.identifier('foo'))
// Output:
!foo
```
### node([type, ]generatCode)
Returns a generic `Node` instance with the given node type (optional) and a `generateCode(node, generator)` function that should be used to generate the code for the node. If a `generateCode(node, generator)` function is not provided the node bust be monkey-patched to add a `generateCode(generator)` method.
@ -624,6 +646,8 @@ a === b
### text(argument, escape)
### unaryExpression(argument, operator, prefix)
### updateExpression(argument, operator, prefix)
### vars(declarations, kind)

View File

@ -1,9 +1,6 @@
'use strict';
var extend = require('raptor-util/extend');
var parseComplexAttribute = require('./util/parseComplexAttribute');
var parseForEach = require('./util/parseForEach');
var parseJavaScriptIdentifier = require('./util/parseJavaScriptIdentifier');
var createLoopNode = require('./util/createLoopNode');
var coreAttrHandlers = [
[
@ -13,49 +10,12 @@ var coreAttrHandlers = [
return;
}
var forEachProps = parseComplexAttribute(forArgument, {
'each': true,
'separator': true,
'iterator': true,
'status-var': true,
'for-loop': true
},
{
removeDashes: true,
defaultName: 'each',
errorHandler: function (message) {
this.addError('Invalid for attribute of "' + attr + '". Error: ' + message);
}
});
var loopNode = createLoopNode(forArgument, null, this.builder);
if (!forEachProps.each) {
this.addError('Invalid "for" attribute.');
}
var statusVarName = forEachProps.statusVar;
if (statusVarName) {
// statusVar is expected to be a String literal expression
// For example: statusVar: "'foo'"
// We need to parse it into an actual string such as "foo"
statusVarName = parseJavaScriptIdentifier(statusVarName);
if (!statusVarName) {
this.addError('Invalid "status-var": ' + forEachProps.statusVar);
}
delete forEachProps.statusVar;
forEachProps.statusVarName = statusVarName;
}
var parsedForEach = parseForEach(forEachProps.each);
delete forEachProps.each;
extend(forEachProps, parsedForEach);
forEachProps.pos = node.pos;
//Copy the position property
var forEachNode = this.builder.forEach(forEachProps);
//Surround the existing node with a "forEach" node
node.wrap(forEachNode);
//Surround the existing node with the newly created loop node
// NOTE: The loop node will be one of the following:
// ForEach, ForRange, ForEachProp or ForStatement
node.wrap(loopNode);
}
],
[

View File

@ -1,4 +1,4 @@
var parseForEach = require('./util/parseForEach');
var createLoopNode = require('./util/createLoopNode');
module.exports = function codeGenerator(elNode, generator) {
var argument = elNode.argument;
@ -7,16 +7,14 @@ module.exports = function codeGenerator(elNode, generator) {
return elNode;
}
var forEachProps = parseForEach(argument);
forEachProps.body = elNode.body;
var builder = generator.builder;
if (elNode.hasAttribute('separator')) {
forEachProps.separator = elNode.getAttributeValue('separator');
var loopNode = createLoopNode(argument, elNode.body, builder);
if (loopNode.error) {
generator.addError(loopNode.error);
return elNode;
}
if (elNode.hasAttribute('status-var')) {
forEachProps.statusVarName = elNode.getAttributeValue('status-var');
}
return generator.builder.forEach(forEachProps);
return loopNode;
};

View File

@ -0,0 +1,23 @@
var parseFor = require('./parseFor');
function createLoopNode(str, body, builder) {
var forDef = parseFor(str);
if (forDef.error) {
return forDef;
}
forDef.body = body;
if (forDef.loopType === 'ForEach') {
return builder.forEach(forDef);
} else if (forDef.loopType === 'ForRange') {
return builder.forRange(forDef);
} else if (forDef.loopType === 'For') {
return builder.forStatement(forDef);
} else {
throw new Error('Unsupported loop type: ' + forDef.loopType);
}
}
module.exports = createLoopNode;

View File

@ -0,0 +1,312 @@
'use strict';
var Expression = require('../../../compiler/ast/Expression');
var Literal = require('../../../compiler/ast/Literal');
var Identifier = require('../../../compiler/ast/Identifier');
var removeComments = require('./removeComments');
var integerRegExp = /^-?\d+$/;
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
var tokenizer = require('./tokenizer').create([
{
name: 'stringDouble',
pattern: /"(?:[^"]|\\")*"/,
},
{
name: 'stringSingle',
pattern: /'(?:[^']|\\')*'/
},
{
name: 'in',
pattern: /\s+in\s+/,
},
{
name: 'from',
pattern: /\s+from\s+/
},
{
name: 'to',
pattern: /\s+to\s+/,
},
{
name: 'step',
pattern: /\s+step\s+/,
},
{
name: 'semicolon',
pattern: /[;]/,
},
{
name: 'separator',
pattern: /separator=/
},
{
name: 'status-var',
pattern: /status\-var=/
},
{
name: 'iterator',
pattern: /iterator=/
},
{
name: 'pipe',
pattern: /\s+\|\s+/
},
{
name: 'groupOpen',
pattern: /[\{\(\[]/
},
{
name: 'groupClose',
pattern: /[\}\)\]]/
}
]);
function createNumberExpression(str) {
if (str == null) {
return null;
}
if (integerRegExp.test(str)) {
return new Literal({value: parseInt(str, 10)});
} else if (numberRegExp.test(str)) {
return new Literal({value: parseFloat(str)});
} else {
return new Expression({value: str});
}
}
/**
* Parses a for loop string in the following forms:
*
* <varName> in <expression>
* <varName> in <expression> | status-var=<varName> separator=<expression>
* <varName> from <expression> to <expression>
* <varName> from <expression> to <expression> step <expression>
* <init>; <test>; <update>
*/
module.exports = function(str) {
str = removeComments(str);
let depth = 0;
var prevToken;
var loopType;
var pipeFound = false;
var varName;
var nameVarName;
var valueVarName;
var inExpression;
var statusVarName;
var separatorExpression;
var fromExpression;
var toExpression;
var stepExpression;
var iteratorExpression;
var forInit;
var forTest;
var forUpdate;
function finishVarName(end) {
varName = str.substring(0, end).trim();
}
function finishPrevPart(end) {
if (!prevToken) {
return;
}
var start = prevToken.end;
var part = str.substring(start, end).trim();
switch(prevToken.name) {
case 'from':
fromExpression = part;
break;
case 'to':
toExpression = part;
break;
case 'in':
inExpression = part;
break;
case 'step':
stepExpression = part;
break;
case 'status-var':
statusVarName = part;
break;
case 'separator':
separatorExpression = part;
break;
case 'iterator':
iteratorExpression = part;
break;
}
}
tokenizer.forEachToken(str, (token) => {
switch(token.name) {
case 'groupOpen':
depth++;
break;
case 'groupClose':
depth--;
break;
case 'in':
if (depth === 0 && !loopType) {
loopType = 'ForEach';
finishVarName(token.start);
prevToken = token;
}
break;
case 'from':
if (depth === 0 && !loopType) {
loopType = 'ForRange';
finishVarName(token.start);
prevToken = token;
}
break;
case 'to':
if (depth === 0 && prevToken && prevToken.name === 'from') {
finishPrevPart(token.start);
prevToken = token;
}
break;
case 'step':
if (depth === 0 && prevToken && prevToken.name === 'to') {
finishPrevPart(token.start);
prevToken = token;
}
break;
case 'semicolon':
if (depth === 0) {
loopType = 'For';
if (forInit == null) {
forInit = str.substring(0, token.start);
} else if (forTest == null) {
forTest = str.substring(prevToken.end, token.start);
forUpdate = str.substring(token.end);
} else {
return {
error: 'Invalid native for loop. Expected format: <init>; <test>; <update>'
};
}
prevToken = token;
}
break;
case 'pipe':
if (depth === 0) {
pipeFound = true;
finishPrevPart(token.start);
prevToken = token;
}
break;
case 'status-var':
if (depth === 0 && pipeFound && str.charAt(token.start-1) === ' ') {
finishPrevPart(token.start);
prevToken = token;
}
break;
case 'separator':
if (depth === 0 && pipeFound && str.charAt(token.start-1) === ' ') {
finishPrevPart(token.start);
prevToken = token;
}
break;
case 'iterator':
if (depth === 0 && pipeFound && str.charAt(token.start-1) === ' ') {
finishPrevPart(token.start);
prevToken = token;
}
break;
}
});
finishPrevPart(str.length);
if (loopType === 'ForEach') {
var nameValue = varName.split(',');
if (nameValue.length === 2) {
nameVarName = new Identifier({name: nameValue[0]});
valueVarName = new Identifier({name: nameValue[1]});
loopType = 'ForEachProp';
}
}
if (inExpression) {
inExpression = new Expression({value: inExpression});
}
if (separatorExpression) {
separatorExpression = new Expression({value: separatorExpression});
}
if (iteratorExpression) {
iteratorExpression = new Expression({value: iteratorExpression});
}
if (fromExpression) {
fromExpression = createNumberExpression(fromExpression);
}
if (toExpression) {
toExpression = createNumberExpression(toExpression);
}
if (stepExpression) {
stepExpression = createNumberExpression(stepExpression);
}
varName = new Identifier({name: varName});
if (statusVarName) {
statusVarName = new Identifier({name: statusVarName});
}
// No more tokens... now we need to sort out what happened
if (loopType === 'ForEach') {
return {
'loopType': loopType,
'varName': varName,
'in': inExpression,
'separator': separatorExpression,
'statusVarName': statusVarName,
'iterator': iteratorExpression
};
} else if (loopType === 'ForEachProp') {
return {
'loopType': loopType,
'nameVarName': nameVarName,
'valueVarName': valueVarName,
'in': inExpression
};
} else if (loopType === 'ForRange') {
return {
'loopType': loopType,
'varName': varName,
'from': fromExpression,
'to': toExpression,
'step': stepExpression
};
} else if (loopType === 'For') {
if (forTest == null) {
return {
error: 'Invalid native for loop. Expected format: <init>; <test>; <update>'
};
}
return {
'loopType': loopType,
'init': forInit,
'test': forTest,
'update': forUpdate
};
} else {
return {
'error': 'Invalid for loop'
};
}
};

View File

@ -1,104 +0,0 @@
var forEachRegEx = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+)$/;
var forEachPropRegEx = /^\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s+in\s+(.+)$/;
var forRangeRegEx = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+from\s+(.+)$/; // i from 0 to 10 or i from 0 to 10 step 5
var forRangeKeywordsRegExp = /"(?:[^"]|\\")*"|'(?:[^']|\\')*'|\s+(to|step)\s+/g;
var integerRegExp = /^-?\d+$/;
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
function convertNumber(str) {
if (!str) {
return str;
}
if (integerRegExp.test(str)) {
return parseInt(str, 10);
} else if (numberRegExp.test(str)) {
return parseFloat(str);
} else {
return str;
}
}
module.exports = function(value) {
var match = value.match(forEachRegEx);
if (match) {
return {
'varName': match[1],
'target': match[2]
};
} else if ((match = value.match(forEachPropRegEx))) {
return {
'nameVar': match[1],
'valueVar': match[2],
'target': match[3]
};
} else if ((match = value.match(forRangeRegEx))) {
var nameVar = match[1];
var remainder = match[2];
var rangeMatches;
var fromStart = 0;
var fromEnd = -1;
var toStart = -1;
var toEnd = remainder.length;
var stepStart = -1;
var stepEnd = -1;
while ((rangeMatches = forRangeKeywordsRegExp.exec(remainder))) {
if (rangeMatches[1] === 'to') {
fromEnd = rangeMatches.index;
toStart = forRangeKeywordsRegExp.lastIndex;
} else if (rangeMatches[1] === 'step') {
if (toStart === -1) {
continue;
}
toEnd = rangeMatches.index;
stepStart = forRangeKeywordsRegExp.lastIndex;
stepEnd = remainder.length;
}
}
if (toStart === -1 || fromEnd === -1) {
throw new Error('Invalid each attribute of "' + value + '"');
}
var from = remainder.substring(fromStart, fromEnd).trim();
var to = remainder.substring(toStart, toEnd).trim();
var step;
from = convertNumber(from);
to = convertNumber(to);
if (stepStart !== -1) {
step = remainder.substring(stepStart, stepEnd).trim();
step = convertNumber(step);
} else {
if (typeof from === 'number' && typeof to === 'number') {
if (from < to) {
step = 1;
} else {
step = -1;
}
} else {
step = 1;
}
}
return {
'varName': nameVar,
'from': from,
'to': to,
'step': step
};
} else {
throw new Error('Invalid each attribute of "' + value + '"');
}
};

View File

@ -0,0 +1,53 @@
'use strict';
var tokenizer = require('./tokenizer').create([
{
name: 'stringDouble',
pattern: /"(?:[^"]|\\")*"/,
},
{
name: 'stringSingle',
pattern: /'(?:[^']|\\')*'/
},
{
name: 'singleLineComment',
pattern: /\/\/.*/
},
{
name: 'multiLineComment',
pattern: /\/\*(?:[\s\S]*?)\*\//
}
]);
/**
* Parses a for loop string in the following forms:
*
* <varName> in <expression>
* <varName> in <expression> | status-var=<varName> separator=<expression>
* <varName> from <expression> to <expression>
* <varName> from <expression> to <expression> step <expression>
* <init>; <test>; <update>
*/
module.exports = function removeComments(str) {
var comments = [];
tokenizer.forEachToken(str, (token) => {
switch(token.name) {
case 'singleLineComment':
case 'multiLineComment':
comments.push(token);
break;
}
});
var len = comments.length;
if (len) {
for (var i=len-1; i>=0; i--) {
var comment = comments[i];
str = str.substring(0, comment.start) + str.substring(comment.end);
}
}
return str;
};

View File

@ -0,0 +1,37 @@
'use strict';
function create(tokens) {
function getToken(matches) {
for (var i=0; i<tokens.length; i++) {
let tokenValue = matches[i + 1];
if (tokenValue != null) {
var tokenDef = tokens[i];
return {
start: matches.index,
end: matches.index + matches[0].length,
name: tokenDef.name,
value: tokenValue
};
}
}
}
var tokensRegExp = new RegExp(tokens
.map((token) => {
return '(' + token.pattern.source + ')';
})
.join('|'), 'g');
return {
forEachToken: function(value, callback, thisObj) {
tokensRegExp.lastIndex = 0; // Start searching from the beginning again
var matches;
while ((matches = tokensRegExp.exec(value))) {
let token = getToken(matches);
callback.call(this, token);
}
}
};
}
exports.create = create;

View File

@ -1,4 +1,12 @@
module.exports = function nodeFactory(el, context) {
var attributes = el.attributes;
return context.builder.vars(attributes);
var builder = context.builder;
var declarations = el.attributes.map((attr) => {
return {
id: builder.identifier(attr.name),
init: builder.expression(attr.value)
};
});
return context.builder.vars(declarations);
};

View File

@ -10,6 +10,11 @@ function autoTest(name, dir, run, options) {
var actualPath = path.join(dir, 'actual' + compareExtension);
var expectedPath = path.join(dir, 'expected' + compareExtension);
try {
fs.unlinkSync(actualPath);
} catch(e) {}
var actual = run(dir);
if (actual === '$PASS$') {
return;

View File

@ -2,13 +2,12 @@
module.exports = function(builder) {
var templateRoot = builder.templateRoot;
var forEach = builder.forEach;
return templateRoot([
forEach({
builder.forEachProp({
nameVarName: 'k',
valueVarName: 'v',
target: 'myArray',
in: 'myArray',
body: [
builder.functionCall('console.log', [
builder.literal('k:'),

View File

@ -6,7 +6,7 @@ function create(__helpers) {
return function render(data, out) {
(function() {
for (var i = 0; i<=myArray.length; i+=2) {
for (var i = 0; i <= myArray.length; i += 2) {
console.log(i);
}
}());

View File

@ -2,10 +2,9 @@
module.exports = function(builder) {
var templateRoot = builder.templateRoot;
var forEach = builder.forEach;
return templateRoot([
forEach({
builder.forRange({
varName: 'i',
from: 0,
to: 'myArray.length',

View File

@ -0,0 +1 @@
!foo

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = function(builder) {
return builder.negate(builder.identifier('foo'));
};

View File

@ -0,0 +1 @@
!foo

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = function(builder) {
return builder.negate(builder.identifier('foo'));
};

View File

@ -0,0 +1,12 @@
{
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "+",
"right": {
"type": "Literal",
"value": "12"
}
}

View File

@ -0,0 +1 @@
foo+'12'

View File

@ -0,0 +1,44 @@
{
"type": "Assignment",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Assignment",
"left": {
"type": "Identifier",
"name": "b"
},
"right": {
"type": "BinaryExpression",
"left": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "b"
}
},
"operator": ">",
"right": {
"type": "Assignment",
"left": {
"type": "Identifier",
"name": "c"
},
"right": {
"type": "Literal",
"value": 2
},
"operator": "+="
}
},
"operator": "="
},
"operator": "="
}

View File

@ -0,0 +1 @@
a = b = (a+b)>(c+=2)

View File

@ -0,0 +1,4 @@
{
"type": "Identifier",
"name": "foo"
}

View File

@ -0,0 +1 @@
foo

View File

@ -0,0 +1,9 @@
{
"type": "UnaryExpression",
"argument": {
"type": "Identifier",
"name": "foo"
},
"operator": "!",
"prefix": true
}

View File

@ -0,0 +1 @@
!foo

View File

@ -0,0 +1,11 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
}
}

View File

@ -0,0 +1 @@
color in colors /* | separator="," */

View File

@ -0,0 +1,23 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "(a in test | 0)"
},
"separator": {
"type": "Expression",
"value": "(iterator==1 ? 'foo' : 'bar')"
},
"statusVarName": {
"type": "Identifier",
"name": "loop"
},
"iterator": {
"type": "Expression",
"value": "my Iterator"
}
}

View File

@ -0,0 +1 @@
color in (a in test | 0) | status-var=loop separator=(iterator==1 ? 'foo' : 'bar') iterator=my Iterator

View File

@ -0,0 +1,15 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
},
"iterator": {
"type": "Expression",
"value": "myIterator"
}
}

View File

@ -0,0 +1 @@
color in colors | iterator=myIterator

View File

@ -0,0 +1,11 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "[(({a} in test | 0))]"
}
}

View File

@ -0,0 +1 @@
color in [(({a} in test | 0))]

View File

@ -0,0 +1,11 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
}
}

View File

@ -0,0 +1 @@
color in colors

View File

@ -0,0 +1,23 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
},
"separator": {
"type": "Expression",
"value": "','"
},
"statusVarName": {
"type": "Identifier",
"name": "loop"
},
"iterator": {
"type": "Expression",
"value": "myIterator"
}
}

View File

@ -0,0 +1 @@
color in colors | status-var=loop separator=',' iterator=myIterator

View File

@ -0,0 +1,19 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
},
"separator": {
"type": "Expression",
"value": "','"
},
"statusVarName": {
"type": "Identifier",
"name": "loop"
}
}

View File

@ -0,0 +1 @@
color in colors | status-var=loop separator=','

View File

@ -0,0 +1,15 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "colors"
},
"statusVarName": {
"type": "Identifier",
"name": "loop"
}
}

View File

@ -0,0 +1 @@
color in colors | status-var=loop

View File

@ -0,0 +1,11 @@
{
"loopType": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "Expression",
"value": "' | separator=\",\";'"
}
}

View File

@ -0,0 +1 @@
color in ' | separator=",";'

View File

@ -0,0 +1,15 @@
{
"loopType": "ForRange",
"varName": {
"type": "Identifier",
"name": "i"
},
"from": {
"type": "Literal",
"value": 0
},
"to": {
"type": "Expression",
"value": "'abc'.length"
}
}

View File

@ -0,0 +1 @@
i from 0 to 'abc'.length

View File

@ -0,0 +1,6 @@
{
"loopType": "For",
"init": "",
"test": " i<foo.length",
"update": " i++"
}

View File

@ -0,0 +1 @@
; i<foo.length; i++

View File

@ -0,0 +1,3 @@
{
"error": "Invalid native for loop. Expected format: <init>; <test>; <update>"
}

View File

@ -0,0 +1 @@
var i=0; i<foo.length

View File

@ -0,0 +1,6 @@
{
"loopType": "For",
"init": "var i=0",
"test": " i<foo.length",
"update": " i++"
}

View File

@ -0,0 +1 @@
var i=0; i<foo.length; i++

View File

@ -6,25 +6,48 @@
"kind": "var",
"declarations": [
{
"name": "x",
"value": {
"id": {
"type": "Identifier",
"name": "x"
},
"init": {
"type": "Literal",
"value": 1
}
},
{
"name": "y",
"value": {
"id": {
"type": "Identifier",
"name": "y"
},
"init": {
"type": "Literal",
"value": 7
}
},
{
"name": "z",
"value": "x+10"
"id": {
"type": "Identifier",
"name": "z"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "x"
},
"operator": "+",
"right": {
"type": "Literal",
"value": 10
}
}
},
{
"name": "unusedvar"
"id": {
"type": "Identifier",
"name": "unusedvar"
}
}
],
"body": [
@ -37,7 +60,10 @@
},
{
"type": "Text",
"argument": "x"
"argument": {
"type": "Identifier",
"name": "x"
}
},
{
"type": "Text",
@ -48,7 +74,10 @@
},
{
"type": "Text",
"argument": "y"
"argument": {
"type": "Identifier",
"name": "y"
}
},
{
"type": "Text",
@ -59,7 +88,10 @@
},
{
"type": "Text",
"argument": "z"
"argument": {
"type": "Identifier",
"name": "z"
}
},
{
"type": "Text",

View File

@ -3,8 +3,15 @@
"body": [
{
"type": "Assignment",
"left": "a",
"right": "1"
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Literal",
"value": 1
},
"operator": "="
}
]
}

View File

@ -46,7 +46,18 @@
},
{
"type": "Html",
"argument": "data.name"
"argument": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "data"
},
"property": {
"type": "Identifier",
"name": "name"
},
"computed": false
}
},
{
"type": "Text",
@ -59,9 +70,23 @@
"type": "If",
"test": {
"type": "FunctionCall",
"callee": "notEmpty",
"callee": {
"type": "Identifier",
"name": "notEmpty"
},
"args": [
"data.colors"
{
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "data"
},
"property": {
"type": "Identifier",
"name": "colors"
},
"computed": false
}
]
},
"body": [
@ -80,9 +105,23 @@
"body": [
{
"type": "FunctionCall",
"callee": "forEach",
"callee": {
"type": "Identifier",
"name": "forEach"
},
"args": [
"data.colors",
{
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "data"
},
"property": {
"type": "Identifier",
"name": "colors"
},
"computed": false
},
{
"type": "FunctionDeclaration",
"name": null,
@ -105,7 +144,10 @@
"body": [
{
"type": "Text",
"argument": "color"
"argument": {
"type": "Identifier",
"name": "color"
}
}
]
}

View File

@ -1,3 +1,3 @@
<div for(item in ['red', 'green', 'blue']; separator = ', '; status-var=loop)>
<div for(item in ['red', 'green', 'blue'] | separator=', ' status-var=loop)>
${item} - ${loop.isFirst()} - ${loop.isLast()} - ${loop.getIndex()} - ${loop.getLength()}
</div>

View File

@ -1,3 +1,3 @@
<b for(item in ['red', 'green', 'blue']; separator=', ')>
<b for(item in ['red', 'green', 'blue'] | separator=', ')>
${item}
</b>

View File

@ -0,0 +1 @@
9876543210

View File

@ -0,0 +1,3 @@
<for(i from 9 to 0 step -1)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
9876543210

View File

@ -0,0 +1,3 @@
<for(i from 9 to 0)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
0123

View File

@ -0,0 +1,3 @@
<for(i from ''.length to 'abc'.length)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
02468

View File

@ -0,0 +1,3 @@
<for(i from 0 to 9 step 2)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
97531

View File

@ -0,0 +1,3 @@
<for(i from 9 to 0 step -2)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
0123

View File

@ -0,0 +1,3 @@
<for(i from 0 to 'abc'.length)>
${i}
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -1 +1 @@
0123456789 - 9876543210 - 9876543210 - 02468 - 97531 - 0123 - 0123
0123456789

View File

@ -1,27 +1,3 @@
<for(i from 0 to 9)>
${i}
</for>
-
<for(i from 9 to 0)>
${i}
</for>
-
<for(i from 9 to 0 step -1)>
${i}
</for>
-
<for(i from 0 to 9 step 2)>
${i}
</for>
-
<for(i from 9 to 0 step -2)>
${i}
</for>
-
<for(i from 0 to 'abc'.length)>
${i}
</for>
-
<for(i from ''.length to 'abc'.length)>
${i}
</for>

View File

@ -0,0 +1 @@
<div>0</div><div>1</div><div>2</div>

View File

@ -0,0 +1,5 @@
<for(var i=0; i<3; i++)>
<div>
${i}
</div>
</for>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -1,4 +1,4 @@
<for(item in ['red', 'green', 'blue']) separator=', ' status-var=loop>
<for(item in ['red', 'green', 'blue'] | separator=', ' status-var=loop)>
<div>
${item} - ${loop.isFirst()} - ${loop.isLast()} - ${loop.getIndex()} - ${loop.getLength()}
</div>

View File

@ -1,4 +1,4 @@
<for(item in ['red', 'green', 'blue']) separator=', '>
<for(item in ['red', 'green', 'blue'] | separator=', ') >
<b>
${item}
</b>

View File

@ -1 +1 @@
FRANK 3650
FRANK 3650 []

View File

@ -1,3 +1,3 @@
<var name='Frank' age=10/>
<var name='Frank' age=10 foo/>
${name.toUpperCase()} ${age*365}
${name.toUpperCase()} ${age*365} [${foo}]

View File

@ -0,0 +1,25 @@
'use strict';
var chai = require('chai');
chai.config.includeStack = true;
var parseExpression = require('../compiler/util/parseExpression');
var autotest = require('./autotest');
var fs = require('fs');
var path = require('path');
describe('parseExpression' , function() {
var autoTestDir = path.join(__dirname, 'fixtures/parseExpression/autotest');
autotest.scanDir(
autoTestDir,
function run(dir) {
var inputPath = path.join(dir, 'input.txt');
var input = fs.readFileSync(inputPath, {encoding: 'utf8'});
var parsed = parseExpression(input);
return parsed;
},
{
deepEqual: true,
compareExtension: '.json'
});
});

Some files were not shown because too many files have changed in this diff Show More