* twitter/master: (34 commits) Check for existence of engines before running tests, better logging version -twitter-b fix a bug in regex generators, make cache global cached regexes remove the backslash escaping updated changelog re-link jsc and add a bit more doc remove i18n stuff fix typo in readme nice clean up to render_section, as suggested by @esbie preserve translation-hint correctly made gettext return the right thing for translation-hint mode send translation mode in single parameter to _ updated i18n test don't render during i18n step, just do replacements remove commented out lines from run_js clean up some logic added test descriptions and instructions to the readme more readme stuff moved new tests into new examples/* files ...tags/0.4.0
| @@ -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) | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -0,0 +1 @@ | |||
| {{#foo}}{{bar}}{{/foo}} | |||
| @@ -0,0 +1,5 @@ | |||
| var double_render = { | |||
| foo: true, | |||
| bar: "{{win}}", | |||
| win: "FAIL" | |||
| }; | |||
| @@ -0,0 +1 @@ | |||
| {{win}} | |||
| @@ -0,0 +1 @@ | |||
| {{#foo}}{{/foo}}foo{{#bar}}{{/bar}} | |||
| @@ -0,0 +1 @@ | |||
| var empty_sections = {}; | |||
| @@ -0,0 +1 @@ | |||
| foo | |||
| @@ -0,0 +1,4 @@ | |||
| {{#foo}} | |||
| {{/foo}} | |||
| {{#bar}} | |||
| {{/bar}} | |||
| @@ -0,0 +1 @@ | |||
| var two_sections = {}; | |||
| @@ -0,0 +1 @@ | |||
| @@ -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" | |||
| } | |||
| @@ -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"); | |||
| } | |||
| @@ -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 | |||