From 28a851723a4beb2c4a5c4eb260d27561f848d498 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 11 Dec 2015 15:48:35 -0700 Subject: [PATCH] More compiler tests, docs and improvements --- compiler/Builder.js | 155 ++++----- compiler/Compiler.js | 3 +- compiler/ast/HtmlElement.js | 3 +- compiler/ast/Node.js | 5 + compiler/ast/UpdateExpression.js | 39 +++ docs/compile-time-tags.md | 84 ++++- docs/compiler-advanced.md | 231 +++++++------ docs/compiler-api.md | 312 ++++++++++++++++++ taglibs/core/for-tag.js | 6 +- taglibs/core/marko-taglib.json | 2 +- test/.gitignore | 1 + .../autotest-pending/forEachProps/expected.js | 15 + .../autotest-pending/forEachProps/index.js | 22 ++ .../codegen/autotest/forEachRange/expected.js | 16 + .../codegen/autotest/forEachRange/index.js | 20 ++ .../codegen/autotest/forStatement/expected.js | 14 + .../codegen/autotest/forStatement/index.js | 22 ++ .../autotest/marko-template/expected.json | 37 +++ .../expected.html | 1 + .../template.marko | 2 + .../tag-code-generator-return-tree/test.js | 3 + .../render/autotest/var-tag/expected.html | 1 + .../render/autotest/var-tag/template.marko | 3 + test/fixtures/render/autotest/var-tag/test.js | 1 + .../code-generator.js | 13 + .../marko-tag.json | 3 + 26 files changed, 819 insertions(+), 195 deletions(-) create mode 100644 compiler/ast/UpdateExpression.js create mode 100644 docs/compiler-api.md create mode 100644 test/.gitignore create mode 100644 test/fixtures/codegen/autotest-pending/forEachProps/expected.js create mode 100644 test/fixtures/codegen/autotest-pending/forEachProps/index.js create mode 100644 test/fixtures/codegen/autotest/forEachRange/expected.js create mode 100644 test/fixtures/codegen/autotest/forEachRange/index.js create mode 100644 test/fixtures/codegen/autotest/forStatement/expected.js create mode 100644 test/fixtures/codegen/autotest/forStatement/index.js create mode 100644 test/fixtures/render/autotest/tag-code-generator-return-tree/expected.html create mode 100644 test/fixtures/render/autotest/tag-code-generator-return-tree/template.marko create mode 100644 test/fixtures/render/autotest/tag-code-generator-return-tree/test.js create mode 100644 test/fixtures/render/autotest/var-tag/expected.html create mode 100644 test/fixtures/render/autotest/var-tag/template.marko create mode 100644 test/fixtures/render/autotest/var-tag/test.js create mode 100644 test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/code-generator.js create mode 100644 test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/marko-tag.json diff --git a/compiler/Builder.js b/compiler/Builder.js index 10c73bdc3..bb5edea21 100644 --- a/compiler/Builder.js +++ b/compiler/Builder.js @@ -24,22 +24,43 @@ 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'); class Builder { - program(body) { - return new Program({body}); + assignment(left, right) { + return new Assignment({left, right}); } - node(type) { - return new Node(type); + binaryExpression(left, operator, right) { + return new BinaryExpression({left, operator, right}); } - templateRoot(body) { - return new TemplateRoot({body}); + elseStatement(body) { + return new Else({body}); } - functionDeclaration(name, params, body) { - return new FunctionDeclaration({name, params, body}); + elseIfStatement(test, body, elseStatement) { + return new ElseIf({ + if: new If({test, body, else: elseStatement}) + }); + } + + forEach(varName, target, body) { + if (typeof varName === 'object') { + var options = varName; + return new ForEach(options); + } else { + return new ForEach({varName, target, body}); + } + } + + forStatement(init, test, update, body) { + if (typeof init === 'object' && !init.type) { + var def = arguments[0]; + return new ForStatement(def); + } else { + return new ForStatement({init, test, update, body}); + } } functionCall(callee, args) { @@ -54,53 +75,16 @@ class Builder { return new FunctionCall({callee, args}); } - selfInvokingFunction(params, args, body) { - if (arguments.length === 1) { - body = arguments[0]; - params = null; - args = null; - } - - return new SelfInvokingFunction({params, args, body}); + functionDeclaration(name, params, body) { + return new FunctionDeclaration({name, params, body}); } - literal(value) { - return new Literal({value}); + html(argument) { + return new Html({argument}); } - identifier(name) { - return new Identifier({name}); - } - - ifStatement(test, body, elseStatement) { - return new If({test, body, else: elseStatement}); - } - - elseIfStatement(test, body, elseStatement) { - return new ElseIf({ - if: new If({test, body, else: elseStatement}) - }); - } - - elseStatement(body) { - return new Else({body}); - } - - assignment(left, right) { - return new Assignment({left, right}); - } - - strictEquality(left, right) { - var operator = '==='; - return new BinaryExpression({left, right, operator}); - } - - vars(declarations, kind) { - return new Vars({declarations, kind}); - } - - returnStatement(argument) { - return new Return({argument}); + htmlComment(comment) { + return new HtmlComment({comment}); } htmlElement(tagName, attributes, body, argument) { @@ -115,29 +99,24 @@ class Builder { return new HtmlElement({tagName, attributes, body, argument}); } - html(argument) { - return new Html({argument}); + identifier(name) { + return new Identifier({name}); } - text(argument, escape) { - return new Text({argument, escape}); + ifStatement(test, body, elseStatement) { + return new If({test, body, else: elseStatement}); } - htmlComment(comment) { - return new HtmlComment({comment}); + literal(value) { + return new Literal({value}); } - forEach(varName, target, body) { - if (typeof varName === 'object') { - var options = varName; - return new ForEach(options); - } else { - return new ForEach({varName, target, body}); - } + node(type) { + return new Node(type); } - slot() { - return new Slot(); + program(body) { + return new Program({body}); } require(path) { @@ -146,17 +125,43 @@ class Builder { return new FunctionCall({callee, args}); } - forStatement(init, test, update, body) { - if (typeof init === 'object' && !init.type) { - var def = arguments[0]; - return new ForStatement(def); - } else { - return new ForStatement({init, test, update, body}); - } + returnStatement(argument) { + return new Return({argument}); } - binaryExpression(left, operator, right) { - return new BinaryExpression({left, operator, right}); + selfInvokingFunction(params, args, body) { + if (arguments.length === 1) { + body = arguments[0]; + params = null; + args = null; + } + + return new SelfInvokingFunction({params, args, body}); + } + + slot() { + return new Slot(); + } + + strictEquality(left, right) { + var operator = '==='; + return new BinaryExpression({left, right, operator}); + } + + templateRoot(body) { + return new TemplateRoot({body}); + } + + text(argument, escape) { + return new Text({argument, escape}); + } + + updateExpression(argument, operator, prefix) { + return new UpdateExpression({argument, operator, prefix}); + } + + vars(declarations, kind) { + return new Vars({declarations, kind}); } } diff --git a/compiler/Compiler.js b/compiler/Compiler.js index 6520d89b5..cf0ce817b 100644 --- a/compiler/Compiler.js +++ b/compiler/Compiler.js @@ -76,9 +76,8 @@ class Compiler { var context = new CompileContext(src, filename, this.builder); var ast = this.parser.parse(src, context); + // console.log('ROOT', JSON.stringify(ast, null, 2)); - console.log('ROOT', JSON.stringify(ast, null, 2)); - var transformedAST = transformTree(ast, context); // console.log('transformedAST', JSON.stringify(ast, null, 2)); diff --git a/compiler/ast/HtmlElement.js b/compiler/ast/HtmlElement.js index a2b6fcc11..b399d23f7 100644 --- a/compiler/ast/HtmlElement.js +++ b/compiler/ast/HtmlElement.js @@ -162,7 +162,8 @@ class HtmlElement extends Node { type: this.type, tagName: this.tagName, attributes: this._attributes, - argument: this.argument + argument: this.argument, + body: this.body }; } } diff --git a/compiler/ast/Node.js b/compiler/ast/Node.js index afdee76e5..e4ab64769 100644 --- a/compiler/ast/Node.js +++ b/compiler/ast/Node.js @@ -24,6 +24,11 @@ class Node { wrapperNode.appendChild(this); } + /** + * Converts the provided `array` into a `ArrayContainer`. If the provided `array` is already an instance of a `Container` then it is simply returned. + * @param {[type]} array [description] + * @return {[type]} [description] + */ makeContainer(array) { if (array instanceof Container) { return array; diff --git a/compiler/ast/UpdateExpression.js b/compiler/ast/UpdateExpression.js new file mode 100644 index 000000000..1a44dfddc --- /dev/null +++ b/compiler/ast/UpdateExpression.js @@ -0,0 +1,39 @@ +'use strict'; + +var Node = require('./Node'); + +class UpdateExpression extends Node { + constructor(def) { + super('UpdateExpression'); + 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.generateCode(operator); + } + + generator.generateCode(argument); + + if (!prefix) { + generator.generateCode(operator); + } + } + + toJSON() { + return { + type: 'UpdateExpression', + argument: this.argument, + operator: this.operator, + prefix: this.prefix + }; + } +} + +module.exports = UpdateExpression; \ No newline at end of file diff --git a/docs/compile-time-tags.md b/docs/compile-time-tags.md index efbd2ba5d..7ff84b3c8 100644 --- a/docs/compile-time-tags.md +++ b/docs/compile-time-tags.md @@ -15,52 +15,104 @@ Let's take a look at how to create a compile-time tag by providing our own code } ``` -A code generator module should export a function with the following signature: +A code generator module should export a function as shown below: ```javascript -function generateCode(elNode, generator) +module.exports = function generateCode(elNode, generator) { + // ... +} ``` -Continuing with the ``, let's assume we have the following template: +The `elNode` argument will be an instance of [`HtmlElement`](../compiler/ast/HtmlElement.js). The `generator` argument will be an instance of [`CodeGenerator`](../compiler/CodeGenerator.js). + +Continuing with the `` example, let's assume we have the following template: ```xml ``` -Let's implement the greeting tag that generates code by return an array of output AST nodes. +Let's implement the greeting tag by generating code that will output `Hello Frank` using `console.log`: ```javascript module.exports = function generateCode(elNode, generator) { var builder = generator.builder; - var nameValue = elNode.getAttributeValue('name'); - return builder.functionCall('console.log', nameValue); + return builder.functionCall('console.log', [ + builder.literal('Hello'), + elNode.getAttributeValue('name') + ]); }; ``` The above code results in the following compiled code: ```javascript -out.w("Hello FrankHello " + - escapeXml(data.name)); +console.log("Hello", data.name); ``` +In the example code above, the `generateCode(elNode, generator)` method returns a new [`FunctionCall`](../compiler/ast/FunctionCall.js) node using the provided [`Builder`](../compiler/Builder.js) instance. The builder provides methods for generating nodes associated with JavaScript primitives. This is the recommended way to produce JavaScript code since it ensures that code is generated correctly and with proper formatting. However, for illustration purposes, the following also _works_ (just don't do it): + +```javascript +module.exports = function generateCode(elNode, generator) { + var nameValue = elNode.getAttributeValue('name'); + generator.write('console.log('); + generator.write('"Hello"'); + generator.write(', '); + generator.generateCode(nameValue); + generator.write(')'); +}; +``` + +Writing to standard out using `console.log()` is probably _not_ what you want to do as a custom tag developer. You typically want to produce HTML output. Continuing with the same example, let's update our custom tag implementation to generate code that produces the following HTML output: + +```html +
Hello Frank
+``` + +To do that we will utilize the builder API to generate the appropriate tree of nodes: ```javascript module.exports = function generateCode(elNode, generator) { var builder = generator.builder; - return [ - builder.text(builder.literal('Hello Frank')), - builder.text(elNode.getAttributeValue('name')) - ]; + + return builder.htmlElement( + 'div', + { + 'class': builder.literal('greeting') + }, + [ + builder.text(builder.literal('Hello ')), + builder.text(elNode.getAttributeValue('name')) + ]); }; ``` +For the following template: -So when should you a custom compile-time tag and when should use use a compile-time transformer? +```xml + +``` -Utilize custom compile-time tags when you need to create new custom tags that are capable of generating JavaScript code at compile-time. Examples of custom compile-time tags: +The code generated by the custom tag will be the following: -- +```javascript +out.w("
Hello Frank
"); +``` -Compile-time transformers are useful for mod \ No newline at end of file +For the following template, where the name is a non-literal JavaScript expression: + +```xml + +``` + +The code generated by the custom tag will be the following: + +```javascript +out.w("
Hello " + + escapeXml(data.name) + + "
"); +``` + +# The Builder API + +The Builder API plays a crucial role in generating code for a tag. The builder provides the building blocks for generating potentially complex JavaScript code. diff --git a/docs/compiler-advanced.md b/docs/compiler-advanced.md index cce79803f..0f188032d 100644 --- a/docs/compiler-advanced.md +++ b/docs/compiler-advanced.md @@ -1,28 +1,27 @@ Compiler Advanced ==================== -Marko allows developers to control how templates generate JavaScript code at compile-time. Developers can create custom compile-time tags and it is also possible to transform the intermediate parse tree by adding, removing, modifying or rearranging nodes at compilation time. +The Marko compiler is responsible for taking an input Marko template and producing an output JavaScript program. The Marko compiler was designed to be flexible and to allow developers to control how JavaScript code is produced. -# Overview +# Compiler stages -The three primary stages of the Marko compiler are parse, transform, generate. Each of these stages is described in more detail below: +The three primary stages of the Marko compiler are parse, transform, generate: + +- __parse__ - Parse the template source to produce an [Abstract Syntax Tree (AST)](https//en.wikipedia.org/wiki/Abstract_syntax_tree). +- __transform__ - Transform the AST (add/remove/modify/rearrange nodes) +- __generate__ - Generate compiled JavaScript code based on the final AST + +Each of these stages is described in more detail in the sections below. ## Parse stage -The first stage of the Marko compiler takes the source template string and produces an Abstract Syntax Tree (AST). +The first stage of the Marko compiler takes the source template string and produces an initial AST. For example, given the following input template: ```xml -Hello ${data.name}! - -
    -
  • - ${color} -
  • -
-
- No colors! +
+ Hello ${data.name}!
``` @@ -32,47 +31,33 @@ The following AST will be produced: { "type": "TemplateRoot", "body": [ - { - "type": "Text", - "argument": { - "type": "Literal", - "value": "Hello " - } - }, - { - "type": "Text", - "argument": "data.name" - }, - { - "type": "Text", - "argument": { - "type": "Literal", - "value": "!\n\n" - } - }, - { - "type": "HtmlElement", - "tagName": "ul", - "attributes": [ - { - "name": "if", - "argument": "notEmpty(data.colors)" - } - ] - }, - { - "type": "Text", - "argument": { - "type": "Literal", - "value": "\n" - } - }, { "type": "HtmlElement", "tagName": "div", "attributes": [ { - "name": "else" + "name": "if", + "argument": "data.name" + } + ], + "body": [ + { + "type": "Text", + "argument": { + "type": "Literal", + "value": "\n Hello " + } + }, + { + "type": "Text", + "argument": "data.name" + }, + { + "type": "Text", + "argument": { + "type": "Literal", + "value": "!\n" + } } ] } @@ -80,61 +65,113 @@ The following AST will be produced: } ``` +---------- -# Creating a compile-time tag +_TIP:_ If you want a pretty dump of an AST tree for debugging purposes you can use the following code: -Let's take a look at how to create a compile-time tag by providing our own code generator. The first step is to register the custom tag in a `marko-taglib.json` file. We will use the `code-generator` property to associate the custom tag with a function that will be used to generate the code for the element node at compile-time: +```javascript +console.log(JSON.stringify(astNode, null ,2)); +``` -```json -{ - "": { - "code-generator": "./greeting-tag" - } +---------- + +## Transform stage + +During the transform stage, the AST is manipulated in order to produce the correct compiled code during the generate stage. For example, AST transformers are used to process special attributes such as the following: + +- `if()` +- `else-if()` +- `else` +- `for()` +- etc. + +In the case of the `if()` attribute, the node is transformed by a builtin Marko transformer (specifically, [taglibs/core/core-transformer.js](../taglibs/core/core-transformer.js)) in order to be wrapped with an actual `If` node using code similar to the following: + +```javascript +var ifAttr = elNode.getAttribute('if'); +if (ifAttr && ifAttr.argument) { + // Remove the if() attribute from the HTML element node + elNode.removeAttribute('if'); + + // Create a new "If" node using the provided "builder" + // (described later) + var ifNode = builder.ifStatement(ifArgument); + + //Surround the existing node with an "If" node + node.wrap(ifNode); } ``` -The code generator module should export a function with the following signature: +Continuing with the previous example, after the transformation stage, the AST will be the following: + +```json +{ + "type": "TemplateRoot", + "body": [ + { + "type": "If", + "test": "data.name", + "body": [ + { + "type": "HtmlElement", + "tagName": "div", + "attributes": [], + "body": [ + { + "type": "Text", + "argument": { + "type": "Literal", + "value": "\n Hello " + } + }, + { + "type": "Text", + "argument": "data.name" + }, + { + "type": "Text", + "argument": { + "type": "Literal", + "value": "!\n" + } + } + ] + } + ] + } + ] +} +``` + +You'll notice in the transformed AST that the `HtmlElement` associated with the `
` tag was wrapped with a new `If` node. After the AST has been transformed it is now time to generate the compiled JavaScript code. + +During the transform stage, the entire AST might be walked multiple times. Not until there are no more nodes transformed does the transform stage complete. + +## Generate stage + +The generate stage is the final stage of the Marko compiler. During the generate stage the Marko compiler will walk the tree to produce the final JavaScript code. Each node in the tree will have an opportunity to generate JavaScript code. The Marko compiler provides a [`CodeGenerator`](../compiler/CodeGenerator.js) class and an API for generating fragments of JavaScript code that makes it easy to produce well-formed and readable JavaScript code as output. + +Every node in the tree must implement one of the following methods: + +- `generateCode(generator)` +- `generateCode(generator)` (e.g. `generateHtmlCode(generator)`) + +The `generator` argument will be an instance of [`CodeGenerator`](../compiler/CodeGenerator.js). + +The Marko compiler supports compiling templates differently based on an "output type". Currently, the only supported output type is "Html". With the "Html" output type, the compiled template will be a program that, when executed, will produce an HTML string as output. In the future we may support other output types such as DOM, Virtual DOM, incremental DOM, etc. For example, with the "DOM" output type, the compiled program could use the web browser's DOM API to produce a DOM tree as output (instead of an HTML string). + +Below is the fragment of code used by the `If` node to generate the output JavaScript code: ```javascript -function generateCode(elNode, generator) : Node +generator.write('if ('); +generator.generateCode(test); +generator.write(') '); +generator.generateBlock(body); +if (elseStatement) { + generator.write(' '); + generator.generateCode(elseStatement); +} else { + generator.write('\n'); +} ``` -Continuing with the ``, let's assume we have the following template: - -```xml - -``` - -Let's implement the greeting tag that generates code that uses `console.log` to output `Hello ` as shown below: - -```javascript -module.exports = function generateCode(elNode, generator) { - var builder = generator.builder; - return [ - builder.text(builder.literal('Hello Frank')), - builder.text(elNode.getAttributeValue('name')) - ]; -}; -``` - - - -```javascript -module.exports = function generateCode(elNode, generator) { - var builder = generator.builder; - return [ - builder.text(builder.literal('Hello Frank')), - builder.text(elNode.getAttributeValue('name')) - ]; -}; -``` - - -So when should you a custom compile-time tag and when should use use a compile-time transformer? - -Utilize custom compile-time tags when you need to create new custom tags that are capable of generating JavaScript code at compile-time. Examples of custom compile-time tags: - -- - -Compile-time transformers are useful for mod - diff --git a/docs/compiler-api.md b/docs/compiler-api.md new file mode 100644 index 000000000..cefd92bcd --- /dev/null +++ b/docs/compiler-api.md @@ -0,0 +1,312 @@ +The Compiler API +================ + +# AST + +## Node + +The `Node` type is the base class for all AST nodes. All AST nodes added to the AST must extend `Node`. + +### Methods + +#### wrap(wrapperNode) + +Makes the current node a child of the provided `wrapperNode`. Similar to the following: + +```javascript +this.container.replaceChild(wrapperNode, this); +wrapperNode.appendChild(this); +``` + +#### makeContainer(array) + +Converts the provided `array` into a `ArrayContainer`. If the provided `array` is already an instance of a `Container` then it is simply returned. + + +#### appendChild(node) + +Appends a child node to the associated container for the node. The `this.body` property is used as the default container, but this method can be overridden in derived nodes. + +### Properties + +#### type + +The node type as a String. Example node types: `"TemplateRoot"`, `"HtmlElement"`, `"Text"`, `"If"`, `"Else"`, `"ForEach"`, etc. + +#### container + +If a `Node` is the child of another `Node` then it will be associated with a `Container`. For example: + +```javascript +if (this.container) { + var parentNode = this.container.node; + + // NOTE: The following lines produce the same result: + this.container.removeChild(this) + this.detach() +} else { + // Either the node is the root node or it is detached from the AST +} +``` + +## Container + +## TemplateRoot + +## HtmlElement + +## Text + +## JavaScript node types + +# Builder + +## methods + +### assignment(left, right) + +Returns a node that generates the following code: + +```javascript + = ; +``` + +For example: + +```javascript +builder.assignment( + builder.identifier('foo'), + builder.literal('123')); + +// Output code: +foo = '123'; +``` + +### binaryExpression(left, operator, right) + +Returns a node that generates the following code: + +```javascript + ; +``` + +For example: + +```javascript +builder.binaryExpression( + builder.identifier('foo'), + '<' + builder.literal(99)); + +// Output code: +foo < 99; +``` + +### elseStatement(body) + +Returns a node that generates the following code: + +```javascript +else { + +} +``` + +For example: + +```javascript +builder.elseStatement([ + builder.functionCall('console.log', ['hello']), + builder.functionCall('console.log', ['world']) +]); + +// Output code: +else { + console.log('hello'); + console.log('world'); +} +``` + +### elseIfStatement(test, body, elseStatement) + +Returns a node that generates the following code: + +```javascript +else if () { + +}[ ] +``` + +For example: + +```javascript +builder.elseIfStatement( + builder.literal(true), + [ + builder.functionCall('console.log', ['hello']), + builder.functionCall('console.log', ['world']) + ]); + +// Output code: +else if (true) { + console.log('hello'); + console.log('world'); +} +``` + +### forEach(def) + +Returns a node that generates code to loop over an array, object properties or a range. + +___array:___ + +```javascript +builder.forEach({ + varName: 'color', + target: 'colors' + body: [ + builder.functionCall('console.log', [ + builder.identifier('color') + ]) + ] +}) + + +// Output code: +var forEach = __helpers.f; // Static variable + +// ... + +forEach(data.colors, function(color) { + out.w(escapeXml(color)); +}); +``` + +___object properties:___ + +TBD + +___range:___ + +```javascript +builder.forEach({ + varName: 'i', + from: 0, + to: 'myArray.length', + step: 2, + body: [ + builder.functionCall('console.log', ['hello']) + ] +}) + +// Output code: +(function() { + for (var i = 0; i<=myArray.length; i+=2) { + console.log(i); + } +}()); +``` + +### forEach(varName, target, body) + +Returns a node that generates a simple `forEach`. See `forEach(def)`. + +### forStatement(init, test, update, body) + +Returns a node that generates the following code: + +```javascript +for (; ; ) { + +} +``` + +For example: + +```javascript +builder.forStatement( + builder.vars([ + { + id: 'i', + init: builder.literal(0) + } + ]), + builder.binaryExpression('i', '<', builder.literal(0)), + builder.updateExpression('i', '++'), + [ + builder.functionCall('console.log', [ + builder.identifier('i') + ]) + ] +) + +// Output: +for (var i = 0; i < 0; i++) { + console.log(i); +} +``` + +### functionCall(callee, args) + +### functionDeclaration(name, params, body) + +### html(argument) + +### htmlComment(comment) + +### htmlElement(tagName, attributes, body, argument) + +### identifier(name) + +### ifStatement(test, body, elseStatement) + +Returns a node that generates the following code: + +```javascript +if () { + +}[ ] +``` + +For example: + +```javascript +builder.ifStatement( + builder.literal(true), + [ + builder.functionCall('console.log', ['hello']), + builder.functionCall('console.log', ['world']) + ]); + +// Output code: +if (true) { + console.log('hello'); + console.log('world'); +} +``` + +### literal(value) + +### node(type) + +### program(body) + +### require(path) + +### returnStatement(argument) + +### selfInvokingFunction(params, args, body) + +### slot() + +### strictEquality(left, right) + +### templateRoot(body) + +### text(argument, escape) + +### updateExpression(argument, operator, prefix) + +### vars(declarations, kind) + +# CodeGenerator \ No newline at end of file diff --git a/taglibs/core/for-tag.js b/taglibs/core/for-tag.js index 2f264c24e..d0b28862e 100644 --- a/taglibs/core/for-tag.js +++ b/taglibs/core/for-tag.js @@ -1,9 +1,9 @@ var parseForEach = require('./util/parseForEach'); -module.exports = function nodeFactory(elNode, context) { +module.exports = function codeGenerator(elNode, generator) { var argument = elNode.argument; if (!argument) { - context.addError(elNode, 'Invalid tag. Argument is missing. Example; '); + generator.addError('Invalid tag. Argument is missing. Example; '); return elNode; } @@ -18,5 +18,5 @@ module.exports = function nodeFactory(elNode, context) { forEachProps.statusVarName = elNode.getAttributeValue('status-var'); } - return context.builder.forEach(forEachProps); + return generator.builder.forEach(forEachProps); }; \ No newline at end of file diff --git a/taglibs/core/marko-taglib.json b/taglibs/core/marko-taglib.json index 5a2385f7f..0020fc8a0 100644 --- a/taglibs/core/marko-taglib.json +++ b/taglibs/core/marko-taglib.json @@ -1,6 +1,6 @@ { "": { - "node-factory": "./for-tag" + "code-generator": "./for-tag" }, "": { "node-factory": "./if-tag" diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..30bc16279 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/test/fixtures/codegen/autotest-pending/forEachProps/expected.js b/test/fixtures/codegen/autotest-pending/forEachProps/expected.js new file mode 100644 index 000000000..5e355b234 --- /dev/null +++ b/test/fixtures/codegen/autotest-pending/forEachProps/expected.js @@ -0,0 +1,15 @@ +function create(__helpers) { + var str = __helpers.s, + empty = __helpers.e, + notEmpty = __helpers.ne, + escapeXml = __helpers.x, + forEach = __helpers.f; + + return function render(data, out) { + forEach(data.colors, function(color) { + out.w(escapeXml(color)); + }); + }; +} + +(module.exports = require("marko").c(__filename)).c(create); diff --git a/test/fixtures/codegen/autotest-pending/forEachProps/index.js b/test/fixtures/codegen/autotest-pending/forEachProps/index.js new file mode 100644 index 000000000..ee1e3dd32 --- /dev/null +++ b/test/fixtures/codegen/autotest-pending/forEachProps/index.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = function(builder) { + var templateRoot = builder.templateRoot; + var forEach = builder.forEach; + + return templateRoot([ + forEach({ + nameVarName: 'k', + valueVarName: 'v', + target: 'myArray', + body: [ + builder.functionCall('console.log', [ + builder.literal('k:'), + builder.identifier('k'), + builder.literal('v:'), + builder.identifier('v') + ]) + ] + }) + ]); +}; \ No newline at end of file diff --git a/test/fixtures/codegen/autotest/forEachRange/expected.js b/test/fixtures/codegen/autotest/forEachRange/expected.js new file mode 100644 index 000000000..0848bfc13 --- /dev/null +++ b/test/fixtures/codegen/autotest/forEachRange/expected.js @@ -0,0 +1,16 @@ +function create(__helpers) { + var str = __helpers.s, + empty = __helpers.e, + notEmpty = __helpers.ne, + escapeXml = __helpers.x; + + return function render(data, out) { + (function() { + for (var i = 0; i<=myArray.length; i+=2) { + console.log(i); + } + }()); + }; +} + +(module.exports = require("marko").c(__filename)).c(create); diff --git a/test/fixtures/codegen/autotest/forEachRange/index.js b/test/fixtures/codegen/autotest/forEachRange/index.js new file mode 100644 index 000000000..557f416dc --- /dev/null +++ b/test/fixtures/codegen/autotest/forEachRange/index.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = function(builder) { + var templateRoot = builder.templateRoot; + var forEach = builder.forEach; + + return templateRoot([ + forEach({ + varName: 'i', + from: 0, + to: 'myArray.length', + step: 2, + body: [ + builder.functionCall('console.log', [ + builder.identifier('i') + ]) + ] + }) + ]); +}; \ No newline at end of file diff --git a/test/fixtures/codegen/autotest/forStatement/expected.js b/test/fixtures/codegen/autotest/forStatement/expected.js new file mode 100644 index 000000000..17567a7e4 --- /dev/null +++ b/test/fixtures/codegen/autotest/forStatement/expected.js @@ -0,0 +1,14 @@ +function create(__helpers) { + var str = __helpers.s, + empty = __helpers.e, + notEmpty = __helpers.ne, + escapeXml = __helpers.x; + + return function render(data, out) { + for (var i = 0; i < 0; i++) { + console.log(i); + } + }; +} + +(module.exports = require("marko").c(__filename)).c(create); diff --git a/test/fixtures/codegen/autotest/forStatement/index.js b/test/fixtures/codegen/autotest/forStatement/index.js new file mode 100644 index 000000000..342ec5412 --- /dev/null +++ b/test/fixtures/codegen/autotest/forStatement/index.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = function(builder) { + + return builder.templateRoot([ + builder.forStatement( + builder.vars([ + { + id: 'i', + init: builder.literal(0) + } + ]), + builder.binaryExpression('i', '<', builder.literal(0)), + builder.updateExpression('i', '++'), + [ + builder.functionCall('console.log', [ + builder.identifier('i') + ]) + ] + ) + ]); +}; \ No newline at end of file diff --git a/test/fixtures/pretty-print/autotest/marko-template/expected.json b/test/fixtures/pretty-print/autotest/marko-template/expected.json index 7af622195..ea0de91b6 100644 --- a/test/fixtures/pretty-print/autotest/marko-template/expected.json +++ b/test/fixtures/pretty-print/autotest/marko-template/expected.json @@ -75,6 +75,43 @@ "value": "colors" } } + ], + "body": [ + { + "type": "FunctionCall", + "callee": "forEach", + "args": [ + "data.colors", + { + "type": "FunctionDeclaration", + "name": null, + "params": [ + "color" + ], + "body": [ + { + "type": "HtmlElement", + "tagName": "li", + "attributes": [ + { + "name": "class", + "value": { + "type": "Literal", + "value": "color" + } + } + ], + "body": [ + { + "type": "Text", + "argument": "color" + } + ] + } + ] + } + ] + } ] } ] diff --git a/test/fixtures/render/autotest/tag-code-generator-return-tree/expected.html b/test/fixtures/render/autotest/tag-code-generator-return-tree/expected.html new file mode 100644 index 000000000..5589abd97 --- /dev/null +++ b/test/fixtures/render/autotest/tag-code-generator-return-tree/expected.html @@ -0,0 +1 @@ +
Hello Frank
Hello John
\ No newline at end of file diff --git a/test/fixtures/render/autotest/tag-code-generator-return-tree/template.marko b/test/fixtures/render/autotest/tag-code-generator-return-tree/template.marko new file mode 100644 index 000000000..ee156107f --- /dev/null +++ b/test/fixtures/render/autotest/tag-code-generator-return-tree/template.marko @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/fixtures/render/autotest/tag-code-generator-return-tree/test.js b/test/fixtures/render/autotest/tag-code-generator-return-tree/test.js new file mode 100644 index 000000000..fd8ffe152 --- /dev/null +++ b/test/fixtures/render/autotest/tag-code-generator-return-tree/test.js @@ -0,0 +1,3 @@ +exports.templateData = { + name: 'John' +}; diff --git a/test/fixtures/render/autotest/var-tag/expected.html b/test/fixtures/render/autotest/var-tag/expected.html new file mode 100644 index 000000000..7ef5e2f97 --- /dev/null +++ b/test/fixtures/render/autotest/var-tag/expected.html @@ -0,0 +1 @@ +FRANK 3650 \ No newline at end of file diff --git a/test/fixtures/render/autotest/var-tag/template.marko b/test/fixtures/render/autotest/var-tag/template.marko new file mode 100644 index 000000000..8029abb9c --- /dev/null +++ b/test/fixtures/render/autotest/var-tag/template.marko @@ -0,0 +1,3 @@ + + +${name.toUpperCase()} ${age*365} \ No newline at end of file diff --git a/test/fixtures/render/autotest/var-tag/test.js b/test/fixtures/render/autotest/var-tag/test.js new file mode 100644 index 000000000..c4013b344 --- /dev/null +++ b/test/fixtures/render/autotest/var-tag/test.js @@ -0,0 +1 @@ +exports.templateData = {}; diff --git a/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/code-generator.js b/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/code-generator.js new file mode 100644 index 000000000..fb180bf75 --- /dev/null +++ b/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/code-generator.js @@ -0,0 +1,13 @@ +module.exports = function generateCode(elNode, generator) { + var builder = generator.builder; + + return builder.htmlElement( + 'div', + { + 'class': builder.literal('greeting') + }, + [ + builder.text(builder.literal('Hello ')), + builder.text(elNode.getAttributeValue('name')) + ]); +}; \ No newline at end of file diff --git a/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/marko-tag.json b/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/marko-tag.json new file mode 100644 index 000000000..0ec7547f9 --- /dev/null +++ b/test/fixtures/taglib/scanned-tags/test-tag-code-generator-return-tree/marko-tag.json @@ -0,0 +1,3 @@ +{ + "@name": "string" +} \ No newline at end of file