Fixes #197 - Better attribute code generation

Use attr helper and handle attribute escaping
Also improved AST and added walking capability
This commit is contained in:
Patrick Steele-Idem 2016-01-07 16:05:26 -07:00
parent c51362e793
commit 8c96302550
93 changed files with 1676 additions and 249 deletions

View File

@ -13,6 +13,7 @@ var ElseIf = require('./ast/ElseIf');
var Else = require('./ast/Else');
var Assignment = require('./ast/Assignment');
var BinaryExpression = require('./ast/BinaryExpression');
var LogicalExpression = require('./ast/LogicalExpression');
var Vars = require('./ast/Vars');
var Return = require('./ast/Return');
var HtmlElement = require('./ast/HtmlElement');
@ -32,6 +33,13 @@ var MemberExpression = require('./ast/MemberExpression');
var Code = require('./ast/Code');
var InvokeMacro = require('./ast/InvokeMacro');
var Macro = require('./ast/Macro');
var ConditionalExpression = require('./ast/ConditionalExpression');
var NewExpression = require('./ast/NewExpression');
var ObjectExpression = require('./ast/ObjectExpression');
var ArrayExpression = require('./ast/ArrayExpression');
var Property = require('./ast/Property');
var VariableDeclarator = require('./ast/VariableDeclarator');
var ThisExpression = require('./ast/ThisExpression');
var parseExpression = require('./util/parseExpression');
@ -46,6 +54,22 @@ function makeNode(arg) {
}
class Builder {
arrayExpression(elements) {
if (elements) {
if (!isArray(elements)) {
elements = [elements];
}
for (var i=0; i<elements.length; i++) {
elements[i] = makeNode(elements[i]);
}
} else {
elements = [];
}
return new ArrayExpression({elements});
}
assignment(left, right, operator) {
if (operator == null) {
operator = '=';
@ -65,6 +89,10 @@ class Builder {
return new Code({value});
}
conditionalExpression(test, consequent, alternate) {
return new ConditionalExpression({test, consequent, alternate});
}
elseStatement(body) {
return new Else({body});
}
@ -190,6 +218,12 @@ class Builder {
return new Literal({value});
}
logicalExpression(left, operator, right) {
left = makeNode(left);
right = makeNode(right);
return new LogicalExpression({left, operator, right});
}
macro(name, params, body) {
return new Macro({name, params, body});
}
@ -209,6 +243,24 @@ class Builder {
return new UnaryExpression({argument, operator, prefix});
}
newExpression(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 = [];
}
return new NewExpression({callee, args});
}
node(type, generateCode) {
if (typeof type === 'function') {
generateCode = arguments[0];
@ -222,10 +274,34 @@ class Builder {
return node;
}
objectExpression(properties) {
if (properties) {
if (!isArray(properties)) {
properties = [properties];
}
for (var i=0; i<properties.length; i++) {
let prop = properties[i];
prop.value = makeNode(prop.value);
}
} else {
properties = [];
}
return new ObjectExpression({properties});
}
program(body) {
return new Program({body});
}
property(key, value) {
key = makeNode(key);
value = makeNode(value);
return new Property({key, value});
}
renderBodyFunction(body) {
let name = 'renderBody';
let params = [new Identifier({name: 'out'})];
@ -278,6 +354,10 @@ class Builder {
return new Text({argument, escape});
}
thisExpression() {
return new ThisExpression();
}
unaryExpression(argument, operator, prefix) {
argument = makeNode(argument);
@ -289,19 +369,53 @@ class Builder {
return new UpdateExpression({argument, operator, prefix});
}
variableDeclarator(id, init) {
if (typeof id === 'string') {
id = new Identifier({name: id});
}
if (init) {
init = makeNode(init);
}
return new VariableDeclarator({id, init});
}
vars(declarations, kind) {
if (declarations) {
if (Array.isArray(declarations)) {
for (let i=0; i<declarations.length; i++) {
var declaration = declarations[i];
if (!declaration) {
throw new Error('Invalid variable declaration');
}
if (typeof declaration === 'string') {
declarations[i] = {
id: makeNode(declaration)
};
declarations[i] = new VariableDeclarator({
id: new Identifier({name: declaration})
});
} else if (declaration instanceof Identifier) {
declarations[i] = {
declarations[i] = new VariableDeclarator({
id: declaration
};
});
} else if (typeof declaration === 'object') {
if (!(declaration instanceof VariableDeclarator)) {
let id = declaration.id;
let init = declaration.init;
if (typeof id === 'string') {
id = new Identifier({name: id});
}
if (!id) {
throw new Error('Invalid variable declaration');
}
if (init) {
init = makeNode(init);
}
declarations[i] = new VariableDeclarator({id, init});
}
}
}
} else if (typeof declarations === 'object') {
@ -309,7 +423,7 @@ class Builder {
declarations = Object.keys(declarations).map((key) => {
let id = new Identifier({name: key});
let init = makeNode(declarations[key]);
return { id, init };
return new VariableDeclarator({ id, init });
});
}
}

View File

@ -342,6 +342,7 @@ class Generator {
}
addWrite(output) {
ok(output, '"output" is required');
if (output instanceof Literal) {
let lastWrite = this._bufferedWrites ?
this._bufferedWrites[this._bufferedWrites.length-1] :

View File

@ -19,6 +19,11 @@ class HtmlJsParser {
onattributeplaceholder(event) {
// placeholder within attribute
if (event.escape) {
event.expression = 'escapeXml(' + event.expression + ')';
} else {
event.expression = 'noEscapeXml(' + event.expression + ')';
}
},
oncdata(event) {

View File

@ -82,7 +82,7 @@ class Parser {
var tagName = el.tagName;
var attributes = el.attributes;
var argument = el.argument; // e.g. For <for(color in colors)>, args will be "color in colors"
var argument = el.argument; // e.g. For <for(color in colors)>, argument will be "color in colors"
if (tagName === 'compiler-options') {
var compilerOptions = this.compilerOptions;

View File

@ -2,41 +2,121 @@
var isArray = Array.isArray;
var Container = require('./ast/Container');
function noop() {}
class Walker {
constructor(options) {
this._visit = options.visit;
this.transform = options.transform !== false;
this._enter = options.enter || noop;
this._exit = options.exit || noop;
this._stopped = false;
this._reset();
this._stack = [];
}
_reset() {
this._skipped = false;
this._replaced = null;
}
skip() {
this._skipped = true;
}
stop() {
this._stopped = true;
}
replace(newNode) {
this._replaced = newNode;
}
_walkArray(array) {
var hasRemoval = false;
array.forEach((node, i) => {
var transformed = this.walk(node);
if (transformed == null) {
array[i] = null;
hasRemoval = true;
} else if (transformed !== node) {
array[i] = transformed;
}
});
if (hasRemoval) {
for (let i=array.length-1; i>=0; i--) {
if (array[i] == null) {
array.splice(i, 1);
}
}
}
return array;
}
_walkContainer(nodes) {
nodes.forEach((node) => {
var transformed = this.walk(node);
if (transformed == null) {
node.container.removeChild(node);
} else if (transformed !== node) {
node.container.replaceChild(transformed, node);
}
});
}
walk(node) {
if (node == null) {
return;
if (!node || this._stopped || typeof node === 'string') {
return node;
}
this._reset();
var parent = this._stack.length ? this._stack[this._stack.length - 1] : undefined;
this._stack.push(node);
var replaced = this._enter(node, parent) || this._replaced;
if (replaced) {
this._stack.pop();
return replaced;
}
if (this._skipped || this._stopped) {
this._stack.pop();
return node;
}
if (isArray(node)) {
let nodes = node;
let len = nodes.length;
for (var i=0; i<len; i++) {
this.walk(nodes[i]);
}
let array = node;
let newArray = this._walkArray(array);
this._stack.pop();
return newArray;
} else if (node instanceof Container) {
let container = node;
if (this.transform) {
container.safeForEach(this.walk, this);
} else {
container.forEach(this.walk, this);
}
this._walkContainer(container);
this._stack.pop();
return container;
} else {
this._visit(node);
if (node.walkChildren) {
node.walkChildren(this);
} else if (node.forEachChild) {
node.forEachChild(this.walk, this);
if (node.walk) {
node.walk(this);
}
}
if (this._stopped) {
this._stack.pop();
return node;
}
this._reset();
replaced = this._exit(node, parent) || this._replaced;
if (replaced) {
this._stack.pop();
return replaced;
}
this._stack.pop();
return node;
}
}

View File

@ -7,28 +7,9 @@ var Container = require('./Container');
class ArrayContainer extends Container {
constructor(node, array) {
super(node);
if (array) {
ok(isArray(array), 'Invalid array');
for (let i=0; i<array.length; i++) {
array[i].container = this;
}
}
this.array = array || [];
this.items = array;
}
// forEach(callback, thisObj) {
// var array = this.array;
//
// for (var i=0; i<array.length; i++) {
// var item = array[i];
// if (item == null) {
// throw new Error('Invalid node in container at index ' + i + '. Array: ' + JSON.stringify(array, null, 2));
// }
// callback.call(thisObj, item, i);
// }
// }
forEach(callback, thisObj) {
var array = this.array.concat([]);
for (var i=0; i<array.length; i++) {
@ -104,6 +85,17 @@ class ArrayContainer extends Container {
get items() {
return this.array;
}
set items(newItems) {
if (newItems) {
ok(isArray(newItems), 'Invalid array');
for (let i=0; i<newItems.length; i++) {
newItems[i].container = this;
}
}
this.array = newItems || [];
}
}
module.exports = ArrayContainer;

View File

@ -0,0 +1,50 @@
'use strict';
var Node = require('./Node');
class ArrayExpression extends Node {
constructor(def) {
super('ArrayExpression');
this.elements = def.elements;
}
generateCode(codegen) {
var elements = this.elements;
if (!elements || !elements.length) {
this.write('[]');
return;
}
codegen.write('[\n');
codegen.incIndent();
elements.forEach((element, i) => {
codegen.writeLineIndent();
codegen.generateCode(element);
if (i < elements.length - 1) {
codegen.write(',\n');
} else {
codegen.write('\n');
}
});
codegen.decIndent();
codegen.writeLineIndent();
codegen.write(']');
}
walk(walker) {
this.elements = walker.walk(this.elements);
}
toJSON() {
return {
type: 'ArrayExpression',
elements: this.elements
};
}
}
module.exports = ArrayExpression;

View File

@ -31,6 +31,11 @@ class Assignment extends Node {
}
}
walk(walker) {
this.left = walker.walk(this.left);
this.right = walker.walk(this.right);
}
isCompoundExpression() {
return true;
}

View File

@ -53,6 +53,11 @@ class BinaryExpression extends Node {
right: this.right
};
}
walk(walker) {
this.left = walker.walk(this.left);
this.right = walker.walk(this.right);
}
}
module.exports = BinaryExpression;

View File

@ -0,0 +1,46 @@
'use strict';
var Node = require('./Node');
class ConditionalExpression extends Node {
constructor(def) {
super('ConditionalExpression');
this.test = def.test;
this.consequent = def.consequent;
this.alternate = def.alternate;
}
generateCode(codegen) {
var test = this.test;
var consequent = this.consequent;
var alternate = this.alternate;
codegen.generateCode(test);
codegen.write(' ? ');
codegen.generateCode(consequent);
codegen.write(' : ');
codegen.generateCode(alternate);
}
isCompoundExpression() {
return true;
}
toJSON() {
return {
type: 'ConditionalExpression',
test: this.test,
consequent: this.consequent,
alternate: this.alternate
};
}
walk(walker) {
this.test = walker.walk(this.test);
this.consequent = walker.walk(this.consequent);
this.alternate = walker.walk(this.alternate);
}
}
module.exports = ConditionalExpression;

View File

@ -20,6 +20,10 @@ class Else extends Node {
codegen.generateBlock(body);
codegen.write('\n');
}
walk(walker) {
this.body = walker.walk(this.body);
}
}
module.exports = Else;

View File

@ -21,6 +21,12 @@ class ElseIf extends Node {
codegen.write('else ');
codegen.generateCode(ifStatement);
}
walk(walker) {
this.test = walker.walk(this.test);
this.body = walker.walk(this.body);
this.else = walker.walk(this.else);
}
}
module.exports = ElseIf;

View File

@ -72,6 +72,15 @@ class ForEach extends Node {
}
}
walk(walker) {
this.varName = walker.walk(this.varName);
this.in = walker.walk(this.in);
this.body = walker.walk(this.body);
this.separator = walker.walk(this.separator);
this.statusVarName = walker.walk(this.statusVarName);
this.iterator = walker.walk(this.iterator);
}
}
module.exports = ForEach;

View File

@ -31,6 +31,13 @@ class ForEachProp extends Node {
]);
}
walk(walker) {
this.nameVarName = walker.walk(this.nameVarName);
this.valueVarName = walker.walk(this.valueVarName);
this.in = walker.walk(this.in);
this.body = walker.walk(this.body);
}
}
module.exports = ForEachProp;

View File

@ -90,7 +90,14 @@ class ForRange extends Node {
body: this.body
})
]);
}
walk(walker) {
this.varName = walker.walk(this.varName);
this.body = walker.walk(this.body);
this.from = walker.walk(this.from);
this.to = walker.walk(this.to);
this.step = walker.walk(this.step);
}
}

View File

@ -41,6 +41,13 @@ class ForStatement extends Node {
codegen.write('\n');
}
walk(walker) {
this.init = walker.walk(this.init);
this.test = walker.walk(this.test);
this.update = walker.walk(this.update);
this.body = walker.walk(this.body);
}
}
module.exports = ForStatement;

View File

@ -37,6 +37,11 @@ class FunctionCall extends Node {
codegen.write(')');
}
walk(walker) {
this.callee = walker.walk(this.callee);
this.args = walker.walk(this.args);
}
}
module.exports = FunctionCall;

View File

@ -61,6 +61,12 @@ class FunctionDeclaration extends Node {
isCompoundExpression() {
return true;
}
walk(walker) {
this.name = walker.walk(this.name);
this.params = walker.walk(this.params);
this.body = walker.walk(this.body);
}
}
module.exports = FunctionDeclaration;

View File

@ -16,6 +16,10 @@ class Html extends Node {
let argument = this.argument;
codegen.addWrite(argument);
}
walk(walker) {
this.argument = walker.walk(this.argument);
}
}
module.exports = Html;

View File

@ -1,14 +1,160 @@
'use strict';
var Node = require('./Node');
var Literal = require('./Literal');
var ok = require('assert').ok;
var escapeXmlAttr = require('raptor-util/escapeXml').attr;
var compiler = require('../');
var parseExpression = require('../util/parseExpression');
class HtmlAttribute {
function isStringLiteral(node) {
return node.type === 'Literal' && typeof node.value === 'string';
}
function isNoEscapeXml(node) {
return node.type === 'FunctionCall' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'noEscapeXml';
}
function isEscapeXml(node) {
return node.type === 'FunctionCall' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'escapeXml';
}
function isStringExpression(node) {
return node.type === 'FunctionCall' && node.callee.type === 'Identifier' &&
(node.callee.name === 'noEscapeXml' || node.callee.name === 'escapeXml');
}
function flattenAttrConcats(node) {
// return [node];
function flattenHelper(node) {
if (node.type === 'BinaryExpression' && node.operator === '+') {
let left = flattenHelper(node.left);
let right = flattenHelper(node.right);
var isString = left.isString || right.isString;
if (isString) {
return {
isString: true,
concats: left.concats.concat(right.concats)
};
} else {
return {
isString: false,
concats: [node]
};
}
}
return {
isString: isStringLiteral(node) || isStringExpression(node),
concats: [node]
};
}
var final = flattenHelper(node);
return final.concats;
}
// function handleEscaping(node) {
//
// function handleEscapingHelper(node, escaping) {
// if (node.type === 'Literal') {
// } else if (isEscapeXml(node)) {
// return handleEscapingHelper(node.arguments[0], true);
// } else if (isNoEscapeXml(node)) {
// return handleEscapingHelper(node.arguments[0], escaping false);
// }
// }
//
// var finalNode = handleEscapingHelper(node, true /* default to escaping */);
// return finalNode;
// }
function removeEscapeFunctions(node) {
var walker = compiler.createWalker({
enter: function(node, parent) {
if (isNoEscapeXml(node) || isEscapeXml(node)) {
return node.args[0];
}
}
});
return walker.walk(node);
}
function generateCodeForExpressionAttr(name, value, codegen) {
var flattenedConcats = flattenAttrConcats(value);
var hasLiteral = false;
for (let i=0; i<flattenedConcats.length; i++) {
if (flattenedConcats[i].type === 'Literal') {
hasLiteral = true;
break;
}
}
if (hasLiteral) {
codegen.addWriteLiteral(' ' + name + '="');
for (let i=0; i<flattenedConcats.length; i++) {
var part = flattenedConcats[i];
if (isStringLiteral(part)) {
part.value = escapeXmlAttr(part.value);
} else if (part.type === 'Literal') {
} else if (isNoEscapeXml(part)) {
part = removeEscapeFunctions(part);
} else {
var escapeXmlAttrVar = codegen.addStaticVar('escapeXmlAttr', '__helpers.xa');
part = removeEscapeFunctions(part);
part = codegen.builder.functionCall(escapeXmlAttrVar, [part]);
}
codegen.addWrite(part);
}
codegen.addWriteLiteral('"');
} else {
// let builder = codegen.builder;
// let valueWithEscaping = handleEscaping(value);
let attrVar = codegen.addStaticVar('attr', '__helpers.a');
var escape = true;
if (isNoEscapeXml(value)) {
escape = false;
}
value = removeEscapeFunctions(value);
let attrArgs = [codegen.builder.literal(name), value];
if (escape === false) {
attrArgs.push(codegen.builder.literal(false));
}
codegen.addWrite(codegen.builder.functionCall(attrVar, attrArgs));
}
}
class HtmlAttribute extends Node {
constructor(def) {
ok(def, 'Invalid attribute definition');
super('HtmlAttribute');
ok(def, 'Invalid attribute definition');
this.type = 'HtmlAttribute';
this.name = def.name.toLowerCase();
this.value = def.value;
if (typeof this.value === 'string') {
this.value = parseExpression(this.value);
}
this.argument = def.argument;
this.def = def.def; // The attribute definition loaded from the taglib (if any)
}
@ -25,6 +171,39 @@ class HtmlAttribute {
return this.isLiteralValue() &&
typeof this.value.value === 'boolean';
}
generateHtmlCode(codegen) {
let name = this.name;
let value = this.value;
let argument = this.argument;
if (this.isLiteralValue()) {
var literalValue = value.value;
if (typeof literalValue === 'boolean') {
if (literalValue === true) {
codegen.addWriteLiteral(' ' + name);
}
} else if (literalValue != null) {
codegen.addWriteLiteral(' ' + name + '="' + escapeXmlAttr(literalValue) + '"');
}
} else if (value != null) {
codegen.isInAttribute = true;
generateCodeForExpressionAttr(name, value, codegen);
codegen.isInAttribute = false;
} else if (argument) {
codegen.addWriteLiteral(' ' + name + '(');
codegen.addWriteLiteral(argument);
codegen.addWriteLiteral(')');
} else {
// Attribute with no value is a boolean attribute
codegen.addWriteLiteral(' ' + name);
}
}
walk(walker) {
this.value = walker.walk(this.value);
}
}
HtmlAttribute.isHtmlAttribute = function(attr) {

View File

@ -7,35 +7,7 @@ var Node = require('./Node');
class HtmlAttributeCollection {
constructor(attributes) {
this.all = [];
this.lookup = {};
if (attributes) {
if (Array.isArray(attributes)) {
attributes.forEach((attr) => {
this.addAttribute(attr);
});
} else {
for (var attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) {
let attrValue = attributes[attrName];
let attrDef;
if (typeof attrValue === 'object' && !(attrValue instanceof Node)) {
attrDef = attrValue;
attrDef.name = attrName;
} else {
attrDef = {
name: attrName,
value: attrValue
};
}
this.addAttribute(attrDef);
}
}
}
}
this.setAttributes(attributes);
}
addAttribute(newAttr) {
@ -135,6 +107,43 @@ class HtmlAttributeCollection {
toString() {
return JSON.stringify(this.all);
}
setAttributes(attributes) {
this.all = [];
this.lookup = {};
if (attributes) {
if (Array.isArray(attributes)) {
attributes.forEach((attr) => {
this.addAttribute(attr);
});
} else {
for (var attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) {
let attrValue = attributes[attrName];
let attrDef;
if (typeof attrValue === 'object' && !(attrValue instanceof Node)) {
attrDef = attrValue;
attrDef.name = attrName;
} else {
attrDef = {
name: attrName,
value: attrValue
};
}
this.addAttribute(attrDef);
}
}
}
}
}
walk(walker) {
var newAttributes = walker.walk(this.all);
this.setAttributes(newAttributes);
}
}
module.exports = HtmlAttributeCollection;

View File

@ -16,6 +16,10 @@ class HtmlComment extends Node {
codegen.addWrite(comment);
codegen.addWrite(literal('-->'));
}
walk(walker) {
this.comment = walker.walk(this.comment);
}
}
module.exports = HtmlComment;

View File

@ -2,7 +2,6 @@
var Node = require('./Node');
var Literal = require('./Literal');
var escapeXmlAttr = require('raptor-util/escapeXml').attr;
var HtmlAttributeCollection = require('./HtmlAttributeCollection');
class StartTag extends Node {
@ -33,31 +32,7 @@ class StartTag extends Node {
if (attributes) {
for (let i=0; i<attributes.length; i++) {
let attr = attributes[i];
let attrName = attr.name;
let attrValue = attr.value;
if (attr.isLiteralValue()) {
var literalValue = attrValue.value;
if (typeof literalValue === 'boolean') {
if (literalValue === true) {
codegen.addWriteLiteral(' ' + attrName);
}
} else if (literalValue != null) {
codegen.addWriteLiteral(' ' + attrName + '="' + escapeXmlAttr(literalValue) + '"');
}
} else if (attrValue) {
codegen.addWriteLiteral(' ' + attrName + '="');
codegen.isInAttribute = true;
// TODO Deal with escaping dynamic HTML attribute expression
codegen.addWrite(attrValue);
codegen.isInAttribute = false;
codegen.addWriteLiteral('"');
} else if (attr.argument) {
codegen.addWriteLiteral(' ' + attrName + '(');
codegen.addWriteLiteral(attr.argument);
codegen.addWriteLiteral(')');
}
codegen.generateCode(attr);
}
}
@ -98,12 +73,13 @@ class HtmlElement extends Node {
this.tagNameExpression = null;
this.setTagName(def.tagName);
this._attributes = def.attributes;
this.body = this.makeContainer(def.body);
this.argument = def.argument;
if (!(this._attributes instanceof HtmlAttributeCollection)) {
this._attributes = new HtmlAttributeCollection(this._attributes);
}
this.body = this.makeContainer(def.body);
this.argument = def.argument;
this.allowSelfClosing = false;
this.startTagOnly = false;
this.dynamicAttributes = undefined;
@ -190,7 +166,6 @@ class HtmlElement extends Node {
codegen.generateCode(body);
codegen.generateCode(endTag);
}
}
}
@ -254,7 +229,8 @@ class HtmlElement extends Node {
} else {
this.tagNameExpression = tagName;
}
} else if (typeof tagName === 'string'){
} else if (typeof tagName === 'string') {
this.tagNameExpression = new Literal({value: tagName});
this.tagName = tagName;
}
}
@ -274,6 +250,12 @@ class HtmlElement extends Node {
setBodyOnlyIf(condition) {
this.bodyOnlyIf = condition;
}
walk(walker) {
this.setTagName(walker.walk(this.tagNameExpression));
this._attributes.walk(walker);
this.body = walker.walk(this.body);
}
}
module.exports = HtmlElement;

View File

@ -77,6 +77,12 @@ class If extends Node {
appendChild(newChild) {
this.body.appendChild(newChild);
}
walk(walker) {
this.test = walker.walk(this.test);
this.body = walker.walk(this.body);
this.else = walker.walk(this.else);
}
}
module.exports = If;

View File

@ -110,6 +110,13 @@ class InvokeMacro extends Node {
return builder.functionCall(builder.identifier(macroDef.functionName), args);
}
walk(walker) {
this.el = walker.walk(this.el);
this.name = walker.walk(this.name);
this.args = walker.walk(this.args);
this.body = walker.walk(this.body);
}
}
module.exports = InvokeMacro;

