From 99afe5fea7a0f76abfcb69a201435206d13aed9c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Thu, 4 Nov 2010 18:23:41 -0700 Subject: [PATCH 01/34] don't double render --- mustache.js | 61 +++++++++++++++++++++++++++---------------- test/mustache_spec.rb | 26 ++++++++++++++---- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/mustache.js b/mustache.js index 35a8976..b2ebcba 100644 --- a/mustache.js +++ b/mustache.js @@ -35,12 +35,28 @@ var Mustache = function() { } template = this.render_pragmas(template); + var html = this.render_section(template, context, partials); - if(in_recursion) { - return this.render_tags(html, context, partials, in_recursion); - } - this.render_tags(html, context, partials, in_recursion); + var stillNeedsToRender = (html === template); + + if (stillNeedsToRender) { + if (in_recursion) { + return this.render_tags(html, context, partials, true); + } + + this.render_tags(html, context, partials, false); + } else { + if(in_recursion) { + return html; + } else { + var lines = html.split("\n"); + for (var i = 0; i < lines.length; i++) { + this.send(lines[i]); + } + return; + } + } }, /* @@ -66,7 +82,7 @@ var Mustache = function() { this.ctag); return template.replace(regex, function(match, pragma, options) { if(!that.pragmas_implemented[pragma]) { - throw({message: + throw({message: "This implementation of mustache doesn't understand the '" + pragma + "' pragma"}); } @@ -103,39 +119,40 @@ var Mustache = function() { } var that = this; - // CSW - Added "+?" so it finds the tighest bound, not the widest - var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + - "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + - "\\s*", "mg"); + var regex = new RegExp("(?:^([\\s\\S]*?))?" + this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\3\\s*" + this.ctag + "\\s*" + + "([\\s\\S]*?)(?=(?:" + this.otag + "(?:\\^|\\#)\\s*(?:.+)\\s*" + this.ctag + ")|$)", "mg"); // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function(match, type, name, content) { + return template.replace(regex, function(match, before, type, name, content, after) { + var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", + renderedAfter = after ? that.render_tags(after, context, partials, true) : ""; + var value = that.find(name, context); if(type == "^") { // inverted section if(!value || that.is_array(value) && value.length === 0) { // false or empty list, render it - return that.render(content, context, partials, true); + return renderedBefore + that.render(content, context, partials, true) + renderedAfter; } else { - return ""; + return renderedBefore + "" + renderedAfter; } } else if(type == "#") { // normal section if(that.is_array(value)) { // Enumerable, Let's loop! - return that.map(value, function(row) { - return that.render(content, that.create_context(row), - partials, true); - }).join(""); + return renderedBefore + that.map(value, function(row) { + return that.render(content, that.create_context(row), partials, true); + }).join("") + renderedAfter; } else if(that.is_object(value)) { // Object, Use it as subcontext! - return that.render(content, that.create_context(value), - partials, true); + return renderedBefore + that.render(content, that.create_context(value), + partials, true) + renderedAfter; } else if(typeof value === "function") { // higher order section - return value.call(context, content, function(text) { + return renderedBefore + value.call(context, content, function(text) { return that.render(text, context, partials, true); - }); + }) + renderedAfter; } else if(value) { // boolean section - return that.render(content, context, partials, true); + return renderedBefore + that.render(content, context, partials, true) + renderedAfter; } else { - return ""; + return renderedBefore + "" + renderedAfter; } } }); diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 9ede6c8..c4900a9 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -49,13 +49,29 @@ describe "mustache" do JS run_js(js).should == "\n" end - + + it "should not double-render" do + js = <<-JS + #{@mustache} + var template = "{{#foo}}{{bar}}{{/foo}}"; + var ctx = { + foo: true, + bar: "{{win}}", + win: "FAIL" + }; + + print(Mustache.to_html(template, ctx)) + JS + + run_js(js).strip.should == "{{win}}" + end + non_partials.each do |testname| - describe testname do + describe testname do it "should generate the correct html" do view, template, expect = load_test(__DIR__, testname) - + runner = <<-JS try { #{@mustache} @@ -101,7 +117,7 @@ describe "mustache" do describe testname do it "should generate the correct html" do - view, template, partial, expect = + view, template, partial, expect = load_test(__DIR__, testname, true) runner = <<-JS @@ -116,7 +132,7 @@ describe "mustache" do print('ERROR: ' + e.message); } JS - + run_js(runner).should == expect end it "should sendFun the correct html" do From a7f71e16356f60a5010a0eb9d6a6abf807ec2428 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Thu, 4 Nov 2010 19:15:54 -0700 Subject: [PATCH 02/34] i18n twitter stuff --- mustache.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/mustache.js b/mustache.js index b2ebcba..97504b1 100644 --- a/mustache.js +++ b/mustache.js @@ -13,7 +13,8 @@ var Mustache = function() { pragmas: {}, buffer: [], pragmas_implemented: { - "IMPLICIT-ITERATOR": true + "IMPLICIT-ITERATOR": true, + "TRANSLATION-HINT": true }, context: {}, @@ -34,8 +35,15 @@ var Mustache = function() { } } + // Branching or moving down the partial stack, save any translation mode info. + if (this.pragmas['TRANSLATION-HINT']) { + context['_mode'] = this.pragmas['TRANSLATION-HINT']['mode']; + } + template = this.render_pragmas(template); + template = this.render_i18n(template, context, partials); + var html = this.render_section(template, context, partials); var stillNeedsToRender = (html === template); @@ -110,6 +118,27 @@ var Mustache = function() { return this.render(partials[name], context[name], partials, true); }, + render_i18n: function(html, context, partials) { + if (html.indexOf(this.otag + "_i") == -1) { + return html; + } + var that = this; + var regex = new RegExp(this.otag + "\\_i" + this.ctag + + "\\s*([\\s\\S]+?)" + this.otag + "\\/i" + this.ctag, "mg"); + + // for each {{_i}}{{/i}} section do... + return html.replace(regex, function(match, content) { + var translation_mode = undefined; + if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"]['mode']) { + translation_mode = { _mode: that.pragmas["TRANSLATION-HINT"]['mode'] }; + } else if (context['_mode']) { + translation_mode = { _mode: context['_mode'] }; + } + + return that.render(_(content, translation_mode), context, partials, true); + }); + }, + /* Renders inverted (^) and normal (#) sections */ @@ -333,7 +362,7 @@ var Mustache = function() { if(send_fun) { renderer.send = send_fun; } - renderer.render(template, view, partials); + renderer.render(template, view || {}, partials); if(!send_fun) { return renderer.buffer.join("\n"); } From 2fc2b8c0d0f2f8ecbd0ba326def817d6c4440a18 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Thu, 4 Nov 2010 19:22:23 -0700 Subject: [PATCH 03/34] don't make the thing a multi-line --- mustache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mustache.js b/mustache.js index 97504b1..bb9f113 100644 --- a/mustache.js +++ b/mustache.js @@ -150,7 +150,7 @@ var Mustache = function() { var that = this; var regex = new RegExp("(?:^([\\s\\S]*?))?" + this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\3\\s*" + this.ctag + "\\s*" + - "([\\s\\S]*?)(?=(?:" + this.otag + "(?:\\^|\\#)\\s*(?:.+)\\s*" + this.ctag + ")|$)", "mg"); + "([\\s\\S]*?)(?=(?:" + this.otag + "(?:\\^|\\#)\\s*(?:.+)\\s*" + this.ctag + ")|$)", "g"); // for each {{#foo}}{{/foo}} section do... return template.replace(regex, function(match, before, type, name, content, after) { From 6a9da0581a741f55e7d636a18e9abbece940b598 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 5 Nov 2010 13:16:53 -0700 Subject: [PATCH 04/34] stubbed _() for tests, and basic i18n test --- test/mustache_spec.rb | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index c4900a9..a49603d 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -24,12 +24,23 @@ end describe "mustache" do before(:all) do - @mustache = File.read(__DIR__ + "/../mustache.js") + mustache = File.read(__DIR__ + "/../mustache.js") + stubbed_gettext = <<-JS + // Stubbed gettext translation method for {{_i}}{{/i}} tags in Mustache. + function _(text) { + return text; + } + JS + + @boilerplate = <<-JS + #{mustache} + #{stubbed_gettext} + JS end it "should return the same when invoked multiple times" do js = <<-JS - #{@mustache} + #{@boilerplate} Mustache.to_html("x") print(Mustache.to_html("x")); JS @@ -39,7 +50,7 @@ describe "mustache" do it "should clear the context after each run" do js = <<-JS - #{@mustache} + #{@boilerplate} Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{x: 1}]}) try { print(Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{}]})); @@ -52,7 +63,7 @@ describe "mustache" do it "should not double-render" do js = <<-JS - #{@mustache} + #{@boilerplate} var template = "{{#foo}}{{bar}}{{/foo}}"; var ctx = { foo: true, @@ -66,6 +77,21 @@ describe "mustache" do run_js(js).strip.should == "{{win}}" end + it "should work with i18n" do + js = <<-JS + #{@boilerplate} + + var template = "{{_i}}foo {{bar}}{{/i}}"; + var ctx = { + bar: "BAR" + }; + + print(Mustache.to_html(template, ctx)); + JS + + run_js(js).strip.should == "foo BAR" + end + non_partials.each do |testname| describe testname do it "should generate the correct html" do @@ -74,7 +100,7 @@ describe "mustache" do runner = <<-JS try { - #{@mustache} + #{@boilerplate} #{view} var template = #{template}; var result = Mustache.to_html(template, #{testname}); @@ -92,7 +118,7 @@ describe "mustache" do runner = <<-JS try { - #{@mustache} + #{@boilerplate} #{view} var chunks = []; var sendFun = function(chunk) { @@ -122,7 +148,7 @@ describe "mustache" do runner = <<-JS try { - #{@mustache} + #{@boilerplate} #{view} var template = #{template}; var partials = {"partial": #{partial}}; @@ -142,7 +168,7 @@ describe "mustache" do runner = <<-JS try { - #{@mustache} + #{@boilerplate} #{view}; var template = #{template}; var partials = {"partial": #{partial}}; From 29cbcf719d55eb680b34714259bd0b16aeba53c5 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 5 Nov 2010 15:00:40 -0700 Subject: [PATCH 05/34] twitter version --- lib/package.json | 2 +- mustache.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/package.json b/lib/package.json index 6325b4e..4a4cbb1 100644 --- a/lib/package.json +++ b/lib/package.json @@ -3,6 +3,6 @@ "author": "http://mustache.github.com/", "description": "{{ mustache }} in JavaScript — Logic-less templates.", "keywords": ["template"], - "version": "0.3.1-dev", + "version": "0.3.1-dev-twitter", "main": "./mustache" } diff --git a/mustache.js b/mustache.js index bb9f113..0e9719c 100644 --- a/mustache.js +++ b/mustache.js @@ -352,7 +352,7 @@ var Mustache = function() { return({ name: "mustache.js", - version: "0.3.1-dev", + version: "0.3.1-dev-twitter", /* Turns a template and view into HTML From 5dc7c74e9bd6e7bb274eac5c5a3221459bbaca85 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 8 Nov 2010 18:00:27 -0800 Subject: [PATCH 06/34] failing tests --- examples/two_sections.html | 7 +++++++ examples/two_sections.js | 1 + examples/two_sections.txt | 3 +++ test/mustache_spec.rb | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 examples/two_sections.html create mode 100644 examples/two_sections.js create mode 100644 examples/two_sections.txt diff --git a/examples/two_sections.html b/examples/two_sections.html new file mode 100644 index 0000000..cc54add --- /dev/null +++ b/examples/two_sections.html @@ -0,0 +1,7 @@ +{{#show_following}} + +{{/show_following}} +{{#show_following}} + +{{/show_following}} + diff --git a/examples/two_sections.js b/examples/two_sections.js new file mode 100644 index 0000000..8546f64 --- /dev/null +++ b/examples/two_sections.js @@ -0,0 +1 @@ +var two_sections = {}; \ No newline at end of file diff --git a/examples/two_sections.txt b/examples/two_sections.txt new file mode 100644 index 0000000..c634376 --- /dev/null +++ b/examples/two_sections.txt @@ -0,0 +1,3 @@ + BAR bar + BAR bar + BAR bar diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index a49603d..6656703 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -92,6 +92,25 @@ describe "mustache" do run_js(js).strip.should == "foo BAR" end + it "should not suck" do + js = <<-JS + try{ + #{@boilerplate} + + print(Mustache.to_html("{{#foo}}\\ +{{/foo}}\\ +{{#bar}}\\ +{{/bar}}\\ +", {})); + } catch(e) { + print(e); + } + JS + + run_js(js).should == "foanfs" + end + + non_partials.each do |testname| describe testname do it "should generate the correct html" do From d5e54b409a5ccf9c555b3af5d9d28f7114fc8e46 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 29 Nov 2010 14:17:29 -0800 Subject: [PATCH 07/34] works with empty sections --- examples/two_sections.html | 14 +++++++------- examples/two_sections.js | 5 ++++- examples/two_sections.txt | 6 +++--- mustache.js | 2 +- test/mustache_spec.rb | 10 +++------- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/two_sections.html b/examples/two_sections.html index cc54add..356ea66 100644 --- a/examples/two_sections.html +++ b/examples/two_sections.html @@ -1,7 +1,7 @@ -{{#show_following}} - -{{/show_following}} -{{#show_following}} - -{{/show_following}} - +{{#foo}} + BAR +{{/foo}} +BAR +{{^bar}} + BAR +{{/bar}} diff --git a/examples/two_sections.js b/examples/two_sections.js index 8546f64..bbd9cb9 100644 --- a/examples/two_sections.js +++ b/examples/two_sections.js @@ -1 +1,4 @@ -var two_sections = {}; \ No newline at end of file +var two_sections = { + foo: true, + bar: false +}; \ No newline at end of file diff --git a/examples/two_sections.txt b/examples/two_sections.txt index c634376..8a62495 100644 --- a/examples/two_sections.txt +++ b/examples/two_sections.txt @@ -1,3 +1,3 @@ - BAR bar - BAR bar - BAR bar + BAR +BAR + BAR diff --git a/mustache.js b/mustache.js index 0e9719c..3eb294d 100644 --- a/mustache.js +++ b/mustache.js @@ -149,7 +149,7 @@ var Mustache = function() { var that = this; var regex = new RegExp("(?:^([\\s\\S]*?))?" + this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + - "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\3\\s*" + this.ctag + "\\s*" + + "\n*([\\s\\S]*?)" + this.otag + "\\/\\s*\\3\\s*" + this.ctag + "\\s*" + "([\\s\\S]*?)(?=(?:" + this.otag + "(?:\\^|\\#)\\s*(?:.+)\\s*" + this.ctag + ")|$)", "g"); // for each {{#foo}}{{/foo}} section do... diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 6656703..b4067a3 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -92,22 +92,18 @@ describe "mustache" do run_js(js).strip.should == "foo BAR" end - it "should not suck" do + it "should work with empty sections" do js = <<-JS try{ #{@boilerplate} - print(Mustache.to_html("{{#foo}}\\ -{{/foo}}\\ -{{#bar}}\\ -{{/bar}}\\ -", {})); + print(Mustache.to_html("{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}", {})); } catch(e) { print(e); } JS - run_js(js).should == "foanfs" + run_js(js).strip.should == "foo" end From b15c6fb8421e764cd83443dc3fd660aedf8fc11c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 30 Nov 2010 13:27:13 -0800 Subject: [PATCH 08/34] tests for brokenness, now in Rhino too --- examples/two_sections.html | 5 +- examples/two_sections.js | 5 +- examples/two_sections.txt | 4 +- test/mustache_spec.rb | 347 ++++++++++++++++++++----------------- 4 files changed, 192 insertions(+), 169 deletions(-) diff --git a/examples/two_sections.html b/examples/two_sections.html index 356ea66..a4b9f2a 100644 --- a/examples/two_sections.html +++ b/examples/two_sections.html @@ -1,7 +1,4 @@ {{#foo}} - BAR {{/foo}} -BAR -{{^bar}} - BAR +{{#bar}} {{/bar}} diff --git a/examples/two_sections.js b/examples/two_sections.js index bbd9cb9..8546f64 100644 --- a/examples/two_sections.js +++ b/examples/two_sections.js @@ -1,4 +1 @@ -var two_sections = { - foo: true, - bar: false -}; \ No newline at end of file +var two_sections = {}; \ No newline at end of file diff --git a/examples/two_sections.txt b/examples/two_sections.txt index 8a62495..8b13789 100644 --- a/examples/two_sections.txt +++ b/examples/two_sections.txt @@ -1,3 +1 @@ - BAR -BAR - BAR + diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index b4067a3..32c75e3 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -23,191 +23,222 @@ def load_test(dir, name, partial=false) end describe "mustache" do - before(:all) do - mustache = File.read(__DIR__ + "/../mustache.js") - stubbed_gettext = <<-JS - // Stubbed gettext translation method for {{_i}}{{/i}} tags in Mustache. - function _(text) { - return text; - } - JS - - @boilerplate = <<-JS - #{mustache} - #{stubbed_gettext} - JS - end - - it "should return the same when invoked multiple times" do - js = <<-JS - #{@boilerplate} - Mustache.to_html("x") - print(Mustache.to_html("x")); - JS - run_js(js).should == "x\n" - - end - - it "should clear the context after each run" do - js = <<-JS - #{@boilerplate} - Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{x: 1}]}) - try { - print(Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{}]})); - } catch(e) { - print('ERROR: ' + e.message); - } - JS - run_js(js).should == "\n" - end - it "should not double-render" do - js = <<-JS - #{@boilerplate} - var template = "{{#foo}}{{bar}}{{/foo}}"; - var ctx = { - foo: true, - bar: "{{win}}", - win: "FAIL" - }; + shared_examples_for "Mustache rendering" do + + before(:all) do + mustache = File.read(__DIR__ + "/../mustache.js") + stubbed_gettext = <<-JS + // Stubbed gettext translation method for {{_i}}{{/i}} tags in Mustache. + function _(text) { + return text; + } + JS + + @boilerplate = <<-JS + #{mustache} + #{stubbed_gettext} + JS + end - print(Mustache.to_html(template, ctx)) - JS + it "should return the same when invoked multiple times" do + js = <<-JS + #{@boilerplate} + Mustache.to_html("x") + print(Mustache.to_html("x")); + JS + run_js(@run_js, js).should == "x\n" - run_js(js).strip.should == "{{win}}" - end + end - it "should work with i18n" do - js = <<-JS - #{@boilerplate} + it "should clear the context after each run" do + js = <<-JS + #{@boilerplate} + Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{x: 1}]}) + try { + print(Mustache.to_html("{{#list}}{{x}}{{/list}}", {list: [{}]})); + } catch(e) { + print('ERROR: ' + e.message); + } + JS + run_js(@run_js, js).should == "\n" + end - var template = "{{_i}}foo {{bar}}{{/i}}"; - var ctx = { - bar: "BAR" - }; + it "should not double-render" do + js = <<-JS + #{@boilerplate} + var template = "{{#foo}}{{bar}}{{/foo}}"; + var ctx = { + foo: true, + bar: "{{win}}", + win: "FAIL" + }; - print(Mustache.to_html(template, ctx)); - JS + print(Mustache.to_html(template, ctx)) + JS - run_js(js).strip.should == "foo BAR" - end + run_js(@run_js, js).strip.should == "{{win}}" + end - it "should work with empty sections" do - js = <<-JS - try{ - #{@boilerplate} + it "should work with i18n" do + js = <<-JS + #{@boilerplate} - print(Mustache.to_html("{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}", {})); - } catch(e) { - print(e); - } - JS + var template = "{{_i}}foo {{bar}}{{/i}}"; + var ctx = { + bar: "BAR" + }; - run_js(js).strip.should == "foo" - end + print(Mustache.to_html(template, ctx)); + JS + run_js(@run_js, js).strip.should == "foo BAR" + end - non_partials.each do |testname| - describe testname do - it "should generate the correct html" do + it "should work with empty sections" do + js = <<-JS + try{ + #{@boilerplate} - view, template, expect = load_test(__DIR__, testname) + print(Mustache.to_html("{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}", {})); + } catch(e) { + print(e); + } + JS - runner = <<-JS - try { - #{@boilerplate} - #{view} - var template = #{template}; - var result = Mustache.to_html(template, #{testname}); - print(result); - } catch(e) { - print('ERROR: ' + e.message); - } - JS + run_js(@run_js, js).strip.should == "foo" + end - run_js(runner).should == expect - end - it "should sendFun the correct html" do - - view, template, expect = load_test(__DIR__, testname) - - runner = <<-JS - try { - #{@boilerplate} - #{view} - var chunks = []; - var sendFun = function(chunk) { - if (chunk != "") { - chunks.push(chunk); + non_partials.each do |testname| + describe testname do + it "should generate the correct html" do + + view, template, expect = load_test(__DIR__, testname) + + runner = <<-JS + try { + #{@boilerplate} + #{view} + var template = #{template}; + var result = Mustache.to_html(template, #{testname}); + print(result); + } catch(e) { + print('ERROR: ' + e.message); + } + JS + + run_js(@run_js, runner).should == expect + end + it "should sendFun the correct html" do + + view, template, expect = load_test(__DIR__, testname) + + runner = <<-JS + try { + #{@boilerplate} + #{view} + var chunks = []; + var sendFun = function(chunk) { + if (chunk != "") { + chunks.push(chunk); + } } + var template = #{template}; + Mustache.to_html(template, #{testname}, null, sendFun); + print(chunks.join("\\n")); + } catch(e) { + print('ERROR: ' + e.message); } - var template = #{template}; - Mustache.to_html(template, #{testname}, null, sendFun); - print(chunks.join("\\n")); - } catch(e) { - print('ERROR: ' + e.message); - } - JS - - run_js(runner).strip.should == expect.strip + JS + + run_js(@run_js, runner).strip.should == expect.strip + end end end - end - partials.each do |testname| - describe testname do - it "should generate the correct html" do - - view, template, partial, expect = - load_test(__DIR__, testname, true) - - runner = <<-JS - try { - #{@boilerplate} - #{view} - var template = #{template}; - var partials = {"partial": #{partial}}; - var result = Mustache.to_html(template, partial_context, partials); - print(result); - } catch(e) { - print('ERROR: ' + e.message); - } - JS - - run_js(runner).should == expect - end - it "should sendFun the correct html" do - - view, template, partial, expect = - load_test(__DIR__, testname, true) - - runner = <<-JS - try { - #{@boilerplate} - #{view}; - var template = #{template}; - var partials = {"partial": #{partial}}; - var chunks = []; - var sendFun = function(chunk) { - if (chunk != "") { - chunks.push(chunk); + partials.each do |testname| + describe testname do + it "should generate the correct html" do + + view, template, partial, expect = + load_test(__DIR__, testname, true) + + runner = <<-JS + try { + #{@boilerplate} + #{view} + var template = #{template}; + var partials = {"partial": #{partial}}; + var result = Mustache.to_html(template, partial_context, partials); + print(result); + } catch(e) { + print('ERROR: ' + e.message); + } + JS + + run_js(@run_js, runner).should == expect + end + it "should sendFun the correct html" do + + view, template, partial, expect = + load_test(__DIR__, testname, true) + + runner = <<-JS + try { + #{@boilerplate} + #{view}; + var template = #{template}; + var partials = {"partial": #{partial}}; + var chunks = []; + var sendFun = function(chunk) { + if (chunk != "") { + chunks.push(chunk); + } } + Mustache.to_html(template, partial_context, partials, sendFun); + print(chunks.join("\\n")); + } catch(e) { + print('ERROR: ' + e.message); } - Mustache.to_html(template, partial_context, partials, sendFun); - print(chunks.join("\\n")); - } catch(e) { - print('ERROR: ' + e.message); - } - JS - - run_js(runner).strip.should == expect.strip + JS + + run_js(@run_js, runner).strip.should == expect.strip + end end end end - def run_js(js) + context "running in JavaScriptCore (WebKit, Safari)" do + before(:each) do + @run_js = :run_js_jsc + end + it_should_behave_like "Mustache rendering" + end + + context "running in Rhino (Mozilla)" do + before(:each) do + @run_js = :run_js_rhino + end + + it_should_behave_like "Mustache rendering" + end + + def run_js(runner, js) + send(runner, js) + # run_js_rhino(js) + # js_jsc = run_js_jsc(js) + # js_rhino = run_js_rhino(js) + # return js_jsc unless js_jsc != js_rhino + end + + def run_js_jsc(js) + File.open("runner.js", 'w') {|f| f << js} + `jsc runner.js` + end + + def run_js_rhino(js) File.open("runner.js", 'w') {|f| f << js} - `js runner.js` + `java org.mozilla.javascript.tools.shell.Main runner.js` end end From 58dfd4f2190698bfb89ac003328454e969a57a09 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 30 Nov 2010 13:45:04 -0800 Subject: [PATCH 09/34] finally fixed movember --- mustache.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/mustache.js b/mustache.js index 3eb294d..58f06e6 100644 --- a/mustache.js +++ b/mustache.js @@ -148,14 +148,32 @@ var Mustache = function() { } var that = this; - var regex = new RegExp("(?:^([\\s\\S]*?))?" + this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + - "\n*([\\s\\S]*?)" + this.otag + "\\/\\s*\\3\\s*" + this.ctag + "\\s*" + - "([\\s\\S]*?)(?=(?:" + this.otag + "(?:\\^|\\#)\\s*(?:.+)\\s*" + this.ctag + ")|$)", "g"); + + // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder + var regex = new RegExp( + "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) + + this.otag + // {{ + "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) + this.ctag + // }} + + "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped + + this.otag + // {{ + "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). + this.ctag + // }} + + "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. + + "g"); // for each {{#foo}}{{/foo}} section do... return template.replace(regex, function(match, before, type, name, content, after) { + // before contains only tags, no sections var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", - renderedAfter = after ? that.render_tags(after, context, partials, true) : ""; + + // after may contain both sections and tags, so use full rendering function + renderedAfter = after ? that.render(after, context, partials, true) : ""; var value = that.find(name, context); if(type == "^") { // inverted section From f3a9e4f3956e9e68f1a9bba89ca115627b41446f Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 30 Nov 2010 14:09:44 -0800 Subject: [PATCH 10/34] don't escape ' temporarily --- mustache.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mustache.js b/mustache.js index 58f06e6..6c109b5 100644 --- a/mustache.js +++ b/mustache.js @@ -313,7 +313,8 @@ var Mustache = function() { case "&": return "&"; case "\\": return "\\\\"; case '"': return '"'; - case "'": return '''; + // leaving this out temporarily, will need to fix twitter to work with this (good) change + // case "'": return '''; case "<": return "<"; case ">": return ">"; default: return s; From 35245c57d4b2348809656580abe9423dbce82cbc Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 30 Nov 2010 17:10:08 -0800 Subject: [PATCH 11/34] small fixes --- mustache.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mustache.js b/mustache.js index 6c109b5..a9fa959 100644 --- a/mustache.js +++ b/mustache.js @@ -46,9 +46,7 @@ var Mustache = function() { var html = this.render_section(template, context, partials); - var stillNeedsToRender = (html === template); - - if (stillNeedsToRender) { + if (html === template) { if (in_recursion) { return this.render_tags(html, context, partials, true); } @@ -173,7 +171,7 @@ var Mustache = function() { var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", // after may contain both sections and tags, so use full rendering function - renderedAfter = after ? that.render(after, context, partials, true) : ""; + renderedAfter = after ? that.render_section(after, context, partials, true) : ""; var value = that.find(name, context); if(type == "^") { // inverted section From 6f3bdedd221709b7241ad9c61be36653606b316c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 30 Nov 2010 19:39:51 -0800 Subject: [PATCH 12/34] back to render instead of render_section --- mustache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mustache.js b/mustache.js index a9fa959..8b4ba03 100644 --- a/mustache.js +++ b/mustache.js @@ -171,7 +171,7 @@ var Mustache = function() { var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", // after may contain both sections and tags, so use full rendering function - renderedAfter = after ? that.render_section(after, context, partials, true) : ""; + renderedAfter = after ? that.render(after, context, partials, true) : ""; var value = that.find(name, context); if(type == "^") { // inverted section From 158f4278370944877d886c2f92a258dd2c6e35a2 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Thu, 2 Dec 2010 16:30:33 -0800 Subject: [PATCH 13/34] reenabled ' --- mustache.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mustache.js b/mustache.js index 8b4ba03..a85afd0 100644 --- a/mustache.js +++ b/mustache.js @@ -311,8 +311,7 @@ var Mustache = function() { case "&": return "&"; case "\\": return "\\\\"; case '"': return '"'; - // leaving this out temporarily, will need to fix twitter to work with this (good) change - // case "'": return '''; + case "'": return '''; case "<": return "<"; case ">": return ">"; default: return s; From 8a0a4a3f3bd037a378dc83057b8aff4d887bad5c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 13:01:31 -0800 Subject: [PATCH 14/34] added i18n to readme --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b013dd6..0c7a73d 100644 --- a/README.md +++ b/README.md @@ -199,11 +199,34 @@ will tell mustache.js to look for a object in the context's property `winnings`. It will then use that object as the context for the template found in `partials` for `winnings`. +## Internationalization + +mustache.js supports i18n using the `{{_i}}{{/i}}` tags. When mustache.js encounters +an internationalized section, it will call out to the standard global gettext function `_()` with the tag contents for a +translation _before_ any rendering is done. For example: + + var template = "{{_i}}{{name}} is using mustache.js!{{/i}}" + + var view = { + name: "Matt" + }; + + var translationTable = { + // Welsh, according to Google Translate + "{{name}} is using mustache.js!": "Mae {{name}} yn defnyddio mustache.js!" + }; + + function _(text) { + return translationTable[text] || text; + } + + alert(Mustache.to_html(template, view)); + // alerts "Mae Matt yn defnyddio mustache.js!" ## Escaping mustache.js does escape all values when using the standard double mustache -syntax. Characters which will be escaped: `& \ " < >`. To disable escaping, +syntax. Characters which will be escaped: `& \ " ' < >`. To disable escaping, simply use triple mustaches like `{{{unescaped_variable}}}`. Example: Using `{{variable}}` inside a template for `5 > 2` will result in `5 > 2`, where as the usage of `{{{variable}}}` will result in `5 > 2`. From 373e2639739549c076cb6278b5b3b1e3e10a4dc4 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 13:09:50 -0800 Subject: [PATCH 15/34] moved new tests into new examples/* files --- examples/double_render.html | 1 + examples/double_render.js | 5 ++++ examples/double_render.txt | 1 + examples/empty_sections.html | 1 + examples/empty_sections.js | 1 + examples/empty_sections.txt | 1 + examples/i18n.html | 1 + examples/i18n.js | 3 +++ examples/i18n.txt | 1 + test/mustache_spec.rb | 45 ------------------------------------ 10 files changed, 15 insertions(+), 45 deletions(-) create mode 100644 examples/double_render.html create mode 100644 examples/double_render.js create mode 100644 examples/double_render.txt create mode 100644 examples/empty_sections.html create mode 100644 examples/empty_sections.js create mode 100644 examples/empty_sections.txt create mode 100644 examples/i18n.html create mode 100644 examples/i18n.js create mode 100644 examples/i18n.txt diff --git a/examples/double_render.html b/examples/double_render.html new file mode 100644 index 0000000..4500fd7 --- /dev/null +++ b/examples/double_render.html @@ -0,0 +1 @@ +{{#foo}}{{bar}}{{/foo}} diff --git a/examples/double_render.js b/examples/double_render.js new file mode 100644 index 0000000..24125dc --- /dev/null +++ b/examples/double_render.js @@ -0,0 +1,5 @@ +var double_render = { + foo: true, + bar: "{{win}}", + win: "FAIL" +}; \ No newline at end of file diff --git a/examples/double_render.txt b/examples/double_render.txt new file mode 100644 index 0000000..b6e652d --- /dev/null +++ b/examples/double_render.txt @@ -0,0 +1 @@ +{{win}} diff --git a/examples/empty_sections.html b/examples/empty_sections.html new file mode 100644 index 0000000..b6065db --- /dev/null +++ b/examples/empty_sections.html @@ -0,0 +1 @@ +{{#foo}}{{/foo}}foo{{#bar}}{{/bar}} diff --git a/examples/empty_sections.js b/examples/empty_sections.js new file mode 100644 index 0000000..6e50514 --- /dev/null +++ b/examples/empty_sections.js @@ -0,0 +1 @@ +var empty_sections = {}; diff --git a/examples/empty_sections.txt b/examples/empty_sections.txt new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/examples/empty_sections.txt @@ -0,0 +1 @@ +foo diff --git a/examples/i18n.html b/examples/i18n.html new file mode 100644 index 0000000..d39bc61 --- /dev/null +++ b/examples/i18n.html @@ -0,0 +1 @@ +{{_i}}foo {{bar}}{{/i}} diff --git a/examples/i18n.js b/examples/i18n.js new file mode 100644 index 0000000..f85a457 --- /dev/null +++ b/examples/i18n.js @@ -0,0 +1,3 @@ +var i18n = { + bar: "BAR" +}; \ No newline at end of file diff --git a/examples/i18n.txt b/examples/i18n.txt new file mode 100644 index 0000000..46c7bbf --- /dev/null +++ b/examples/i18n.txt @@ -0,0 +1 @@ +foo BAR diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 32c75e3..7e93fc9 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -64,51 +64,6 @@ describe "mustache" do run_js(@run_js, js).should == "\n" end - it "should not double-render" do - js = <<-JS - #{@boilerplate} - var template = "{{#foo}}{{bar}}{{/foo}}"; - var ctx = { - foo: true, - bar: "{{win}}", - win: "FAIL" - }; - - print(Mustache.to_html(template, ctx)) - JS - - run_js(@run_js, js).strip.should == "{{win}}" - end - - it "should work with i18n" do - js = <<-JS - #{@boilerplate} - - var template = "{{_i}}foo {{bar}}{{/i}}"; - var ctx = { - bar: "BAR" - }; - - print(Mustache.to_html(template, ctx)); - JS - - run_js(@run_js, js).strip.should == "foo BAR" - end - - it "should work with empty sections" do - js = <<-JS - try{ - #{@boilerplate} - - print(Mustache.to_html("{{#foo}}{{/foo}}foo{{#bar}}{{/bar}}", {})); - } catch(e) { - print(e); - } - JS - - run_js(@run_js, js).strip.should == "foo" - end - non_partials.each do |testname| describe testname do it "should generate the correct html" do From e54ef155e608470245736bb5eeb2710bf73523c1 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 13:38:58 -0800 Subject: [PATCH 16/34] more readme stuff --- CHANGES.md | 6 ++++++ README.md | 18 ++++++++++++++++++ THANKS.md | 2 ++ mustache.js | 2 +- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 61cde5a..8489b9e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # mustache.js Changes +## 0.3.1-twitter (12/3/2010) + +* Added i18n {{_i}}{{/i}} support +* fixed double-rendering bug +* added Rhino test-runner alongside JavaScriptCore + ## 0.3.1 (??-??-????) ## 0.3.0 (21-07-2010) diff --git a/README.md b/README.md index 0c7a73d..22af4d9 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,20 @@ translation _before_ any rendering is done. For example: alert(Mustache.to_html(template, view)); // alerts "Mae Matt yn defnyddio mustache.js!" +### The TRANSLATION-HINT Pragma + +Some single words in English have different translations based on usage context. Mustache.js supports this with the TRANSLATION-HINT pragma. For example, the word "Tweet" can be used as a noun, or a verb. The following template is ambiguous: + +
{{_i}}Tweet{{/i}}
+ +By adding a pragma, we can provide the right context for a given template: + + {{%TRANSLATION-HINT mode=tweet_button}} + +
{{_i}}Tweet{{/i}}
+ +This will lookup every translation in that template with the mode, e.g. `_('Tweet', {_mode: "tweet_button"})`, which your gettext implementation can handle as appropriate. + ## Escaping mustache.js does escape all values when using the standard double mustache @@ -279,6 +293,10 @@ own iteration marker: {{bob}} {{/foo}} +### TRANSLATION-HINT + +See the "Internationalization" section above for info on this pragma. + ## F.A.Q. ### Why doesn’t Mustache allow dot notation like `{{variable.member}}`? diff --git a/THANKS.md b/THANKS.md index 6dac939..22418c8 100644 --- a/THANKS.md +++ b/THANKS.md @@ -18,3 +18,5 @@ Mustache.js wouldn't kick ass if it weren't for these fine souls: * Jason Smith / jhs * Aaron Gibralter / agibralter * Ross Boucher / boucher + * Matt Sanford / mzsanford + * Ben Cherry / bcherry diff --git a/mustache.js b/mustache.js index a85afd0..aa2a1d4 100644 --- a/mustache.js +++ b/mustache.js @@ -126,7 +126,7 @@ var Mustache = function() { // for each {{_i}}{{/i}} section do... return html.replace(regex, function(match, content) { - var translation_mode = undefined; + var translation_mode; if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"]['mode']) { translation_mode = { _mode: that.pragmas["TRANSLATION-HINT"]['mode'] }; } else if (context['_mode']) { From 88ec36afe55ca5492b5ff22ac806f995dd225213 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 14:01:30 -0800 Subject: [PATCH 17/34] added test descriptions and instructions to the readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 22af4d9..a6ace7d 100644 --- a/README.md +++ b/README.md @@ -344,8 +344,32 @@ directory. Run `rake commonjs` to get a CommonJS compatible plugin file in the `mustache-commonjs/` directory which you can also use with [Node.js][]. +## Testing + +To run the mustache.js test suite, run `rake spec`. All specs will be run first with JavaScriptCore (using `jsc`) +and again with Rhino, using `java org.mozilla.javascript.tools.shell.Main`. To install Rhino on OSX, follow [these instructions](Rhino Install). + +### Adding Tests + +Tests are located in the `examples/` directory. Adding a new test requires three files. Here's an example to add a test named "foo": + +`examples/foo.html` (the template): + + foo {{bar}} + +`examples/foo.js` (the view context): + + var foo = { + bar: "baz" + }; + +`examples/foo.txt` (the expected output): + + foo baz + [jQuery]: http://jquery.com/ [Dojo]: http://www.dojotoolkit.org/ [Yui]: http://developer.yahoo.com/yui/ [CommonJS]: http://www.commonjs.org/ [Node.js]: http://nodejs.org/ +[Rhino Install]: http://michaux.ca/articles/installing-rhino-on-os-x From 90bc5a9a84d32b55035ce04cb3fb936a4e0cc224 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 14:35:50 -0800 Subject: [PATCH 18/34] clean up some logic --- mustache.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/mustache.js b/mustache.js index aa2a1d4..78c0574 100644 --- a/mustache.js +++ b/mustache.js @@ -40,28 +40,24 @@ var Mustache = function() { context['_mode'] = this.pragmas['TRANSLATION-HINT']['mode']; } + // get the pragmas together template = this.render_pragmas(template); + // handle all translations template = this.render_i18n(template, context, partials); + // render the template var html = this.render_section(template, context, partials); - if (html === template) { - if (in_recursion) { - return this.render_tags(html, context, partials, true); - } + // render_section did not find any sections, we still need to render the tags + if (html === false) { + html = this.render_tags(template, context, partials, in_recursion); + } - this.render_tags(html, context, partials, false); + if (in_recursion) { + return html; } else { - if(in_recursion) { - return html; - } else { - var lines = html.split("\n"); - for (var i = 0; i < lines.length; i++) { - this.send(lines[i]); - } - return; - } + this.sendLines(html); } }, @@ -74,6 +70,15 @@ var Mustache = function() { } }, + sendLines: function(text) { + if (text) { + var lines = text.split("\n"); + for (var i = 0; i < lines.length; i++) { + this.send(lines[i]); + } + } + }, + /* Looks for %PRAGMAS */ @@ -142,7 +147,8 @@ var Mustache = function() { */ render_section: function(template, context, partials) { if(!this.includes("#", template) && !this.includes("^", template)) { - return template; + // did not render anything, there were no sections + return false; } var that = this; From 5467067beed226bc8af99af6d4b36d83b82f815c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Fri, 3 Dec 2010 14:49:55 -0800 Subject: [PATCH 19/34] remove commented out lines from run_js --- test/mustache_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 7e93fc9..2e5b098 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -180,10 +180,6 @@ describe "mustache" do def run_js(runner, js) send(runner, js) - # run_js_rhino(js) - # js_jsc = run_js_jsc(js) - # js_rhino = run_js_rhino(js) - # return js_jsc unless js_jsc != js_rhino end def run_js_jsc(js) From 25d0c02b715b8474607d2fd9bf372ce48711c5dd Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 6 Dec 2010 17:38:41 -0800 Subject: [PATCH 20/34] don't render during i18n step, just do replacements --- mustache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mustache.js b/mustache.js index 78c0574..857193d 100644 --- a/mustache.js +++ b/mustache.js @@ -138,7 +138,7 @@ var Mustache = function() { translation_mode = { _mode: context['_mode'] }; } - return that.render(_(content, translation_mode), context, partials, true); + return _(content, translation_mode); }); }, From 033bdd9f7986447ed19f3bdf5831244f01f021c7 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 6 Dec 2010 17:45:00 -0800 Subject: [PATCH 21/34] updated i18n test --- examples/i18n.js | 3 ++- examples/i18n.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/i18n.js b/examples/i18n.js index f85a457..2053321 100644 --- a/examples/i18n.js +++ b/examples/i18n.js @@ -1,3 +1,4 @@ var i18n = { - bar: "BAR" + bar: "don't double render me... {{baz}}", + baz: "FAIL" }; \ No newline at end of file diff --git a/examples/i18n.txt b/examples/i18n.txt index 46c7bbf..5ab5fb8 100644 --- a/examples/i18n.txt +++ b/examples/i18n.txt @@ -1 +1 @@ -foo BAR +foo don't double render me... {{baz}} From 077b7b5eaf049170e8b875801ea23fabeb889218 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 6 Dec 2010 17:48:42 -0800 Subject: [PATCH 22/34] send translation mode in single parameter to _ --- mustache.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/mustache.js b/mustache.js index 857193d..5810822 100644 --- a/mustache.js +++ b/mustache.js @@ -131,14 +131,24 @@ var Mustache = function() { // for each {{_i}}{{/i}} section do... return html.replace(regex, function(match, content) { - var translation_mode; - if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"]['mode']) { - translation_mode = { _mode: that.pragmas["TRANSLATION-HINT"]['mode'] }; - } else if (context['_mode']) { - translation_mode = { _mode: context['_mode'] }; + var translationMode; + + if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"].mode) { + translationMode = that.pragmas["TRANSLATION-HINT"].mode; + } else if (context['_TRANSLATION-HINT_mode']) { + translationMode = context['_TRANSLATION-HINT_mode']; + } + + var params = content; + + if (translationMode) { + params = { + text: content, + mode: translationMode + }; } - return _(content, translation_mode); + return _(params); }); }, From deff91a4e07f75d5edbde8c7ae4753d85d10e60c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 6 Dec 2010 17:51:27 -0800 Subject: [PATCH 23/34] made gettext return the right thing for translation-hint mode --- test/mustache_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 2e5b098..ff3995d 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -30,8 +30,12 @@ describe "mustache" do mustache = File.read(__DIR__ + "/../mustache.js") stubbed_gettext = <<-JS // Stubbed gettext translation method for {{_i}}{{/i}} tags in Mustache. - function _(text) { - return text; + function _(params) { + if (typeof params === "string") { + return params + } + + return params.text; } JS From 4930a1dfe7996850bdc59d9bea1bebb5945f4d0f Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 6 Dec 2010 17:54:54 -0800 Subject: [PATCH 24/34] preserve translation-hint correctly --- mustache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mustache.js b/mustache.js index 5810822..f088cdd 100644 --- a/mustache.js +++ b/mustache.js @@ -37,7 +37,7 @@ var Mustache = function() { // Branching or moving down the partial stack, save any translation mode info. if (this.pragmas['TRANSLATION-HINT']) { - context['_mode'] = this.pragmas['TRANSLATION-HINT']['mode']; + context['_TRANSLATION-HINT_mode'] = this.pragmas['TRANSLATION-HINT'].mode; } // get the pragmas together From 6438149345848f4312563b9d659e2c6eafeb4743 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Wed, 8 Dec 2010 00:22:58 -0800 Subject: [PATCH 25/34] nice clean up to render_section, as suggested by @esbie --- mustache.js | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/mustache.js b/mustache.js index f088cdd..dd217df 100644 --- a/mustache.js +++ b/mustache.js @@ -187,35 +187,41 @@ var Mustache = function() { var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", // after may contain both sections and tags, so use full rendering function - renderedAfter = after ? that.render(after, context, partials, true) : ""; + renderedAfter = after ? that.render(after, context, partials, true) : "", - var value = that.find(name, context); - if(type == "^") { // inverted section - if(!value || that.is_array(value) && value.length === 0) { + // will be computed below + renderedContent, + + value = that.find(name, context); + + if (type === "^") { // inverted section + if (!value || that.is_array(value) && value.length === 0) { // false or empty list, render it - return renderedBefore + that.render(content, context, partials, true) + renderedAfter; + renderedContent = that.render(content, context, partials, true); } else { - return renderedBefore + "" + renderedAfter; + renderedContent = ""; } - } else if(type == "#") { // normal section - if(that.is_array(value)) { // Enumerable, Let's loop! - return renderedBefore + that.map(value, function(row) { + } else if (type === "#") { // normal section + if (that.is_array(value)) { // Enumerable, Let's loop! + renderedContent = that.map(value, function(row) { return that.render(content, that.create_context(row), partials, true); - }).join("") + renderedAfter; - } else if(that.is_object(value)) { // Object, Use it as subcontext! - return renderedBefore + that.render(content, that.create_context(value), - partials, true) + renderedAfter; - } else if(typeof value === "function") { + }).join(""); + } else if (that.is_object(value)) { // Object, Use it as subcontext! + renderedContent = that.render(content, that.create_context(value), + partials, true); + } else if (typeof value === "function") { // higher order section - return renderedBefore + value.call(context, content, function(text) { + renderedContent = value.call(context, content, function(text) { return that.render(text, context, partials, true); - }) + renderedAfter; - } else if(value) { // boolean section - return renderedBefore + that.render(content, context, partials, true) + renderedAfter; + }); + } else if (value) { // boolean section + renderedContent = that.render(content, context, partials, true); } else { - return renderedBefore + "" + renderedAfter; + renderedContent = ""; } } + + return renderedBefore + renderedContent + renderedAfter; }); }, From 59bbd3510b441064f3f0d0f941eed1731eaff62c Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Thu, 9 Dec 2010 18:12:54 -0800 Subject: [PATCH 26/34] fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6ace7d..4e58d2a 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ By adding a pragma, we can provide the right context for a given template:
{{_i}}Tweet{{/i}}
-This will lookup every translation in that template with the mode, e.g. `_('Tweet', {_mode: "tweet_button"})`, which your gettext implementation can handle as appropriate. +This will lookup every translation in that template with the mode, e.g. `_('Tweet', {mode: "tweet_button"})`, which your gettext implementation can handle as appropriate. ## Escaping From 3ebd5ef1b3b8c6ff77067f412c15e5d4154654cc Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 21 Feb 2011 15:40:21 -0800 Subject: [PATCH 27/34] remove i18n stuff --- README.md | 42 ------------------------------------------ examples/i18n.html | 1 - examples/i18n.js | 4 ---- examples/i18n.txt | 1 - mustache.js | 42 +----------------------------------------- 5 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 examples/i18n.html delete mode 100644 examples/i18n.js delete mode 100644 examples/i18n.txt diff --git a/README.md b/README.md index 4e58d2a..9247f5d 100644 --- a/README.md +++ b/README.md @@ -199,44 +199,6 @@ will tell mustache.js to look for a object in the context's property `winnings`. It will then use that object as the context for the template found in `partials` for `winnings`. -## Internationalization - -mustache.js supports i18n using the `{{_i}}{{/i}}` tags. When mustache.js encounters -an internationalized section, it will call out to the standard global gettext function `_()` with the tag contents for a -translation _before_ any rendering is done. For example: - - var template = "{{_i}}{{name}} is using mustache.js!{{/i}}" - - var view = { - name: "Matt" - }; - - var translationTable = { - // Welsh, according to Google Translate - "{{name}} is using mustache.js!": "Mae {{name}} yn defnyddio mustache.js!" - }; - - function _(text) { - return translationTable[text] || text; - } - - alert(Mustache.to_html(template, view)); - // alerts "Mae Matt yn defnyddio mustache.js!" - -### The TRANSLATION-HINT Pragma - -Some single words in English have different translations based on usage context. Mustache.js supports this with the TRANSLATION-HINT pragma. For example, the word "Tweet" can be used as a noun, or a verb. The following template is ambiguous: - -
{{_i}}Tweet{{/i}}
- -By adding a pragma, we can provide the right context for a given template: - - {{%TRANSLATION-HINT mode=tweet_button}} - -
{{_i}}Tweet{{/i}}
- -This will lookup every translation in that template with the mode, e.g. `_('Tweet', {mode: "tweet_button"})`, which your gettext implementation can handle as appropriate. - ## Escaping mustache.js does escape all values when using the standard double mustache @@ -293,10 +255,6 @@ own iteration marker: {{bob}} {{/foo}} -### TRANSLATION-HINT - -See the "Internationalization" section above for info on this pragma. - ## F.A.Q. ### Why doesn’t Mustache allow dot notation like `{{variable.member}}`? diff --git a/examples/i18n.html b/examples/i18n.html deleted file mode 100644 index d39bc61..0000000 --- a/examples/i18n.html +++ /dev/null @@ -1 +0,0 @@ -{{_i}}foo {{bar}}{{/i}} diff --git a/examples/i18n.js b/examples/i18n.js deleted file mode 100644 index 2053321..0000000 --- a/examples/i18n.js +++ /dev/null @@ -1,4 +0,0 @@ -var i18n = { - bar: "don't double render me... {{baz}}", - baz: "FAIL" -}; \ No newline at end of file diff --git a/examples/i18n.txt b/examples/i18n.txt deleted file mode 100644 index 5ab5fb8..0000000 --- a/examples/i18n.txt +++ /dev/null @@ -1 +0,0 @@ -foo don't double render me... {{baz}} diff --git a/mustache.js b/mustache.js index dd217df..a06d117 100644 --- a/mustache.js +++ b/mustache.js @@ -13,8 +13,7 @@ var Mustache = function() { pragmas: {}, buffer: [], pragmas_implemented: { - "IMPLICIT-ITERATOR": true, - "TRANSLATION-HINT": true + "IMPLICIT-ITERATOR": true }, context: {}, @@ -35,17 +34,9 @@ var Mustache = function() { } } - // Branching or moving down the partial stack, save any translation mode info. - if (this.pragmas['TRANSLATION-HINT']) { - context['_TRANSLATION-HINT_mode'] = this.pragmas['TRANSLATION-HINT'].mode; - } - // get the pragmas together template = this.render_pragmas(template); - // handle all translations - template = this.render_i18n(template, context, partials); - // render the template var html = this.render_section(template, context, partials); @@ -121,37 +112,6 @@ var Mustache = function() { return this.render(partials[name], context[name], partials, true); }, - render_i18n: function(html, context, partials) { - if (html.indexOf(this.otag + "_i") == -1) { - return html; - } - var that = this; - var regex = new RegExp(this.otag + "\\_i" + this.ctag + - "\\s*([\\s\\S]+?)" + this.otag + "\\/i" + this.ctag, "mg"); - - // for each {{_i}}{{/i}} section do... - return html.replace(regex, function(match, content) { - var translationMode; - - if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"].mode) { - translationMode = that.pragmas["TRANSLATION-HINT"].mode; - } else if (context['_TRANSLATION-HINT_mode']) { - translationMode = context['_TRANSLATION-HINT_mode']; - } - - var params = content; - - if (translationMode) { - params = { - text: content, - mode: translationMode - }; - } - - return _(params); - }); - }, - /* Renders inverted (^) and normal (#) sections */ From 58bf8da9ba7a0925549ba6591cb8fff277ad0011 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 21 Feb 2011 15:49:32 -0800 Subject: [PATCH 28/34] re-link jsc and add a bit more doc --- README.md | 8 +++++++- test/mustache_spec.rb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9247f5d..763f83d 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,13 @@ Run `rake commonjs` to get a CommonJS compatible plugin file in the ## Testing To run the mustache.js test suite, run `rake spec`. All specs will be run first with JavaScriptCore (using `jsc`) -and again with Rhino, using `java org.mozilla.javascript.tools.shell.Main`. To install Rhino on OSX, follow [these instructions](Rhino Install). +and again with Rhino, using `java org.mozilla.javascript.tools.shell.Main`. + +JavaScriptCore is used from the OSX default location: + + /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc + +To install Rhino on OSX, follow [these instructions](Rhino Install). ### Adding Tests diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index ff3995d..a52cf38 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -188,7 +188,7 @@ describe "mustache" do def run_js_jsc(js) File.open("runner.js", 'w') {|f| f << js} - `jsc runner.js` + `/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc runner.js` end def run_js_rhino(js) From 42e06a5aac9da741f9f27df3f4b73534091afdb6 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 21 Feb 2011 15:51:16 -0800 Subject: [PATCH 29/34] updated changelog --- CHANGES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8489b9e..e5d0d1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,7 @@ # mustache.js Changes -## 0.3.1-twitter (12/3/2010) +## 0.3.1-dev-twitter (12/3/2010) -* Added i18n {{_i}}{{/i}} support * fixed double-rendering bug * added Rhino test-runner alongside JavaScriptCore From 9110fe006193c8a33a3ed48d632b5295c81eed64 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Mon, 25 Apr 2011 21:41:36 -0700 Subject: [PATCH 30/34] remove the backslash escaping --- mustache.js | 1 - 1 file changed, 1 deletion(-) diff --git a/mustache.js b/mustache.js index a06d117..7272f6b 100644 --- a/mustache.js +++ b/mustache.js @@ -291,7 +291,6 @@ var Mustache = function() { return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) { switch(s) { case "&": return "&"; - case "\\": return "\\\\"; case '"': return '"'; case "'": return '''; case "<": return "<"; From 8e4641f8bf47527963858e2a9a79a8f45dd51d25 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Wed, 13 Jul 2011 14:50:05 -0700 Subject: [PATCH 31/34] cached regexes --- mustache.js | 63 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/mustache.js b/mustache.js index 7272f6b..83184df 100644 --- a/mustache.js +++ b/mustache.js @@ -80,8 +80,10 @@ var Mustache = function() { } var that = this; - var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + - this.ctag); + var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) { + return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag); + }); + return template.replace(regex, function(match, pragma, options) { if(!that.pragmas_implemented[pragma]) { throw({message: @@ -123,23 +125,26 @@ var Mustache = function() { var that = this; - // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder - var regex = new RegExp( - "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) + var regex = this.getCachedRegex("render_section", function(otag, ctag) { + // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder + return new RegExp( + "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) - this.otag + // {{ - "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) - this.ctag + // }} + otag + // {{ + "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) + ctag + // }} - "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped + "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped - this.otag + // {{ - "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). - this.ctag + // }} + otag + // {{ + "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). + ctag + // }} - "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. + "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. + + "g"); + }); - "g"); // for each {{#foo}}{{/foo}} section do... return template.replace(regex, function(match, before, type, name, content, after) { @@ -192,9 +197,12 @@ var Mustache = function() { // tit for tat var that = this; + + var new_regex = function() { - return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + - that.ctag + "+", "g"); + return that.getCachedRegex("render_tags", function(otag, ctag) { + return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g"); + }); }; var regex = new_regex(); @@ -344,6 +352,29 @@ var Mustache = function() { } return r; } + }, + + getCachedRegex: function(name, generator) { + if (!this._regexCache) { + this._regexCache = {}; + } + + var byOtag = this._regexCache[this.otag]; + if (!byOtag) { + byOtag = this._regexCache[this.otag] = {}; + } + + var byCtag = byOtag[this.ctag]; + if (!byCtag) { + byCtag = byOtag[this.ctag] = {}; + } + + var regex = byCtag[name]; + if (!regex) { + regex = byCtag[name] = generator(this.ctag, this.otag); + } + + return regex; } }; From e3fe0ae3522ed3673d8182c80b421579bfe2f44a Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Wed, 13 Jul 2011 15:04:44 -0700 Subject: [PATCH 32/34] fix a bug in regex generators, make cache global --- mustache.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mustache.js b/mustache.js index 83184df..1c0f029 100644 --- a/mustache.js +++ b/mustache.js @@ -5,6 +5,7 @@ */ var Mustache = function() { + var regexCache = {}; var Renderer = function() {}; Renderer.prototype = { @@ -355,13 +356,9 @@ var Mustache = function() { }, getCachedRegex: function(name, generator) { - if (!this._regexCache) { - this._regexCache = {}; - } - - var byOtag = this._regexCache[this.otag]; + var byOtag = regexCache[this.otag]; if (!byOtag) { - byOtag = this._regexCache[this.otag] = {}; + byOtag = regexCache[this.otag] = {}; } var byCtag = byOtag[this.ctag]; @@ -371,7 +368,7 @@ var Mustache = function() { var regex = byCtag[name]; if (!regex) { - regex = byCtag[name] = generator(this.ctag, this.otag); + regex = byCtag[name] = generator(this.otag, this.ctag); } return regex; From c29e6f1fa211dd88ed759dc77240281adc7a8c52 Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 23 Aug 2011 11:37:54 -0700 Subject: [PATCH 33/34] version -twitter-b --- CHANGES.md | 4 ++++ mustache.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e5d0d1e..40d50b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # mustache.js Changes +## 0.3.1-dev-twitter-b (8/23/2011) + +* Cached regexes for improved performance + ## 0.3.1-dev-twitter (12/3/2010) * fixed double-rendering bug diff --git a/mustache.js b/mustache.js index 264acc9..8f58683 100644 --- a/mustache.js +++ b/mustache.js @@ -377,7 +377,7 @@ var Mustache = function() { return({ name: "mustache.js", - version: "0.3.1-dev-twitter", + version: "0.3.1-dev-twitter-b", /* Turns a template and view into HTML From 4fd1b8bd3b0413733ddfbe0b8b3df218ed2fa3ef Mon Sep 17 00:00:00 2001 From: Ben Cherry Date: Tue, 23 Aug 2011 12:23:34 -0700 Subject: [PATCH 34/34] Check for existence of engines before running tests, better logging --- foo | 0 test/mustache_spec.rb | 61 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) delete mode 100644 foo diff --git a/foo b/foo deleted file mode 100644 index e69de29..0000000 diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index a52cf38..62edd4e 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -22,11 +22,17 @@ def load_test(dir, name, partial=false) end end +JSC_PATH = "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc" +RHINO_JAR = "org.mozilla.javascript.tools.shell.Main" + +engines_run = 0 + describe "mustache" do shared_examples_for "Mustache rendering" do before(:all) do + engines_run += 1 mustache = File.read(__DIR__ + "/../mustache.js") stubbed_gettext = <<-JS // Stubbed gettext translation method for {{_i}}{{/i}} tags in Mustache. @@ -168,18 +174,57 @@ describe "mustache" do end context "running in JavaScriptCore (WebKit, Safari)" do - before(:each) do - @run_js = :run_js_jsc + if File.exists?(JSC_PATH) + before(:each) do + @run_js = :run_js_jsc + end + + before(:all) do + puts "\nTesting mustache.js in JavaScriptCore:\n" + end + + after(:all) do + puts "\nDone\n" + end + + it_should_behave_like "Mustache rendering" + else + puts "\nSkipping tests in JavaScriptCore (jsc not found)\n" end - it_should_behave_like "Mustache rendering" end context "running in Rhino (Mozilla)" do - before(:each) do - @run_js = :run_js_rhino + if !`java #{RHINO_JAR} 'foo' 2>&1`.match(/ClassNotFoundException/) + before(:each) do + @run_js = :run_js_rhino + end + + before(:all) do + puts "\nTesting mustache.js in Rhino:\n" + end + + after(:all) do + puts "\nDone\n" + end + + it_should_behave_like "Mustache rendering" + else + puts "\nSkipping tests in Rhino (JAR #{RHINO_JAR} was not found)\n" + end + end + + context "suite" do + before(:all) do + puts "\nVerifying that we ran at the tests in at least one engine\n" end - it_should_behave_like "Mustache rendering" + after(:all) do + puts "\nDone\n" + end + + it "should have run at least one time" do + engines_run.should > 0 + end end def run_js(runner, js) @@ -188,12 +233,12 @@ describe "mustache" do def run_js_jsc(js) File.open("runner.js", 'w') {|f| f << js} - `/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc runner.js` + `#{JSC_PATH} runner.js` end def run_js_rhino(js) File.open("runner.js", 'w') {|f| f << js} - `java org.mozilla.javascript.tools.shell.Main runner.js` + `java #{RHINO_JAR} runner.js` end end