UNPKG

vash

Version:

Razor syntax for JS templating

1,760 lines (1,602 loc) 65.9 kB
var vows = require('vows') ,assert = require('assert') ,util = require('util') ,path = require('path') ,vm = require('vm') ,vash = require( process.env.VASHPATH ) vash.config.useWith = false; vash.config.debug = true; var tryCompile = function(str){ vash.config.useWith = true; vash.compile(str); // because we want to see the stupid error assert.doesNotThrow( function(){ vash.compile(str) }, Error ); var ret; try { ret = vash.compile(str); } catch(e){ ret = function(){}; } vash.config.useWith = false; return ret; }; // SUPER HACK: var aeToStringVows = assert.AssertionError.prototype.toString; assert.AssertionError.prototype.toString = function wrapper() { // ...Put the default back to get around vows#278 caused by recursively // calling itself. assert.AssertionError.prototype.toString = Error.prototype.toString; var out = aeToStringVows.call(this); assert.AssertionError.prototype.toString = wrapper; return out; } vows.describe('vash templating library').addBatch({ 'a plain text template': { topic: function(){ var tpl = vash.compile('<a href="">this is a <br /> simple template</a>'); return tpl; } ,'sends back the same plain text': function(topic){ assert.equal( topic(), '<a href="">this is a <br /> simple template</a>'); } } ,'during "why are you using a template?"-style idiotic edge-cased interpolation': { topic: function(){ return vash.compile('@i', { htmlEscape: false, useWith: true }); } ,'we get 2 from just @i': function(topic){ assert.equal( topic({ i: 2 }), 2 ); } ,'we get <li class="what"></li> from just @i': function(topic){ assert.equal( topic({ i: '<li class="what"></li>' }), '<li class="what"></li>' ); } } ,'during simple interpolation': { topic: function(){ var str = '<li class="@className">@itemName</li>'; //console.log( vash._parse(str) ) return vash.compile(str, { useWith: true }); } ,'we get <li class="blue">the blue item</li>': function(topic){ //console.log(topic); assert.equal( topic( { className: 'blue' ,itemName: 'the blue item' } ) ,'<li class="blue">the blue item</li>' ); } } ,'during simple interpolation with self-closing tags': { topic: function(){ var str = '<img src="@src" alt="github" />'; //console.log( vash._parse(str) ) return vash.compile(str, { useWith: true }); } ,'we get the full self-closed tag': function(topic){ //console.log(topic); assert.equal( topic( { src: 'https://github.com' } ) ,'<img src="https://github.com" alt="github" />' ); } } ,'property references': { topic: function(){ var str = '<li>@model.name</li>' return vash.compile(str, { useWith: true }); } ,'are interpolated': function(topic){ assert.equal( topic({ model: {name: 'what'}}), "<li>what</li>" ); } } ,'deep property references': { topic: function(){ return '@model.repository.url/tree/@model.payload.ref' } ,'are fine': function(topic){ var tpl = vash.compile(topic, { useWith: false }); assert.equal( tpl({ repository: { url: 'URL' }, payload: { ref: 'REF' } }), 'URL/tree/REF' ); } } ,'property references with whitespace': { topic: function(){ return '@model.actor created a @model.payload' } ,'are not contiguous': function(topic){ var tpl = vash.compile(topic, { useWith: false }); assert.equal( tpl({ actor: 'act', payload: 'pay' }), 'act created a pay' ); } } ,'ellipses': { topic: function(){ return "@(model.payload.desc.substring(0, 4) + '...')"; } ,'are not infinite': function(topic){ var tpl = vash.compile(topic, { useWith: false }); assert.equal( tpl({ payload: { desc: 'description!!!' } }), 'desc...' ); } } ,'.. (double dot notation)': { topic: function(){ return "@( false || 1..toString() )"; } ,'is valid in expression': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), '1' ); } } ,'for blocks': { topic: function(){ var str = '@for(var i = 0; i < 10; i++){ \n }'; return vash.compile(str); } ,'output nothing': function(topic){ assert.equal( topic(), '' ); } } ,'for blocks and markup': { topic: function(){ var str = "@for(var i = 0; i < 1; i++){ <li class=\"\">list item</li> \n }"; return vash.compile(str); } ,'output markup': function(topic){ assert.equal(topic(), '<li class="">list item</li>'); } } ,'for blocks and markup with interpolation/expression': { topic: function(){ var str = "@for(var i = 0; i < 1; i++){ <li class=\"@i\">list item</li> \n }"; return vash.compile(str, { useWith: true }); } ,'output markup': function(topic){ assert.equal(topic(), '<li class="0">list item</li>'); } } ,'for blocks and markup with complex interpolation/expression': { topic: function(){ var str = "@for(var i = 0; i < 1; i++){ <li class=\"@(i % 2 == 0 ? \"blue\" : \"red\")\">list item</li> \n }"; return vash.compile(str, { useWith: true }); } ,'output markup': function(topic){ assert.equal(topic(), '<li class="blue">list item</li>'); } } ,'nested for blocks and markup with complex interpolation/expression': { topic: function(){ var str = "@for(var i = 0; i < 1; i++){ for(var j = 0; j < 2; j++) { <li class=\"@(i % 2 == 0 ? \"blue\" : \"red\")\">list item</li> \n } }"; return vash.compile(str, { useWith: true }); } ,'output markup': function(topic){ assert.equal(topic(), '<li class="blue">list item</li><li class="blue">list item</li>'); } } ,'nested for blocks on new lines with markup and even more complex interpolation/expressions': { topic: function(){ var str = "@for(var i = 0; i < somearr.length; i++){ \n" + " <li class=\"@(i % 2 === 0 ? 'even' : 'odd')\">Some element, number @i, value @somearr[i]</li> \n" + " @for(var j = 0; j < anotherarr.length; j++){" + " <li class=\"@j-what\">some text, @( (j+2) % 2 === 0 ? 'even' : 'odd' ), value @anotherarr[j]</li> \n" + " }" + "}"; return vash.compile(str, { useWith: true }); } ,'output markup': function(topic){ var model = { somearr: ['a', 'b'] ,anotherarr: ['z', 'y'] }; assert.equal(topic(model), '<li class="even">Some element, number 0, value a</li><li class="0-what">some text, even, value z</li><li class="1-what">some text, odd, value y</li><li class="odd">Some element, number 1, value b</li><li class="0-what">some text, even, value z</li><li class="1-what">some text, odd, value y</li>'); } } ,'forEach': { 'and markup with complex interpolation/expression': { topic: function(){ var str = '@model.forEach( function(p){ <li class="@(p.x % 2 == 0 ? \'blue\' : \'red\')">list item</li> })'; return vash.compile(str); } ,'output markup': function(topic){ var model = [{ x: 0, y: 1 }]; assert.equal(topic(model), '<li class="blue">list item</li>'); } } ,'wrapped in tags': { topic: function(){ var str = '<ul>@model.forEach( function(p){ <li>@p</li> })</ul>'; return str; } ,'output markup': function(topic){ var topic = vash.compile(topic); var model = ['a', 'b']; assert.equal(topic(model), '<ul><li>a</li><li>b</li></ul>'); } } ,'no whitespace': { topic: function(){ var str = '<ul class="friends">@friends.forEach(function(friend){<li></li>})</ul>'; return str; } ,'output markup': function(topic){ var topic = vash.compile(topic, {useWith: true}) ,model = { friends: [ 'a' ] }; assert.equal( topic(model), '<ul class="friends"><li></li></ul>' ); } } } ,'empty try/catch block': { topic: function(){ var str = "@try { var i = 0; } catch(e){ }"; return vash.compile(str); } ,'outputs nothing': function(topic){ assert.equal(topic(), '') } } ,'when try/catch block throws exception': { topic: function(){ var str = "@try { throw new Error('error') } catch(e){ <li>list item</li> \n }"; return vash.compile(str); } ,'catch block outputs markup': function(topic){ assert.equal(topic(), '<li>list item</li>') } } ,'when try/catch block does not throw exception': { topic: function(){ var str = "@try { <li>list item</li> \n } catch(e){ }"; return vash.compile(str); } ,'try block outputs markup': function(topic){ assert.equal(topic(), '<li>list item</li>') } } ,'when try/catch/finally block does not throw exception': { topic: function(){ var str = "@try { <li>list item</li> \n } catch(e){ } finally{ <li>list item 2</li> \n }"; return vash.compile(str); } ,'try block outputs markup': function(topic){ assert.equal(topic(), '<li>list item</li><li>list item 2</li>') } } ,'simple expression': { topic: function(){ var str = '<a href="@(true)"></a>'; return vash.compile(str); } ,'outputs true': function(topic){ assert.equal(topic(), '<a href="true"></a>'); } } ,'simple expression with valid identifier following': { topic: function(){ var str = '<a href="@(true)that"></a>'; return str; } ,'outputs true': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<a href="truethat"></a>'); } } ,'expression with nested parenthesis': { topic: function(){ var str = '<a href="@( true == (Math.random() + 1 >= 1 ? true : false) ? "red" : "blue" )"></a>'; return vash.compile(str); } ,'outputs red': function(topic){ assert.equal(topic(), '<a href="red"></a>'); } } ,'expression with indexed properties': { topic: function(){ var str = '<a href="@what.how[0]"></a>'; return vash.compile(str, { useWith: true }); } ,'outputs G': function(topic){ assert.equal( topic({ what: { how: 'G' }}), '<a href="G"></a>'); } } ,'expression with indexed properties and method call': { topic: function(){ var str = '<a href="@what.how()[0]"></a>'; return vash.compile(str, { useWith: true }); } ,'outputs G': function(topic){ assert.equal( topic({ what: { how: function() { return 'G'; } }}), '<a href="G"></a>'); } } ,'expression with indexed properties and method call with additional property': { topic: function(){ var str = '<a href="@what.how()[0].length"></a>'; return vash.compile(str, { useWith: true }); } ,'outputs 1': function(topic){ assert.equal( topic({ what: { how: function() { return 'G'; } }}), '<a href="1"></a>'); } } ,'expression with indexed property followed by valid identifer': { topic: function(){ var str = '<a href="@what[0]yeah"></a>'; return vash.compile(str, { useWith: true }); } ,'outputs 1yeah': function(topic){ assert.equal( topic({ what: '1'}), '<a href="1yeah"></a>'); } } ,'explicit expression followed by bracket, @escape': { topic: function(){ var str = '<a href="somename_@(what.how)@[0]"></a>'; return vash.compile(str, { useWith: true }); } ,'uses brackets as markup': function(topic){ assert.equal( topic({ what: { how: 'yes' }}), '<a href="somename_yes[0]"></a>'); } } ,'explicit expression followed by bracket': { topic: function(){ var str = '<a href="somename_@(what.how)[0]"></a>'; return vash.compile(str, { useWith: true }); } ,'uses brackets as markup': function(topic){ assert.equal( topic({ what: { how: 'yes' }}), '<a href="somename_yes[0]"></a>'); } } ,'expression followed by empty bracket': { topic: function(){ var str = '<a href="somename_@what.how[]"></a>'; return vash.compile(str, { useWith: true }); } ,'uses brackets as markup': function(topic){ assert.equal( topic({ what: { how: 'yes' }}), '<a href="somename_yes[]"></a>'); } } ,'explicit expression with parens as immediate child': { topic: function(){ var str = '@((1) > 0)'; return vash.compile(str); } ,'create a separate explicit expression': function(topic){ assert.equal(topic(), 'true'); } } ,'explicit expressions containing quoted characters': { topic: function() { // For now just include ASCII. var UNICODE_MAX = 127; // 1114111; // 0-31 are control characters. for(var i = 32, chars = []; i <= UNICODE_MAX; i++) { chars.push(String.fromCharCode(i)); } return chars; } ,'do not need to be escaped': function(chars){ var str = chars.map(wrapCharWithExpression).join(''); var tpl = vash.compile(str, { htmlEscape: false }); var expected = chars.map(wrapCharWithP); assert.equal( tpl(), expected.join('') ); function wrapCharWithExpression(chr) { return '<p>@("' + escapeIfNeeded(chr) + '")</p>\n'; } function wrapCharWithP(chr) { return '<p>' + chr + '</p>\n'; } function escapeIfNeeded(chr) { if (chr === '"' || chr === '\n' || chr === '\\') return '\\' + chr; else return chr; } } } ,'a parent expression': { topic: '@(function() { <p>)</p> }())' ,'should not consume content PAREN_CLOSE': function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<p>)</p>' ); } } ,'a parent block': { topic: '@{ function what() { <p>}</p> } }' ,'should not consume content BRACE_CLOSE': function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '' ); } } ,'anonymous blocks': { 'empty,': { topic: function(){ var str = "@{ }"; return vash.compile(str); } ,'outputs nothing': function(topic){ assert.equal(topic(), ''); } } ,'empty, with same-line markup': { topic: function(){ var str = "@{ <li>list item</li> }"; return vash.compile(str); } ,'outputs markup': function(topic){ assert.equal(topic(), '<li>list item</li>'); } } ,'and markup with quotes': { topic: function(){ var str = "@{ <li class=\"1\">list item</li> \n }"; return vash.compile(str); } ,'outputs markup': function(topic){ assert.equal(topic(), '<li class="1">list item</li>'); } } ,'and nested markup': { topic: function(){ var str = "@{ <li class=\"1\">list item</li> @{ <li class=\"2\">list item</li> } }"; return vash.compile(str); } ,'outputs markup': function(topic){ assert.equal(topic(), '<li class=\"1\">list item</li><li class=\"2\">list item</li>'); } } ,'and nested for loop': { topic: function(){ var str = "@{ <li class=\"1\">list item</li> @for(var i = 0; i < 1; i++){ <li class=\"2\">list item</li> } }"; return vash.compile(str); } ,'outputs markup': function(topic){ assert.equal( topic(), '<li class=\"1\">list item</li><li class=\"2\">list item</li>' ); } } ,'and named function defined': { topic: function(){ var str = "@{ <li class=\"1\">list item</li> @function testFunc(param1, param2){ <li class=\"2\">list item</li> } }"; return vash.compile(str); } ,'outputs non-function defined markup': function(topic){ assert.equal( topic(), '<li class=\"1\">list item</li>' ); } } ,'and named function defined and called': { topic: function(){ var str = "@{ <li class=\"1\">list item</li> @function testFunc(param1, param2){ <li class=\"2\">list item</li> \n } testFunc(); \n }"; return vash.compile(str); } ,'outputs non-function defined markup': function(topic){ assert.equal( topic(), '<li class=\"1\">list item</li><li class=\"2\">list item</li>' ); } } ,'and while loop with manual increment': { topic: function(){ var str = "@{ var countNum = 0; while(countNum < 1){ \n countNum += 1; \n <p>Line #@countNum</p> \n } }"; return vash.compile(str); } ,'outputs 1 line': function(topic){ assert.equal( topic(), '<p>Line #1</p>'); } } ,'and while loop with manual increment post': { topic: function(){ var str = "@{ var countNum = 0; while(countNum < 2){ \n countNum += 1; \n <p>Line #@countNum</p> \n countNum += 1; \n } }"; return vash.compile(str); } ,'outputs 1 line': function(topic){ assert.equal( topic(), '<p>Line #1</p>'); } } } ,'immediate function invocation within expression': { topic: function(){ var str = '<a>@(false || (function(){ <b>YES</b> })())</a>'; return vash.compile(str, { debugCompiler: false }); } ,'returns properly': function(topic){ assert.equal( topic(), '<a><b>YES</b></a>' ) } } ,'array literal within markup': { topic: '<a>@["a", "b", "c"].join("")</a>' ,'outputs': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), '<a>abc</a>' ); } } ,'function invocation within expression buffers': { topic: function(){ var str = '<a>@model.map(function(l){ return "__" + l + "__"; }).forEach(function(l){ <b>@l</b> })</a>'; return vash.compile(str, { debugCompiler: false }); } ,'returns properly': function(topic){ assert.equal( topic(["a", "b", "c"]), '<a><b>__a__</b><b>__b__</b><b>__c__</b></a>' ) } } ,'<text> escape': { 'single line': { topic: function(){ var str = '@if (true) { \n' + '<text>Plain Text</text>\n' + '}'; return vash.compile(str); } ,'outputs plain text': function(topic){ assert.equal( topic(), 'Plain Text' ); } } ,'multiple lines': { topic: function(){ var str = '@if (true) { \n' + '<text>Plain Text \n Plain Text \n</text>\n' + '}'; return vash.compile(str); } ,'outputs plain text': function(topic){ assert.equal( topic(), 'Plain Text \n Plain Text \n' ); } } ,'can be escaped': { topic: '@("<text>")More@("</text>")' ,'with html escaping': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), '&lt;text&gt;More&ls;/text&gt;' ); } } ,'can be escaped': { topic: '@html.raw("<text>")More@html.raw("</text>")' ,'without html escaping': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), '<text>More</text>' ); } } } ,'@: escape': { 'single line': { topic: function(){ var str = '@if (true) { \n' + '@:Plain Text\n' + '}'; return str; } ,'outputs plain text': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), 'Plain Text\n' ); } } ,'multiple lines': { topic: function(){ var str = '@if (true) { \n' + '@:Plain Text\n' + '@:Plain Text\n' + '}'; return str; } ,'outputs plain text': function(topic){ var tpl = vash.compile(topic) assert.equal( tpl(), 'Plain Text\nPlain Text\n' ); } } ,'single line with content on next line': { topic: function(){ var str = '@if (true) { \n' + '@:Plain Text\n' + 'var a = "what";' + '}'; return str; } ,'throws error': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), 'Plain Text\n' ); } } ,'can be escaped': { topic: '<p>@@:</p>' ,'and is': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), '<p>@:</p>' ); } } } ,'markup within a code block': { topic: function(){ var str = '@if(true){ \n' + '<span>this is text \n' + 'that spans multiple lines</span> \n' + '}'; return str; } ,'disregards newline re-entry into BLK mode': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<span>this is text \nthat spans multiple lines</span>'); } } ,'markup within a code block followed by else': { topic: function(){ var str = '@if(true){ \n' + '<span>this is text \n' + 'that spans multiple lines</span> \n' + '} else { \n' + '<span>different text</span> \n' + '}'; return str; } ,'disregards newline re-entry into BLK mode': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<span>this is text \nthat spans multiple lines</span>'); } } ,'markup within a code block with if/else on new lines': { topic: '@if(true)\n{\n<strong>Hello</strong>\n}\n else\n{\n <strong>World!</strong>\n }' ,'outputs': function(topic) { var tpl = vash.compile(topic); assert.equal(tpl(), '<strong>Hello</strong>'); } } ,'markup within a code block followed by else with markup': { topic: function(){ var str = '@if(false){ \n' + '<span>this is text \n' + 'that spans multiple lines</span> \n' + '} else { \n' + '<span>different text</span> \n' + '}'; return str; } ,'disregards newline re-entry into BLK mode': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<span>different text</span>'); } } ,'markup within a code block with an expression in the tag name': { topic: function(){ var str = '@if(true){ \n' + '<span-@name>this is text \n' + 'that spans multiple lines</span-@name> \n' + '}'; return str; } ,'parses': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl({ name: 'what' }), '<span-what>this is text \nthat spans multiple lines</span-what>'); } } ,'markup within a code block with an expression after the tag name': { topic: function(){ var str = '@if(true){ \n' + '<span-@name>this is text \n' + 'that spans multiple lines</span-@name> \n' + '<span class="@name">this is text \n' + 'that spans multiple lines</span> \n' + '}'; return str; } ,'parses': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl({ name: 'what' }), '<span-what>this is text \nthat spans multiple lines</span-what><span class="what">this is text \nthat spans multiple lines</span>'); } } ,'markup within a code block within markup within a code block': { topic: function(){ var str = '@if(true){ \n' + '<span>this is text \n' + '@if(true){ <b>important</b> \n } ' + 'that spans multiple lines</span> \n' + '}'; return str; } ,'nests properly': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<span>this is text \n<b>important</b> that spans multiple lines</span>'); } } ,'markup within a code block within markup within a code block with keyword': { topic: function(){ var str = '@if(true){ \n' + '<span>this is text \n' + '@if(true){ <b>delete</b> \n } ' + 'that spans multiple lines</span> \n' + '}'; return str; } ,'nests properly': function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), '<span>this is text \n<b>delete</b> that spans multiple lines</span>'); } } ,'markup within markup within a block': { topic: function(){ var str = '@if(true){ <p>This is content that is <strong>important</strong> but outside.</p> }' return str; } ,'is consumed by markup, not block': function(topic){ assert.equal(vash.compile(topic)(), '<p>This is content that is <strong>important</strong> but outside.</p>'); } } ,'self-closing tag within tag within BLK': { topic: '@if(true){ <p>Hello<br />world</p> }' ,'does not prematurely exit MKP': function(topic){ var tpl = vash.compile(topic) ,expected = '<p>Hello<br />world</p>'; assert.equal(tpl(), expected); } ,'does not prematurely exit MKP, even without closing /': function(topic){ var tpl = vash.compile(topic.replace('<br />', '<br>')) ,expected = '<p>Hello<br>world</p>'; assert.equal(tpl(), expected); } } ,'self-closing tag within BLK': { topic: '@if(true){ <br /> true; }' ,'exits MKP': function(topic){ var tpl = vash.compile(topic) ,expected = '<br />'; assert.equal(tpl(), expected); } ,'exits MKP with expression': function(topic){ var str = topic.replace('/>', '@(true)/>') var tpl = vash.compile(str) ,expected = '<br true />'; assert.equal(tpl(), expected); } /* ,'exits MKP without closing /': function(topic){ var tpl = vash.compile(topic.replace('/>', '>')) ,expected = '<br >'; assert.equal(tpl(), expected); }*/ } ,'if statement is not confused': { topic: '@{ if(-1 <= 0){ <br /> } }' ,'with self-closing tag': function(topic){ var tpl = vash.compile(topic) ,expected = '<br />'; assert.equal(tpl(), expected); } } ,'self-closing tag containing >': { topic: '<button data-bind="enable: @model.length > 0" />' ,'compiles and renders': function(topic) { var tpl = vash.compile(topic) ,expected = '<button data-bind="enable: 0 > 0" />'; assert.equal(tpl([]), expected); } } ,'self-closing tag containing > with no whitespace': { topic: '<button data-bind="enable:@model.length>0"/>' ,'compiles and renders': function(topic) { var tpl = vash.compile(topic) ,expected = '<button data-bind="enable:0>0" />'; assert.equal(tpl([]), expected); } } ,'self-closing tag does not grab too much': { topic: '@{{<b ></b>}<img />}' ,'when preceeded by tag with whitespace': function(topic) { var tpl = vash.compile(topic) , expected = '<b></b><img />' assert.equal(tpl(), expected); } } ,'markup with numbers': { topic: function(){ var str = "<div>" + " <h1 class='header'>@header</h1>" + " <h2 class='header2'>@header2</h2>" + "</div>" return str; } ,'is named properly': function(topic){ assert.equal( vash.compile(topic, { useWith: true })( { header: 'a', header2: 'b' } ), '<div>' + ' <h1 class=\'header\'>a</h1>' + ' <h2 class=\'header2\'>b</h2>' + '</div>' ); } } ,'simple expression as tagname': { topic: function(){ return '<@name>This is content</@name>'; } ,'is allowed': function(topic){ assert.equal( vash.compile(topic, { useWith: true })({name: 'what'}), '<what>This is content</what>' ); } } ,'simple expression as tagname within block': { topic: function(){ return '@if(true){ <@name>This is content</@name> }'; } ,'is allowed': function(topic){ assert.equal( vash.compile(topic, { useWith: true })({name: 'what'}), '<what>This is content</what>' ); } } ,'complex expression as tagname': { topic: function(){ return '<@name[0].length>This is content</@name[0].length>'; } ,'is allowed': function(topic){ assert.equal( vash.compile(topic, { useWith: true })({name: 'what'}), '<1>This is content</1>' ); } } ,'email address': { topic: function(){ return 'some.gr-at%email_address-indeed@complex-domain.subdom.edu' } ,'included as content': { topic: function(address){ return 'Hi ' + address; } ,'does not leave markup mode': function(topic){ var tpl = vash.compile( topic ); assert.equal( tpl(), topic ); } } ,'included within a href': { topic: function(address){ return '<a href="mailto:' + address + '">' + address + '</a>' } ,'is still just an email': function(topic){ var tpl = vash.compile( topic ); assert.equal( tpl(), topic ); } } ,'are not confused with': { topic: '@model.title@console.log("")' ,'concatenated expressions': function( topic ){ var tpl = vash.compile( topic, { useWith: false } ); assert.equal( tpl({ title: 'who' }), 'who' ); } } ,'can have any two-letter tld-ish': { topic: 'who@what.de' ,'and still register': function(topic) { var tpl = vash.compile(topic); assert.equal(tpl(), topic); } } } ,'explicit expression': { topic: function(){ var str = '<span>ISBN@(isbnNumber)</span>'; return vash.compile(str, { useWith: true }); } ,'does not trip e-mail escape': function(topic){ assert.equal( topic({isbnNumber: 10101}), '<span>ISBN10101</span>' ) } } ,'explicit expression with unmatched parenthesis': { topic: function(){ var str = '<span>ISBN@(isbnNumber</span>'; return str; } ,'throws syntax error': function(topic){ assert.throws( function(){ vash.compile(topic) }, Error ); } } ,'expression with spaces in func call': { topic: function(){ var str = '<span>@a.replace("x", "o")</span>'; return str; } ,'renders': function(topic){ assert.equal(vash.compile(topic, { useWith: true })({ a: 'xxx' }), '<span>oxx</span>'); } } ,'regex': { 'simple expression': { topic: '<span>@a.replace(/\\)a"\'/gi, "o")</span>' ,'replaces': function( topic ){ var tpl = vash.compile( topic, { useWith: true } ); assert.equal( tpl({ a: ')a"\'' }), '<span>o</span>'); } } ,'period meta character': { topic: '<span>@a.replace(/\\)."\'/gi, "o")</span>' ,'replaces': function( topic ){ var tpl = vash.compile( topic, { useWith: true } ); assert.equal( tpl({ a: ')a"\'' }), '<span>o</span>') } } ,'within BLK': { topic: '@{ var re = /[@}\'"]/gi; }<span>@a.replace(re, "o")</span>' ,'replaces': function( topic ){ var tpl = vash.compile( topic, { useWith: true } ); assert.equal( tpl({ a: '@' }), '<span>o</span>'); } } ,'within an expression': { topic: '@(/a/.exec(\'abc\')[0])' ,outputs: function( topic ){ var tpl = vash.compile(topic); assert.equal( tpl(), 'a' ); } } ,'literal': { 'within markup': { topic: '<span>@/a/.test(\'abc\')</span>' ,outputs: function ( topic ) { var tpl = vash.compile(topic); assert.equal( tpl(), '<span>true</span>' ); } } ,'within markup attribute': { topic: '<span b="@/a/.exec(\'abc\')[0]"></span>' ,outputs: function ( topic ) { var tpl = vash.compile(topic); assert.equal( tpl(), '<span b="a"></span>' ); } } } ,'following conditional': { topic: '@if (true) /a/.test(\'abc\')' ,outputs: function ( topic ) { var tpl = vash.compile(topic); assert.equal( tpl(), '' ); } } ,'are not mistaken for': { 'division expression': { topic: '@{ var test = 100/2; }<span>@test</span>' ,'is not mistaken for regex': function (topic) { var tpl = vash.compile(topic); assert.equal( tpl({}), '<span>50</span>' ); } } ,'division within condition': { topic: '@{ if(100/2) <span></span> }' ,'is not mistaken for regex': function (topic) { var tpl = vash.compile(topic); assert.equal( tpl({}), '<span></span>' ); } } ,'division after expression': { topic: '@(Math.round(20.22) / 100)' ,'is not mistaken for regex': function (topic) { var tpl = vash.compile(topic); assert.equal( tpl({}), '0.2' ); } } ,'division within block': { topic: '@{ Math.round(2 * 1) / Number.MAX_VALUE }' ,'is not mistaken for regex': function (topic) { var tpl = vash.compile(topic); assert.equal( tpl({}), '' ); } } } } ,'escaping the @ symbol': { 'within content': { topic: function(){ var str = '<span>In vash, you use the @@foo to display the value of foo</span>'; return vash.compile(str); } ,'leaves just a single @': function(topic){ assert.equal( topic(), '<span>In vash, you use the @foo to display the value of foo</span>' ) } } ,'within quoted string in BLK': { topic: '@{ var a = "Twitter: @KirbySaysHi"; }<text>@a</text>' ,'is not required': function(topic){ var tpl = vash.compile(topic); assert.equal( tpl(), 'Twitter: @KirbySaysHi' ); } } ,'within ProgramNode (root) in front of keywords': { topic: '' + '@@if(model.type){\n' + ' <p>I\'m a @@model.type!</p>\n' + '} else if(model.name){\n' + ' <p>My name is @@model.name.</p>\n' + '} else {\n' + ' <p>I DON\'T KNOW WHO OR WHAT I AM...</p>\n' + '}\n' ,'outputs': function(topic) { var tpl = vash.compile( topic ); assert.equal( tpl(), '' + '@if(model.type){\n' + ' <p>I\'m a @model.type!</p>\n' + '} else if(model.name){\n' + ' <p>My name is @model.name.</p>\n' + '} else {\n' + ' <p>I DON\'T KNOW WHO OR WHAT I AM...</p>\n' + '}\n'); } } ,'within markup attribute': { topic: '<figure id="fig-@@(figcount++)"></figure>' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<figure id="fig-@(figcount++)"></figure>' ); } } ,'within markup node': { topic: '<f@@e></f@@e>' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<f@e></f@e>' ); } } ,'<ul class="@@(model.active ? \'highlight\' : \'\')"></ul>': { topic: '<ul class="@@(model.active ? \'highlight\' : \'\')"></ul>' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<ul class="@(model.active ? \'highlight\' : \'\')"></ul>' ); } } ,'@@@@' : { topic: '`@@@@`' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '`@@`' ); } } ,'@@{ }': { topic: '@@{ }' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '@{ }' ); } } } ,'PHP-like tags are not confused for attributes': { topic: '<? $what = \'what\' ?>' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<? $what = \'what\' ?>' ); } } ,'attribute parsing allows for quoted = (equal) signs': function() { var topic = '<meta name="viewport" content="width=device-width, initial-scale=1">'; var tpl = vash.compile(topic); assert.equal( tpl(), topic ); } ,'"server-side" comments': { 'multiline': { topic: function(){ var str = '@* \n' + 'This is a server side \n' + 'multiline comment \n' + '*@ and this content should be'; return vash.compile(str); } ,'output nothing': function(topic){ assert.equal( topic(), ' and this content should be' ) } } ,'singleline': { topic: function(){ var str = '@*' + 'This is a server side ' + 'comment' + '*@ and this content should be'; return vash.compile(str); } ,'output nothing': function(topic){ assert.equal( topic(), ' and this content should be' ) } } ,'within a block': { topic: function(){ var str = '@if(true){ @*' + 'This is a server side ' + 'comment' + '*@ } and this content should be'; return str; } ,'output nothing': function(topic){ topic = tryCompile(topic) assert.equal( topic(), ' and this content should be' ) } } ,'unclosed': { topic: function(){ var str = '@* \n' + 'This is a server side \n' + 'multiline comment \n'; return str; } ,'throws exception': function(topic){ assert.throws( function(){ vash.compile(topic) }, Error ); } } ,'can be escaped': { topic: 'with `@@*` and `*@@`' ,'successfully': function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), 'with `@*` and `*@`' ); } } } ,'mixing expressions and text': { topic: function(){ var str = 'Hello @title. @name.'; return vash.compile(str, { useWith: true }); } ,'outputs text': function(topic){ assert.equal( topic({ title: 'Mr', name: 'Doob' }), 'Hello Mr. Doob.'); } } ,'excluding "with"': { topic: function(){ var str = '<li>@model.name</li>' ,tpl; tpl = vash.compile(str, { useWith: false }); return tpl; } ,'ensures it is not there': function(topic){ assert.equal( topic({name: 'what'}), "<li>what</li>" ); } } ,'including "with"': { topic: function(){ var str = '<li>@name</li>' ,tpl; tpl = vash.compile(str, { useWith: true }); return tpl; } ,'ensures it is there': function(topic){ assert.equal( topic({name: 'what'}), "<li>what</li>" ); } } ,'model name': { topic: function(){ var str = '<li>@it.name</li>' ,tpl; vash.config.modelName = 'it'; tpl = vash.compile(str, { useWith: false }); vash.config.modelName = 'model'; return tpl; } ,'is configurable': function(topic){ assert.equal( topic({name: 'what'}), "<li>what</li>" ); } } ,'same line } in block after markup': { topic: function(){ var str = '@{ var a = 0; a += 1; <span>text</span> } <span>text</span>'; return str; } ,'closes block without neccessity of newline': function(topic){ var tpl = tryCompile(topic); assert.equal( tpl(), '<span>text</span> <span>text</span>' ); } } ,'misnested html tags in block': { topic: function(){ var str = '@if(true) { <li><p></li></p> }'; return str; } ,'does not throw "UMATCHED" exception': function(topic){ //vash.compile(topic); assert.doesNotThrow( function(){ vash.compile(topic) }, Error ); } } ,'self closing html tag inside block': { topic: function(){ var str = '@if(true) { <img src="" /> \n}'; return str; } ,'does not bork the block stack': function(topic){ //assert.doesNotThrow( function(){ vash.compile(topic); }, vash._err.MALFORMEDHTML ); assert.equal( vash.compile(topic)(), '<img src="" />' ); } } ,'self closing html tag with expression': { topic: '<img src="@model.a" />' ,'allows expression': function(topic){ assert.equal( vash.compile(topic)({ a: 'a' }), '<img src="a" />' ); } } ,'nested self closing html tag inside block': { topic: function(){ var str = '@if(true) { <li><img src="" /></li> \n}'; return str; } ,'does not bork the block stack': function(topic){ //assert.doesNotThrow( function(){ vash.compile(topic); }, vash._err.MALFORMEDHTML ); assert.equal( vash.compile(topic)(), '<li><img src="" /></li>' ); } } ,'content following parens preserves whitespace': { topic: '<(bin/vash <docs/helpers/* --helper) > README2.md' ,outputs: function(topic) { var tpl = vash.compile(topic); assert.equal( tpl(), '<(bin/vash <docs/helpers/* --helper) > README2.md' ); } } ,'content } in closed markup': { topic: function(){ var str = '@if(true) { <li> } </li> }'; return str; } ,'does not need to be escaped': function(topic){ assert.doesNotThrow( function(){ vash.compile(topic) }, Error ); assert.equal( vash.compile(topic)(), '<li> } </li>'); } } ,'content } in expression': { topic: function(){ var str = '@( false || "}" )'; return str; } ,'does not need to be escaped': function(topic){ //assert.doesNotThrow( function(){ vash.compile(topic) }, Error); assert.doesNotThrow( function(){ vash.compile(topic) }, Error ); assert.equal( vash.compile(topic)(), '}'); } } ,'markup followed by for loop': { topic: function(){ var str = '<div class="how"> @for(var i = 0; i < 1; i++){ <div class="item-@i">I be an item!</div> } </div>'; return vash.compile(str); } ,'renders': function(topic){ assert.equal( topic(), '<div class="how"> <div class="item-0">I be an item!</div> </div>' ); } } ,'unclosed block': { // throws UNMATCHED exception topic: function(){ var str = '<div class="yeah"> @for(var i = 0; i < 1; i++){ </div>'; return str; } ,'throws UNMATCHED': function(topic){ assert.throws( function(){ vash.compile(topic) }, Error ); } } ,'HTML5:': { /*'unclosed tags': { topic: function(){ var str = '<div class="how what">This is content @for(var i = 0; i < 1; i++){ <p>@i }'; return str; } ,'do not bork': function(topic){ assert.equal( vash.compile(topic)(), '<div class="how what">This is content <p>0 ' ); } } ,*/'unclosed tag followed by previous closing tag': { topic: function(){ var str = '<div class="how what">This is content @for(var i = 0; i < 1; i++){ <p>@i </div> }'; return str; } ,'throws': function(topic){ assert.throws( function(){ vash.compile(topic)() }, Error ); } } ,'self-closing tags WITHOUT /': { topic: function(){ var str = '<div class="how what">This is content @for(var i = 0; i < 1; i++){ <text><br>@i</text> } </div>' return str; } ,'does not throw UNMATCHED': function(topic){ assert.doesNotThrow( function(){ vash.compile(topic)() }, Error ); } } ,'explicitly unclosed, non-void tags': { topic: '<a><b><c>' ,'throw': function(topic) { assert.throws(function() { var tpl = vash.compile(topic); }, Error); } } ,'void tag with closing tag': { topic: '<img></img>' ,'throws': function(topic) { assert.throws(function() { var tpl = vash.compile(topic); }, Error) } } ,'void tag with closing tag surrounded by BLK': { topic: '@{ <img></img> }' ,'throws': function(topic) { assert.throws(function() { var tpl = vash.compile(topic); }, Error) } } /*,'closing tag within block': { topic: function(){ var str = '<div>This is content @if(true){ </div> } else { </div> }'; return str; } ,'closes parent': function(topic){ assert.doesNotThrow( function(){ vash.compile(topic)() }, Error ); assert.equal( vash.compile(topic)(), '<div>This is content </div>' ); } }*/ ,'operators': { topic: function(){ return '@for( var i = 0; i <= 0; i++ ){<p></p>}'; } ,'are not mistaken for tags': function(topic){ var tpl = vash.compile( topic ); assert.equal( tpl(), '<p></p>' ); } } ,'newlines within nested markup': { topic: '<span>@model<span\n></span></span>' ,'does not prevent HTML matching': function(topic) { var tpl = vash.compile(topic, {useWith: false}) , actual = tpl('1'); assert.equal( '<span>1<span></span></span>', actual ) } } ,'dashes within tag names': { topic: '<accordion-group>hey</accordion-group>' ,'are included as the tag': function(topic) { var tpl = vash.compile(topic, {useWith: false}) , actual = tpl(); assert.equal( actual, topic ) } } } ,'xml': { 'tag namespaces parse as tags': function () { var str = '<core:AnotherElement>Hello</core:AnotherElement>'; var tpl = vash.compile(str); assert.equal( tpl(), str ); } ,'AT within namespaces are ok': function () { var str = '<c:@model.a>Hello</c:@model.a>'; var tpl = vash.compile(str); assert.equal( tpl({ a: 'a' }), '<c:a>Hello</c:a>' ); } ,'directives are ok': function () { var str = '<?xml version="1.0" encoding="UTF-8"?><p></p>'; var tpl = vash.compile(str); assert.equal( tpl(), str ); } ,'attribute namespaces are ok': function () { var str = '' + '<ADI \n' + ' xmlns:core="blah" \n' + ' xmlns:ext="URN:NNDS:CMS:ADI3:01"\n' + ' xmlns="http://www.cablelabs.com/namespaces/metadata/xsd/vod30/1">\n' + ' @(model.what)\n' + '</ADI>'; var tpl = vash.compile(str); var expected = '' + '<ADI ' + 'xmlns:core="blah" ' + 'xmlns:ext="URN:NNDS:CMS:ADI3:01" ' + 'xmlns="http://www.cablelabs.com/namespaces/metadata/xsd/vod30/1">\n' + ' what\n' + '</ADI>' var actual = tpl({ what: 'what' }); console.log('actual', actual) console.log('expected', expected) assert.equal( actual, expected ); } } ,'unbalanced characters are ok': { // https://github.com/kirbysayshi/vash/issues/26 'open paren': { topic: '@("(")' ,'outputs': function( topic ) { var tpl = vash.compile(topic); assert.equal( tpl(), '(' ); } } ,'open paren within markup within block': { topic: '@(function(model) { <p>(</p> }())' ,'outputs': function( topic ) { var tpl = vash.compile(topic); assert.equal( tpl(), '<p>(</p>' ); } } } ,'simple expression followed by @()': { topic: function(){ return '<li data-score="@model.Score" class="user-panel-track @(model.i % 2 === 0 ? \'even\' : \'odd\')"></li>'; } ,'renders': function(topic){ assert.equal( vash.compile(topic)({ Score: '1', i: 0 }) , '<li data-score="1" class="user-panel-track even"></li>'); } } ,'empty string': { topic: function(){ return "" } ,'returns empty string': function(topic){ assert.throws( function(){ vash.compile(topic)() }, Error ); } } ,'non-string parameter': { topic: function(){ return {} } ,'throws exception': function(topic){ assert.throws( function(){ vash.compile(topic)() }, Error ); } } ,'@function': { // the space before @ counts as markup topic: function(){ return '@function doWhat(){ return "what"; } @doWhat()' } ,compiles: function(topic){ assert.doesNotThrow( function(){ vash.compile(topic) }, Error ); } ,'can be called': function(topic){ assert.equal( vash.compile(topic)(), ' what' ); } } ,'@function with markup': { topic: function(){ return '@function doWhat(input){ <li>@input.name</li> } @doWhat(model)' } ,compiles: function(topic){ assert.doesNotThrow( function(){ vash.compile(topic) }, Error ); } ,'can be called': function(topic){ assert.equal( vash.compile(topic)({ name: 'what' }), ' <li>what</li>' ); } } ,'this was an infinite parser bug #68 at some point': { topic: '@:Some Text\n@html.block(\'test\')\n' ,'does not hang': function(topic) { var tpl = vash.compile(topic); assert.ok( tpl ); } } /*,'fat arrow': { 'with single parameter': { topic: function(){ return vash.compile( '<ul>@arr.forEach( i => <li>@i</li> )</ul>' ); } ,'succeeds': function(topic){ assert.equal( topic( { arr: ['a','b'] } ), '<ul><li>a</li> <li>b</li> </ul>' ) } } // this is technically incorrect, but pretty cool that you can do it ,'with multiple unparenthetized parameters': { topic: function(){ return vash.compile( '<ul>@arr.forEach( i, k => <li>@i</li> )</ul>' ); } ,'succeeds': function(topic){ assert.equal( topic( { arr: ['a','b'] } ), '<ul><li>a</li> <li>b</li> </ul>' ) } } ,'with multiple parameters': { topic: function(){ return vash.compile( '<ul>@arr.forEach( (i,k) => <li>@i</li> )</ul>' ); } ,'succeeds': function(topic){ assert.equal( topic( { arr: ['a','b'] } ), '<ul><li>a</li> <li>b</li> </ul>' ) } } ,'with multiple parameters with function block': { topic: function(){ return vash.compile( '<ul>@arr.forEach( (i,k) => { <li>@i</li> } )</ul>' ); } ,'succeeds': function(topic){ assert.equal( topic( { arr: ['a','b'] } ), '<ul><li>a</li> <li>b</li> </ul>' ) } } }*/ ,'html escaping:': { 'basic': { topic: function(){ return vash.compile( '<span>@it</span>', { useWith: true } ); } ,'is escaped': function(topic){ assert.equal( topic({ it: '<b>texted</b>' }), '<span>&lt;b&gt;texted&lt;/b&gt;</span>' ); } } ,'force no escaping': { topic: function(){ return vash.compile( '<span>@it</span>', { htmlEscape: false, useWith: true } ); } ,'is escaped': function(topic){ assert.equal( topic({ it: '<b>texted</b>' }), '<span><b>texted</b></span>' ); } } ,'force no escaping per call (html.raw)': { topic: function(){ return vash.compile( '<span>@html.raw(it)</span>', { useWith: true } ); } ,'is escaped': function(topic){ assert.equal( topic({ it: '<b>texted</b>' }), '<span><b>texted</b></span>' ); } } ,'multiple function calls': { topic: function(){ return vash.compile( '@function f(i){ <b>@i</b> }<span>@f(it)</span>@f(it)', { useWith: true } ); } ,'are escaped': function(topic){ assert.equal( topic({ it: '<b>texted</b>' }), '<span><b>&lt;b&gt;texted&lt;/b&gt;</b></span><b>&lt;b&gt;texted&lt;/b&gt;</b>' ); } } ,'multiple function calls are not double escaped': { topic: function(){ return vash.compile( '@function f(i){ <b>@i</b> }<span>@f(model.it)</span>@f(model.it)', { useWith: false } ); } ,'are escaped': function(topic){ //console.log( topic.toString() ); assert.equal( topic({ it: '<b>texted</b>' }), '<span><b>&lt;b&gt;texted&lt;/b&gt;</b></span><b>&lt;b&gt;texted&lt;/b&gt;</b>' ); } } ,'multiple nested function calls': { topic: function(){ return vash.compile( '@function f(i){ <b>@i</b> function d(i){ <b>@i</b> } d(model.it) }<span>@f(model.it)</span>@f(model.it)' ); } ,'are escaped': function(topic){ assert.equal( topic({ it: '<b>texted</b>' }), '<span><b>&lt;b&gt;texted&lt;/b&gt;</b><b>&lt;b&gt;texted&lt;/b&gt;</b></span><b>&lt;b&gt;texted&lt;/b&gt;</b><b>&lt;b&gt;texted&lt;/b&gt;</b>' ); } } } ,"markup quotation marks:": { "single quotes come out": { topic: function(){ return "<text>It's followed by primary content.</text>" } ,"as single quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), "It's followed by primary content.") } } ,"double quotes come out": { topic: function(){ return '<text>It is "followed" by primary content.</text>' } ,"as double quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), 'It is "followed" by primary content.') } } ,"escaped single quotes come out": { topic: function(){ return "<text>'It\\'s followed by primary content.'</text>" } ,"as single quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), "'It\\'s followed by primary content.'") } } ,"escaped double quotes come out": { topic: function(){ return "<text>It is \"followed\" by primary content.</text>" } ,"as double quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), 'It is "followed" by primary content.') } } } ,"block quotation marks:": { "single quotes come out": { topic: function(){ return "@{ var a = \"It's followed by primary content.\"; } @html.raw(a)" } ,"as single quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), " It's followed by primary content.") } } ,"double quotes come out": { topic: function(){ return '@{ var a = \'It is "followed" by primary content.\'; } @html.raw(a)' } ,"as double quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), ' It is "followed" by primary content.') } } ,"escaped single quotes come out": { topic: function(){ return "@{ var a = 'It\\'s followed by primary content.'; } @html.raw(a)" } ,"as single quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), " It's followed by primary content.") } } ,"escaped double quotes come out": { topic: function(){ return '@{ var a = \'It is \"followed\" by primary content.\'; } @html.raw(a)' } ,"as double quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), ' It is "followed" by primary content.') } } } ,"expression quotation marks:": { "single quotes come out": { topic: function(){ return "@html.raw(\"It's followed by primary content.\")" } ,"as single quotes": function(topic){ var tpl = tryCompile(topic); assert.equal(tpl(), "It's followed by primary content.