diff --git a/CHANGES.md b/CHANGES.md index 61cde5a..40d50b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # 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 +* added Rhino test-runner alongside JavaScriptCore + ## 0.3.1 (??-??-????) ## 0.3.0 (21-07-2010) diff --git a/README.md b/README.md index 7ed5d42..cebaadb 100644 --- a/README.md +++ b/README.md @@ -199,11 +199,10 @@ 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`. - ## 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`. @@ -304,8 +303,38 @@ 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`. + +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 + +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 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/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/two_sections.html b/examples/two_sections.html new file mode 100644 index 0000000..a4b9f2a --- /dev/null +++ b/examples/two_sections.html @@ -0,0 +1,4 @@ +{{#foo}} +{{/foo}} +{{#bar}} +{{/bar}} 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..8b13789 --- /dev/null +++ b/examples/two_sections.txt @@ -0,0 +1 @@ + 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 0c219d7..e639deb 100644 --- a/mustache.js +++ b/mustache.js @@ -5,6 +5,7 @@ */ var Mustache = function() { + var regexCache = {}; var Renderer = function() {}; Renderer.prototype = { @@ -34,13 +35,22 @@ var Mustache = function() { } } + // get the pragmas together template = this.render_pragmas(template); + + // render the template var html = this.render_section(template, context, partials); - if(in_recursion) { - return this.render_tags(html, context, partials, in_recursion); + + // 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, in_recursion); + if (in_recursion) { + return html; + } else { + this.sendLines(html); + } }, /* @@ -52,6 +62,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 */ @@ -62,11 +81,13 @@ var Mustache = function() { } var that = this; - var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + - this.ctag, "g"); + var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) { + return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); + }); + 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"}); } @@ -99,45 +120,74 @@ 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; - // 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 = 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) + + otag + // {{ + "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) + ctag + // }} + + "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped + + otag + // {{ + "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). + 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, type, name, content) { - var value = that.find(name, context); - if(type == "^") { // inverted section - if(!value || that.is_array(value) && value.length === 0) { + 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) : "", + + // after may contain both sections and tags, so use full rendering function + renderedAfter = after ? that.render(after, context, partials, true) : "", + + // 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 that.render(content, context, partials, true); + renderedContent = that.render(content, context, partials, true); } else { - return ""; + renderedContent = ""; } - } 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); + } 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(""); - } else if(that.is_object(value)) { // Object, Use it as subcontext! - return that.render(content, that.create_context(value), + } 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") { + } else if (typeof value === "function") { // higher order section - return value.call(context, content, function(text) { + renderedContent = value.call(context, content, function(text) { return that.render(text, context, partials, true); }); - } else if(value) { // boolean section - return that.render(content, context, partials, true); + } else if (value) { // boolean section + renderedContent = that.render(content, context, partials, true); } else { - return ""; + renderedContent = ""; } } + + return renderedBefore + renderedContent + renderedAfter; }); }, @@ -148,9 +198,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(); @@ -300,12 +353,31 @@ var Mustache = function() { } return r; } + }, + + getCachedRegex: function(name, generator) { + var byOtag = regexCache[this.otag]; + if (!byOtag) { + byOtag = 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.otag, this.ctag); + } + + return regex; } }; return({ name: "mustache.js", - version: "0.3.1-dev", + version: "0.3.1-dev-twitter-b", /* Turns a template and view into HTML @@ -315,7 +387,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"); } diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 9ede6c8..62edd4e 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -22,135 +22,223 @@ def load_test(dir, name, partial=false) end end -describe "mustache" do - before(:all) do - @mustache = File.read(__DIR__ + "/../mustache.js") - end +JSC_PATH = "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc" +RHINO_JAR = "org.mozilla.javascript.tools.shell.Main" - it "should return the same when invoked multiple times" do - js = <<-JS - #{@mustache} - Mustache.to_html("x") - print(Mustache.to_html("x")); - JS - run_js(js).should == "x\n" +engines_run = 0 - end +describe "mustache" do - it "should clear the context after each run" do - js = <<-JS - #{@mustache} - 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 - - 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 { - #{@mustache} - #{view} - var template = #{template}; - var result = Mustache.to_html(template, #{testname}); - print(result); - } catch(e) { - print('ERROR: ' + e.message); + 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. + function _(params) { + if (typeof params === "string") { + return params } - JS - run_js(runner).should == expect + return params.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(@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(@run_js, js).should == "\n" + end + + 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); + } + JS + + run_js(@run_js, runner).strip.should == expect.strip + end end - it "should sendFun the correct html" do - - view, template, expect = load_test(__DIR__, testname) - - runner = <<-JS - try { - #{@mustache} - #{view} - var chunks = []; - var sendFun = function(chunk) { - if (chunk != "") { - chunks.push(chunk); + 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(@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); } - var template = #{template}; - Mustache.to_html(template, #{testname}, null, sendFun); - print(chunks.join("\\n")); - } catch(e) { - print('ERROR: ' + e.message); - } - JS + JS - run_js(runner).strip.should == expect.strip + 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 { - #{@mustache} - #{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 + context "running in JavaScriptCore (WebKit, Safari)" do + 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 - it "should sendFun the correct html" do - - view, template, partial, expect = - load_test(__DIR__, testname, true) - - runner = <<-JS - try { - #{@mustache} - #{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); - } - JS - run_js(runner).strip.should == expect.strip + after(:all) do + puts "\nDone\n" end + + it_should_behave_like "Mustache rendering" + else + puts "\nSkipping tests in JavaScriptCore (jsc not found)\n" + end + end + + context "running in Rhino (Mozilla)" do + 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 - def run_js(js) + context "suite" do + before(:all) do + puts "\nVerifying that we ran at the tests in at least one engine\n" + end + + 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) + send(runner, js) + end + + def run_js_jsc(js) + File.open("runner.js", 'w') {|f| f << js} + `#{JSC_PATH} runner.js` + end + + def run_js_rhino(js) File.open("runner.js", 'w') {|f| f << js} - `js runner.js` + `java #{RHINO_JAR} runner.js` end end