mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
Fixes #170 - macro support in Marko v3
This commit is contained in:
parent
70d47e6d5b
commit
c51362e793
@ -30,6 +30,8 @@ var UpdateExpression = require('./ast/UpdateExpression');
|
|||||||
var UnaryExpression = require('./ast/UnaryExpression');
|
var UnaryExpression = require('./ast/UnaryExpression');
|
||||||
var MemberExpression = require('./ast/MemberExpression');
|
var MemberExpression = require('./ast/MemberExpression');
|
||||||
var Code = require('./ast/Code');
|
var Code = require('./ast/Code');
|
||||||
|
var InvokeMacro = require('./ast/InvokeMacro');
|
||||||
|
var Macro = require('./ast/Macro');
|
||||||
|
|
||||||
var parseExpression = require('./util/parseExpression');
|
var parseExpression = require('./util/parseExpression');
|
||||||
|
|
||||||
@ -176,10 +178,22 @@ class Builder {
|
|||||||
return new If({test, body, else: elseStatement});
|
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) {
|
literal(value) {
|
||||||
return new Literal({value});
|
return new Literal({value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro(name, params, body) {
|
||||||
|
return new Macro({name, params, body});
|
||||||
|
}
|
||||||
|
|
||||||
memberExpression(object, property, computed) {
|
memberExpression(object, property, computed) {
|
||||||
object = makeNode(object);
|
object = makeNode(object);
|
||||||
property = makeNode(property);
|
property = makeNode(property);
|
||||||
@ -212,6 +226,12 @@ class Builder {
|
|||||||
return new Program({body});
|
return new Program({body});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderBodyFunction(body) {
|
||||||
|
let name = 'renderBody';
|
||||||
|
let params = [new Identifier({name: 'out'})];
|
||||||
|
return new FunctionDeclaration({name, params, body});
|
||||||
|
}
|
||||||
|
|
||||||
require(path) {
|
require(path) {
|
||||||
path = makeNode(path);
|
path = makeNode(path);
|
||||||
|
|
||||||
|
|||||||
@ -532,7 +532,7 @@ class Generator {
|
|||||||
errorInfo.node = node;
|
errorInfo.node = node;
|
||||||
this.context.addError(errorInfo);
|
this.context.addError(errorInfo);
|
||||||
} else {
|
} else {
|
||||||
this.context.addError({node, code, message});
|
this.context.addError({node, message, code});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ var PosInfo = require('./util/PosInfo');
|
|||||||
var CompileError = require('./CompileError');
|
var CompileError = require('./CompileError');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var Node = require('./ast/Node');
|
var Node = require('./ast/Node');
|
||||||
|
var macros = require('./util/macros');
|
||||||
|
|
||||||
function getTaglibPath(taglibPath) {
|
function getTaglibPath(taglibPath) {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@ -38,6 +39,7 @@ class CompileContext {
|
|||||||
this._srcCharProps = null;
|
this._srcCharProps = null;
|
||||||
this._flags = {};
|
this._flags = {};
|
||||||
this._errors = [];
|
this._errors = [];
|
||||||
|
this._macros = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPosInfo(pos) {
|
getPosInfo(pos) {
|
||||||
@ -60,6 +62,23 @@ class CompileContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addError(errorInfo) {
|
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));
|
this._errors.push(new CompileError(errorInfo, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +222,30 @@ class CompileContext {
|
|||||||
|
|
||||||
return node;
|
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;
|
module.exports = CompileContext;
|
||||||
@ -17,12 +17,12 @@ class StartTag extends Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateCode(codegen) {
|
generateCode(codegen) {
|
||||||
|
var builder = codegen.builder;
|
||||||
|
|
||||||
var tagName = this.tagName;
|
var tagName = this.tagName;
|
||||||
var selfClosing = this.selfClosing;
|
var selfClosing = this.selfClosing;
|
||||||
var dynamicAttributes = this.dynamicAttributes;
|
var dynamicAttributes = this.dynamicAttributes;
|
||||||
|
|
||||||
var builder = codegen.builder;
|
|
||||||
|
|
||||||
// Starting tag
|
// Starting tag
|
||||||
codegen.addWriteLiteral('<');
|
codegen.addWriteLiteral('<');
|
||||||
|
|
||||||
@ -94,22 +94,9 @@ class EndTag extends Node {
|
|||||||
class HtmlElement extends Node {
|
class HtmlElement extends Node {
|
||||||
constructor(def) {
|
constructor(def) {
|
||||||
super('HtmlElement');
|
super('HtmlElement');
|
||||||
|
|
||||||
var tagName = def.tagName;
|
|
||||||
|
|
||||||
this.tagName = null;
|
this.tagName = null;
|
||||||
this.dynamicTagName = null;
|
this.tagNameExpression = null;
|
||||||
|
this.setTagName(def.tagName);
|
||||||
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._attributes = def.attributes;
|
this._attributes = def.attributes;
|
||||||
|
|
||||||
if (!(this._attributes instanceof HtmlAttributeCollection)) {
|
if (!(this._attributes instanceof HtmlAttributeCollection)) {
|
||||||
@ -130,7 +117,16 @@ class HtmlElement extends Node {
|
|||||||
if (tagName) {
|
if (tagName) {
|
||||||
tagName = codegen.builder.literal(tagName);
|
tagName = codegen.builder.literal(tagName);
|
||||||
} else {
|
} 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;
|
var attributes = this._attributes && this._attributes.all;
|
||||||
@ -247,14 +243,20 @@ class HtmlElement extends Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagName(newTagName) {
|
setTagName(tagName) {
|
||||||
this.tagName = newTagName;
|
|
||||||
this.dynamicTagName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDynamicTagName(dynamicTagName) {
|
|
||||||
this.tagName = null;
|
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() {
|
toJSON() {
|
||||||
|
|||||||
115
compiler/ast/InvokeMacro.js
Normal file
115
compiler/ast/InvokeMacro.js
Normal 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
34
compiler/ast/Macro.js
Normal 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;
|
||||||
@ -1,22 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function safeVarName(varName) {
|
var safeVarName = require('./safeVarName');
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class UniqueVars {
|
class UniqueVars {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
88
compiler/util/macros.js
Normal file
88
compiler/util/macros.js
Normal 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;
|
||||||
18
compiler/util/safeVarName.js
Normal file
18
compiler/util/safeVarName.js
Normal 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;
|
||||||
59
compiler/util/splitJavaScriptArgs.js
Normal file
59
compiler/util/splitJavaScriptArgs.js
Normal 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;
|
||||||
|
};
|
||||||
@ -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)
|
### 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:
|
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)
|
### negate(argument)
|
||||||
|
|
||||||
Returns a node that generates the following code:
|
Returns a node that generates the following code:
|
||||||
|
|||||||
7
taglibs/core/macro-body-tag.js
Normal file
7
taglibs/core/macro-body-tag.js
Normal 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
26
taglibs/core/macro-tag.js
Normal 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);
|
||||||
|
};
|
||||||
@ -11,6 +11,12 @@
|
|||||||
"<if>": {
|
"<if>": {
|
||||||
"node-factory": "./if-tag"
|
"node-factory": "./if-tag"
|
||||||
},
|
},
|
||||||
|
"<macro>": {
|
||||||
|
"code-generator": "./macro-tag"
|
||||||
|
},
|
||||||
|
"<macro-body>": {
|
||||||
|
"code-generator": "./macro-body-tag"
|
||||||
|
},
|
||||||
"<template-init>": {
|
"<template-init>": {
|
||||||
"code-generator": "./template-init-tag",
|
"code-generator": "./template-init-tag",
|
||||||
"body": "static-text"
|
"body": "static-text"
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -2,12 +2,12 @@
|
|||||||
var Expression = require('../../../compiler/ast/Expression');
|
var Expression = require('../../../compiler/ast/Expression');
|
||||||
var Literal = require('../../../compiler/ast/Literal');
|
var Literal = require('../../../compiler/ast/Literal');
|
||||||
var Identifier = require('../../../compiler/ast/Identifier');
|
var Identifier = require('../../../compiler/ast/Identifier');
|
||||||
var removeComments = require('./removeComments');
|
var removeComments = require('../../../compiler/util/removeComments');
|
||||||
|
|
||||||
var integerRegExp = /^-?\d+$/;
|
var integerRegExp = /^-?\d+$/;
|
||||||
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
|
var numberRegExp = /^-?(?:\d+|\d+\.\d*|\d*\.\d+|\d+\.\d+)$/;
|
||||||
|
|
||||||
var tokenizer = require('./tokenizer').create([
|
var tokenizer = require('../../../compiler/util/tokenizer').create([
|
||||||
{
|
{
|
||||||
name: 'stringDouble',
|
name: 'stringDouble',
|
||||||
pattern: /"(?:[^"]|\\")*"/,
|
pattern: /"(?:[^"]|\\")*"/,
|
||||||
|
|||||||
8
test/fixtures/codegen/autotest/invokeMacro-body/expected.js
vendored
Normal file
8
test/fixtures/codegen/autotest/invokeMacro-body/expected.js
vendored
Normal 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");
|
||||||
|
});
|
||||||
19
test/fixtures/codegen/autotest/invokeMacro-body/index.js
vendored
Normal file
19
test/fixtures/codegen/autotest/invokeMacro-body/index.js
vendored
Normal 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'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
};
|
||||||
6
test/fixtures/codegen/autotest/invokeMacro/expected.js
vendored
Normal file
6
test/fixtures/codegen/autotest/invokeMacro/expected.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function macro_greeting(name, age, out, renderBody) {
|
||||||
|
out.w("Hello " +
|
||||||
|
escapeXml(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_greeting("Frank", 10, out);
|
||||||
16
test/fixtures/codegen/autotest/invokeMacro/index.js
vendored
Normal file
16
test/fixtures/codegen/autotest/invokeMacro/index.js
vendored
Normal 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),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
};
|
||||||
8
test/fixtures/codegen/autotest/invokeMacroFromEl-body-and-argument/expected.js
vendored
Normal file
8
test/fixtures/codegen/autotest/invokeMacroFromEl-body-and-argument/expected.js
vendored
Normal 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");
|
||||||
|
});
|
||||||
17
test/fixtures/codegen/autotest/invokeMacroFromEl-body-and-argument/index.js
vendored
Normal file
17
test/fixtures/codegen/autotest/invokeMacroFromEl-body-and-argument/index.js
vendored
Normal 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 */)
|
||||||
|
]);
|
||||||
|
};
|
||||||
6
test/fixtures/codegen/autotest/invokeMacroFromEl/expected.js
vendored
Normal file
6
test/fixtures/codegen/autotest/invokeMacroFromEl/expected.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function macro_greeting(name, age, out, renderBody) {
|
||||||
|
out.w("Hello " +
|
||||||
|
escapeXml(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_greeting("Frank", 10, out);
|
||||||
14
test/fixtures/codegen/autotest/invokeMacroFromEl/index.js
vendored
Normal file
14
test/fixtures/codegen/autotest/invokeMacroFromEl/index.js
vendored
Normal 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)
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
};
|
||||||
4
test/fixtures/codegen/autotest/macro/expected.js
vendored
Normal file
4
test/fixtures/codegen/autotest/macro/expected.js
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
function macro_greeting(name, age, out, renderBody) {
|
||||||
|
out.w("Hello " +
|
||||||
|
escapeXml(name));
|
||||||
|
}
|
||||||
8
test/fixtures/codegen/autotest/macro/index.js
vendored
Normal file
8
test/fixtures/codegen/autotest/macro/index.js
vendored
Normal 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'))
|
||||||
|
]);
|
||||||
|
};
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
1
test/fixtures/render/autotest/macro-body/expected.html
vendored
Normal file
1
test/fixtures/render/autotest/macro-body/expected.html
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div><h1>My Alert</h1><p>Something went wrong!</p></div>
|
||||||
14
test/fixtures/render/autotest/macro-body/template.marko
vendored
Normal file
14
test/fixtures/render/autotest/macro-body/template.marko
vendored
Normal 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>
|
||||||
1
test/fixtures/render/autotest/macro-boolean/expected.html
vendored
Normal file
1
test/fixtures/render/autotest/macro-boolean/expected.html
vendored
Normal 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>
|
||||||
10
test/fixtures/render/autotest/macro-boolean/template.marko
vendored
Normal file
10
test/fixtures/render/autotest/macro-boolean/template.marko
vendored
Normal 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 />
|
||||||
1
test/fixtures/render/autotest/macro-boolean/test.js
vendored
Normal file
1
test/fixtures/render/autotest/macro-boolean/test.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
exports.templateData = {};
|
||||||
1
test/fixtures/render/autotest/macro-positional-args/expected.html
vendored
Normal file
1
test/fixtures/render/autotest/macro-positional-args/expected.html
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello Frank! You are 30 years old.Hello Doe, John! You are 10 years old.
|
||||||
6
test/fixtures/render/autotest/macro-positional-args/template.marko
vendored
Normal file
6
test/fixtures/render/autotest/macro-positional-args/template.marko
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<macro greeting(name, age)>
|
||||||
|
Hello ${name}! You are ${age} years old.
|
||||||
|
</macro>
|
||||||
|
|
||||||
|
<greeting('Frank', 30)/>
|
||||||
|
<greeting('Doe, John', 10)/>
|
||||||
1
test/fixtures/render/autotest/macro-positional-args/test.js
vendored
Normal file
1
test/fixtures/render/autotest/macro-positional-args/test.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
exports.templateData = {};
|
||||||
1
test/fixtures/render/autotest/macro/expected.html
vendored
Normal file
1
test/fixtures/render/autotest/macro/expected.html
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello Frank! You are 30 years old.Hello John! You are 10 years old.
|
||||||
6
test/fixtures/render/autotest/macro/template.marko
vendored
Normal file
6
test/fixtures/render/autotest/macro/template.marko
vendored
Normal 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/>
|
||||||
1
test/fixtures/render/autotest/macro/test.js
vendored
Normal file
1
test/fixtures/render/autotest/macro/test.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
exports.templateData = {};
|
||||||
Loading…
x
Reference in New Issue
Block a user