Marko v3: Improved for loop parsing

This commit is contained in:
Patrick Steele-Idem 2016-02-05 11:51:07 -07:00
parent 7491d23f7c
commit f89f9c548e
27 changed files with 256 additions and 114 deletions

View File

@ -10,7 +10,19 @@ var coreAttrHandlers = [
return false; return false;
} }
var loopNode = createLoopNode(forArgument, null, this.builder); var loopNode;
try {
loopNode = createLoopNode(forArgument, null, this.builder);
} catch(e) {
if (e.code === 'INVALID_FOR') {
this.addError(e.message);
return;
} else {
throw e;
}
}
//Surround the existing node with the newly created loop node //Surround the existing node with the newly created loop node
// NOTE: The loop node will be one of the following: // NOTE: The loop node will be one of the following:

View File

@ -9,12 +9,15 @@ module.exports = function codeGenerator(elNode, codegen) {
var builder = codegen.builder; var builder = codegen.builder;
var loopNode = createLoopNode(argument, elNode.body, builder); try {
var loopNode = createLoopNode(argument, elNode.body, builder);
if (loopNode.error) { return loopNode;
codegen.addError(loopNode.error); } catch(e) {
return elNode; if (e.code === 'INVALID_FOR') {
codegen.addError(e.message);
} else {
throw e;
}
} }
return loopNode;
}; };

View File

@ -3,10 +3,6 @@ var parseFor = require('./parseFor');
function createLoopNode(str, body, builder) { function createLoopNode(str, body, builder) {
var forDef = parseFor(str); var forDef = parseFor(str);
if (forDef.error) {
return forDef;
}
forDef.body = body; forDef.body = body;
if (forDef.loopType === 'ForEach') { if (forDef.loopType === 'ForEach') {

View File

@ -1,8 +1,6 @@
'use strict'; 'use strict';
var Expression = require('../../../compiler/ast/Expression');
var Literal = require('../../../compiler/ast/Literal');
var Identifier = require('../../../compiler/ast/Identifier');
var removeComments = require('../../../compiler/util/removeComments'); var removeComments = require('../../../compiler/util/removeComments');
var compiler = require('../../../compiler');
var integerRegExp = /^-?\d+$/; var integerRegExp = /^-?\d+$/;
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/; var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
@ -62,18 +60,39 @@ var tokenizer = require('../../../compiler/util/tokenizer').create([
} }
]); ]);
function throwError(message) {
var error = new Error(message);
error.code = 'INVALID_FOR';
throw error;
}
function createNumberExpression(str) { function buildIdentifier(name, errorMessage) {
try {
return compiler.builder.identifier(name);
} catch(e) {
throwError(errorMessage + ': ' + e.message);
}
}
function parseExpression(str, errorMessage) {
try {
return compiler.builder.parseExpression(str);
} catch(e) {
throwError(errorMessage + ': ' + e.message);
}
}
function createNumberExpression(str, errorMessage) {
if (str == null) { if (str == null) {
return null; return null;
} }
if (integerRegExp.test(str)) { if (integerRegExp.test(str)) {
return new Literal({value: parseInt(str, 10)}); return compiler.builder.literal(parseInt(str, 10));
} else if (numberRegExp.test(str)) { } else if (numberRegExp.test(str)) {
return new Literal({value: parseFloat(str)}); return compiler.builder.literal(parseFloat(str));
} else { } else {
return new Expression({value: str}); return parseExpression(str, errorMessage);
} }
} }
@ -143,6 +162,12 @@ module.exports = function(str) {
case 'iterator': case 'iterator':
iteratorExpression = part; iteratorExpression = part;
break; break;
case 'pipe':
if (part.length !== 0) {
throwError('Unexpected input: ' + part);
return;
}
break;
} }
} }
@ -190,9 +215,7 @@ module.exports = function(str) {
forTest = str.substring(prevToken.end, token.start); forTest = str.substring(prevToken.end, token.start);
forUpdate = str.substring(token.end); forUpdate = str.substring(token.end);
} else { } else {
return { throwError('Invalid native for loop. Expected format: <init>; <test>; <update>');
error: 'Invalid native for loop. Expected format: <init>; <test>; <update>'
};
} }
prevToken = token; prevToken = token;
@ -229,42 +252,46 @@ module.exports = function(str) {
finishPrevPart(str.length); finishPrevPart(str.length);
if (loopType === 'ForEach') { if (loopType === 'ForEach') {
var nameValue = varName.split(','); var nameValue = varName.split(/\s*,\s*/);
if (nameValue.length === 2) { if (nameValue.length === 2) {
nameVarName = new Identifier({name: nameValue[0]}); nameVarName = buildIdentifier(nameValue[0], 'Invalid name variable');
valueVarName = new Identifier({name: nameValue[1]}); valueVarName = buildIdentifier(nameValue[1], 'Invalid value variable');
varName = null;
loopType = 'ForEachProp'; loopType = 'ForEachProp';
} }
} }
if (inExpression) { if (inExpression) {
inExpression = new Expression({value: inExpression}); inExpression = parseExpression(inExpression, 'Invalid "in" expression');
} }
if (separatorExpression) { if (separatorExpression) {
separatorExpression = new Expression({value: separatorExpression}); separatorExpression = parseExpression(separatorExpression, 'Invalid "separator" expression');
} }
if (iteratorExpression) { if (iteratorExpression) {
iteratorExpression = new Expression({value: iteratorExpression}); iteratorExpression = parseExpression(iteratorExpression, 'Invalid "iterator" expression');
} }
if (fromExpression) { if (fromExpression) {
fromExpression = createNumberExpression(fromExpression); fromExpression = createNumberExpression(fromExpression, 'Invalid "from" expression');
} }
if (toExpression) { if (toExpression) {
toExpression = createNumberExpression(toExpression); toExpression = createNumberExpression(toExpression, 'Invalid "to" expression');
} }
if (stepExpression) { if (stepExpression) {
stepExpression = createNumberExpression(stepExpression); stepExpression = createNumberExpression(stepExpression, 'Invalid "step" expression');
}
if (varName != null) {
varName = buildIdentifier(varName, 'Invalid variable name');
} }
varName = new Identifier({name: varName});
if (statusVarName) { if (statusVarName) {
statusVarName = new Identifier({name: statusVarName}); statusVarName = buildIdentifier(statusVarName, 'Invalid status-var option');
} }
// No more tokens... now we need to sort out what happened // No more tokens... now we need to sort out what happened
@ -294,9 +321,7 @@ module.exports = function(str) {
}; };
} else if (loopType === 'For') { } else if (loopType === 'For') {
if (forTest == null) { if (forTest == null) {
return { throwError('Invalid native for loop. Expected format: <init>; <test>; <update>');
error: 'Invalid native for loop. Expected format: <init>; <test>; <update>'
};
} }
return { return {
'loopType': loopType, 'loopType': loopType,
@ -305,8 +330,6 @@ module.exports = function(str) {
'update': forUpdate 'update': forUpdate
}; };
} else { } else {
return { throwError('Invalid for loop');
'error': 'Invalid for loop'
};
} }
}; };

