From b64729b0229521400f065576e938eabcbd600971 Mon Sep 17 00:00:00 2001 From: Tobias Bocanegra Date: Wed, 29 Jul 2020 15:58:12 +0900 Subject: [PATCH] fix(compiler): refactor template resolution (#220) fixes #220, fixes #216 BREAKING CHANGE: - The templateLoader and scriptResolver are now 2 separate functions that can be set on the compiler - The Runtime.template() has an extra argument 'id' that specifies the group (script) the template is defined. --- src/compiler/Compiler.js | 117 +++++++++++------- src/compiler/DomHandler.js | 15 ++- src/compiler/JSCodeGenVisitor.js | 7 +- src/compiler/ScriptResolver.js | 57 +++++++++ src/compiler/TemplateLoader.js | 53 +------- src/parser/commands/CommandStream.js | 4 +- ...{TemplateReference.js => FileReference.js} | 11 +- src/parser/commands/FunctionBlock.js | 9 ++ src/parser/plugins/TemplatePlugin.js | 12 +- src/parser/plugins/UsePlugin.js | 6 +- src/runtime/Runtime.js | 32 ++--- test/compiler_test.js | 4 +- test/specs/call_spec.txt | 14 +-- test/specs/template_spec.js | 17 +++ test/specs/template_spec.txt | 41 ++++-- test/specs/template_spec/group.htl | 3 + test/specs/template_spec/item.htl | 3 + test/specs/template_spec/library.htl | 11 +- test/specs/template_spec_hast.txt | 4 +- 19 files changed, 265 insertions(+), 155 deletions(-) create mode 100644 src/compiler/ScriptResolver.js rename src/parser/commands/{TemplateReference.js => FileReference.js} (86%) create mode 100644 test/specs/template_spec/group.htl create mode 100644 test/specs/template_spec/item.htl diff --git a/src/compiler/Compiler.js b/src/compiler/Compiler.js index 7006d17..93235b0 100644 --- a/src/compiler/Compiler.js +++ b/src/compiler/Compiler.js @@ -12,22 +12,21 @@ /* eslint-disable no-await-in-loop */ -// built-in modules const path = require('path'); -// declared dependencies +const crypto = require('crypto'); const fse = require('fs-extra'); const { SourceMapGenerator } = require('source-map'); -// local modules const TemplateParser = require('../parser/html/TemplateParser'); const ThrowingErrorListener = require('../parser/htl/ThrowingErrorListener'); const JSCodeGenVisitor = require('./JSCodeGenVisitor'); const ExpressionFormatter = require('./ExpressionFormatter'); -const TemplateReference = require('../parser/commands/TemplateReference'); +const FileReference = require('../parser/commands/FileReference'); const VariableBinding = require('../parser/commands/VariableBinding'); const RuntimeCall = require('../parser/htl/nodes/RuntimeCall'); const Identifier = require('../parser/htl/nodes/Identifier'); const FunctionBlock = require('../parser/commands/FunctionBlock'); const TemplateLoader = require('./TemplateLoader.js'); +const ScriptResolver = require('./ScriptResolver.js'); const DEFAULT_TEMPLATE = 'JSCodeTemplate.js'; const RUNTIME_TEMPLATE = 'JSRuntimeTemplate.js'; @@ -62,7 +61,8 @@ module.exports = class Compiler { this._sourceFile = null; this._sourceOffset = 0; this._moduleImportGenerator = Compiler.defaultModuleGenerator; - this._templateLoader = TemplateLoader('.'); + this._templateLoader = TemplateLoader(); + this._scriptResolver = ScriptResolver('.'); } /** @@ -79,7 +79,7 @@ module.exports = class Compiler { */ withDirectory(dir) { this._dir = dir; - this._templateLoader = TemplateLoader(dir); + this._scriptResolver = ScriptResolver(dir); return this; } @@ -163,13 +163,22 @@ module.exports = class Compiler { /** * Sets the function that loads the templates. - * @param {function} fn the async function taking the baseDir and file name. + * @param {function} fn the async function taking the file path. */ withTemplateLoader(fn) { this._templateLoader = fn; return this; } + /** + * Sets the function that resolves a script. + * @param {function} fn the async function taking the baseDir and file name. + */ + withScriptResolver(fn) { + this._scriptResolver = fn; + return this; + } + /** * Compiles the specified source file and saves the result, overwriting the * file name. @@ -250,56 +259,62 @@ module.exports = class Compiler { * @param {String} source the HTL template code * @param {String} baseDir the base directory to resolve file references * @param {object} mods object with module mappings from use classes + * @param {object} templates Object with resolved templates * @returns {object} The result object with a `commands` stream and `templates`. */ - async _parse(source, baseDir, mods) { + async _parse(source, baseDir, mods, templates = {}) { const commands = new TemplateParser() .withErrorListener(ThrowingErrorListener.INSTANCE) .withDefaultMarkupContext(this._defaultMarkupContext) .parse(source); - const templates = []; - // find any templates references and use classes and process them for (let i = 0; i < commands.length; i += 1) { const c = commands[i]; - if (c instanceof TemplateReference) { + if (c instanceof FileReference) { if (c.isTemplate()) { - const { - data: templateSource, - path: templatePath, - } = await this._templateLoader(baseDir, c.filename); - - const res = await this._parse(templateSource, path.dirname(templatePath), mods); - - // add recursive templates, if any. - templates.push(...res.templates); - - // add this templates - const template = { - file: templatePath, - commands: [], - }; - templates.push(template); - // prefix all templates with the variable name of the use class and discard commands - // outside functions. - let inside = false; - res.commands.forEach((cmd) => { - if (cmd instanceof FunctionBlock.Start) { - inside = true; - // eslint-disable-next-line no-underscore-dangle,no-param-reassign - cmd._expression = `${c.name}.${cmd._expression}`; - } else if (cmd instanceof FunctionBlock.End) { - inside = false; - } else if (!inside) { - return; - } - template.commands.push(cmd); - }); - - // remove the template reference from the stream - commands.splice(i, 1); - i -= 1; + const templatePath = await this._scriptResolver(baseDir, c.filename); + let template = templates[templatePath]; + if (!template) { + // hash the template path for a somewhat stable id + const id = crypto.createHash('md5').update(templatePath, 'utf-8').digest().toString('hex'); + + // load template + const { + data: templateSource, + } = await this._templateLoader(templatePath); + + // register template + template = { + id, + file: templatePath, + commands: [], + }; + // eslint-disable-next-line no-param-reassign + templates[templatePath] = template; + + // parse the template + const res = await this._parse( + templateSource, path.dirname(templatePath), mods, templates, + ); + + // extract the template functions and discard commands outside the functions + let inside = false; + res.commands.forEach((cmd) => { + if (cmd instanceof FunctionBlock.Start) { + inside = true; + // eslint-disable-next-line no-param-reassign + cmd.id = template.id; + } else if (cmd instanceof FunctionBlock.End) { + inside = false; + } else if (!inside) { + return; + } + template.commands.push(cmd); + }); + } + // remember the file id on the use statement + c.id = template.id; } else { let file = c.filename; if (file.startsWith('./') || file.startsWith('../')) { @@ -345,7 +360,7 @@ module.exports = class Compiler { }); const { - code, templateCode, mappings, templateMappings, + code, templateCode, mappings, templateMappings, globalTemplateNames, } = new JSCodeGenVisitor() .withIndent(' ') .withSourceMap(this._sourceMap) @@ -366,6 +381,14 @@ module.exports = class Compiler { } imports += ` ${exp}\n`; }); + // add global template names + globalTemplateNames.forEach((ident) => { + if (this._runtimeGlobals.indexOf(ident) >= 0) { + imports += ` ${ident} = $.template('global').${ident};\n`; + } else { + imports += ` let ${ident} = $.template('global').${ident};\n`; + } + }); let template = this._codeTemplate; if (!template) { diff --git a/src/compiler/DomHandler.js b/src/compiler/DomHandler.js index 535e83e..fa87124 100644 --- a/src/compiler/DomHandler.js +++ b/src/compiler/DomHandler.js @@ -17,6 +17,7 @@ module.exports = class DomHandler { constructor(generator) { this._gen = generator; this._out = generator.out.bind(generator); + this._globalTemplates = {}; } beginDocument() { @@ -72,15 +73,23 @@ module.exports = class DomHandler { } } + bindFunction(cmd) { + this._out(`const ${cmd.name} = $.template('${cmd.id}');`); + } + functionStart(cmd) { const exp = ExpressionFormatter.escapeVariable(ExpressionFormatter.format(cmd.expression)); - const functionName = `_template_${exp.replace(/\./g, '_')}`; - this._out(`$.template('${exp}', function* ${functionName}(args) { `); + const id = cmd.id || 'global'; + const functionName = `_template_${id}_${exp.replace(/\./g, '_')}`; + this._out(`$.template('${id}', '${exp}', function* ${functionName}(args) { `); this._gen.indent(); cmd.args.forEach((arg) => { this._out(`const ${ExpressionFormatter.escapeVariable(arg)} = args[1]['${arg}'] || '';`); }); this._out('let $t, $n = args[0];'); + if (!cmd.id) { + this._globalTemplates[exp] = true; + } } functionEnd() { @@ -91,6 +100,6 @@ module.exports = class DomHandler { functionCall(cmd) { const funcName = ExpressionFormatter.format(cmd.functionName); const args = ExpressionFormatter.format(cmd.args); - this._out(`yield $.call($.template().${funcName}, [$n, ${args}]);`); + this._out(`yield $.call(${funcName}, [$n, ${args}]);`); } }; diff --git a/src/compiler/JSCodeGenVisitor.js b/src/compiler/JSCodeGenVisitor.js index c765996..4bf6f8f 100644 --- a/src/compiler/JSCodeGenVisitor.js +++ b/src/compiler/JSCodeGenVisitor.js @@ -12,6 +12,7 @@ const OutText = require('../parser/commands/OutText'); const VariableBinding = require('../parser/commands/VariableBinding'); +const FileReference = require('../parser/commands/FileReference'); const FunctionBlock = require('../parser/commands/FunctionBlock'); const FunctionCall = require('../parser/commands/FunctionCall'); const Conditional = require('../parser/commands/Conditional'); @@ -127,7 +128,7 @@ module.exports = class JSCodeGenVisitor { // then process the templates this._sourceOffset = 0; - templates.forEach((t) => { + Object.values(templates).forEach((t) => { this._sourceFile = t.file; t.commands.forEach((c) => { c.accept(this); @@ -157,6 +158,8 @@ module.exports = class JSCodeGenVisitor { mappings: this._main.map, templateCode, templateMappings, + // eslint-disable-next-line no-underscore-dangle + globalTemplateNames: Object.keys(this._dom._globalTemplates), }; } @@ -212,6 +215,8 @@ module.exports = class JSCodeGenVisitor { this.out(`const ${ExpressionFormatter.escapeVariable(cmd.variableName)} = ${exp};`); } else if (cmd instanceof VariableBinding.End) { // nop + } else if (cmd instanceof FileReference) { + this._dom.bindFunction(cmd); } else if (cmd instanceof FunctionBlock.Start) { this.pushBlock(cmd.expression); this._dom.functionStart(cmd); diff --git a/src/compiler/ScriptResolver.js b/src/compiler/ScriptResolver.js new file mode 100644 index 0000000..4f9cc28 --- /dev/null +++ b/src/compiler/ScriptResolver.js @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +const path = require('path'); +const fse = require('fs-extra'); + +/** + * Creates a script resolver. + * @param {string[]} roots Root directories for resolution. + */ +module.exports = function createResolver(roots) { + if (!Array.isArray(roots)) { + // eslint-disable-next-line no-param-reassign + roots = [roots]; + } + + /** + * Resolves the script against the given roots. + * @param {string} baseDir additional root + * @param {string} uri script uri + * @returns {Promise} + */ + async function resolve(baseDir, uri) { + let bases = [baseDir, ...roots]; + + // if uri starts with '.' or '..', only consider specified bases. + // otherwise also consider apps and libs. + if (!uri.startsWith('./') && !uri.startsWith('../')) { + bases = bases.reduce((prev, root) => { + prev.push(root); + prev.push(path.resolve(root, 'apps')); + prev.push(path.resolve(root, 'libs')); + return prev; + }, []); + } + + // eslint-disable-next-line no-restricted-syntax + for (const base of bases) { + const scriptPath = path.resolve(base, uri); + // eslint-disable-next-line no-await-in-loop + if (await fse.pathExists(scriptPath)) { + return scriptPath; + } + } + throw Error(`Unable to resolve script: ${uri}. Search Path: ${bases}`); + } + + return resolve; +}; diff --git a/src/compiler/TemplateLoader.js b/src/compiler/TemplateLoader.js index 6d2e6b0..5d14ceb 100644 --- a/src/compiler/TemplateLoader.js +++ b/src/compiler/TemplateLoader.js @@ -9,61 +9,20 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -const path = require('path'); const fse = require('fs-extra'); /** * Creates an template loader. - * @param {string[]} roots Root directories for resolution. */ -module.exports = function createLoader(roots) { - if (!Array.isArray(roots)) { - // eslint-disable-next-line no-param-reassign - roots = [roots]; - } - - /** - * Resolves the template against the given roots. - * @param {string} baseDir additional root - * @param {string} uri template uri - * @returns {Promise} - */ - async function resolve(baseDir, uri) { - let bases = [baseDir, ...roots]; - - // if uri starts with '.' or '..', only consider specified bases. - // otherwise also consider apps and libs. - if (!uri.startsWith('./') && !uri.startsWith('../')) { - bases = bases.reduce((prev, root) => { - prev.push(root); - prev.push(path.resolve(root, 'apps')); - prev.push(path.resolve(root, 'libs')); - return prev; - }, []); - } - - // eslint-disable-next-line no-restricted-syntax - for (const base of bases) { - const templatePath = path.resolve(base, uri); - // eslint-disable-next-line no-await-in-loop - if (await fse.pathExists(templatePath)) { - return templatePath; - } - } - throw Error(`Unable to resolve template: ${uri}. Search Path: ${bases}`); - } - +module.exports = function createLoader() { /** - * Load the template. - * @param {string} baseDir additional root - * @param {string} uri template uri - * @returns {Promise<>} the template source and resolved path + * @param {string} filePath Template path + * @returns {Promise<>} the template source and path */ - async function load(baseDir, uri) { - const templatePath = await resolve(baseDir, uri); + async function load(filePath) { return { - data: await fse.readFile(templatePath, 'utf-8'), - path: templatePath, + data: await fse.readFile(filePath, 'utf-8'), + path: filePath, }; } diff --git a/src/parser/commands/CommandStream.js b/src/parser/commands/CommandStream.js index ab66cd2..65e2aec 100644 --- a/src/parser/commands/CommandStream.js +++ b/src/parser/commands/CommandStream.js @@ -21,8 +21,8 @@ module.exports = class CommandStream { this._ignore = 0; } - write(command) { - if (this._ignore) { + write(command, force) { + if (this._ignore && !force) { return; } const isText = command instanceof OutText; diff --git a/src/parser/commands/TemplateReference.js b/src/parser/commands/FileReference.js similarity index 86% rename from src/parser/commands/TemplateReference.js rename to src/parser/commands/FileReference.js index 981cb4a..4cf85ae 100644 --- a/src/parser/commands/TemplateReference.js +++ b/src/parser/commands/FileReference.js @@ -12,12 +12,13 @@ const Command = require('./Command'); -module.exports = class TemplateReference extends Command { +module.exports = class FileReference extends Command { constructor(name, filename, args) { super(); this._name = name; this._filename = filename; this._args = args || []; + this._id = null; } get name() { @@ -28,6 +29,14 @@ module.exports = class TemplateReference extends Command { return this._filename; } + set id(value) { + this._id = value; + } + + get id() { + return this._id; + } + get args() { return this._args; } diff --git a/src/parser/commands/FunctionBlock.js b/src/parser/commands/FunctionBlock.js index 6913bc8..14124cb 100644 --- a/src/parser/commands/FunctionBlock.js +++ b/src/parser/commands/FunctionBlock.js @@ -18,6 +18,15 @@ class Start extends Command { super(); this._expression = expression; this._options = options; + this._id = null; + } + + set id(value) { + this._id = value; + } + + get id() { + return this._id; } get expression() { diff --git a/src/parser/plugins/TemplatePlugin.js b/src/parser/plugins/TemplatePlugin.js index fe47c8e..26d45a9 100644 --- a/src/parser/plugins/TemplatePlugin.js +++ b/src/parser/plugins/TemplatePlugin.js @@ -12,20 +12,18 @@ const Plugin = require('../html/Plugin'); module.exports = class TemplatePlugin extends Plugin { - // eslint-disable-next-line class-methods-use-this beforeElement(stream) { - stream.beginIgnore(); - } - - beforeChildren(stream) { const variableName = this._signature.getVariableName(null); - if (variableName === null) { throw new Error('data-sly-template must be called with an identifier'); } + stream.beginFunction(variableName, this._expression.options); + stream.beginIgnore(); + } + // eslint-disable-next-line class-methods-use-this + beforeChildren(stream) { stream.endIgnore(); - stream.beginFunction(variableName, this._expression.options); } // eslint-disable-next-line class-methods-use-this diff --git a/src/parser/plugins/UsePlugin.js b/src/parser/plugins/UsePlugin.js index fec23cc..24b6011 100644 --- a/src/parser/plugins/UsePlugin.js +++ b/src/parser/plugins/UsePlugin.js @@ -11,7 +11,7 @@ */ const Plugin = require('../html/Plugin'); -const TemplateReference = require('../commands/TemplateReference'); +const FileReference = require('../commands/FileReference'); const MapLiteral = require('../htl/nodes/MapLiteral'); const StringConstant = require('../htl/nodes/StringConstant'); @@ -22,9 +22,9 @@ module.exports = class UsePlugin extends Plugin { const variableName = this._signature.getVariableName(DEFAULT_VARIABLE_NAME); if (this._expression.root instanceof StringConstant) { const lib = this._expression.root.text; - stream.write(new TemplateReference( + stream.write(new FileReference( variableName, lib, [new MapLiteral(this._expression.options)], - )); + ), true); return; } throw new Error('data-sly-use only supports static references.'); diff --git a/src/runtime/Runtime.js b/src/runtime/Runtime.js index 0d23720..9d7393d 100644 --- a/src/runtime/Runtime.js +++ b/src/runtime/Runtime.js @@ -206,23 +206,23 @@ module.exports = class Runtime { return this.run(callable); } - template(name, callback) { - if (!name) { - // this is called to retrieve the template map, so that it looks like the template is - // like a function reference - return this._templates; + /** + * Registers a template. + * @param {string} id The id of the use group + * @param {string} name The name of the template function + * @param {Function} fn Template function + * @returns {object} the use group. + */ + template(id, name, fn) { + let group = this._templates[id]; + if (!group) { + group = {}; + this._templates[id] = group; } - - return name.split('.').reduce((prev, seg, idx, arr) => { - if (idx === arr.length - 1) { - // eslint-disable-next-line no-param-reassign - prev[seg] = callback; - } else { - // eslint-disable-next-line no-param-reassign - prev[seg] = prev[seg] || {}; - } - return prev[seg]; - }, this._templates); + if (name) { + group[name] = fn; + } + return group; } exec(name, value, arg0, arg1) { diff --git a/test/compiler_test.js b/test/compiler_test.js index cd722e0..9a9cd4e 100644 --- a/test/compiler_test.js +++ b/test/compiler_test.js @@ -22,7 +22,7 @@ const { JSDOM } = require('jsdom'); const Runtime = require('../src/runtime/Runtime'); const fsResourceLoader = require('../src/runtime/fsResourceLoader'); const Compiler = require('../src/compiler/Compiler'); -const TemplateLoader = require('../src/compiler/TemplateLoader.js'); +const ScriptResolver = require('../src/compiler/ScriptResolver.js'); function serializeDom(node) { if (node.doctype) { @@ -89,7 +89,7 @@ function runTests(specs, typ = '', runtimeFn = () => {}, resultFn = (ret) => ret } const compiler = new Compiler() .withDirectory(outputDir) - .withTemplateLoader(TemplateLoader([outputDir, rootProject1])) + .withScriptResolver(ScriptResolver([outputDir, rootProject1])) .withRuntimeVar(Object.keys(payload)) .withSourceFile(sourceFile) .withSourceMap(true); diff --git a/test/specs/call_spec.txt b/test/specs/call_spec.txt index 8f21bca..ae84719 100644 --- a/test/specs/call_spec.txt +++ b/test/specs/call_spec.txt @@ -24,7 +24,7 @@
Template
^^^ 37: $.dom.append($n, var_0); -46: yield $.call($.template().foo, [$n, {"param": "Template", }]); +47: yield $.call(foo, [$n, {"param": "Template", }]); # ### sightly call function with colon in name # @@ -51,7 +51,7 @@
^^^ 37: $.dom.append($n, var_0); -46: yield $.call($.template().foo, [$n, {}]); +47: yield $.call(foo, [$n, {}]); # ### sightly call function can be redeclared # @@ -82,7 +82,7 @@ ^^^ 37: $.dom.append($n, var_0); -49: yield $.call($.template().foo, [$n, {}]); +50: yield $.call(foo, [$n, {}]); # ### sightly call function can call other templates # @@ -106,9 +106,9 @@ ^^^ -39: yield $.call($.template().bar, [$n, {"a": a, }]); +39: yield $.call(bar, [$n, {"a": a, }]); 52: $.dom.append($n, var_0); -63: yield $.call($.template().foo, [$n, {"a": 123, }]); +65: yield $.call(foo, [$n, {"a": 123, }]); # ### sightly call function can call itself recursively # @@ -174,7 +174,7 @@ ^^^ 37: if (var_size1) { 49: $.dom.append($n, var_3); -51: yield $.call($.template().foo, [$n, {"pages": item["pages"], }]); -66: yield $.call($.template().foo, [$n, {"pages": bar["pages"], }]); +51: yield $.call(foo, [$n, {"pages": item["pages"], }]); +67: yield $.call(foo, [$n, {"pages": bar["pages"], }]); # ### diff --git a/test/specs/template_spec.js b/test/specs/template_spec.js index 1e11691..f33be65 100644 --- a/test/specs/template_spec.js +++ b/test/specs/template_spec.js @@ -20,5 +20,22 @@ module.exports = { }, page: { title: 'This is the title', + list: [ + { title: 'item 1', children: [] }, + { + title: 'item 2', + children: [ + { title: 'item 2.1', children: [] }, + { + title: 'item 2.2', + children: [ + { title: ' item 2.2.1', children: [] }, + ], + }, + { title: 'item 2.3', children: [] }, + ], + }, + { title: 'item 2', children: [] }, + ], }, }; diff --git a/test/specs/template_spec.txt b/test/specs/template_spec.txt index da40724..58de311 100644 --- a/test/specs/template_spec.txt +++ b/test/specs/template_spec.txt @@ -9,8 +9,8 @@ blah 1 # ### use external template lib and invoke template 2 # -
+
===
blah 2
@@ -58,13 +58,13 @@ blah Hello, world. ^^^ -40: $.dom.append($n, var_0); -51: $.dom.append($n, var_0); -62: yield $.call($.template().comps["button"], [$n, {"data": data, }]); -67: yield $.call($.template().comps["heading"], [$n, {"data": data, }]); -78: $.dom.append($n, var_0); -85: yield $.call($.template().lib["lib"], [$n, {"name": "heading", "data": component["data"], }]); -92: yield $.call($.template().lib["lib"], [$n, {"name": component["name"], "data": component["data"], }]); +42: yield $.call(comps1["button"], [$n, {"data": data, }]); +47: yield $.call(comps2["heading"], [$n, {"data": data, }]); +58: $.dom.append($n, var_0); +69: $.dom.append($n, var_0); +81: $.dom.append($n, var_0); +88: yield $.call(lib["lib"], [$n, {"name": "heading", "data": component["data"], }]); +95: yield $.call(lib["lib"], [$n, {"name": component["name"], "data": component["data"], }]); # ### template defining and calling another template # @@ -76,10 +76,10 @@ blah Hello, world. ^^^ -40: $.dom.append($n, var_0); -57: $.dom.append($n, var_0); -65: yield $.call($.template().lib["bar"], [$n, {"a": a, }]); -74: yield $.call($.template().lib["foo"], [$n, {"a": 123, }]); +46: $.dom.append($n, var_0); +55: yield $.call(lib["bar"], [$n, {"a": a, }]); +66: $.dom.append($n, var_0); +76: yield $.call(lib["foo"], [$n, {"a": 123, }]); # ### template can use global var # @@ -97,4 +97,21 @@ blah Hello, world.

Project This is the title

# +### recursive template calls +# +
+=== +
  • item 1
  • +
  • item 2
    • item 2.1
    • +
    • item 2.2
      • item 2.2.1
      • +
      +
    • +
    • item 2.3
    • +
    +
  • +
  • item 2
  • +
+
+# ### +# diff --git a/test/specs/template_spec/group.htl b/test/specs/template_spec/group.htl new file mode 100644 index 0000000..8f7623c --- /dev/null +++ b/test/specs/template_spec/group.htl @@ -0,0 +1,3 @@ + diff --git a/test/specs/template_spec/item.htl b/test/specs/template_spec/item.htl new file mode 100644 index 0000000..7cc7f5b --- /dev/null +++ b/test/specs/template_spec/item.htl @@ -0,0 +1,3 @@ + diff --git a/test/specs/template_spec/library.htl b/test/specs/template_spec/library.htl index 4e6fbee..1df9d44 100644 --- a/test/specs/template_spec/library.htl +++ b/test/specs/template_spec/library.htl @@ -1,6 +1,7 @@ - - -