diff --git a/CHANGES b/CHANGES index 953420c..cc0f1f8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ += HEAD + + * Fixed a bug with higher-order sections that prevented them from being + passed the raw text of the section from the original template. + = 0.6.0 / 31 Aug 2012 * Use JavaScript's definition of falsy when determining whether to render an diff --git a/mustache.js b/mustache.js index 579a959..16e85ba 100644 --- a/mustache.js +++ b/mustache.js @@ -219,24 +219,6 @@ var Mustache; this._partialCache = {}; }; - Renderer.prototype.compile = function (tokens, tags) { - if (typeof tokens === "string") { - tokens = parse(tokens, tags); - } - - var fn = compileTokens(tokens), - self = this; - - return function (view) { - return fn(Context.make(view), self); - }; - }; - - Renderer.prototype.compilePartial = function (name, tokens, tags) { - this._partialCache[name] = this.compile(tokens, tags); - return this._partialCache[name]; - }; - Renderer.prototype.render = function (template, view) { var fn = this._cache[template]; @@ -248,7 +230,22 @@ var Mustache; return fn(view); }; - Renderer.prototype._section = function (name, context, callback) { + Renderer.prototype.compile = function (template, tags) { + var tokens = parse(template, tags); + var render = compileTokens(tokens); + + var self = this; + return function (view) { + return render(Context.make(view), self, template); + }; + }; + + Renderer.prototype.compilePartial = function (name, template, tags) { + this._partialCache[name] = this.compile(template, tags); + return this._partialCache[name]; + }; + + Renderer.prototype._section = function (name, context, text, callback) { var value = context.lookup(name); switch (typeof value) { @@ -265,15 +262,12 @@ var Mustache; return value ? callback(context.push(value), this) : ""; case "function": - // TODO: The text should be passed to the callback plain, not rendered. - var sectionText = callback(context, this), - self = this; - + var self = this; var scopedRender = function (template) { return self.render(template, context); }; - return value.call(context.view, sectionText, scopedRender) || ""; + return value.call(context.view, text, scopedRender) || ""; default: if (value) { return callback(context, this); @@ -321,6 +315,24 @@ var Mustache; return string; }; + /** + * Calculates the bounds of the section represented by the given `token` in + * the original template by drilling down into nested sections to find the + * last token that is part of that section. Returns an array of [start, end]. + */ + function sectionBounds(token) { + var start = token.end; + var end = start; + + var tokens; + while ((tokens = token.tokens) && tokens.length) { + token = tokens[tokens.length - 1]; + end = token.end; + } + + return [start, end]; + } + /** * Low-level function that compiles the given `tokens` into a * function that accepts two arguments: a Context and a @@ -329,23 +341,28 @@ var Mustache; */ function compileTokens(tokens, returnBody) { var body = ['""']; - var token, method, escape; + var token, escape, bounds, text; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; switch (token.type) { case "#": + bounds = sectionBounds(token); + text = "t.slice(" + bounds[0] + ", " + bounds[1] + ")"; + body.push("r._section(" + quote(token.value) + ", c, " + text + ", function (c, r) {\n" + + " " + compileTokens(token.tokens, true) + "\n" + + "})"); + break; case "^": - method = (token.type === "#") ? "_section" : "_inverted"; - body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" + + body.push("r._inverted(" + quote(token.value) + ", c, function (c, r) {\n" + " " + compileTokens(token.tokens, true) + "\n" + "})"); break; case "{": case "&": case "name": - escape = token.type === "name" ? "true" : "false"; + escape = String(token.type === "name"); body.push("r._name(" + quote(token.value) + ", c, " + escape + ")"); break; case ">": @@ -368,7 +385,7 @@ var Mustache; } // For great evil! - return new Function("c, r", body); + return new Function("c, r, t", body); } function escapeTags(tags) { @@ -487,9 +504,10 @@ var Mustache; nonSpace = false; }; - var type, value, chr; + var start, type, value, chr; while (!scanner.eos()) { + start = scanner.pos; value = scanner.scanUntil(tagRes[0]); if (value) { @@ -502,7 +520,7 @@ var Mustache; nonSpace = true; } - tokens.push({type: "text", value: chr}); + tokens.push({type: "text", value: chr, start: start, end: scanner.pos}); if (chr === "\n") { stripSpace(); // Check for whitespace on the current line. @@ -510,6 +528,8 @@ var Mustache; } } + start = scanner.pos; + // Match the opening tag. if (!scanner.scan(tagRes[0])) { break; @@ -540,7 +560,7 @@ var Mustache; throw new Error("Unclosed tag at " + scanner.pos); } - tokens.push({type: type, value: value}); + tokens.push({type: type, value: value, start: start, end: scanner.pos}); if (type === "name" || type === "{" || type === "&") { nonSpace = true; diff --git a/test/_files/higher_order_sections.js b/test/_files/higher_order_sections.js index 7b52c0d..bacb0a4 100644 --- a/test/_files/higher_order_sections.js +++ b/test/_files/higher_order_sections.js @@ -3,7 +3,7 @@ helper: "To tinker?", bolder: function () { return function (text, render) { - return "" + render(text) + ' ' + this.helper; + return text + ' => ' + render(text) + ' ' + this.helper; } } }) diff --git a/test/_files/higher_order_sections.txt b/test/_files/higher_order_sections.txt index 9db786a..be50ad7 100644 --- a/test/_files/higher_order_sections.txt +++ b/test/_files/higher_order_sections.txt @@ -1 +1 @@ -Hi Tater. To tinker? +Hi {{name}}. => Hi Tater. To tinker? diff --git a/test/context_test.js b/test/context_test.js index 71c5830..167b9ca 100644 --- a/test/context_test.js +++ b/test/context_test.js @@ -1,6 +1,6 @@ -var assert = require("assert"), - vows = require("vows"), - Context = require("./../mustache").Context; +var assert = require("assert"); +var vows = require("vows"); +var Context = require("./../mustache").Context; vows.describe("Mustache.Context").addBatch({ "A Context": { diff --git a/test/parse_test.js b/test/parse_test.js index 8586a25..c54b57f 100644 --- a/test/parse_test.js +++ b/test/parse_test.js @@ -1,63 +1,78 @@ -var assert = require("assert"), - vows = require("vows"), - parse = require("./../mustache").parse; +var assert = require("assert"); +var vows = require("vows"); +var parse = require("./../mustache").parse; // A map of templates to their expected token output. var expectations = { - "{{hi}}" : [ { type: 'name', value: 'hi' } ], - "{{hi.world}}" : [ { type: 'name', value: 'hi.world' } ], - "{{hi . world}}" : [ { type: 'name', value: 'hi . world' } ], - "{{ hi}}" : [ { type: 'name', value: 'hi' } ], - "{{hi }}" : [ { type: 'name', value: 'hi' } ], - "{{ hi }}" : [ { type: 'name', value: 'hi' } ], - "{{{hi}}}" : [ { type: '{', value: 'hi' } ], - "{{!hi}}" : [ { type: '!', value: 'hi' } ], - "{{! hi}}" : [ { type: '!', value: 'hi' } ], - "{{! hi }}" : [ { type: '!', value: 'hi' } ], - "{{ !hi}}" : [ { type: '!', value: 'hi' } ], - "{{ ! hi}}" : [ { type: '!', value: 'hi' } ], - "{{ ! hi }}" : [ { type: '!', value: 'hi' } ], - "a{{hi}}" : [ { type: 'text', value: 'a' }, { type: 'name', value: 'hi' } ], - "a {{hi}}" : [ { type: 'text', value: 'a ' }, { type: 'name', value: 'hi' } ], - " a{{hi}}" : [ { type: 'text', value: ' a' }, { type: 'name', value: 'hi' } ], - " a {{hi}}" : [ { type: 'text', value: ' a ' }, { type: 'name', value: 'hi' } ], - "a{{hi}}b" : [ { type: 'text', value: 'a' }, { type: 'name', value: 'hi' }, { type: 'text', value: 'b' } ], - "a{{hi}} b" : [ { type: 'text', value: 'a' }, { type: 'name', value: 'hi' }, { type: 'text', value: ' b' } ], - "a{{hi}}b " : [ { type: 'text', value: 'a' }, { type: 'name', value: 'hi' }, { type: 'text', value: 'b ' } ], - "a\n{{hi}} b \n" : [ { type: 'text', value: 'a\n' }, { type: 'name', value: 'hi' }, { type: 'text', value: ' b \n' } ], - "a\n {{hi}} \nb" : [ { type: 'text', value: 'a\n ' }, { type: 'name', value: 'hi' }, { type: 'text', value: ' \nb' } ], - "a\n {{!hi}} \nb" : [ { type: 'text', value: 'a\n' }, { type: '!', value: 'hi' }, { type: 'text', value: 'b' } ], - "a\n{{#a}}{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}{{/a}} \nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n{{#a}}\n{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{/a}} \nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n{{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: '#', value: 'b', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: '#', value: 'b', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}} \nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [] }, { type: '#', value: 'b', tokens: [] }, { type: 'text', value: 'b' } ], - "a\n{{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [ { type: '#', value: 'b', tokens: [] } ] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [ { type: '#', value: 'b', tokens: [] } ] }, { type: 'text', value: 'b' } ], - "a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}} \nb" : [ { type: 'text', value: 'a\n' }, { type: '#', value: 'a', tokens: [ { type: '#', value: 'b', tokens: [] } ] }, { type: 'text', value: 'b' } ], - "{{>abc}}" : [ { type: '>', value: 'abc' } ], - "{{> abc }}" : [ { type: '>', value: 'abc' } ], - "{{ > abc }}" : [ { type: '>', value: 'abc' } ], - "{{=<% %>=}}" : [ { type: '=', value: '<% %>' } ], - "{{= <% %> =}}" : [ { type: '=', value: '<% %>' } ], - "{{=<% %>=}}<%={{ }}=%>" : [ { type: '=', value: '<% %>' }, { type: '=', value: '{{ }}' } ], - "{{=<% %>=}}<%hi%>" : [ { type: '=', value: '<% %>' }, { type: 'name', value: 'hi' } ], - "{{#a}}{{/a}}hi{{#b}}{{/b}}\n" : [ { type: '#', value: 'a', tokens: [] }, { type: 'text', value: 'hi' }, { type: '#', value: 'b', tokens: [] }, { type: 'text', value: '\n' } ], - "{{a}}\n{{b}}\n\n{{#c}}\n{{/c}}\n" : [ { type: 'name', value: 'a' }, { type: 'text', value: '\n' }, { type: 'name', value: 'b' }, { type: 'text', value: '\n\n' }, { type: '#', value: 'c', tokens: [] } ], + "{{hi}}" : [ [ 'name', 'hi', 0, 6 ] ], + "{{hi.world}}" : [ [ 'name', 'hi.world', 0, 12 ] ], + "{{hi . world}}" : [ [ 'name', 'hi . world', 0, 14 ] ], + "{{ hi}}" : [ [ 'name', 'hi', 0, 7 ] ], + "{{hi }}" : [ [ 'name', 'hi', 0, 7 ] ], + "{{ hi }}" : [ [ 'name', 'hi', 0, 8 ] ], + "{{{hi}}}" : [ [ '{', 'hi', 0, 8 ] ], + "{{!hi}}" : [ [ '!', 'hi', 0, 7 ] ], + "{{! hi}}" : [ [ '!', 'hi', 0, 8 ] ], + "{{! hi }}" : [ [ '!', 'hi', 0, 9 ] ], + "{{ !hi}}" : [ [ '!', 'hi', 0, 8 ] ], + "{{ ! hi}}" : [ [ '!', 'hi', 0, 9 ] ], + "{{ ! hi }}" : [ [ '!', 'hi', 0, 10 ] ], + "a{{hi}}" : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ] ], + "a {{hi}}" : [ [ 'text', 'a ', 0, 2 ], [ 'name', 'hi', 2, 8 ] ], + " a{{hi}}" : [ [ 'text', ' a', 0, 2 ], [ 'name', 'hi', 2, 8 ] ], + " a {{hi}}" : [ [ 'text', ' a ', 0, 3 ], [ 'name', 'hi', 3, 9 ] ], + "a{{hi}}b" : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b', 7, 8 ] ], + "a{{hi}} b" : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', ' b', 7, 9 ] ], + "a{{hi}}b " : [ [ 'text', 'a', 0, 1 ], [ 'name', 'hi', 1, 7 ], [ 'text', 'b ', 7, 9 ] ], + "a\n{{hi}} b \n" : [ [ 'text', 'a\n', 0, 2 ], [ 'name', 'hi', 2, 8 ], [ 'text', ' b \n', 8, 12 ] ], + "a\n {{hi}} \nb" : [ [ 'text', 'a\n ', 0, 3 ], [ 'name', 'hi', 3, 9 ], [ 'text', ' \nb', 9, 12 ] ], + "a\n {{!hi}} \nb" : [ [ 'text', 'a\n', 0, 3 ], [ '!', 'hi', 3, 10 ], [ 'text', 'b', 10, 13 ] ], + "a\n{{#a}}{{/a}}\nb" : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [] ], [ 'text', 'b', 14, 16 ] ], + "a\n {{#a}}{{/a}}\nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ 'text', 'b', 15, 17 ] ], + "a\n {{#a}}{{/a}} \nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ 'text', 'b', 15, 18 ] ], + "a\n{{#a}}\n{{/a}}\nb" : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [] ], [ 'text', 'b', 15, 17 ] ], + "a\n {{#a}}\n{{/a}}\nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ 'text', 'b', 16, 18 ] ], + "a\n {{#a}}\n{{/a}} \nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ 'text', 'b', 16, 19 ] ], + "a\n{{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb" : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [] ], [ '#', 'b', 16, 22, [] ], [ 'text', 'b', 29, 31 ] ], + "a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}}\nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ '#', 'b', 17, 23, [] ], [ 'text', 'b', 30, 32 ] ], + "a\n {{#a}}\n{{/a}}\n{{#b}}\n{{/b}} \nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [] ], [ '#', 'b', 17, 23, [] ], [ 'text', 'b', 30, 33 ] ], + "a\n{{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb" : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [ [ '#', 'b', 9, 15, [] ] ] ], [ 'text', 'b', 29, 31 ] ], + "a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [] ] ] ], [ 'text', 'b', 30, 32 ] ], + "a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}} \nb" : [ [ 'text', 'a\n', 0, 3 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [] ] ] ], [ 'text', 'b', 30, 33 ] ], + "{{>abc}}" : [ [ '>', 'abc', 0, 8 ] ], + "{{> abc }}" : [ [ '>', 'abc', 0, 10 ] ], + "{{ > abc }}" : [ [ '>', 'abc', 0, 11 ] ], + "{{=<% %>=}}" : [ [ '=', '<% %>', 0, 11 ] ], + "{{= <% %> =}}" : [ [ '=', '<% %>', 0, 13 ] ], + "{{=<% %>=}}<%={{ }}=%>" : [ [ '=', '<% %>', 0, 11 ], [ '=', '{{ }}', 11, 22 ] ], + "{{=<% %>=}}<%hi%>" : [ [ '=', '<% %>', 0, 11 ], [ 'name', 'hi', 11, 17 ] ], + "{{#a}}{{/a}}hi{{#b}}{{/b}}\n" : [ [ '#', 'a', 0, 6, [] ], [ 'text', 'hi', 12, 14 ], [ '#', 'b', 14, 20, [] ], [ 'text', '\n', 26, 27 ] ], + "{{a}}\n{{b}}\n\n{{#c}}\n{{/c}}\n" : [ [ 'name', 'a', 0, 5 ], [ 'text', '\n', 5, 6 ], [ 'name', 'b', 6, 11 ], [ 'text', '\n\n', 11, 13 ], [ '#', 'c', 13, 19, [] ] ], "{{#foo}}\n {{#a}}\n {{b}}\n {{/a}}\n{{/foo}}\n" - : [ { type: "#", value: "foo", tokens: [ { type: "#", value: "a", tokens: [ { type: "text", value: " " }, { type: "name", value: "b" }, { type: "text", value: "\n" } ] } ] } ] + : [ [ '#', 'foo', 0, 8, [ [ '#', 'a', 11, 17, [ [ 'text', ' ', 17, 22 ], [ 'name', 'b', 22, 27 ], [ 'text', '\n', 27, 30 ] ] ] ] ] ] }; +function makeToken(tokenArray) { + var token = { + type: tokenArray[0], + value: tokenArray[1], + start: tokenArray[2], + end: tokenArray[3] + }; + + if (tokenArray[4]) { + token.tokens = tokenArray[4].map(makeToken); + } + + return token; +} + var spec = {}; for (var template in expectations) { (function (template, tokens) { spec["knows how to parse " + JSON.stringify(template)] = function () { - assert.deepEqual(parse(template), tokens); + assert.deepEqual(parse(template), tokens.map(makeToken)); }; })(template, expectations[template]); } diff --git a/test/render_test.js b/test/render_test.js index 9638638..859cb71 100644 --- a/test/render_test.js +++ b/test/render_test.js @@ -1,7 +1,7 @@ -var fs = require("fs"), - path = require("path"), - assert = require("assert"), - vows = require("vows"); +var fs = require("fs"); +var path = require("path"); +var assert = require("assert"); +var vows = require("vows"); var Mustache = require(path.join(__dirname, "..", "mustache")); var _files = path.join(__dirname, "_files"); diff --git a/test/scanner_test.js b/test/scanner_test.js index 7c03a2a..3e9277e 100644 --- a/test/scanner_test.js +++ b/test/scanner_test.js @@ -1,6 +1,6 @@ -var assert = require("assert"), - vows = require("vows"), - Scanner = require("./../mustache").Scanner; +var assert = require("assert"); +var vows = require("vows"); +var Scanner = require("./../mustache").Scanner; vows.describe("Mustache.Scanner").addBatch({ "A Scanner": {