View File

@ -5,7 +5,7 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
} }
} }

View File

@ -5,19 +5,54 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "BinaryExpression",
"value": "(a in test | 0)" "left": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "a"
},
"operator": "in",
"right": {
"type": "Identifier",
"name": "test"
}
},
"operator": "|",
"right": {
"type": "Literal",
"value": 0
}
}, },
"separator": { "separator": {
"type": "Expression", "type": "ConditionalExpression",
"value": "(iterator==1 ? 'foo' : 'bar')" "test": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "iterator"
},
"operator": "==",
"right": {
"type": "Literal",
"value": 1
}
},
"consequent": {
"type": "Literal",
"value": "foo"
},
"alternate": {
"type": "Literal",
"value": "bar"
}
}, },
"statusVarName": { "statusVarName": {
"type": "Identifier", "type": "Identifier",
"name": "loop" "name": "loop"
}, },
"iterator": { "iterator": {
"type": "Expression", "type": "Identifier",
"value": "my Iterator" "name": "myIterator"
} }
} }

View File

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

View File

@ -0,0 +1,3 @@
{
"error": "Unexpected input: foo"
}

View File

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

View File

@ -0,0 +1,3 @@
{
"error": "Invalid status-var option: Invalid JavaScript identifier: test foo"
}

View File

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

View File

@ -5,11 +5,11 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
}, },
"iterator": { "iterator": {
"type": "Expression", "type": "Identifier",
"value": "myIterator" "name": "myIterator"
} }
} }

View File

@ -5,7 +5,40 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "ArrayExpression",
"value": "[(({a} in test | 0))]" "elements": [
{
"type": "BinaryExpression",
"left": {
"type": "BinaryExpression",
"left": {
"type": "ObjectExpression",
"properties": [
{
"type": "Property",
"key": {
"type": "Identifier",
"name": "a"
},
"value": {
"type": "Identifier",
"name": "a"
}
}
]
},
"operator": "in",
"right": {
"type": "Identifier",
"name": "test"
}
},
"operator": "|",
"right": {
"type": "Literal",
"value": 0
}
}
]
} }
} }

