/* * Copyright 2011 Twitter, Inc. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var Hogan = Hogan || require('../lib/hogan') , doc = this["document"] function testScanTextNoTags() { var text = "

hi

"; var tokens = Hogan.scan(text); is(tokens.length, 1, "One token"); is(tokens[0]+'', text, "text is equal to first token"); } function testScanOneTag() { var text = "{{hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 1, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); } function testScanMultipleTags() { var text = "asdf{{hmm}}asdf2{{hmm2}}asdf3"; var tokens = Hogan.scan(text); is(tokens.length, 5, "3 text tokens, 2 tag tokens."); is(tokens[0]+'', "asdf", "first token is text"); is(tokens[1].n, "hmm", "second token is tag"); is(tokens[1].tag, "_v", "second token is a variable"); is(tokens[2]+'', "asdf2", "third token is text"); is(tokens[3].n, "hmm2", "fourth token is tag"); is(tokens[3].tag, "_v", "fourth token is a variable"); is(tokens[4]+'', "asdf3", "Fifth token is text"); } function testScanSectionOpen() { var text = "{{#hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 1, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); is(tokens[0].tag, "#", "First token is a section."); } function testScanSectionClose() { var text = "{{/hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 1, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); is(tokens[0].tag, "/", "First token is a section."); } function testScanSection() { var text = "{{#hmm}}{{/hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 2, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); is(tokens[0].tag, "#", "First token is a section."); is(tokens[1].n, "hmm", "Second token content is variable name."); is(tokens[1].tag, "/", "Second token is a section."); } function testScanSectionInContent() { var text = "abc{{#hmm}}def{{/hmm}}ghi"; var tokens = Hogan.scan(text); is(tokens.length, 5, "3 text tokens, 2 tag tokens."); is(tokens[0]+'', "abc", "first token is text"); is(tokens[1].n, "hmm", "second token is tag"); is(tokens[1].tag, "#", "second token is a variable"); is(tokens[2]+'', "def", "third token is text"); is(tokens[3].n, "hmm", "fourth token is tag"); is(tokens[3].tag, "/", "fourth token is a variable"); is(tokens[4]+'', "ghi", "Fifth token is text"); } function testScanNegativeSection() { var text = "{{^hmm}}{{/hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 2, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); is(tokens[0].tag, "^", "First token is a negative section."); is(tokens[1].n, "hmm", "First token content is variable name."); is(tokens[1].tag, "/", "Second token is a section."); } function testScanPartial() { var text = "{{>hmm}}"; var tokens = Hogan.scan(text); is(tokens.length, 1, "One token"); is(tokens[0].n, "hmm", "First token content is variable name."); is(tokens[0].tag, ">", "First token is a partial."); } function testScanBackwardPartial() { var text = "{{
\' \" &"}); is(s, "< > <div> ' " &", "input correctly escaped."); var ec ={ "'": "'", '"': """, "<": "<", ">": ">", "&": "&"} for (var char in ec) { var s = t.render({foo: char + " just me"}); is(s, ec[char] + " just me", "input correctly escaped."); } } function testMustacheInjection() { var text = "{{foo}}"; var t = Hogan.compile(text); s = t.render({foo:"{{{<42}}}"}) is(s, "{{{<42}}}", "Can't inject mustache"); } function testTripleStache() { var text = "{{{foo}}}"; var t = Hogan.compile(text); var s = t.render({foo: "< >
\' \" &"}); is(s, "< >
\' \" &", "input correctly not-escaped."); } function testAmpNoEscaping() { var text = "{{&foo}}"; var t = Hogan.compile(text); var s = t.render({foo: "< >
\' \" &"}); is(s, "< >
\' \" &", "input correctly not-escaped."); } function testPartial() { var partialText = "this is text from the partial--the magic number {{foo}} is from a variable"; var p = Hogan.compile(partialText); var text = "This template contains a partial ({{>testPartial}})." var t = Hogan.compile(text); var s = t.render({foo: 42}, {testPartial: p}); is(s, "This template contains a partial (this is text from the partial--the magic number 42 is from a variable).", "partials work"); } function testNestedPartials() { var partialText = "this is text from the partial--the magic number {{foo}} is from a variable"; var p = Hogan.compile(partialText); var partialText2 = "This template contains a partial ({{>testPartial}})." var p2 = Hogan.compile(partialText2); var text = "This template contains a partial that contains a partial [{{>testPartial2}}]." var t = Hogan.compile(text); var s = t.render({foo: 42}, {testPartial: p, testPartial2: p2}); is(s, "This template contains a partial that contains a partial [This template contains a partial (this is text from the partial--the magic number 42 is from a variable).].", "nested partials work"); } function testNegativeSection() { var text = "This template {{^foo}}BOO {{/foo}}contains an inverted section." var t = Hogan.compile(text); var s = t.render(); is(s, "This template BOO contains an inverted section.", "inverted sections with no context work"); s = t.render({foo:[]}); is(s, "This template BOO contains an inverted section.", "inverted sections with empty list context work"); s = t.render({ foo:false }); is(s, "This template BOO contains an inverted section.", "inverted sections with false context work"); s = t.render({foo:''}); is(s, "This template contains an inverted section.", "inverted sections with empty string context work"); s = t.render({foo:true}); is(s, "This template contains an inverted section.", "inverted sections with true context work"); s = t.render({foo: function() { return false; }}); is(s, "This template BOO contains an inverted section.", "inverted sections with false returning method in context work"); } function testSectionElision() { var text = "This template {{#foo}}BOO {{/foo}}contains a section." var t = Hogan.compile(text); var s = t.render(); is(s, "This template contains a section.", "sections with no context work"); s = t.render({foo:[]}); is(s, "This template contains a section.", "sections with empty list context work"); s = t.render({foo:false}); is(s, "This template contains a section.", "sections with false context work"); } function testSectionObjectContext() { var text = "This template {{#foo}}{{bar}} {{/foo}}contains a section." var t = Hogan.compile(text); var s = t.render({foo:{bar:42}}); is(s, "This template 42 contains a section.", "sections with object context work"); } function testSectionArrayContext() { var text = "This template {{#foo}}{{bar}} {{/foo}}contains a section." var t = Hogan.compile(text); var s = t.render({foo:[{bar:42}, {bar:43}, {bar:44}]}); is(s, "This template 42 43 44 contains a section.", "sections with object ctx and array values work"); } function testFalsyVariableNoRender() { var text = "I ({{cannot}}) be seen!"; var t = Hogan.compile(text); var s = t.render(); is(s, "I () be seen!", "missing value doesn't render."); } function testSectionExtensions() { var text = "Test {{_//|__foo}}bar{{/foo}}"; var options = {sectionTags:[{o:'_//|__foo', c:'foo'}]}; var tree = Hogan.parse(Hogan.scan(text), options); is(tree[1].tag, "#", "_//|__foo node transformed to section"); is(tree[1].n, "_//|__foo", "_//|__foo node transformed to section"); var t = Hogan.compile(text, options ); var s = t.render({'_//|__foo':true}); is(s, "Test bar", "Custom sections work"); } function testMisnestedSectionExtensions() { var text = "Test {{__foo}}bar{{/bar}}"; var options = {sectionTags:[{o:'__foo', c:'foo'}, {o:'__bar', c:'bar'}]}; var msg = ''; try { var tree = Hogan.parse(Hogan.scan(text), options); } catch (e) { msg = e.message; } is(msg, "Nesting error: __foo vs. bar", "Error is generated"); } function testNestedSection() { var text = "{{#foo}}{{#bar}}{{baz}}{{/bar}}{{/foo}}"; var t = Hogan.compile(text); var s = t.render({foo: 42, bar: 42, baz:42}); is(s, "42", "can reach up context stack"); } function testDottedNames() { var text = '"{{person.name}}" == "{{#person}}{{name}}{{/person}}"'; var t = Hogan.compile(text); var s = t.render({person:{name:'Joe'}}); is(s, '"Joe" == "Joe"', "dotted names work"); } function testImplicitIterator() { var text = '{{#stuff}} {{.}} {{/stuff}}'; var t = Hogan.compile(text); var s = t.render({stuff:[42,43,44]}); is(s, " 42 43 44 ", "implicit iterators work"); } function testPartialsAndDelimiters() { var text = '{{>include}}*\n{{= | | =}}\n*|>include|'; var partialText = ' .{{value}}. '; var partial = Hogan.compile(partialText); var t = Hogan.compile(text); var s = t.render({value:"yes"}, {'include':partial}); is(s, " .yes. *\n* .yes. ", "partials work around delimiters"); } function testStringPartials() { var text = "foo{{>mypartial}}baz"; var partialText = " bar "; var t = Hogan.compile(text); var s = t.render({}, {'mypartial': partialText}); is(s, "foo bar baz", "string partial works."); } function testMissingPartials() { var text = "foo{{>mypartial}} bar"; var t = Hogan.compile(text); var s = t.render({}); is(s, "foo bar", "missing partial works."); } function testIndentedStandaloneComment() { var text = 'Begin.\n {{! Indented Comment Block! }}\nEnd.'; var t = Hogan.compile(text); var s = t.render(); is(s, 'Begin.\nEnd.', "Standalone comment blocks are removed."); } function testNewLineBetweenDelimiterChanges() { var data = { section: true, data: 'I got interpolated.' }; var text = '\n{{#section}}\n {{data}}\n |data|\n{{/section}}x\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|'; var t = Hogan.compile(text); var s = t.render(data); is(s, '\n I got interpolated.\n |data|\nx\n\n {{data}}\n I got interpolated.\n', 'render correct') } function testMustacheJSApostrophe() { var text = '{{apos}}{{control}}'; var t = Hogan.compile(text); var s = t.render({'apos':"'", 'control':"X"}); is(s, ''X', 'Apostrophe is escaped.'); } function testMustacheJSArrayOfImplicitPartials() { var text = 'Here is some stuff!\n{{#numbers}}\n{{>partial}}\n{{/numbers}}\n'; var partialText = '{{.}}\n'; var t = Hogan.compile(text); var s = t.render({numbers:[1,2,3,4]}, {partial: partialText}); is(s, 'Here is some stuff!\n1\n2\n3\n4\n', 'Partials with implicit iterators work.'); } function testMustacheJSArrayOfPartials() { var text = 'Here is some stuff!\n{{#numbers}}\n{{>partial}}\n{{/numbers}}\n'; var partialText = '{{i}}\n'; var t = Hogan.compile(text); var s = t.render({numbers:[{i:1},{i:2},{i:3},{i:4}]}, {partial: partialText}); is(s, 'Here is some stuff!\n1\n2\n3\n4\n', 'Partials with arrays work.'); } function testMustacheJSArrayOfStrings() { var text = '{{#strings}}{{.}} {{/strings}}'; var t = Hogan.compile(text); var s = t.render({strings:['foo', 'bar', 'baz']}); is(s, 'foo bar baz ', 'array of strings works with implicit iterators.'); } function testMustacheJSUndefinedString() { var text = 'foo{{bar}}baz'; var t = Hogan.compile(text); var s = t.render({bar:undefined}); is(s, 'foobaz', 'undefined value does not render.'); } function testMustacheJSTripleStacheAltDelimiter() { var text = '{{=<% %>=}}<% foo %> {{foo}} <%{bar}%> {{{bar}}}'; var t = Hogan.compile(text); var s = t.render({foo:'yeah', bar:'hmm'}); is(s, 'yeah {{foo}} hmm {{{bar}}}', 'triple stache inside alternate delimiter works.'); } /* shootout benchmark tests */ function testShootOutString() { var text = "Hello World!"; var expected = "Hello World!" var t = Hogan.compile(text) var s = t.render({}) is(s, expected, "Shootout String compiled correctly"); } function testShootOutReplace() { var text = "Hello {{name}}! You have {{count}} new messages."; var expected = "Hello Mick! You have 30 new messages."; var t = Hogan.compile(text) var s = t.render({ name: "Mick", count: 30 }) is(s, expected, "Shootout Replace compiled correctly"); } function testShootOutArray() { var text = "{{#names}}{{name}}{{/names}}"; var expected = "MoeLarryCurlyShemp"; var t = Hogan.compile(text); var s = t.render({ names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }) is(s, expected, "Shootout Array compiled correctly"); } function testShootOutObject() { var text = "{{#person}}{{name}}{{age}}{{/person}}"; var expected = "Larry45"; var t = Hogan.compile(text) var s = t.render({ person: { name: "Larry", age: 45 } }) is(s, expected, "Shootout Object compiled correctly"); } function testShootOutPartial() { var text = "{{#peeps}}{{>replace}}{{/peeps}}"; var t = Hogan.compile(text); var partial = Hogan.compile(" Hello {{name}}! You have {{count}} new messages."); var s = t.render({ peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 2}] }, { replace: partial }); var expected = " Hello Moe! You have 15 new messages. Hello Larry! You have 5 new messages. Hello Curly! You have 2 new messages."; is(s, expected, "Shootout Partial compiled correctly"); } function testShootOutRecurse() { var text = "{{name}}{{#kids}}{{>recursion}}{{/kids}}"; var t = Hogan.compile(text); var partial = Hogan.compile("{{name}}{{#kids}}{{>recursion}}{{/kids}}"); var s = t.render({ name: '1', kids: [ { name: '1.1', kids: [ { name: '1.1.1', kids: [] } ] } ] }, { recursion: partial }); var expected = "11.11.1.1"; is(s, expected, "Shootout Recurse compiled correctly"); } function testShootOutFilter() { var text = "{{#filter}}foo {{bar}}{{/filter}}"; var t = Hogan.compile(text); var s = t.render({ filter: function() { return function(text, render) { return render(text).toUpperCase(); } }, bar: "bar" }); var expected = "FOO BAR" is(s, expected, "Shootout Filter compiled correctly"); } function testShootOutComplex() { var text = "

{{header}}

" + "{{#hasItems}}" + "
    " + "{{#items}}" + "{{#current}}" + "
  • {{name}}
  • " + "{{/current}}" + "{{^current}}" + "
  • {{name}}
  • " + "{{/current}}" + "{{/items}}" + "
" + "{{/hasItems}}" + "{{^hasItems}}" + "

The list is empty.

" + "{{/hasItems}}"; var expected = "

Colors

"; var t = Hogan.compile(text) var s = t.render({ header: function() { return "Colors"; }, items: [ {name: "red", current: true, url: "#Red"}, {name: "green", current: false, url: "#Green"}, {name: "blue", current: false, url: "#Blue"} ], hasItems: function() { return this.items.length !== 0; }, empty: function() { return this.items.length === 0; } }) is(s, expected, "Shootout Complex compiled correctly"); } function testRenderOutput() { if (doc) return var fs = require('fs'); var inPath = 'test/templates'; var outPath = 'test/html'; fs.readdirSync(inPath).forEach(function (file) { var i = fs.readFileSync([inPath, file].join('/'), 'utf-8'); var t = Hogan.compile(i); var r = t.render({}); var o = fs.readFileSync([outPath, file].join('/').replace(/mustache$/, 'html')).toString(); is(r === o, true, file + ' should correctly render html') }) } function testDefaultRenderImpl() { var ht = new Hogan.Template(); is(ht.render() === '', true, 'default renderImpl returns an array.'); } function appendText(el, text) { var textNode = document.createTextNode(text); el.appendChild(textNode); el.appendChild(document.createElement('br')); } if (!this["output"]) { var output = function (s) { return doc ? appendText(doc.getElementById('console'), s) : console.log(s); }; } var passed = 0; var failed = 0; function is(got, expected, msg) { if (got === expected) { output("OK: " + msg); ++passed; } else { output("FAIL: " + msg); output("Expected |" + expected + "|"); output(" Got |" + got + "|"); ++failed; } } function complete() { output("\nTests Complete"); output("--------------"); output("Passed: " + passed); output("Failed: " + failed); output("\n"); } function runTests() { output("Tests Starting"); output("--------------"); testScanTextNoTags(); testScanOneTag(); testScanMultipleTags(); testScanSectionOpen(); testScanSectionClose(); testScanSection(); testScanSectionInContent(); testScanNegativeSection(); testScanPartial(); testScanBackwardPartial(); testScanAmpersandNoEscapeTag(); testScanTripleStache(); testScanSectionWithTripleStacheInside(); testScanSetDelimiter(); testScanResetDelimiter(); testSetDelimiterWithWhitespace(); testSingleCharDelimiter(); testParseBasic(); testParseVariables(); testParseSection(); testParseIndexes(); testParseNegativeSection(); testParseNestedSections(); testMissingClosingTag(); testBadNesting(); testBasicOutput(); //testBasicOutputAsString(); testOneVariable(); //testOneVariableAsString(); testMultipleVariables(); testNumberValues(); testObjectRender(); testObjectToStringRender(); testArrayRender(); testEscaping(); testMustacheInjection(); testTripleStache(); testAmpNoEscaping(); testPartial(); testNestedPartials(); testNegativeSection(); testSectionElision(); testSectionObjectContext(); testSectionArrayContext(); testRenderWithWhitespace(); testRenderWithWhitespaceAroundTripleStache(); testRenderWithWhitespaceAroundAmpersand(); testFalsyVariableNoRender(); testRenderOutput(); testDefaultRenderImpl(); testSectionExtensions(); testMisnestedSectionExtensions(); testNestedSection(); testShootOutString(); testShootOutReplace(); testShootOutArray(); testShootOutObject(); testShootOutPartial(); testShootOutRecurse(); testShootOutFilter(); testShootOutComplex(); testDottedNames(); testImplicitIterator(); testPartialsAndDelimiters(); testStringPartials(); testMissingPartials(); testIndentedStandaloneComment(); testNewLineBetweenDelimiterChanges(); testMustacheJSApostrophe(); testMustacheJSArrayOfImplicitPartials(); testMustacheJSArrayOfPartials(); testMustacheJSArrayOfStrings(); testMustacheJSUndefinedString(); testMustacheJSTripleStacheAltDelimiter(); complete(); } if (doc) { window.onload = runTests; } else { runTests(); }