diff --git a/mustache.js b/mustache.js index 3772938..8dcecb6 100644 --- a/mustache.js +++ b/mustache.js @@ -1,230 +1,235 @@ /* Shamless port of http://github.com/defunkt/mustache - by Jan Lehnardt + by Jan Lehnardt , Alexander Lang Thanks @defunkt for the awesome code. See http://github.com/defunkt/mustache for more info. */ -var Mustache = { - name: "mustache.js", - version: "0.1", - context: {}, - otag: "{{", - ctag: "}}", - - /* - Public method. Turns a template and view into HTML - */ - to_html: function(template, view) { - return this.render(template, view); - }, - - // Private Methods - render: function(template, view) { - // fail fast - if(template.indexOf(this.otag) == -1) { - return template; - } +var Mustache = function() { + var Renderer = function() {}; + + Renderer.prototype = { + otag: "{{", + ctag: "}}", + + render: function(template, view) { + // fail fast + if(template.indexOf(this.otag) == -1) { + return template; + } - // keep context around for recursive calls - this.context = context = this.merge((this.context || {}), this.create_context(view)); + // keep context around for recursive calls + this.context = context = this.merge((this.context || {}), this.create_context(view)); - // first, render all sections - var html = this.render_section(template); + // first, render all sections + var html = this.render_section(template); - // restore context, recursion might have messed it up - this.context = context; + // restore context, recursion might have messed it up + this.context = context; - // finally, render tags - - // render until all is rendered - return this.render_tags(html); - }, - - create_context: function(_context) { - if(this.is_object(_context)) { - return _context; - } else { - return {'.': _context}; - } - }, - - is_object: function(a) { - return a && typeof a == 'object' - }, - - /* - Tries to find a partial in the global scope and render it - */ - render_partial: function(name) { - // FIXME: too hacky - var evil_name = eval(name) - switch(typeof evil_name) { - case "string": // a tring partial, we simply render - return this.to_html(evil_name, ""); - case "object": // a view partial needs a `name_template` template - var tpl = name + "_template"; - return this.to_html(eval(tpl), evil_name); - default: // should not happen #famouslastwords - throw("Unknown partial type."); - } - }, + // finally, render tags - /* - Renders boolean and enumerable sections - */ - render_section: function(template) { - if(template.indexOf(this.otag + "#") == -1) { - return template; - } - var that = this; - var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + - "\\s*([\\s\\S]+)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); - - // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function(match, name, content) { - var value = that.find(name); - if(that.is_array(value)) { // Enumerable, Let's loop! - return value.map(function(row) { - return that.render(content, row); - }).join(''); - } else if(value) { // boolean section - return that.render(content); - } else { - return ""; - } + // render until all is rendered + return this.render_tags(html); + }, + + create_context: function(_context) { + if(this.is_object(_context)) { + return _context; + } else { + return {'.': _context}; + } + }, + + is_object: function(a) { + return a && typeof a == 'object' + }, + + /* + Tries to find a partial in the global scope and render it + */ + render_partial: function(name) { + // FIXME: too hacky + var evil_name = eval(name) + switch(typeof evil_name) { + case "string": // a tring partial, we simply render + return this.render(evil_name, ""); + case "object": // a view partial needs a `name_template` template + var tpl = name + "_template"; + return this.render(eval(tpl), evil_name); + default: // should not happen #famouslastwords + throw("Unknown partial type."); } - ); - }, - - /* - Replace {{foo}} and friends with values from our view - */ - render_tags: function(template) { - var pop_first = function(lines) { - var lines_array = lines.split("\n"); - lines_array.shift(); - return lines_array.join("\n"); - }; - - var new_regex = function() { - return new RegExp(that.otag + - "(=|!|<|\\{)?([^\/#]+?)\\1?" + that.ctag + "+", "m"); - }; - - // tit for tat - var that = this; - - regex = new_regex(); - var lines = template; - - // for each {{(!<{)?foo}} tag, do... - while(regex.test(lines)) { - template = template.replace(regex, function (match, operator, name) { - switch(operator) { - case "!": // ignore comments - return match; - case "=": // set new delimiters - that.set_delimiters(name); + }, + + /* + Renders boolean and enumerable sections + */ + render_section: function(template) { + if(template.indexOf(this.otag + "#") == -1) { + return template; + } + var that = this; + var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + + "\\s*([\\s\\S]+)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); + + // for each {{#foo}}{{/foo}} section do... + return template.replace(regex, function(match, name, content) { + var value = that.find(name); + if(that.is_array(value)) { // Enumerable, Let's loop! + return value.map(function(row) { + return that.render(content, row); + }).join(''); + } else if(value) { // boolean section + return that.render(content); + } else { return ""; - case "<": // render partial - return that.render_partial(name); - case "{": // the triple mustache is unescaped - return that.find(name); - default: // escape the value - return that.escape(that.find(name)); + } } - }, this); - regex = new_regex(); - lines = pop_first(lines); - } - return template; - }, - - set_delimiters: function(delimiters) { - var dels = delimiters.split(" "); - this.otag = this.escape_regex(dels[0]); - this.ctag = this.escape_regex(dels[1]); - }, - - escape_regex: function(text) { - // thank you Simon Willison - if(!arguments.callee.sRE) { - var specials = [ - '/', '.', '*', '+', '?', '|', - '(', ')', '[', ']', '{', '}', '\\' - ]; - arguments.callee.sRE = new RegExp( - '(\\' + specials.join('|\\') + ')', 'g' ); - } - return text.replace(arguments.callee.sRE, '\\$1'); - }, - - /* - find `name` in current `context`. That is find me a value - from the view object - */ - find: function(name) { - name = this.trim(name); - var context = this.context; - if(typeof context[name] === "function") { - return context[name].apply(context); - } - if(context[name] !== undefined) { - return context[name]; - } - throw("Can't find " + name + " in " + context); - }, - - // Utility methods - - /* - Does away with nasty characters - */ - escape: function(s) { - return s.toString().replace(/[&"<>\\]/g, function(s) { - switch(s) { - case "&": return "&"; - case "\\": return "\\\\";; - case '"': return '\"';; - case "<": return "<"; - case ">": return ">"; - default: return s; + }, + + /* + Replace {{foo}} and friends with values from our view + */ + render_tags: function(template) { + var pop_first = function(lines) { + var lines_array = lines.split("\n"); + lines_array.shift(); + return lines_array.join("\n"); + }; + + var new_regex = function() { + return new RegExp(that.otag + + "(=|!|<|\\{)?([^\/#]+?)\\1?" + that.ctag + "+", "m"); + }; + + // tit for tat + var that = this; + + regex = new_regex(); + var lines = template; + + // for each {{(!<{)?foo}} tag, do... + while(regex.test(lines)) { + template = template.replace(regex, function (match, operator, name) { + switch(operator) { + case "!": // ignore comments + return match; + case "=": // set new delimiters + that.set_delimiters(name); + return ""; + case "<": // render partial + return that.render_partial(name); + case "{": // the triple mustache is unescaped + return that.find(name); + default: // escape the value + return that.escape(that.find(name)); + } + }, this); + regex = new_regex(); + lines = pop_first(lines); + } + return template; + }, + + set_delimiters: function(delimiters) { + var dels = delimiters.split(" "); + this.otag = this.escape_regex(dels[0]); + this.ctag = this.escape_regex(dels[1]); + }, + + escape_regex: function(text) { + // thank you Simon Willison + if(!arguments.callee.sRE) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + arguments.callee.sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); } - }); - }, - - /* - Merges all properties of object `b` into object `a`. - `b.property` overwrites a.property` - */ - merge: function(a, b) { - for(var name in b) { - if(b.hasOwnProperty(name)) { - a[name] = b[name]; + return text.replace(arguments.callee.sRE, '\\$1'); + }, + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + find: function(name) { + name = this.trim(name); + var context = this.context; + if(typeof context[name] === "function") { + return context[name].apply(context); } + if(context[name] !== undefined) { + return context[name]; + } + throw("Can't find " + name + " in " + context); + }, + + // Utility methods + + /* + Does away with nasty characters + */ + escape: function(s) { + return s.toString().replace(/[&"<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case "\\": return "\\\\";; + case '"': return '\"';; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); + }, + + /* + Merges all properties of object `b` into object `a`. + `b.property` overwrites a.property` + */ + merge: function(a, b) { + for(var name in b) { + if(b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + return a; + }, + + /* + Thanks Doug Crockford + JavaScript — The Good Parts lists an alternative that works better with + frames. Frames can suck it, we use the simple version. + */ + is_array: function(a) { + return (a && + typeof a === 'object' && + a.constructor === Array); + }, + + /* + Gets rid of leading and trailing whitespace + */ + trim: function(s) { + return s.replace(/^\s*|\s*$/g, ''); + } + }; + + return({ + name: "mustache.js", + version: "0.1", + + /* + Turns a template and view into HTML + */ + to_html: function(template, view) { + return new Renderer().render(template, view); } - return a; - }, - - /* - Thanks Doug Crockford - JavaScript — The Good Parts lists an alternative that works better with - frames. Frames can suck it, we use the simple version. - */ - is_array: function(a) { - return (a && - typeof a === 'object' && - a.constructor === Array); - }, - - /* - Gets rid of leading and trailing whitespace - */ - trim: function(s) { - return s.replace(/^\s*|\s*$/g, ''); - }, -}; + }); +}(); diff --git a/test/mustache_spec.rb b/test/mustache_spec.rb index 22310cf..a01de57 100644 --- a/test/mustache_spec.rb +++ b/test/mustache_spec.rb @@ -8,18 +8,34 @@ testnames = Dir.glob(__DIR__ + '/../examples/*.js').map do |name| end describe "mustache" do + before(:all) do + @mustache = File.read(__DIR__ + "/../mustache.js") + end + + 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); + } + JS + run_js(js).should == "ERROR: Can't find x in [object Object]\n" + end + testnames.each do |testname| - describe testname do it "should generate the correct html" do view = File.read(__DIR__ + "/../examples/#{testname}.js") template = File.read(__DIR__ + "/../examples/#{testname}.html").to_json expect = File.read(__DIR__ + "/../examples/#{testname}.txt") - - mustache = File.read(__DIR__ + "/../mustache.js") + + runner = <<-JS try { - #{mustache} + #{@mustache} #{view} var template = #{template}; var result = Mustache.to_html(template, #{testname}); @@ -28,13 +44,16 @@ describe "mustache" do print('ERROR: ' + e.message); } JS - - File.open("runner.js", 'w') {|f| f << runner} - - result = `js runner.js` - result.should == expect + + + run_js(runner).should == expect end end end + + def run_js(js) + File.open("runner.js", 'w') {|f| f << js} + `js runner.js` + end end