View File

@ -5,7 +5,7 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
} }
} }

View File

@ -5,19 +5,19 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
}, },
"separator": { "separator": {
"type": "Expression", "type": "Literal",
"value": "','" "value": ","
}, },
"statusVarName": { "statusVarName": {
"type": "Identifier", "type": "Identifier",
"name": "loop" "name": "loop"
}, },
"iterator": { "iterator": {
"type": "Expression", "type": "Identifier",
"value": "myIterator" "name": "myIterator"
} }
} }

View File

@ -5,12 +5,12 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
}, },
"separator": { "separator": {
"type": "Expression", "type": "Literal",
"value": "','" "value": ","
}, },
"statusVarName": { "statusVarName": {
"type": "Identifier", "type": "Identifier",

View File

@ -5,8 +5,8 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Identifier",
"value": "colors" "name": "colors"
}, },
"statusVarName": { "statusVarName": {
"type": "Identifier", "type": "Identifier",

View File

@ -5,7 +5,7 @@
"name": "color" "name": "color"
}, },
"in": { "in": {
"type": "Expression", "type": "Literal",
"value": "' | separator=\",\";'" "value": " | separator=\",\";"
} }
} }

View File

@ -0,0 +1,38 @@
{
"loopType": "ForEachProp",
"nameVarName": {
"type": "Identifier",
"name": "name"
},
"valueVarName": {
"type": "Identifier",
"name": "value"
},
"in": {
"type": "ObjectExpression",
"properties": [
{
"type": "Property",
"key": {
"type": "Literal",
"value": "foo"
},
"value": {
"type": "Literal",
"value": "low"
}
},
{
"type": "Property",
"key": {
"type": "Literal",
"value": "bar"
},
"value": {
"type": "Literal",
"value": "high"
}
}
]
}
}

View File

@ -0,0 +1 @@
name, value in {'foo': 'low', 'bar': 'high'}

View File

@ -9,7 +9,15 @@
"value": 0 "value": 0
}, },
"to": { "to": {
"type": "Expression", "type": "MemberExpression",
"value": "'abc'.length" "object": {
"type": "Literal",
"value": "abc"
},
"property": {
"type": "Identifier",
"name": "length"
},
"computed": false
} }
} }

View File

@ -1,17 +0,0 @@
<for>
Missing each attribute
</for>
<for each="item">
</for>
<for each="item in items" invalid="true">
Invalid attribute
</for>
<for each="item in items" separator="${;">
Invalid separator
</for>
<div for="item in items; invalid=true">
</div>

View File

@ -1,22 +0,0 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.options = {
handleCompileError: function(e) {
expect(e.toString()).to.contain('template.marko:8:0');
expect(e.toString()).to.contain('does not support attribute "invalid"');
expect(e.toString()).to.contain('template.marko:12:0');
expect(e.toString()).to.contain('Invalid attribute value of "${;"');
expect(e.toString()).to.contain('template.marko:1:0');
expect(e.toString()).to.contain('"each" attribute is required');
expect(e.toString()).to.contain('template.marko:5:0');
expect(e.toString()).to.contain('Invalid each attribute of "item"');
expect(e.toString()).to.contain('template.marko:16:0');
expect(e.toString()).to.contain('Invalid for attribute of "item in items; invalid=true"');
}
};

View File

@ -0,0 +1 @@
<div>red</div><div>green</div><div>blue</div>

View File

@ -0,0 +1,5 @@
<for(item in ['red', 'green', 'blue'] | foo)>
<div>
${item}
</div>
</for>

View File

@ -0,0 +1,7 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.checkError = function(err) {
expect(err.toString()).to.contain('template.marko:1:0] Unexpected input: foo');
};

View File

@ -13,10 +13,21 @@ describe('parseFor' , function() {
autotest.scanDir( autotest.scanDir(
autoTestDir, autoTestDir,
function run(dir) { function run(dir) {
var inputPath = path.join(dir, 'input.txt'); let inputPath = path.join(dir, 'input.txt');
var input = fs.readFileSync(inputPath, {encoding: 'utf8'}); let input = fs.readFileSync(inputPath, {encoding: 'utf8'});
var parsed = parseFor(input); try {
return parsed; let parsed = parseFor(input);
return parsed;
} catch(e) {
if (e.code === 'INVALID_FOR') {
return {
error: e.message
};
} else {
throw e;
}
}
}, },
{ {
deepEqual: true, deepEqual: true,