View File

@ -0,0 +1,63 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
function generateCodeForOperand(node, codegen) {
var wrap = isCompoundExpression(node);
if (wrap) {
codegen.write('(');
}
codegen.generateCode(node);
if (wrap) {
codegen.write(')');
}
}
class LogicalExpression extends Node {
constructor(def) {
super('LogicalExpression');
this.left = def.left;
this.operator = def.operator;
this.right = def.right;
}
generateCode(codegen) {
var left = this.left;
var operator = this.operator;
var right = this.right;
if (!left || !right) {
throw new Error('Invalid LogicalExpression: ' + this);
}
generateCodeForOperand(left, codegen);
codegen.write(' ');
codegen.generateCode(operator);
codegen.write(' ');
generateCodeForOperand(right, codegen);
}
isCompoundExpression() {
return true;
}
toJSON() {
return {
type: 'LogicalExpression',
left: this.left,
operator: this.operator,
right: this.right
};
}
walk(walker) {
this.left = walker.walk(this.left);
this.right = walker.walk(this.right);
}
}
module.exports = LogicalExpression;

View File

@ -29,6 +29,10 @@ class Macro extends Node {
var functionName = macroDef.functionName;
return builder.functionDeclaration(functionName, macroDef.params, body);
}
walk(walker) {
this.body = walker.walk(this.body);
}
}
module.exports = Macro;

