From 97736b7a4985f0ec66bedb835c6b52b7bb090b24 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 16 Jul 2010 18:27:17 -0400 Subject: [PATCH 1/7] Submit template compiler. --- mustache.js | 383 ++++++++++++++++++++++++++++++++++++++------------- test/unit.js | 13 ++ 2 files changed, 301 insertions(+), 95 deletions(-) diff --git a/mustache.js b/mustache.js index 784fbab..4441d73 100644 --- a/mustache.js +++ b/mustache.js @@ -9,8 +9,24 @@ var Mustache = function() { this.message = message; } - var Renderer = function(send_func) { - this.send_func = send_func; + var Renderer = function(send_func, mode) { + this.user_send_func = send_func; + if (mode==='interpreter' || !mode) { + this.commandSet = this.interpreter; + + this.send_func = function(text) { + this.user_send_func(text); + } + } else if (mode==='compiler') { + this.commandSet = this.compiler; + + this.cached_output = []; + this.send_func = function(text) { + this.cached_output.push(text); + } + } else { + throw new ParserException('Unsupported mode.'); + } this.pragmas = {}; }; @@ -119,6 +135,8 @@ var Mustache = function() { // make sure the parser finished at an appropriate terminal state if (state!=='text') { this.stateMachine['endOfDoc'].call(this, parserContext, contextStack); + } else { + this.commandSet.text.call(this); } }, @@ -141,6 +159,8 @@ var Mustache = function() { text: function(parserContext, contextStack) { switch (parserContext.tokens[parserContext.index]) { case parserContext.openTag: + this.commandSet.text.call(this); + return 'openMustache'; default: this.send_func(parserContext.tokens[parserContext.index]); @@ -367,13 +387,13 @@ var Mustache = function() { parserContext.stack.push({sectionType:'invertedSection', key:key, content:[], depth:1}); return 'endSectionScan'; case 'variable': - this.render_variable(key, contextStack); + this.commandSet.variable.call(this, key, contextStack); return 'text'; case 'unescapedVariable': - this.render_unescaped_variable(key, contextStack); + this.commandSet.unescaped_variable.call(this, key, contextStack); return 'text'; case 'partial': - this.render_partial(key, + this.commandSet.partial.call(this, key, contextStack, parserContext.partials, parserContext.openTag, @@ -384,7 +404,7 @@ var Mustache = function() { var section = parserContext.stack.pop(); if (--section.depth === 0) { if (section.key === key) { - this.render_section(section.sectionType, + this.commandSet.section.call(this, section.sectionType, section.content.join(''), key, contextStack, @@ -466,109 +486,261 @@ var Mustache = function() { return Object.prototype.toString.call(a) === '[object Array]'; }, - render_variable: function(key, contextStack) { - function escapeHTML(str) { - return ('' + str).replace(/&/g,'&') - .replace(//g,'>'); - } + interpreter: { + text: function() { + // in this implementation, rendering text is meaningless + // since the send_func method simply forwards to user_send_func + }, + variable: function(key, contextStack) { + function escapeHTML(str) { + return ('' + str).replace(/&/g,'&') + .replace(//g,'>'); + } - var result = this.find_in_stack(key, contextStack); - if (result!==undefined) { - this.send_func(escapeHTML(result)); - } - }, - render_unescaped_variable: function(key, contextStack) { - var result = this.find_in_stack(key, contextStack); - if (result!==undefined) { - this.send_func(result); - } - }, - render_partial: function(key, contextStack, partials, openTag, closeTag) { - if (!partials || partials[key] === undefined) { - throw new ParserException('Unknown partial \'' + key + '\''); - } - - var res = this.find_in_stack(key, contextStack); - if (this.is_object(res)) { - contextStack.push(res); - } - - var tokens = this.tokenize(partials[key], openTag, closeTag); + var result = this.find_in_stack(key, contextStack); + if (result!==undefined) { + this.user_send_func(escapeHTML(result)); + } + }, + unescaped_variable: function(key, contextStack) { + var result = this.find_in_stack(key, contextStack); + if (result!==undefined) { + this.user_send_func(result); + } + }, + partial: function(key, contextStack, partials, openTag, closeTag) { + if (!partials || partials[key] === undefined) { + throw new ParserException('Unknown partial \'' + key + '\''); + } + + var res = this.find_in_stack(key, contextStack); + if (this.is_object(res)) { + contextStack.push(res); + } + + var tokens = this.tokenize(partials[key], openTag, closeTag); - this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); + this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); + + if (this.is_object(res)) { + contextStack.pop(); + } + }, + section: function(sectionType, mustacheFragment, key, contextStack, partials, openTag, closeTag) { + // by @langalex, support for arrays of strings + var that = this; + function create_context(_context) { + if(that.is_object(_context)) { + return _context; + } else { + var iterator = '.'; + + if(that.pragmas["IMPLICIT-ITERATOR"] && + that.pragmas["IMPLICIT-ITERATOR"].iterator) { + iterator = that.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; + } + } - if (this.is_object(res)) { - contextStack.pop(); - } - }, - render_section: function(sectionType, mustacheFragment, key, contextStack, partials, openTag, closeTag) { - // by @langalex, support for arrays of strings - var that = this; - function create_context(_context) { - if(that.is_object(_context)) { - return _context; - } else { - var iterator = '.'; - - if(that.pragmas["IMPLICIT-ITERATOR"] && - that.pragmas["IMPLICIT-ITERATOR"].iterator) { - iterator = that.pragmas["IMPLICIT-ITERATOR"].iterator; + var value = this.find_in_stack(key, contextStack); + + var tokens; + if (sectionType==='invertedSection') { + if (!value || this.is_array(value) && value.length === 0) { + // false or empty list, render it + tokens = this.tokenize(mustacheFragment, openTag, closeTag); + + this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); } - var ctx = {}; - ctx[iterator] = _context; - return ctx; + } else if (sectionType==='section') { + if (this.is_array(value)) { // Enumerable, Let's loop! + tokens = this.tokenize(mustacheFragment, openTag, closeTag); + + for (var i=0, n=value.length; i/g,'>'); + } - var tokens; - if (sectionType==='invertedSection') { - if (!value || this.is_array(value) && value.length === 0) { - // false or empty list, render it - tokens = this.tokenize(mustacheFragment, openTag, closeTag); - - this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); + var that = this; + this.user_send_func(function(contextStack, send_func) { + var result = that.find_in_stack(key, contextStack); + if (result!==undefined) { + send_func(escapeHTML(result)); + } + }); + }, + unescaped_variable: function(key/*, contextStack*/) { + var that = this; + this.user_send_func(function(contextStack, send_func) { + var result = that.find_in_stack(key, contextStack); + if (result!==undefined) { + send_func(result); + } + }); + }, + partial: function(key, reserved/*contextStack*/, partials, openTag, closeTag) { + if (!partials || partials[key] === undefined) { + throw new ParserException('Unknown partial \'' + key + '\''); } - } else if (sectionType==='section') { - if (this.is_array(value)) { // Enumerable, Let's loop! - tokens = this.tokenize(mustacheFragment, openTag, closeTag); + + var old_user_send_func = this.user_send_func; + var commands = []; + this.user_send_func = function(command) { commands.push(command); }; + + var tokens = this.tokenize(partials[key], openTag, closeTag); + this.parse(this.createParserContext(tokens, partials, openTag, closeTag), reserved); + + this.user_send_func = old_user_send_func; + + var that = this; + this.user_send_func(function(contextStack, send_func) { + var res = that.find_in_stack(key, contextStack); + if (that.is_object(res)) { + contextStack.push(res); + } + + for (var i=0,n=commands.length; ipartial}}.', {partial: 'i love {{sugar}}'}); + equals(template({sugar: 'chocolate'}), 'the grand poobah says: i love chocolate.'); + + template = Mustache.compile('the grand poobah says: {{#hos}}i love chocolate{{/hos}}.', {}); + equals(template({hos:function() { return function(text, renderer) { return '' + text + ''; } }}), 'the grand poobah says: i love chocolate.'); +}); + test("Basic Variables", function() { expect(3); From 081772c3f4850847e69ed84cd7bdd6e0c4e1936f Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 16 Jul 2010 18:36:24 -0400 Subject: [PATCH 2/7] replicate interpreter tests into compiler tests. --- test/{unit.js => unit.compiler.js} | 29 +- test/unit.html | 3 +- test/unit.interpreter.js | 581 +++++++++++++++++++++++++++++ 3 files changed, 599 insertions(+), 14 deletions(-) rename test/{unit.js => unit.compiler.js} (94%) create mode 100644 test/unit.interpreter.js diff --git a/test/unit.js b/test/unit.compiler.js similarity index 94% rename from test/unit.js rename to test/unit.compiler.js index 3e93a1d..631651e 100644 --- a/test/unit.js +++ b/test/unit.compiler.js @@ -1,3 +1,19 @@ +// the compiler tests are the exact same as the interpreter tests +// so instead of writing all the tests twice, override the to_html +// method +module('Compiler', { + setup: function() { + this._oldToHtml = Mustache.to_html; + Mustache.to_html = function(template, view, partials) { + var compiler = Mustache.compile(template, partials); + return compiler(view); + } + }, + teardown: function() { + Mustache.to_html = this._oldToHtml; + } +}); + test("Parser", function() { expect(3); @@ -53,19 +69,6 @@ test("Parser", function() { }); -test("Compiler", function() { - expect(3); - - var template = Mustache.compile('the grand poobah says: {{variable}}.', {}); - equals(template({variable: 'hello'}), 'the grand poobah says: hello.'); - - template = Mustache.compile('the grand poobah says: {{>partial}}.', {partial: 'i love {{sugar}}'}); - equals(template({sugar: 'chocolate'}), 'the grand poobah says: i love chocolate.'); - - template = Mustache.compile('the grand poobah says: {{#hos}}i love chocolate{{/hos}}.', {}); - equals(template({hos:function() { return function(text, renderer) { return '' + text + ''; } }}), 'the grand poobah says: i love chocolate.'); -}); - test("Basic Variables", function() { expect(3); diff --git a/test/unit.html b/test/unit.html index 5f373f2..64cbc28 100644 --- a/test/unit.html +++ b/test/unit.html @@ -5,7 +5,8 @@ - + +

mustache.js unit tests

diff --git a/test/unit.interpreter.js b/test/unit.interpreter.js new file mode 100644 index 0000000..9cf8015 --- /dev/null +++ b/test/unit.interpreter.js @@ -0,0 +1,581 @@ +module('Interpreter'); + +test("Parser", function() { + expect(3); + + // matches whitespace_partial.html + equals( + Mustache.to_html( + '

{{ greeting }}

\n{{> partial }}\n

{{ farewell }}

', + { + greeting: function() { + return "Welcome"; + }, + + farewell: function() { + return "Fair enough, right?"; + }, + + partial: { + name: "Chris", + value: 10000, + taxed_value: function() { + return this.value - (this.value * 0.4); + }, + in_ca: true + } + }, + {partial:'Hello {{ name}}\nYou have just won ${{value }}!\n{{# in_ca }}\nWell, ${{ taxed_value }}, after taxes.\n{{/ in_ca }}\n'} + ), + '

Welcome

\nHello Chris\nYou have just won $10000!\n\nWell, $6000, after taxes.\n\n\n

Fair enough, right?

', + 'Whitespace in Tag names' + ); + + equals( + Mustache.to_html( + '{{tag1}}\n\n\n{{tag2}}\n', + { tag1: 'Hello', tag2: 'World' }, + {} + ), + 'Hello\n\n\nWorld\n', + 'Preservation of white space' + ); + + try { + Mustache.to_html( + '{{=tag1}}', + { tag1: 'Hello' }, + {} + ); + + ok(false); + } catch (e) { + equals(e.message, 'Unexpected end of document.'); + } + +}); + +test("Basic Variables", function() { + expect(3); + + // matches escaped.html + equals( + Mustache.to_html( + '

{{title}}

\nBut not {{entities}}.\n', + { + title: function() { + return "Bear > Shark"; + }, + entities: """ + }, + {} + ), + '

Bear > Shark

\nBut not ".\n', + 'HTML Escaping' + ); + + // matches null_string.html + equals( + Mustache.to_html( + 'Hello {{name}}\nglytch {{glytch}}\nbinary {{binary}}\nvalue {{value}}\nnumeric {{numeric}}', + { + name: "Elise", + glytch: true, + binary: false, + value: null, + numeric: function() { + return NaN; + } + }, + {} + ), + 'Hello Elise\nglytch true\nbinary false\nvalue \nnumeric NaN', + 'Different variable types' + ); + + // matches two_in_a_row.html + equals( + Mustache.to_html( + '{{greeting}}, {{name}}!', + { + name: "Joe", + greeting: "Welcome" + }, + {} + ), + 'Welcome, Joe!' + ); + +}); + +test("'{' or '&' (Unescaped Variable)", function() { + expect(2); + + // matches unescaped.html + equals( + Mustache.to_html( + '

{{{title}}}

', + { + title: function() { + return "Bear > Shark"; + } + }, + {} + ), + '

Bear > Shark

', + '{ character' + ); + + equals( + Mustache.to_html( + '

{{&title}}

', + { + title: function() { + return "Bear > Shark"; + } + }, + {} + ), + '

Bear > Shark

', + '& character' + ); +}); + +test("'#' (Sections)", function() { + expect(7); + + // matches array_of_partials_implicit_partial.html + equals( + Mustache.to_html( + 'Here is some stuff!\n{{#numbers}}\n{{>partial}}\n{{/numbers}}', + { numbers: ['1', '2', '3', '4'] }, + { partial: '{{.}}' } + ), + 'Here is some stuff!\n\n1\n\n2\n\n3\n\n4\n', + 'Array of Partials (Implicit)' + ); + + // matches array_of_partials_partial.html + equals( + Mustache.to_html( + 'Here is some stuff!\n{{#numbers}}\n{{>partial}}\n{{/numbers}}', + { numbers: [{i: '1'}, {i: '2'}, {i: '3'}, {i: '4'}] }, + { partial: '{{i}}' } + ), + 'Here is some stuff!\n\n1\n\n2\n\n3\n\n4\n', + 'Array of Partials (Explicit)' + ); + + // matches array_of_strings.html + equals( + Mustache.to_html( + '{{#array_of_strings}}{{.}} {{/array_of_strings}}', + {array_of_strings: ['hello', 'world']}, + {} + ), + 'hello world ', + 'Array of Strings' + ); + + // mathces higher_order_sections.html + equals( + Mustache.to_html( + '{{#bolder}}Hi {{name}}.{{/bolder}}\n', + { + "name": "Tater", + "helper": "To tinker?", + "bolder": function() { + return function(text, render) { + return "" + render(text) + ' ' + this.helper; + } + } + }, + {} + ), + 'Hi Tater. To tinker?\n' + ); + + // matches recursion_with_same_names.html + equals( + Mustache.to_html( + '{{ name }}\n{{ description }}\n\n{{#terms}}\n {{name}}\n {{index}}\n{{/terms}}\n', + { + name: 'name', + description: 'desc', + terms: [ + {name: 't1', index: 0}, + {name: 't2', index: 1}, + ] + }, + {} + ), + 'name\ndesc\n\n\n t1\n 0\n\n t2\n 1\n\n' + ); + + // matches reuse_of_enumerables.html + equals( + Mustache.to_html( + '{{#terms}}\n {{name}}\n {{index}}\n{{/terms}}\n{{#terms}}\n {{name}}\n {{index}}\n{{/terms}}\n', + { + terms: [ + {name: 't1', index: 0}, + {name: 't2', index: 1}, + ] + }, + {} + ), + '\n t1\n 0\n t2\n 1\n\n t1\n 0\n t2\n 1\n\n', + 'Lazy match of Section and Inverted Section' + ); + + // matches section_as_context.html + equals( + Mustache.to_html( + '{{#a_object}}\n

{{title}}

\n

{{description}}

\n
    \n {{#a_list}}\n
  • {{label}}
  • \n {{/a_list}}\n
\n{{/a_object}}\n', + { + a_object: { + title: 'this is an object', + description: 'one of its attributes is a list', + a_list: [{label: 'listitem1'}, {label: 'listitem2'}] + } + }, + {} + ), + '\n

this is an object

\n

one of its attributes is a list

\n
    \n
  • listitem1
  • \n
  • listitem2
  • \n
\n', + 'Lazy match of Section and Inverted Section' + ); +}); + +test("'^' (Inverted Section)", function() { + expect(1); + + // matches inverted_section.html + equals( + Mustache.to_html( + '{{#repo}}{{name}}{{/repo}}\n{{^repo}}No repos :({{/repo}}\n', + { + "repo": [] + }, + {} + ), + '\nNo repos :(\n' + ); +}); + +test("'>' (Partials)", function() { + expect(5); + + // matches view_partial.html + equals( + Mustache.to_html( + '

{{greeting}}

\n{{>partial}}\n

{{farewell}}

', + { + greeting: function() { + return "Welcome"; + }, + + farewell: function() { + return "Fair enough, right?"; + }, + + partial: { + name: "Chris", + value: 10000, + taxed_value: function() { + return this.value - (this.value * 0.4); + }, + in_ca: true + } + }, + {partial: 'Hello {{name}}\nYou have just won ${{value}}!\n{{#in_ca}}\nWell, ${{ taxed_value }}, after taxes.\n{{/in_ca}}\n'} + ), + '

Welcome

\nHello Chris\nYou have just won $10000!\n\nWell, $6000, after taxes.\n\n\n

Fair enough, right?

' + ); + + // matches array_partial.html + equals( + Mustache.to_html( + '{{>partial}}', + { + partial: { + array: ['1', '2', '3', '4'] + } + }, + { partial: 'Here\'s a non-sense array of values\n{{#array}}\n {{.}}\n{{/array}}' } + ), + 'Here\'s a non-sense array of values\n\n 1\n\n 2\n\n 3\n\n 4\n' + ); + + // matches template_partial.html + equals( + Mustache.to_html( + '

{{title}}

\n{{>partial}}', + { + title: function() { + return "Welcome"; + }, + partial: { + again: "Goodbye" + } + }, + {partial:'Again, {{again}}!'} + ), + '

Welcome

\nAgain, Goodbye!' + ); + + // matches partial_recursion.html + equals( + Mustache.to_html( + '{{name}}\n{{#kids}}\n{{>partial}}\n{{/kids}}', + { + name: '1', + kids: [ + { + name: '1.1', + children: [ + {name: '1.1.1'} + ] + } + ] + }, + {partial:'{{name}}\n{{#children}}\n{{>partial}}\n{{/children}}'} + ), + '1\n\n1.1\n\n1.1.1\n\n\n' + ); + + try { + Mustache.to_html( + '{{>partial}}', + {}, + {partal: ''} + ); + ok(false); + } catch(e) { + equals(e.message, "Unknown partial 'partial'"); + } +}); + +test("'=' (Set Delimiter)", function() { + expect(1); + + // matches delimiter.html + equals( + Mustache.to_html( + '{{=<% %>=}}*\n<% first %>\n* <% second %>\n<%=| |=%>\n* | third |\n|={{ }}=|\n* {{ fourth }}', + { + first: "It worked the first time.", + second: "And it worked the second time.", + third: "Then, surprisingly, it worked the third time.", + fourth: "Fourth time also fine!." + }, + {} + ), + '*\nIt worked the first time.\n* And it worked the second time.\n\n* Then, surprisingly, it worked the third time.\n\n* Fourth time also fine!.', + 'Simple Set Delimiter' + ); +}); + +test("'!' (Comments)", function() { + expect(1); + + // matches comments.html + equals( + Mustache.to_html( + '

{{title}}{{! just something interesting... or not... }}

\n', + { + title: function() { + return "A Comedy of Errors"; + } + }, + {} + ), + '

A Comedy of Errors

\n' + ); +}); + +test("'%' (Pragmas)", function() { + expect(3); + + // matches array_of_strings_options.html + equals( + Mustache.to_html( + '{{%IMPLICIT-ITERATOR iterator=rob}}\n{{#array_of_strings_options}}{{rob}} {{/array_of_strings_options}}', + {array_of_strings_options: ['hello', 'world']}, + {} + ), + '\nhello world ', + 'IMPLICIT-ITERATOR pragma' + ); + + // matches unknown_pragma.txt + try { + equals( + Mustache.to_html( + '{{%I-HAVE-THE-GREATEST-MUSTACHE}}\n', + {}, + {} + ), + 'hello world ', + 'IMPLICIT-ITERATOR pragma' + ); + ok(false); + } catch (e) { + equals(e.message, 'This implementation of mustache doesn\'t understand the \'I-HAVE-THE-GREATEST-MUSTACHE\' pragma'); + } + + equals( + Mustache.to_html( + '{{%IMPLICIT-ITERATOR}}{{#dataSet}}{{.}}:{{/dataSet}}', + { dataSet: [ 'Object 1', 'Object 2', 'Object 3' ] }, + {} + ), + "Object 1:Object 2:Object 3:" + ); +}); + +test("Empty", function() { + expect(2); + + // matches empty_template.html + equals( + Mustache.to_html( + '

Test

', + {}, + {} + ), + '

Test

', + 'Empty Template' + ); + + // matches empty_partial.html + equals( + Mustache.to_html( + 'hey {{foo}}\n{{>partial}}\n', + { + foo: 1 + }, + {partial: 'yo'} + ), + 'hey 1\nyo\n', + 'Empty Partial' + ); +}); + +test("Demo", function() { + expect(2); + + // matches simple.html + equals( + Mustache.to_html( + 'Hello {{name}}\nYou have just won ${{value}}!\n{{#in_ca}}\nWell, ${{ taxed_value }}, after taxes.\n{{/in_ca}}', + { + name: "Chris", + value: 10000, + taxed_value: function() { + return this.value - (this.value * 0.4); + }, + in_ca: true + }, + {} + ), + 'Hello Chris\nYou have just won $10000!\n\nWell, $6000, after taxes.\n', + 'A simple template' + ); + + // matches complex.html + var template = [ + '

{{header}}

', + '{{#list}}', + '
    ', + ' {{#item}}', + ' {{#current}}', + '
  • {{name}}
  • ', + ' {{/current}}', + ' {{#link}}', + '
  • {{name}}
  • ', + ' {{/link}}', + ' {{/item}}', + '
', + '{{/list}}', + '{{#empty}}', + '

The list is empty.

', + '{{/empty}}', + ].join('\n'); + + var view = { + header: function() { + return "Colors"; + }, + item: [ + {name: "red", current: true, url: "#Red"}, + {name: "green", current: false, url: "#Green"}, + {name: "blue", current: false, url: "#Blue"} + ], + link: function() { + return this["current"] !== true; + }, + list: function() { + return this.item.length !== 0; + }, + empty: function() { + return this.item.length === 0; + } + }; + + var expected_result = [ + '

Colors

', + ' ', + '' + ].join('\n'); + + equals( + Mustache.to_html( + template, + view, + {} + ), + expected_result, + 'A complex template' + ); +}); + +test("Regression Suite", function() { + expect(3); + + // matches bug_11_eating_whitespace.html + equals( + Mustache.to_html( + '{{tag}} foo', + { tag: "yo" }, + {} + ), + 'yo foo', + 'Issue 11' + ); + + // matches delimiters_partial.html + equals( + Mustache.to_html( + '{{#enumerate}}\n{{>partial}}\n{{/enumerate}}', + { enumerate: [ { text: 'A' }, { text: 'B' } ] }, + { partial: '{{=[[ ]]=}}\n{{text}}\n[[={{ }}=]]' } + ), + '\n\n{{text}}\n\n\n\n{{text}}\n\n', + 'Issue 44' + ); + + // matches bug_46_set_delimiter.html + equals( + Mustache.to_html( + '{{=[[ ]]=}}[[#IsMustacheAwesome]]mustache is awesome![[/IsMustacheAwesome]]', + {IsMustacheAwesome: true}, + {} + ), + 'mustache is awesome!', + 'Issue 46' + ); +}); From afbb2089db00e3ac9ad7841e61780f2a0f0fb3af Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 16 Jul 2010 22:18:16 -0400 Subject: [PATCH 3/7] pass all tests in both interpreter and compiler mode --- mustache.js | 62 +++++++++++++++++++++++++------------------ test/unit.compiler.js | 14 +++------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/mustache.js b/mustache.js index ade8120..d830e55 100644 --- a/mustache.js +++ b/mustache.js @@ -575,19 +575,22 @@ var Mustache = function() { var result = value.call(contextStack[contextStack.length-1], mustacheFragment, function(resultFragment) { var tempStream = []; - var old_send_func = that.send_func; - that.send_func = function(text) { tempStream.push(text); }; + var old_send_func = that.user_send_func; + that.user_send_func = function(text) { tempStream.push(text); }; tokens = that.tokenize(resultFragment, openTag, closeTag); that.parse(that.createParserContext(tokens, partials, openTag, closeTag), contextStack); - that.send_func = old_send_func; + that.user_send_func = old_send_func; return tempStream.join(''); }); this.user_send_func(result); - } + } else if (value) { + tokens = this.tokenize(mustacheFragment, openTag, closeTag); + this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); + } } else { throw new ParserException('Unknown section type ' + sectionType); } @@ -632,30 +635,35 @@ var Mustache = function() { throw new ParserException('Unknown partial \'' + key + '\''); } - var old_user_send_func = this.user_send_func; - var commands = []; - this.user_send_func = function(command) { commands.push(command); }; - - var tokens = this.tokenize(partials[key], openTag, closeTag); - this.parse(this.createParserContext(tokens, partials, openTag, closeTag), reserved); - - this.user_send_func = old_user_send_func; - - var that = this; - this.user_send_func(function(contextStack, send_func) { - var res = that.find_in_stack(key, contextStack); - if (that.is_object(res)) { - contextStack.push(res); - } + if (!this.is_function(partials[key])) { + var old_user_send_func = this.user_send_func; + var commands = []; + this.user_send_func = function(command) { commands.push(command); }; + + var tokens = this.tokenize(partials[key], openTag, closeTag); + partials[key] = function() {}; // blank out the paritals so that infinite recursion doesn't happen + this.parse(this.createParserContext(tokens, partials, openTag, closeTag), reserved); - for (var i=0,n=commands.length; ithis is an object\n

one of its attributes is a list

\n
    \n
  • listitem1
  • \n
  • listitem2
  • \n
\n', + '\n

this is an object

\n

one of its attributes is a list

\n
    \n \n
  • listitem1
  • \n \n
  • listitem2
  • \n \n
\n\n', 'Lazy match of Section and Inverted Section' ); }); @@ -536,15 +536,7 @@ test("Demo", function() { } }; - var expected_result = [ - '

Colors

', - ' ', - '' - ].join('\n'); + var expected_result = '

Colors

\n\n
    \n \n \n
  • red
  • \n \n \n
  • red
  • \n \n \n \n \n
  • green
  • \n \n \n \n \n
  • blue
  • \n \n \n
\n\n'; equals( Mustache.to_html( From 4ed4db0101e2c3a687d08497e9c364e11cf23400 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 16 Jul 2010 22:33:03 -0400 Subject: [PATCH 4/7] Performance test for Compiler path --- test/unit.compiler.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/unit.compiler.js b/test/unit.compiler.js index 2103ac0..c88e2f4 100644 --- a/test/unit.compiler.js +++ b/test/unit.compiler.js @@ -549,6 +549,38 @@ test("Demo", function() { ); }); +test("Performance", function() { + expect(1); + + var start, end; + + var view = []; + for (var i=0;i<1000;++i) { + view.push({name:i}); + } + + var template = '{{#count}}{{name}}\n{{/count}}'; + + start = Date.now(); + for (var j=0;j<1000;++j) { + this._oldToHtml(template, view, {}); + } + end = Date.now(); + + var interpreter_time = end - start; + + start = Date.now(); + var compiler = Mustache.compile(template, {}); + for (var k=0;k<1000;++k) { + compiler(view); + } + end = Date.now(); + + var compiler_time = end - start; + + ok(compiler_time Date: Fri, 16 Jul 2010 22:51:18 -0400 Subject: [PATCH 5/7] Partials compiler should be non-destructive --- mustache.js | 9 ++++++++- test/unit.compiler.js | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mustache.js b/mustache.js index d830e55..2188673 100644 --- a/mustache.js +++ b/mustache.js @@ -773,11 +773,18 @@ var Mustache = function() { } }, compile: function(template, partials) { + var p = {}; + for (var key in partials) { + if (partials.hasOwnProperty(key)) { + p[key] = partials[key]; + } + } + var commands = []; var s = function(command) { commands.push(command); }; var renderer = new Renderer(s, 'compiler'); - renderer.render(template, {}, partials); + renderer.render(template, {}, p); return function(view, send_func) { var o = send_func ? undefined : []; diff --git a/test/unit.compiler.js b/test/unit.compiler.js index c88e2f4..b098074 100644 --- a/test/unit.compiler.js +++ b/test/unit.compiler.js @@ -15,7 +15,7 @@ module('Compiler', { }); test("Parser", function() { - expect(3); + expect(4); // matches whitespace_partial.html equals( @@ -67,6 +67,10 @@ test("Parser", function() { equals(e.message, 'Unexpected end of document.'); } + var partials = { 'partial' : '{{key}}' }; + Mustache.compile('{{>partial}}', partials ); + + equals(partials['partial'], '{{key}}', 'Partials compiler must be non-destructive'); }); test("Basic Variables", function() { From f39ec6d08a1595edeb30ed16fa75b8f41d4f4563 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 17 Jul 2010 10:18:38 -0400 Subject: [PATCH 6/7] replace interpreted version of higher order section with a compiled version --- mustache.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mustache.js b/mustache.js index 2188673..7f99cbe 100644 --- a/mustache.js +++ b/mustache.js @@ -727,18 +727,26 @@ var Mustache = function() { contextStack.pop(); } else if (that.is_function(value)) { // higher order section - // note that HOS triggers a full interpreter call on the result fragment - // this is slow in comparison to a compiled call + // note that HOS triggers a compilation on the resultFragment. + // this is slow (in relation to a fully compiled template) + // since it invokes a call to the parser var result = value.call(contextStack[contextStack.length-1], mustacheFragment, function(resultFragment) { - var o = []; - var s = function(output) { o.push(output); }; + var cO = []; + var s = function(command) { cO.push(command); }; - var hos_renderer = new Renderer(s, 'interpreter'); + var hos_renderer = new Renderer(s, 'compiler'); resultFragment = hos_renderer.parse_pragmas(resultFragment, openTag, closeTag); var tokens = hos_renderer.tokenize(resultFragment, openTag, closeTag); hos_renderer.parse(hos_renderer.createParserContext(tokens, partials, openTag, closeTag), contextStack); + var o = []; + var sT = function(output) { o.push(output); }; + + for (var i=0,n=cO.length; i Date: Sat, 17 Jul 2010 10:31:25 -0400 Subject: [PATCH 7/7] Beef up entry points for bad input checks. --- mustache.js | 25 ++++++++++++++++++++----- test/unit.compiler.js | 9 +++++++++ test/unit.interpreter.js | 9 +++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/mustache.js b/mustache.js index 7f99cbe..8ac2905 100644 --- a/mustache.js +++ b/mustache.js @@ -770,6 +770,11 @@ var Mustache = function() { Turns a template and view into HTML */ to_html: function(template, view, partials, send_func) { + if (!template) { return ''; } + + partials = partials || {}; + view = view || {}; + var o = send_func ? undefined : []; var s = send_func || function(output) { o.push(output); }; @@ -780,11 +785,20 @@ var Mustache = function() { return o.join(''); } }, + + /* + Compiles a template into an equivalent JS function for faster + repeated execution. + */ compile: function(template, partials) { + if (!template) { return function() { return '' }; } + var p = {}; - for (var key in partials) { - if (partials.hasOwnProperty(key)) { - p[key] = partials[key]; + if (partials) { + for (var key in partials) { + if (partials.hasOwnProperty(key)) { + p[key] = partials[key]; + } } } @@ -795,9 +809,10 @@ var Mustache = function() { renderer.render(template, {}, p); return function(view, send_func) { - var o = send_func ? undefined : []; - var s = send_func || function(output) { o.push(output); }; + view = view || {}; + var o = send_func ? undefined : []; + var s = send_func || function(output) { o.push(output); }; for (var i=0,n=commands.length; ihi}}', undefined, {hi:'{{p}}'}), '', 'Partial but no view'); +}); + test("Parser", function() { expect(4); diff --git a/test/unit.interpreter.js b/test/unit.interpreter.js index fac0bfd..175b809 100644 --- a/test/unit.interpreter.js +++ b/test/unit.interpreter.js @@ -1,5 +1,14 @@ module('Interpreter'); +test("Argument validation", function() { + expect(4); + + equals(Mustache.to_html(undefined), '', 'No parameters'); + equals(Mustache.to_html('{{hi}}'), '', ' No View or Partials'); + equals(Mustache.to_html('{{hi}}', {hi:'Hi.'}), 'Hi.', 'No Partials'); + equals(Mustache.to_html('{{>hi}}', undefined, {hi:'{{p}}'}), '', 'Partial but no view'); +}); + test("Parser", function() { expect(3);