Restored most of the looping functionality

This commit is contained in:
Patrick Steele-Idem 2015-12-03 16:07:11 -07:00
parent 6ecb5b32dd
commit c78076f877
296 changed files with 475 additions and 145 deletions

View File

@ -6,6 +6,7 @@ var Literal = require('./ast/Literal');
var Identifier = require('./ast/Identifier');
var ok = require('assert').ok;
var Container = require('./ast/Container');
var util = require('util');
class Slot {
constructor(generator) {
@ -112,7 +113,7 @@ class Generator {
if (!generateCodeFunc) {
throw new Error('Missing generator method for node of type "' +
node.type +
'". Node: ' + node);
'". Node: ' + util.inspect(node));
}
}

View File

@ -7,51 +7,7 @@ var charProps = require('char-props');
var deresolve = require('./util/deresolve');
var UniqueVars = require('./util/UniqueVars');
var PosInfo = require('./util/PosInfo');
class CompileError {
constructor(errorInfo, context) {
this.context = context;
this.node = errorInfo.node;
this.message = errorInfo.message;
this.code = errorInfo.code;
var pos = errorInfo.pos;
var endPos = errorInfo.endPos;
if (pos == null) {
pos = this.node && this.node.pos;
}
if (endPos == null) {
endPos = this.node && this.node.endPos;
}
if (pos != null) {
pos = context.getPosInfo(pos);
}
if (endPos != null) {
endPos = context.getPosInfo(endPos);
}
this.pos = pos;
this.endPos = endPos;
}
toString() {
var pos = this.pos;
if (pos) {
pos = '[' + pos + '] ';
} else {
pos = '';
}
var str = pos + this.message;
if (this.node) {
str += ' (' + this.node.toString() + ')';
}
return str;
}
}
var CompileError = require('./CompileError');
class CompileContext {
constructor(src, filename, builder) {

48
compiler/CompileError.js Normal file
View File

@ -0,0 +1,48 @@
'use strict';
class CompileError {
constructor(errorInfo, context) {
this.context = context;
this.node = errorInfo.node;
this.message = errorInfo.message;
this.code = errorInfo.code;
var pos = errorInfo.pos;
var endPos = errorInfo.endPos;
if (pos == null) {
pos = this.node && this.node.pos;
}
if (endPos == null) {
endPos = this.node && this.node.endPos;
}
if (pos != null) {
pos = context.getPosInfo(pos);
}
if (endPos != null) {
endPos = context.getPosInfo(endPos);
}
this.pos = pos;
this.endPos = endPos;
}
toString() {
var pos = this.pos;
if (pos) {
pos = '[' + pos + '] ';
} else {
pos = '';
}
var str = pos + this.message;
if (this.node) {
str += ' (' + this.node.toString() + ')';
}
return str;
}
}
module.exports = CompileError;

View File

@ -159,6 +159,8 @@ class Parser {
node.pos = el.pos;
var foundAttrs = {};
// Validate the attributes
attributes.forEach((attr) => {
let attrName = attr.name;
@ -175,8 +177,36 @@ class Parser {
}
return;
}
attr.def = attrDef;
foundAttrs[attrName] = true;
});
if (tagDef) {
// Add default values for any attributes. If an attribute has a declared
// default value and the attribute was not found on the element
// then add the attribute with the specified default value
tagDef.forEachAttribute(function (attrDef) {
var attrName = attrDef.name;
if (attrDef.hasOwnProperty('defaultValue') && !foundAttrs.hasOwnProperty(attrName)) {
attributes.push({
name: attrName,
value: builder.literal(attrDef.defaultValue)
});
} else if (attrDef.required === true) {
// TODO Only throw an error if there is no data argument provided (just HTML attributes)
if (!foundAttrs.hasOwnProperty(attrName)) {
context.addError({
node: node,
message: 'The "' + attrName + '" attribute is required for tag "' + tagName + '" in taglib "' + getTaglibPath(tagDef.taglibId) + '".'
});
}
}
});
}
this.parentNode.appendChild(node);
this.stack.push({
@ -196,6 +226,8 @@ class Parser {
}
handleComment(comment) {
this.prevTextNode = null;
var builder = this.context.builder;
var compilerOptions = this.compilerOptions;
@ -209,6 +241,8 @@ class Parser {
}
handleBodyTextPlaceholder(expression, escape) {
this.prevTextNode = null;
var builder = this.context.builder;
var textOutput = builder.textOutput(expression, escape);

View File

@ -13,16 +13,52 @@ function removeExt(filename) {
}
}
function buildInputProps(node, builder) {
function buildInputProps(node, context) {
var inputProps = {};
node.forEachAttribute((attr) => {
var attrName = attr.name;
var propName = removeDashes(attrName);
inputProps[propName] = attr.value;
var attrDef = attr.def || context.taglibLookup.getAttribute(node.tagName, attr.name);
var propName;
var parentPropName;
if (attrDef.dynamicAttribute) {
// Dynamic attributes are allowed attributes
// that are not declared (i.e. "*" attributes)
//
if (attrDef.preserveName === false) {
propName = removeDashes(attrName);
} else {
propName = attrName;
}
if (attrDef.targetProperty) {
parentPropName = attrDef.targetProperty;
}
} else {
// Attributes map to properties and we allow the taglib
// author to control how an attribute name resolves
// to a property name.
if (attrDef.targetProperty) {
propName = attrDef.targetProperty;
} else if (attrDef.preserveName) {
propName = attr.name;
} else {
propName = removeDashes(attr.name);
}
}
if (parentPropName) {
let parent = inputProps[parentPropName] = (inputProps[parentPropName] = {});
parent[propName] = attr.value;
} else {
inputProps[propName] = attr.value;
}
});
return builder.literal(inputProps);
return context.builder.literal(inputProps);
}
class CustomTag extends HtmlElement {
@ -48,7 +84,7 @@ class CustomTag extends HtmlElement {
let loadRendererFunctionCall = builder.functionCall(loadRendererVar, [ requireRendererFunctionCall ]);
let rendererVar = generator.addStaticVar(removeExt(rendererPath), loadRendererFunctionCall);
var inputProps = buildInputProps(this, builder);
var inputProps = buildInputProps(this, context);
var tagArgs = [ 'out', rendererVar, inputProps ];
var tagFunctionCall = builder.functionCall(tagVar, tagArgs);
return tagFunctionCall;

View File

@ -8,6 +8,11 @@ class ForEach extends Node {
this.varName = def.varName;
this.target = def.target;
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, '"target" is required');
@ -16,15 +21,76 @@ class ForEach extends Node {
generateCode(generator) {
var varName = this.varName;
var target = this.target;
var separator = this.separator;
var statusVarName = this.statusVarName;
var builder = generator.builder;
generator.addStaticVar('forEach', '__helpers.f');
if (this.from) {
// 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;
}
// template.statement('(function() {').indent(function () {
// template.statement('for (var ' + nameVar + '=' + from + '; ' + nameVar + comparison + to + '; ' + step + ') {').indent(function () {
// this.generateCodeForChildren(template);
// }, this).line('}');
// }, this).line('}());');
return;
}
if (separator && !statusVarName) {
statusVarName = '__loop';
}
if (statusVarName) {
let forEachVarName = generator.addStaticVar('forEachWithStatusVar', '__helpers.fv');
let body = this.body;
if (separator) {
body = body.items.concat([
builder.ifStatement('!' + statusVarName + '.isLast()', [
builder.textOutput(separator)
])
]);
}
return builder.functionCall(forEachVarName, [
target,
builder.functionDeclaration(null, [varName, statusVarName], body)
]);
} else {
let forEachVarName = generator.addStaticVar('forEach', '__helpers.f');
return builder.functionCall(forEachVarName, [
target,
builder.functionDeclaration(null, [varName], this.body)
]);
}
return builder.functionCall('forEach', [
target,
builder.functionDeclaration(null, [varName], this.body)
]);
}
}

View File

@ -5,10 +5,11 @@ var ok = require('assert').ok;
class HtmlAttribute {
constructor(def) {
ok(def, 'Invalid attribute definition');
this.name = def.name.toLowerCase();
this.value = def.value;
this.argument = def.argument;
this.def = def.def; // The attribute definition loaded from the taglib (if any)
}
isLiteralValue() {

View File

@ -111,6 +111,17 @@ class HtmlElement extends Node {
this._dynamicAttributesExpressionArray.push(expression);
}
getAttribute(name) {
return this._attributes != null && this._attributes.getAttribute(name);
}
getAttributeValue(name) {
var attr = this._attributes != null && this._attributes.getAttribute(name);
if (attr) {
return attr.value;
}
}
removeAttribute(name) {
if (this._attributes) {
this._attributes.removeAttribute(name);

View File

@ -2,7 +2,6 @@
var Node = require('./Node');
var Literal = require('./Literal');
var ok = require('assert').ok;
function trim(textOutputNode) {
var text = textOutputNode.argument.value;
@ -86,8 +85,6 @@ class TextOutput extends Node {
}
if (curChild.type === 'TextOutput' && curChild.isLiteral()) {
if (currentTextLiteral) {
currentTextLiteral.argument.value += curChild.argument.value;
curChild.detach();
@ -111,6 +108,8 @@ class TextOutput extends Node {
}
literalTextNodes.forEach(trim);
}
isWhitespace() {

View File

@ -1,12 +1,10 @@
'use strict';
var Builder = require('./Builder');
var CodeGenerator = require('./CodeGenerator');
var Compiler = require('./Compiler');
var Walker = require('./Walker');
var Parser = require('./Parser');
var HtmlJsParser = require('./HtmlJsParser');
var CompileContext = require('./CompileContext');
var defaultBuilder = new Builder();
var defaultParser = new Parser(new HtmlJsParser());

View File

@ -0,0 +1,10 @@
var reservedWords = require('./javaScriptReservedWords');
var varNameRegExp = /^[$A-Z_][0-9A-Z_$]*$/i;
module.exports = function isValidJavaScriptVarName(varName) {
if (reservedWords[varName]) {
return false;
}
return varNameRegExp.test(varName);
};

View File

@ -0,0 +1,65 @@
module.exports = {
'abstract': true,
'arguments': true,
'boolean': true,
'break': true,
'byte': true,
'case': true,
'catch': true,
'char': true,
'class': true,
'const': true,
'continue': true,
'debugger': true,
'default': true,
'delete': true,
'do': true,
'double': true,
'else': true,
'enum*': true,
'eval': true,
'export': true,
'extends': true,
'false': true,
'final': true,
'finally': true,
'float': true,
'for': true,
'function': true,
'goto': true,
'if': true,
'implements': true,
'import': true,
'in': true,
'instanceof': true,
'int': true,
'interface': true,
'let': true,
'long': true,
'native': true,
'new': true,
'null': true,
'package': true,
'private': true,
'protected': true,
'public': true,
'return': true,
'short': true,
'static': true,
'super': true,
'switch': true,
'synchronized': true,
'this': true,
'throw': true,
'throws': true,
'transient': true,
'true': true,
'try': true,
'typeof': true,
'var': true,
'void': true,
'volatile': true,
'while': true,
'with': true,
'yield': true
};

View File

@ -1,70 +1,71 @@
{
"name": "marko",
"description": "Marko is an extensible, streaming, asynchronous, high performance, HTML-based templating language that can be used in Node.js or in the browser.",
"keywords": [
"templating",
"template",
"async",
"streaming"
],
"repository": {
"type": "git",
"url": "https://github.com/marko-js/marko.git"
},
"scripts": {
"init-tests": "./test/init-tests.sh",
"test": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test && node_modules/.bin/jshint compiler/ runtime/ taglibs/",
"test-fast": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-test",
"test-async": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test",
"test-taglib-loader": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/taglib-loader-test",
"jshint": "node_modules/.bin/jshint compiler/ runtime/ taglibs/"
},
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": [
"Patrick Steele-Idem <pnidem@gmail.com>"
],
"dependencies": {
"app-module-path": "^1.0.0",
"async-writer": "^1.4.0",
"browser-refresh-client": "^1.0.0",
"char-props": "~0.1.5",
"events": "^1.0.2",
"jsonminify": "^0.2.3",
"minimatch": "^0.2.14",
"property-handlers": "^1.0.0",
"raptor-args": "^1.0.0",
"raptor-json": "^1.0.1",
"raptor-logging": "^1.0.1",
"raptor-modules": "^1.0.5",
"raptor-polyfill": "^1.0.0",
"raptor-promises": "^1.0.1",
"raptor-regexp": "^1.0.0",
"raptor-strings": "^1.0.0",
"raptor-util": "^1.0.0",
"resolve-from": "^1.0.0",
"try-require": "^1.2.1"
},
"devDependencies": {
"bluebird": "^2.9.30",
"chai": "^3.3.0",
"jshint": "^2.5.0",
"mocha": "^2.3.3",
"through": "^2.3.4"
},
"license": "Apache-2.0",
"bin": {
"markoc": "bin/markoc"
},
"main": "runtime/marko-runtime.js",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"browser": {
"./node-require.js": "./node-require-browser.js"
},
"homepage": "http://markojs.com/",
"version": "3.0.0-beta.1",
"logo": {
"url": "https://raw.githubusercontent.com/marko-js/branding/master/marko-logo-small.png"
}
"name": "marko",
"description": "Marko is an extensible, streaming, asynchronous, high performance, HTML-based templating language that can be used in Node.js or in the browser.",
"keywords": [
"templating",
"template",
"async",
"streaming"
],
"repository": {
"type": "git",
"url": "https://github.com/marko-js/marko.git"
},
"scripts": {
"init-tests": "./test/init-tests.sh",
"test": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test && node_modules/.bin/jshint compiler/ runtime/ taglibs/",
"test-fast": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-test",
"test-async": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test",
"test-taglib-loader": "npm run init-tests && node_modules/.bin/mocha --ui bdd --reporter spec ./test/taglib-loader-test",
"jshint": "node_modules/.bin/jshint compiler/ runtime/ taglibs/"
},
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": [
"Patrick Steele-Idem <pnidem@gmail.com>"
],
"dependencies": {
"app-module-path": "^1.0.0",
"async-writer": "^1.4.0",
"browser-refresh-client": "^1.0.0",
"char-props": "~0.1.5",
"esprima": "^2.7.0",
"events": "^1.0.2",
"jsonminify": "^0.2.3",
"minimatch": "^0.2.14",
"property-handlers": "^1.0.0",
"raptor-args": "^1.0.0",
"raptor-json": "^1.0.1",
"raptor-logging": "^1.0.1",
"raptor-modules": "^1.0.5",
"raptor-polyfill": "^1.0.0",
"raptor-promises": "^1.0.1",
"raptor-regexp": "^1.0.0",
"raptor-strings": "^1.0.0",
"raptor-util": "^1.0.0",
"resolve-from": "^1.0.0",
"try-require": "^1.2.1"
},
"devDependencies": {
"bluebird": "^2.9.30",
"chai": "^3.3.0",
"jshint": "^2.5.0",
"mocha": "^2.3.3",
"through": "^2.3.4"
},
"license": "Apache-2.0",
"bin": {
"markoc": "bin/markoc"
},
"main": "runtime/marko-runtime.js",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"browser": {
"./node-require.js": "./node-require-browser.js"
},
"homepage": "http://markojs.com/",
"version": "3.0.0-beta.1",
"logo": {
"url": "https://raw.githubusercontent.com/marko-js/branding/master/marko-logo-small.png"
}
}

View File

@ -3,6 +3,7 @@
var extend = require('raptor-util/extend');
var parseComplexAttribute = require('./util/parseComplexAttribute');
var parseForEach = require('./util/parseForEach');
var parseJavaScriptIdentifier = require('./util/parseJavaScriptIdentifier');
var coreAttrHandlers = [
[
@ -13,8 +14,8 @@ var coreAttrHandlers = [
}
var forEachProps = parseComplexAttribute(forArgument, {
each: true,
separator: true,
'each': true,
'separator': true,
'iterator': true,
'status-var': true,
'for-loop': true
@ -31,11 +32,26 @@ var coreAttrHandlers = [
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
@ -108,6 +124,7 @@ var coreAttrHandlers = [
class AttributeTransformer {
constructor(context, el) {
this.context = context;
this.builder = context.builder;
this.el = el;
}
@ -131,8 +148,11 @@ class AttributeTransformer {
return node;
}
addError(error) {
this.compiler.addError(this.el, error);
addError(message) {
this.context.addError({
node: this.el,
message: message
});
}
}

View File

@ -4,9 +4,19 @@ exports.generateCode = function(generator) {
var argument = this.argument;
if (!argument) {
generator.addError('Invalid <for> tag. Argument is missing. Example; <for(color in colors)>');
return;
}
var forEachProps = parseForEach(argument);
forEachProps.body = this.body;
if (this.hasAttribute('separator')) {
forEachProps.separator = this.getAttributeValue('separator');
}
if (this.hasAttribute('status-var')) {
forEachProps.statusVarName = this.getAttributeValue('status-var');
}
return generator.builder.forEach(forEachProps);
};

View File

@ -0,0 +1,23 @@
var esprima = require('esprima');
function parseJavaScriptIdentifier(src, requireQuotes) {
var program = esprima.parse(src);
if (program.body.length === 1) {
var expressionStatement = program.body[0];
if (expressionStatement.type === 'ExpressionStatement') {
var expression = expressionStatement.expression;
if (expression.type === 'Literal') {
var value = expression.value;
if (typeof value === 'string') {
return value;
}
} else if (requireQuotes !== true && expression.type === 'Identifier') {
return expression.name;
}
}
}
return undefined;
}
module.exports = parseJavaScriptIdentifier;

View File

@ -3,7 +3,18 @@
"renderer": "./taglib/test-declared-attributes/renderer.js",
"@name": "string"
},
"<test-target-property>": {
"renderer": "./taglib/test-target-property/renderer.js",
"@name-foo": {
"target-property": "name"
}
},
"<test-invalid-attr>": {
"@foo": "string"
},
"<test-missing-required-attr>": {
"@name": {
"required": true
}
}
}

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