Fixes #170 - macro support in Marko v3

This commit is contained in:
Patrick Steele-Idem 2016-01-04 17:30:11 -07:00
parent 70d47e6d5b
commit c51362e793
42 changed files with 633 additions and 219 deletions

View File

@ -30,6 +30,8 @@ var UpdateExpression = require('./ast/UpdateExpression');
var UnaryExpression = require('./ast/UnaryExpression');
var MemberExpression = require('./ast/MemberExpression');
var Code = require('./ast/Code');
var InvokeMacro = require('./ast/InvokeMacro');
var Macro = require('./ast/Macro');
var parseExpression = require('./util/parseExpression');
@ -176,10 +178,22 @@ class Builder {
return new If({test, body, else: elseStatement});
}
invokeMacro(name, args, body) {
return new InvokeMacro({name, args, body});
}
invokeMacroFromEl(el) {
return new InvokeMacro({el});
}
literal(value) {
return new Literal({value});
}
macro(name, params, body) {
return new Macro({name, params, body});
}
memberExpression(object, property, computed) {
object = makeNode(object);
property = makeNode(property);
@ -212,6 +226,12 @@ class Builder {
return new Program({body});
}
renderBodyFunction(body) {
let name = 'renderBody';
let params = [new Identifier({name: 'out'})];
return new FunctionDeclaration({name, params, body});
}
require(path) {
path = makeNode(path);

View File

@ -532,7 +532,7 @@ class Generator {
errorInfo.node = node;
this.context.addError(errorInfo);
} else {
this.context.addError({node, code, message});
this.context.addError({node, message, code});
}
}

View File

@ -10,6 +10,7 @@ var PosInfo = require('./util/PosInfo');
var CompileError = require('./CompileError');
var path = require('path');
var Node = require('./ast/Node');
var macros = require('./util/macros');
function getTaglibPath(taglibPath) {
if (typeof window === 'undefined') {
@ -38,6 +39,7 @@ class CompileContext {
this._srcCharProps = null;
this._flags = {};
this._errors = [];
this._macros = null;
}
getPosInfo(pos) {
@ -60,6 +62,23 @@ class CompileContext {
}
addError(errorInfo) {
if (errorInfo instanceof Node) {
let node = arguments[0];
let message = arguments[1];
let code = arguments[2];
errorInfo = {
node,
message,
code
};
} else if (typeof errorInfo === 'string') {
let message = arguments[0];
let code = arguments[1];
errorInfo = {
message,
code
};
}
this._errors.push(new CompileError(errorInfo, this));
}
@ -203,6 +222,30 @@ class CompileContext {
return node;
}
isMacro(name) {
if (!this._macros) {
return false;
}
return this._macros.isMacro(name);
}
getRegisteredMacro(name) {
if (!this._macros) {
return undefined;
}
return this._macros.getRegisteredMacro(name);
}
registerMacro(name, params) {
if (!this._macros) {
this._macros = macros.createMacrosContext();
}
return this._macros.registerMacro(name, params);
}
}
module.exports = CompileContext;

View File

@ -17,12 +17,12 @@ class StartTag extends Node {
}
generateCode(codegen) {
var builder = codegen.builder;
var tagName = this.tagName;
var selfClosing = this.selfClosing;
var dynamicAttributes = this.dynamicAttributes;
var builder = codegen.builder;
// Starting tag
codegen.addWriteLiteral('<');
@ -94,22 +94,9 @@ class EndTag extends Node {
class HtmlElement extends Node {
constructor(def) {
super('HtmlElement');
var tagName = def.tagName;
this.tagName = null;
this.dynamicTagName = null;
if (tagName instanceof Node) {
if (tagName instanceof Literal) {
this.tagName = tagName.value;
} else {
this.dynamicTagName = tagName;
}
} else if (typeof tagName === 'string'){
this.tagName = tagName;
}
this.tagNameExpression = null;
this.setTagName(def.tagName);
this._attributes = def.attributes;
if (!(this._attributes instanceof HtmlAttributeCollection)) {
@ -130,7 +117,16 @@ class HtmlElement extends Node {
if (tagName) {
tagName = codegen.builder.literal(tagName);
} else {
tagName = this.dynamicTagName;
tagName = this.tagNameExpression;
}
var context = codegen.context;
if (context.isMacro(this.tagName)) {
// At code generation time, if this tag corresponds to a registered macro
// then invoke the macro based on this HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(this);
}
var attributes = this._attributes && this._attributes.all;
@ -247,14 +243,20 @@ class HtmlElement extends Node {
}
}
setTagName(newTagName) {
this.tagName = newTagName;
this.dynamicTagName = null;
}
getDynamicTagName(dynamicTagName) {
setTagName(tagName) {
this.tagName = null;
this.dynamicTagName = dynamicTagName;
this.tagNameExpression = null;
if (tagName instanceof Node) {
if (tagName instanceof Literal) {
this.tagName = tagName.value;
this.tagNameExpression = tagName;
} else {
this.tagNameExpression = tagName;
}
} else if (typeof tagName === 'string'){
this.tagName = tagName;
}
}
toJSON() {

115
compiler/ast/InvokeMacro.js Normal file
View File

@ -0,0 +1,115 @@
'use strict';
var Node = require('./Node');
var ok = require('assert').ok;
var splitJavaScriptArgs = require('../util/splitJavaScriptArgs');
function removeTrailingUndefineds(args) {
var i;
var last = args.length-1;
for (i=last; i>=0; i--) {
if (args[i].type !== 'Literal' || args[i].value !== undefined) {
break;
}
}
if (i !== last) {
args = args.slice(0, i+1);
}
return args;
}
class InvokeMacro extends Node {
constructor(def) {
super('InvokeMacro');
this.el = def.el;
this.name = def.name;
this.args = def.args;
this.body = this.makeContainer(def.body);
if (this.name != null) {
ok(typeof this.name === 'string', 'Invalid macro name: ' + this.name);
}
}
generateCode(codegen) {
var el = this.el;
var name = this.name;
var args = this.args;
var body = this.body;
var builder = codegen.builder;
var macroDef;
if (el) {
name = el.tagName;
body = el.body;
if (typeof name !== 'string') {
codegen.context.addError(el, 'Element node with a dynamic tag name cannot be used to invoke a macro', 'ERR_INVOKE_MACRO');
return;
}
macroDef = codegen.context.getRegisteredMacro(name);
if (!macroDef) {
codegen.context.addError(el, 'Element node does not correspond to a macro', 'ERR_INVOKE_MACRO');
return;
}
if (el.argument) {
args = splitJavaScriptArgs(el.argument);
} else {
args = new Array(macroDef.params.length);
for (let i=0; i<args.length; i++) {
args[i] = builder.literal(undefined);
}
el.forEachAttribute((attr) => {
var paramName = attr.name;
var paramIndex = macroDef.getParamIndex(paramName);
if (paramIndex == null) {
codegen.context.addError(el, 'The "' + name + '" macro does not have a parameter named "' + paramName + '"', 'ERR_INVOKE_MACRO');
return;
}
var value = attr.value;
if (value == null) {
value = builder.literal(true);
}
args[paramIndex] = value;
});
}
} else {
macroDef = codegen.context.getRegisteredMacro(name);
if (!macroDef) {
codegen.addError('Macro not found with name "' + name + '"', 'ERR_INVOKE_MACRO');
return;
}
}
if (!args) {
args = [];
}
while (args.length < macroDef.params.length) {
args.push(builder.literal(undefined));
}
if (body && body.length) {
args[macroDef.getParamIndex('renderBody')] = builder.renderBodyFunction(body);
}
args[macroDef.getParamIndex('out')] = builder.identifier('out');
args = removeTrailingUndefineds(args);
return builder.functionCall(builder.identifier(macroDef.functionName), args);
}
}
module.exports = InvokeMacro;

34
compiler/ast/Macro.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
var Node = require('./Node');
var ok = require('assert').ok;
class Macro extends Node {
constructor(def) {
super('Macro');
this.name = def.name;
this.params = def.params;
this.body = this.makeContainer(def.body);
if (this.params == null) {
this.params = [];
} else {
ok(Array.isArray(this.params), '"params" should be an array');
}
}
generateCode(codegen) {
var name = this.name;
var params = this.params || [];
var body = this.body;
var builder = codegen.builder;
var macroDef = codegen.context.registerMacro(name, params);
var functionName = macroDef.functionName;
return builder.functionDeclaration(functionName, macroDef.params, body);
}
}
module.exports = Macro;

View File

@ -1,22 +1,6 @@
'use strict';
function safeVarName(varName) {
var parts = varName.split(/[\\/]/);
if (parts.length >= 2) {
// The varname looks like it was based on a path.
// Let's just use the last two parts
varName = parts.slice(-2).join('_');
}
return varName.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[0-9]+/, function(match) {
var str = '';
for (var i=0; i<match.length; i++) {
str += '_';
}
return str;
});
}
var safeVarName = require('./safeVarName');
class UniqueVars {
constructor() {

88
compiler/util/macros.js Normal file
View File

@ -0,0 +1,88 @@
'use strict';
var safeVarName = require('./safeVarName');
var ok = require('assert').ok;
class MacrosContext {
constructor() {
this._byName = {};
}
isMacro(name) {
if (!name) {
return false;
}
if (name.type === 'Literal') {
name = name.value;
}
return this._byName.hasOwnProperty(name);
}
getRegisteredMacro(name) {
return this._byName[name];
}
registerMacro(name, params) {
ok(name, '"name" is required');
ok(typeof name === 'string', '"name" should be a string');
if (params == null) {
params = [];
} else {
ok(Array.isArray(params), '"params" should be an array');
}
var hasOut = false;
var hasRenderBody = false;
params.forEach((param) => {
if (param === 'out') {
hasOut = true;
} else if (param === 'renderBody') {
hasRenderBody = true;
}
});
if (!hasOut) {
params.push('out');
}
if (!hasRenderBody) {
params.push('renderBody');
}
var paramIndexes = {};
params.forEach((param, i) => {
paramIndexes[param] = i;
if (param === 'out') {
hasOut = true;
} else if (param === 'renderBody') {
hasRenderBody = true;
}
});
var functionName = 'macro_' + safeVarName(name);
var macroDef = {
name: name,
params: params,
functionName: functionName,
getParamIndex: function(param) {
return paramIndexes[param];
}
};
this._byName[name] = macroDef;
return macroDef;
}
}
function createMacrosContext() {
return new MacrosContext();
}
exports.createMacrosContext = createMacrosContext;

View File

@ -0,0 +1,18 @@
function safeVarName(varName) {
var parts = varName.split(/[\\/]/);
if (parts.length >= 2) {
// The varname looks like it was based on a path.
// Let's just use the last two parts
varName = parts.slice(-2).join('_');
}
return varName.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[0-9]+/, function(match) {
var str = '';
for (var i=0; i<match.length; i++) {
str += '_';
}
return str;
});
}
module.exports = safeVarName;

View File

@ -0,0 +1,59 @@
'use strict';
var removeComments = require('./removeComments');
var parseExpression = require('./parseExpression');
var tokenizer = require('./tokenizer').create([
{
name: 'stringDouble',
pattern: /"(?:[^"]|\\")*"/,
},
{
name: 'stringSingle',
pattern: /'(?:[^']|\\')*'/
},
{
name: 'groupOpen',
pattern: /[\{\(\[]/
},
{
name: 'groupClose',
pattern: /[\}\)\]]/
},
{
name: 'comma',
pattern: /[,]/
}
]);
module.exports = function(str) {
str = removeComments(str);
let depth = 0;
var argStart = 0;
var args = [];
function finishPrevArg(end) {
var arg = str.substring(argStart, end);
args.push(parseExpression(arg));
}
tokenizer.forEachToken(str, (token) => {
switch(token.name) {
case 'groupOpen':
depth++;
break;
case 'groupClose':
depth--;
break;
case 'comma':
if (depth === 0) {
finishPrevArg(token.start);
argStart = token.end;
}
break;
}
});
finishPrevArg(str.length);
return args;
};

View File

@ -422,9 +422,41 @@ if (true) {
}
```
### invokeMacro(name, args, body)
Returns a node to generate the code to invoke a macro with the given name, args and body
For example:
```javascript
builder.invokeMacro(
'greeting',
[
builder.literal('Frank'),
builder.literal(10),
],
[
builder.text(builder.literal('This is the body passed to the macro'))
])
```
### invokeMacroFromEl(el)
Returns a node to generate the code to invoke a macro based on the provided `HtmlElement` node.
For example:
```javascript
var el = builder.htmlElement('greeting', {
name: builder.literal('Frank'),
age: builder.literal(10)
});
builder.invokeMacroFromEl(el)
```
### literal(value)
Returns code to generate a JavaScript code for literal strings, numbers, booleans, objects and arrays.
Returns a node to generate a JavaScript code for literal strings, numbers, booleans, objects and arrays.
For example:
@ -467,6 +499,10 @@ var aString = "abc",
]
```
### 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.
### negate(argument)
Returns a node that generates the following code:

View File

@ -0,0 +1,7 @@
module.exports = function codeGenerator(elNode, codegen) {
var builder = codegen.builder;
return builder.ifStatement(builder.identifier('renderBody'), [
builder.functionCall('renderBody', ['out'])
]);
};

26
taglibs/core/macro-tag.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = function codeGenerator(elNode, codegen) {
var attributes = elNode.attributes;
if (!attributes.length) {
return;
}
var defAttr = attributes[0];
if (!defAttr.argument) {
return;
}
var body = elNode.body;
var macroName = defAttr.name;
var argument = defAttr.argument;
var params;
if (argument) {
params = argument.split(/\s*,\s*/);
} else {
params = [];
}
var builder = codegen.builder;
return builder.macro(macroName, params, body);
};

View File

@ -11,6 +11,12 @@
"<if>": {
"node-factory": "./if-tag"
},
"<macro>": {
"code-generator": "./macro-tag"
},
"<macro-body>": {
"code-generator": "./macro-body-tag"
},
"<template-init>": {
"code-generator": "./template-init-tag",
"body": "static-text"

View File

@ -1,146 +0,0 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Utility class to support sub-attributes in an XML attribute. Each sub-attribute must
* be separated by a semicolon. Within each sub-attribute, the name/value pair must
* be split using an equal sign. However, the name for the first sub-attribute
* is optional and a default name can be provided when reading the sub-attributes.
*
* <p>
* Sub-attribute format:
* (<attr-value>)?(<attr-name>=<attr-value>;)*(<attr-name>=<attr-value>)
*
*
*
*/
'use strict';
var regExp = /"(?:[^"]|\\")*"|'(?:[^']|\\')*'|==|===|[;=]/g;
/**
* Parses the provided string to find the sub-attributes that it contains.
* The parsed output can be either returned as an array or a map. By default,
* the parsed output is returned as a map where each property corresponds
* to a sub-attribute. However, if the order of the sub-attributes is important
* then the "ordered" option can be set to "true" and
* an array will instead be returned where each element in the array is an object
* with a name and value property that corresponds to the matching sub-attribute.
*
* <p>
* Supported options:
* <ul>
* <li>ordered (boolean, defaults to "false") - If true then an array is returned (see above). Otherwise, an object is returned.
* </ul>
*
* @memberOf raptor/templating/compiler$AttributeSplitter
* @param attr {String} The attribute to split
* @param types {Object} Type definitions for the possible sub-attributes.
* @param options
* @returns
*/
module.exports = function (attr, types, options) {
if (!options) {
options = {};
}
var partStart = 0;
var ordered = options.ordered === true;
var defaultName = options.defaultName;
var removeDashes = options.removeDashes === true;
var matches;
var equalIndex = -1;
var result = ordered ? [] : {};
function handleError(message) {
if (options.errorHandler) {
options.errorHandler(message);
return;
} else {
throw new Error(message);
}
}
function finishPart(endIndex) {
if (partStart === endIndex) {
//The part is an empty string... ignore it
return;
}
var name;
var value;
if (equalIndex != -1) {
name = attr.substring(partStart, equalIndex).trim();
value = attr.substring(equalIndex + 1, endIndex).trim();
} else {
if (defaultName) {
name = defaultName;
value = attr.substring(partStart, endIndex).trim();
if (!value.length) {
return; //ignore empty parts
}
} else {
name = attr.substring(partStart, endIndex).trim();
}
}
if (!name.length && !value.length) {
equalIndex = -1;
return; //ignore empty parts
}
if (types) {
var type = types[name] || types['*'];
if (type) {
if (type.name) {
name = type.name;
}
} else {
return handleError('Invalid sub-attribute name of "' + name + '"');
}
}
if (name && removeDashes) {
name = name.replace(/-([a-z])/g, function (match, lower) {
return lower.toUpperCase();
});
}
if (ordered) {
result.push({
name: name,
value: value
});
} else {
result[name] = value;
}
equalIndex = -1; //Reset the equal index
}
/*
* Keep searching the string for the relevant tokens.
*
* NOTE: The regular expression will also return matches for JavaScript strings,
* but they are just ignored. This ensures that semicolons inside strings
* are not treated as
*/
while ((matches = regExp.exec(attr))) {
//console.error(matches[0]);
if (matches[0] == ';') {
finishPart(matches.index);
partStart = matches.index + 1;
equalIndex = -1;
} else if (matches[0] == '=') {
if (equalIndex == -1) {
equalIndex = matches.index;
}
}
}
finishPart(attr.length);
//console.error("AttributeSplitter - result: ", result);
return result;
};

View File

@ -2,12 +2,12 @@
var Expression = require('../../../compiler/ast/Expression');
var Literal = require('../../../compiler/ast/Literal');
var Identifier = require('../../../compiler/ast/Identifier');
var removeComments = require('./removeComments');
var removeComments = require('../../../compiler/util/removeComments');
var integerRegExp = /^-?\d+$/;
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
var tokenizer = require('./tokenizer').create([
var tokenizer = require('../../../compiler/util/tokenizer').create([
{
name: 'stringDouble',
pattern: /"(?:[^"]|\\")*"/,

View File

@ -0,0 +1,8 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
}
macro_greeting("Frank", 10, out, function renderBody(out) {
out.w("This is the body passed to the macro");
});

View File

@ -0,0 +1,19 @@
'use strict';
module.exports = function(builder) {
return builder.program([
builder.macro('greeting', ['name', 'age'], [
builder.text(builder.literal('Hello ')),
builder.text(builder.identifier('name'))
]),
builder.invokeMacro(
'greeting',
[
builder.literal('Frank'),
builder.literal(10),
],
[
builder.text(builder.literal('This is the body passed to the macro'))
])
]);
};

View File

@ -0,0 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
}
macro_greeting("Frank", 10, out);

View File

@ -0,0 +1,16 @@
'use strict';
module.exports = function(builder) {
return builder.program([
builder.macro('greeting', ['name', 'age'], [
builder.text(builder.literal('Hello ')),
builder.text(builder.identifier('name'))
]),
builder.invokeMacro(
'greeting',
[
builder.literal('Frank'),
builder.literal(10),
])
]);
};

View File

@ -0,0 +1,8 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
}
macro_greeting("Frank", 10, out, function renderBody(out) {
out.w("This is the body passed to the macro");
});

View File

@ -0,0 +1,17 @@
'use strict';
module.exports = function(builder) {
return builder.program([
builder.macro('greeting', ['name', 'age'], [
builder.text(builder.literal('Hello ')),
builder.text(builder.identifier('name'))
]),
builder.invokeMacroFromEl(builder.htmlElement(
'greeting',
[], /* empty attributes */
[
builder.text(builder.literal('This is the body passed to the macro'))
],
'"Frank", 10') /* argument string */)
]);
};

View File

@ -0,0 +1,6 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
}
macro_greeting("Frank", 10, out);

View File

@ -0,0 +1,14 @@
'use strict';
module.exports = function(builder) {
return builder.program([
builder.macro('greeting', ['name', 'age'], [
builder.text(builder.literal('Hello ')),
builder.text(builder.identifier('name'))
]),
builder.invokeMacroFromEl(builder.htmlElement('greeting', {
name: builder.literal('Frank'),
age: builder.literal(10)
}))
]);
};

View File

@ -0,0 +1,4 @@
function macro_greeting(name, age, out, renderBody) {
out.w("Hello " +
escapeXml(name));
}

View File

@ -0,0 +1,8 @@
'use strict';
module.exports = function(builder) {
return builder.macro('greeting', ['name', 'age'], [
builder.text(builder.literal('Hello ')),
builder.text(builder.identifier('name'))
]);
};

View File

@ -1 +0,0 @@
<p class="greeting">Hello, World!</p>, <p class="greeting">Hello, Frank!</p><div class="section"><h1><a href="http://www.ebay.com/">ebay</a></h1><p><i>Visit eBay</i></p></div>

View File

@ -1,26 +0,0 @@
<def function="greeting(name)">
<p class="greeting">Hello, ${name}!</p>
</def>
${greeting("World")},
${greeting("Frank")}
<def function="section(url, title, body)" body-param="body">
<div class="section">
<h1>
<a href="$url">
$title
</a>
</h1>
<p>
${body}
</p>
</div>
</def>
<invoke function="section" url="http://www.ebay.com/" title="ebay">
<i>
Visit eBay
</i>
</invoke>

View File

@ -0,0 +1 @@
<div><h1>My Alert</h1><p>Something went wrong!</p></div>

View File

@ -0,0 +1,14 @@
<macro alert(title)>
<div>
<h1>
${title}
</h1>
<p>
<macro-body/>
</p>
</div>
</macro>
<alert title="My Alert">
Something went wrong!
</alert>

View File

@ -0,0 +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>

View File

@ -0,0 +1,10 @@
<macro greeting(name, age, hidden)>
<div class=(hidden === true ? 'hidden' : null)>
Hello ${name}! You are ${age} years old.
</div>
</macro>
<greeting name="Frank" age=30/>
<greeting name="John" age=10 hidden/>
<greeting name="John" age=10 hidden=true />
<greeting name="John" age=10 hidden=false />

View File

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

View File

@ -0,0 +1 @@
Hello Frank! You are 30 years old.Hello Doe, John! You are 10 years old.

View File

@ -0,0 +1,6 @@
<macro greeting(name, age)>
Hello ${name}! You are ${age} years old.
</macro>
<greeting('Frank', 30)/>
<greeting('Doe, John', 10)/>

View File

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

View File

@ -0,0 +1 @@
Hello Frank! You are 30 years old.Hello John! You are 10 years old.

View File

@ -0,0 +1,6 @@
<macro greeting(name, age)>
Hello ${name}! You are ${age} years old.
</macro>
<greeting name="Frank" age=30/>
<greeting name="John" age=10/>

View File

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