View File

@ -35,6 +35,11 @@ class MemberExpression extends Node {
computed: this.computed
};
}
walk(walker) {
this.object = walker.walk(this.object);
this.property = walker.walk(this.property);
}
}
module.exports = MemberExpression;

View File

@ -0,0 +1,70 @@
'use strict';
var Node = require('./Node');
var isCompoundExpression = require('../util/isCompoundExpression');
class NewExpression extends Node {
constructor(def) {
super('NewExpression');
this.callee = def.callee;
this.args = def.args;
}
generateCode(codegen) {
var callee = this.callee;
var args = this.args;
codegen.write('new ');
var wrap = isCompoundExpression(callee);
if (wrap) {
codegen.write('(');
}
codegen.generateCode(callee);
if (wrap) {
codegen.write(')');
}
codegen.write('(');
if (args && args.length) {
for (let i=0, argsLen = args.length; i<argsLen; i++) {
if (i !== 0) {
codegen.write(', ');
}
let arg = args[i];
if (!arg) {
throw new Error('Arg ' + i + ' is not valid for new expression: ' + JSON.stringify(this.toJSON()));
}
codegen.generateCode(arg);
}
}
codegen.write(')');
}
isCompoundExpression() {
return true;
}
toJSON() {
return {
type: 'NewExpression',
callee: this.callee,
args: this.args
};
}
walk(walker) {
this.callee = walker.walk(this.callee);
this.args = walker.walk(this.args);
}
}
module.exports = NewExpression;

View File

@ -83,7 +83,9 @@ class Node {
}
detach() {
this.container.removeChild(this);
if (this.container) {
this.container.removeChild(this);
}
}
/**

View File

@ -0,0 +1,50 @@
'use strict';
var Node = require('./Node');
class ObjectExpression extends Node {
constructor(def) {
super('ObjectExpression');
this.properties = def.properties;
}
generateCode(codegen) {
var properties = this.properties;
if (!properties || !properties.length) {
this.write('{}');
return;
}
codegen.write('{\n');
codegen.incIndent();
properties.forEach((prop, i) => {
codegen.writeLineIndent();
codegen.generateCode(prop);
if (i < properties.length - 1) {
codegen.write(',\n');
} else {
codegen.write('\n');
}
});
codegen.decIndent();
codegen.writeLineIndent();
codegen.write('}');
}
toJSON() {
return {
type: 'ObjectExpression',
properties: this.properties
};
}
walk(walker) {
this.properties = walker.walk(this.properties);
}
}
module.exports = ObjectExpression;

View File

@ -15,6 +15,10 @@ class Program extends Node {
codegen._flushBufferedWrites();
}
}
walk(walker) {
this.body = walker.walk(this.body);
}
}
module.exports = Program;

35
compiler/ast/Property.js Normal file
View File

@ -0,0 +1,35 @@
'use strict';
var Node = require('./Node');
class Property extends Node {
constructor(def) {
super('Property');
this.key = def.key;
this.value = def.value;
}
generateCode(codegen) {
var key = this.key;
var value = this.value;
codegen.generateCode(key);
codegen.write(': ');
codegen.generateCode(value);
}
toJSON() {
return {
type: 'Property',
key: this.key,
value: this.value
};
}
walk(walker) {
this.key = walker.walk(this.key);
this.value = walker.walk(this.value);
}
}
module.exports = Property;

View File

@ -22,6 +22,10 @@ class Return extends Node {
codegen.write('return');
}
}
walk(walker) {
this.argument = walker.walk(this.argument);
}
}
module.exports = Return;

View File

@ -22,6 +22,12 @@ class SelfInvokingFunction extends Node {
codegen.write(')');
}
walk(walker) {
this.params = walker.walk(this.params);
this.args = walker.walk(this.args);
this.body = walker.walk(this.body);
}
}
module.exports = SelfInvokingFunction;

View File

@ -64,6 +64,10 @@ class TemplateRoot extends Node {
body: this.body
};
}
walk(walker) {
this.body = walker.walk(this.body);
}
}
module.exports = TemplateRoot;

View File

@ -0,0 +1,15 @@
'use strict';
var Node = require('./Node');
class ThisExpression extends Node {
constructor(def) {
super('ThisExpression');
}
generateCode(codegen) {
codegen.write('this');
}
}
module.exports = ThisExpression;

View File

@ -53,6 +53,10 @@ class UnaryExpression extends Node {
prefix: this.prefix
};
}
walk(walker) {
this.argument = walker.walk(this.argument);
}
}
module.exports = UnaryExpression;

View File

@ -49,6 +49,10 @@ class UpdateExpression extends Node {
prefix: this.prefix
};
}
walk(walker) {
this.argument = walker.walk(this.argument);
}
}
module.exports = UpdateExpression;

View File

@ -0,0 +1,35 @@
'use strict';
var Node = require('./Node');
var Identifier = require('./Identifier');
class VariableDeclarator extends Node {
constructor(def) {
super('VariableDeclarator');
this.id = def.id;
this.init = def.init;
}
generateCode(codegen) {
var id = this.id;
var init = this.init;
if (!(id instanceof Identifier) && typeof id !== 'string') {
throw new Error('Invalid variable name: ' + id);
}
codegen.generateCode(id);
if (init != null) {
codegen.write(' = ');
codegen.generateCode(init);
}
}
walk(walker) {
this.id = walker.walk(this.id);
this.init = walker.walk(this.init);
}
}
module.exports = VariableDeclarator;

View File

@ -1,7 +1,6 @@
'use strict';
var Node = require('./Node');
var Identifier = require('./Identifier');
class Vars extends Node {
constructor(def) {
@ -29,7 +28,7 @@ class Vars extends Node {
}
for (let i=0; i<declarations.length; i++) {
var declaration = declarations[i];
var declarator = declarations[i];
if (i === 0) {
codegen.write(kind + ' ');
@ -38,26 +37,7 @@ class Vars extends Node {
codegen.writeLineIndent();
}
var varId = declaration.id || declaration.name;
if (!(varId instanceof Identifier) && typeof varId !== 'string') {
throw new Error('Invalid variable name: ' + varId);
}
// TODO Validate the variable name
codegen.generateCode(varId);
var initValue;
if (declaration.hasOwnProperty('init')) {
initValue = declaration.init;
} else if (declaration.hasOwnProperty('value')) {
initValue = declaration.value;
}
if (initValue != null) {
codegen.write(' = ');
codegen.generateCode(initValue);
}
codegen.generateCode(declarator);
if (i !== 0) {
codegen.decIndent(4);
@ -75,6 +55,10 @@ class Vars extends Node {
codegen.generateCode(body);
}
}
walk(walker) {
this.argument = walker.walk(this.argument);
}
}
module.exports = Vars;

View File

@ -7,42 +7,25 @@ var compiler = require('../');
function convert(node) {
var builder = compiler.defaultBuilder;
if (Array.isArray(node)) {
let nodes = node;
for (let i=0; i<nodes.length; i++) {
var converted = convert(nodes[i]);
if (converted == null) {
return null;
}
nodes[i] = converted;
}
return nodes;
}
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) {
case 'ArrayExpression': {
let elements = convert(node.elements);
if (!elements) {
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);
return builder.arrayExpression(elements);
}
case 'AssignmentExpression': {
let left = convert(node.left);
@ -57,6 +40,99 @@ function convert(node) {
return builder.assignment(left, right, node.operator);
}
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 'CallExpression': {
let callee = convert(node.callee);
if (!callee) {
return null;
}
let args = convert(node.arguments);
if (!args) {
return null;
}
return builder.functionCall(callee, args);
}
case 'ConditionalExpression': {
let test = convert(node.test);
if (!test) {
return null;
}
let consequent = convert(node.consequent);
if (!consequent) {
return null;
}
let alternate = convert(node.alternate);
if (!alternate) {
return null;
}
return builder.conditionalExpression(test, consequent, alternate);
}
case 'ExpressionStatement': {
return convert(node.expression);
}
case 'FunctionDeclaration':
case 'FunctionExpression': {
let name = null;
if (node.id) {
name = convert(node.id);
if (name == null) {
return null;
}
}
let params = convert(node.params);
if (!params) {
return null;
}
let body = convert(node.body);
if (!body) {
return null;
}
return builder.functionDeclaration(name, params, body);
}
case 'Identifier': {
return builder.identifier(node.name);
}
case 'Literal': {
return builder.literal(node.value);
}
case 'LogicalExpression': {
let left = convert(node.left);
if (!left) {
return null;
}
let right = convert(node.right);
if (!right) {
return null;
}
return builder.logicalExpression(left, node.operator, right);
}
case 'MemberExpression': {
let object = convert(node.object);
if (!object) {
@ -70,13 +146,62 @@ function convert(node) {
return builder.memberExpression(object, property, node.computed);
}
case 'NewExpression': {
let callee = convert(node.callee);
if (!callee) {
return null;
}
let args = convert(node.arguments);
if (!args) {
return null;
}
return builder.newExpression(callee, args);
}
case 'Program': {
if (node.body && node.body.length === 1) {
return convert(node.body[0]);
}
return null;
}
case 'ObjectExpression': {
let properties = convert(node.properties);
if (!properties) {
return null;
}
return builder.objectExpression(properties);
}
case 'Property': {
let key = convert(node.key);
if (!key) {
return null;
}
let value = convert(node.value);
if (!value) {
return null;
}
return builder.property(key, value);
}
case 'ThisExpression': {
return builder.thisExpression();
}
case 'UnaryExpression': {
let argument = convert(node.argument);
if (!argument) {
return null;
}
return builder.unaryExpression(argument, node.operator, node.prefix);
}
default:
return null;
}
}
function parseExpression(src) {
let jsAST = esprima.parse(src);
let jsAST = esprima.parse('(' + src + ')');
var converted = convert(jsAST);
if (converted == null) {
converted = new Expression({value: src});

View File

@ -63,6 +63,8 @@ if (this.container) {
## methods
### arrayExpression(elements)
### assignment(left, right)
Returns a node that generates the following code:
@ -87,7 +89,7 @@ foo = '123';
Returns a node that generates the following code:
```javascript
<left> <operator> <right>;
<left> <operator> <right>
```
For example:
@ -121,6 +123,26 @@ var b = 2;
b = 3;
```
### conditionalExpression(test, consequent, alternate)
Returns a node that generates the following code:
```javascript
<test> ? <consequent> : <alternate>
```
For example:
```javascript
builder.conditionalExpression(
builder.identifier('isHidden'),
builder.literal('hidden'),
builder.literal('visible'));
// Output code:
isHidden ? "hidden" : "visible"
```
### elseStatement(body)
Returns a node that generates the following code:
@ -499,6 +521,8 @@ var aString = "abc",
]
```
### logicalExpression(left, operator, right)
### macro(name, params, body)
Returns a node that generates a macro function with the given name, params and body content. The `InvokeMacro` node should be used to generate the code to invoke the macro.
@ -520,6 +544,8 @@ builder.negate(builder.identifier('foo'))
!foo
```
### newExpression(callee, args)
### 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.
@ -536,6 +562,8 @@ builder.node(function(node, generator) {
out.w("Hello World!");
```
### objectExpression(properties)
### program(body)
Returns a node to generate the code for the root statements of a JavaScript code.
@ -559,6 +587,8 @@ var name = "Frank";
console.log("Hello", name);
```
### property(key, value)
### require(path)
Returns a node that generates the following code:
@ -701,10 +731,14 @@ a === b
### text(argument, escape)
### thisExpression()
### unaryExpression(argument, operator, prefix)
### updateExpression(argument, operator, prefix)
### variableDeclarator(id, init)
### vars(declarations, kind)
# CodeGenerator

3
test/.gitignore vendored
View File

@ -1 +1,2 @@
/node_modules
/node_modules
/scratch.js

View File

@ -1,8 +1,8 @@
if (true) {
if (!(!data.url)) {
out.w("<a href=\"" +
data.url +
"\">");
out.w("<a" +
attr("href", data.url) +
">");
}
out.w("Hello World");

View File

@ -0,0 +1 @@
isHidden ? "hidden" : "visible"

View File

@ -0,0 +1,8 @@
'use strict';
module.exports = function(builder) {
return builder.conditionalExpression(
builder.identifier('isHidden'),
builder.literal('hidden'),
builder.literal('visible'));
};

View File

@ -0,0 +1,3 @@
out.w("<div class=\"greeting\"" +
attr("foo", bar) +
">Hello World</div>");

View File

@ -0,0 +1,18 @@
'use strict';
module.exports = function(builder) {
return builder.htmlElement(
'div',
[
{
name: 'class',
value: builder.literal('greeting')
},
{
name: 'foo',
value: builder.identifier('bar')
}
],
[
builder.text(builder.literal('Hello World'))
]);
};

View File

@ -0,0 +1 @@
new Foo("Frank", "human")

View File

@ -0,0 +1,10 @@
'use strict';
module.exports = function(builder) {
return builder.newExpression(
builder.identifier('Foo'),
[
builder.literal('Frank'),
builder.literal('human')
]);
};

View File

@ -0,0 +1,25 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x,
attr = __helpers.a,
escapeXmlAttr = __helpers.xa;
return function render(data, out) {
out.w("<div" +
attr("class", data.className) +
attr("class2", data.className, false) +
" foo=\"a" +
escapeXmlAttr(data.foo) +
"b\" bar=\"a " +
escapeXmlAttr(data.foo) +
" b\" baz=\"a " +
data.foo +
" b\" nested=\"a " +
data.foo + ("nested " + data.bar) +
" b\"></div>");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,7 @@
<div class=data.className
class2="$!{data.className}"
foo=('a' + data.foo + 'b')
bar="a ${data.foo} b"
baz="a $!{data.foo} b"
nested="a $!{data.foo + 'nested ${data.bar}'} b">
</div>

View File

@ -0,0 +1,17 @@
{
"type": "ArrayExpression",
"elements": [
{
"type": "Literal",
"value": "a"
},
{
"type": "Literal",
"value": "b"
},
{
"type": "Literal",
"value": 123
}
]
}

View File

@ -0,0 +1 @@
['a', 'b', 123]

View File

@ -11,31 +11,50 @@
"name": "b"
},
"right": {
"type": "BinaryExpression",
"type": "LogicalExpression",
"left": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "a"
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "b"
}
},
"operator": "+",
"operator": ">",
"right": {
"type": "Identifier",
"name": "b"
"type": "Assignment",
"left": {
"type": "Identifier",
"name": "c"
},
"right": {
"type": "Literal",
"value": 2
},
"operator": "+="
}
},
"operator": ">",
"operator": "||",
"right": {
"type": "Assignment",
"left": {
"type": "ConditionalExpression",
"test": {
"type": "Identifier",
"name": "c"
"name": "isHidden"
},
"right": {
"consequent": {
"type": "Literal",
"value": 2
"value": "hidden"
},
"operator": "+="
"alternate": {
"type": "Literal",
"value": "visible"
}
}
},
"operator": "="

View File

@ -1 +1 @@
a = b = (a+b)>(c+=2)
a = b = (a+b)>(c+=2) || (isHidden ? 'hidden' : 'visible')

View File

@ -0,0 +1,13 @@
{
"type": "NewExpression",
"callee": {
"type": "Identifier",
"name": "Foo"
},
"args": [
{
"type": "Literal",
"value": "abc"
}
]
}

View File

@ -0,0 +1 @@
new Foo('abc')

View File

@ -0,0 +1,16 @@
{
"type": "NewExpression",
"callee": {
"type": "LogicalExpression",
"left": {
"type": "Identifier",
"name": "A"
},
"operator": "||",
"right": {
"type": "Identifier",
"name": "B"
}
},
"args": []
}

View File

@ -0,0 +1 @@
new (A || B)()

View File

@ -0,0 +1,8 @@
{
"type": "NewExpression",
"callee": {
"type": "Identifier",
"name": "Foo"
},
"args": []
}

View File

@ -0,0 +1 @@
new Foo()

View File

@ -0,0 +1,27 @@
{
"type": "ObjectExpression",
"properties": [
{
"type": "Property",
"key": {
"type": "Identifier",
"name": "hello"
},
"value": {
"type": "Literal",
"value": "world"
}
},
{
"type": "Property",
"key": {
"type": "Literal",
"value": "class"
},
"value": {
"type": "Literal",
"value": "foo"
}
}
]
}

View File

@ -0,0 +1,4 @@
{
hello: 'world',
"class": "foo"
}

View File

@ -0,0 +1,11 @@
{
"type": "MemberExpression",
"object": {
"type": "ThisExpression"
},
"property": {
"type": "Identifier",
"name": "foo"
},
"computed": false
}

View File

@ -0,0 +1 @@
this.foo

View File

@ -6,6 +6,7 @@
"kind": "var",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "x"
@ -16,6 +17,7 @@
}
},
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "y"
@ -26,6 +28,7 @@
}
},
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "z"
@ -44,6 +47,7 @@
}
},
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "unusedvar"

View File

@ -13,16 +13,61 @@
"kind": "var",
"declarations": [
{
"id": "str",
"init": "__helpers.s"
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "str"
},
"init": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "__helpers"
},
"property": {
"type": "Identifier",
"name": "s"
},
"computed": false
}
},
{
"id": "empty",
"init": "__helpers.e"
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "empty"
},
"init": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "__helpers"
},
"property": {
"type": "Identifier",
"name": "e"
},
"computed": false
}
},
{
"id": "notEmpty",
"init": "__helpers.ne"
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "notEmpty"
},
"init": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "__helpers"
},
"property": {
"type": "Identifier",
"name": "ne"
},
"computed": false
}
}
],
"body": []
@ -95,6 +140,7 @@
"tagName": "ul",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "Literal",
@ -134,6 +180,7 @@
"tagName": "li",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "Literal",

View File

@ -0,0 +1 @@
<select multiple><option value="red">Red</option><option value="green" selected>Green</option><option value="blue">Blue</option></select>

View File

@ -0,0 +1,5 @@
<select multiple>
<option value="red">Red</option>
<option value="green" selected=(data.isGreenSelected)>Green</option>
<option value="blue" selected=(data.isBlueSelected)>Blue</option>
</select>

View File

@ -0,0 +1,4 @@
exports.templateData = {
isGreenSelected: true,
isBlueSelected: false
};

View File

@ -0,0 +1 @@
<select multiple><option value="red">Red</option><option value="green" selected>Green</option><option value="blue">Blue</option></select>

View File

@ -0,0 +1,5 @@
<select multiple>
<option value="red">Red</option>
<option value="green" selected=true>Green</option>
<option value="blue">Blue</option>
</select>

View File

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

View File

@ -1 +1 @@
<div data-encoding="&quot;hello&quot;" style="background-color: #FF0000; &lt;test&gt;" class="my-div">Hello World!</div>
<div data-encoding="&quot;hello&quot;" style="background-color: #FF0000; &lt;test&gt;" class="my-div" checked>Hello World!</div>

View File

@ -1,6 +1,7 @@
exports.templateData = {
"myAttrs": {
"style": "background-color: #FF0000; <test>",
"class": "my-div"
"class": "my-div",
"checked": true
}
};

View File

@ -1 +1 @@
<div class="null">Hello Frank! You are 30 years old.</div><div class="hidden">Hello John! You are 10 years old.</div><div class="hidden">Hello John! You are 10 years old.</div><div class="null">Hello John! You are 10 years old.</div>
<div>Hello Frank! You are 30 years old.</div><div class="hidden">Hello John! You are 10 years old.</div><div class="hidden">Hello John! You are 10 years old.</div><div>Hello John! You are 10 years old.</div>

View File

@ -1,23 +0,0 @@
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x,
forEach = __helpers.f;
return function render(data, out) {
if (notEmpty(data.colors)) {
out.w("<ul class=\"colors\">");
forEach(data.colors, function(color) {
out.w("<li class=\"color\">" +
escapeXml(color) +
"</li>");
});
out.w("</ul>");
}
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,15 @@
{
"suffix": "12",
"node": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "bar"
}
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"suffix": "12",
"node": {
"type": "Identifier",
"name": "foo"
}
}

View File

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

View File

@ -0,0 +1,15 @@
{
"prefix": "12",
"node": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "foo"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "bar"
}
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"prefix": "12",
"node": {
"type": "Identifier",
"name": "foo"
}
}

View File

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

View File

@ -0,0 +1,71 @@
{
"type": "TemplateRoot",
"body": [
{
"type": "If",
"test": "notEmpty(data.colors)",
"body": [
{
"type": "HtmlElement",
"tagName": "ul",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "Literal",
"value": "colors"
}
}
],
"body": [
{
"type": "ForEach",
"varName": {
"type": "Identifier",
"name": "color"
},
"in": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "data"
},
"property": {
"type": "Identifier",
"name": "colors"
},
"computed": false
},
"body": [
{
"type": "HtmlElement",
"tagName": "li",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "Literal",
"value": "color"
}
}
],
"body": [
{
"type": "Text",
"argument": {
"type": "Identifier",
"name": "color"
}
}
]
}
]
}
]
}
]
}
]
}

View File

@ -35,7 +35,7 @@ module.exports = function(compiler) {
]);
let walker = compiler.createWalker({
visit: function(node) {
enter(node) {
if (node.type === 'HtmlElement') {
if (node.hasAttribute('for')) {
node.wrap(forEach('color', 'data.colors'));

View File

@ -0,0 +1,52 @@
{
"type": "TemplateRoot",
"body": [
{
"type": "HtmlElement",
"tagName": "ul",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "data"
},
"property": {
"type": "Identifier",
"name": "foo"
},
"computed": false
}
}
],
"body": [
{
"type": "HtmlElement",
"tagName": "li",
"attributes": [
{
"type": "HtmlAttribute",
"name": "class",
"value": {
"type": "Literal",
"value": "color"
}
}
],
"body": [
{
"type": "Text",
"argument": {
"type": "Identifier",
"name": "color"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,36 @@
'use strict';
module.exports = function(compiler) {
let builder = compiler.createBuilder();
let rootNode = builder.templateRoot([
builder.htmlElement(
'ul',
{
'class': 'escapeXml(data.foo)'
},
[
builder.htmlElement(
'li',
{
'class': builder.literal('color')
},
[
builder.text('color')
])
])
]);
let walker = compiler.createWalker({
enter(node) {
if (node.type === 'FunctionCall' && node.callee.type === 'Identifier' && node.callee.name === 'escapeXml') {
return node.args[0];
}
}
});
walker.walk(rootNode);
return rootNode;
};

View File

@ -1,30 +0,0 @@
'use strict';
var chai = require('chai');
chai.config.includeStack = true;
var path = require('path');
var compiler = require('../compiler');
var autotest = require('./autotest');
var builder = compiler.createBuilder();
var CompileContext = require('../compiler/CompileContext');
var CodeGenerator = require('../compiler/CodeGenerator');
function createGenerator() {
var context = new CompileContext('dummy', 'dummy.marko', builder);
return new CodeGenerator(context);
}
describe('compiler/transform', function() {
var autoTestDir = path.join(__dirname, 'fixtures/transform/autotest');
autotest.scanDir(autoTestDir, function run(dir) {
var getAST = require(path.join(dir, 'index.js'));
var ast = getAST(compiler);
var codeGenerator = createGenerator();
codeGenerator.generateCode(ast);
return codeGenerator.getCode();
});
});

22
test/walker-test.js Normal file
View File

@ -0,0 +1,22 @@
'use strict';
var chai = require('chai');
chai.config.includeStack = true;
var path = require('path');
var compiler = require('../compiler');
var autotest = require('./autotest');
describe('compiler/walker', function() {
var autoTestDir = path.join(__dirname, 'fixtures/walker/autotest');
autotest.scanDir(autoTestDir, function run(dir) {
var getAST = require(path.join(dir, 'index.js'));
return getAST(compiler);
},
{
deepEqual: true,
compareExtension: '.json'
});
});