Browse files

initial commit

  • Loading branch information...
0 parents commit fe72382d31ec288437d30ec2cb8fe4bf7eedaf56 @amccollum committed Sep 6, 2011
Showing with 839 additions and 0 deletions.
  1. +23 −0 Cakefile
  2. +20 −0 LICENSE
  3. 0 README
  4. +12 −0 lib/ender.js
  5. +110 −0 lib/template.js
  6. +110 −0 lib/wings.js
  7. +20 −0 package.json
  8. +178 −0 src/test/template.coffee
  9. +8 −0 src/wings/ender.coffee
  10. +98 −0 src/wings/wings.coffee
  11. +260 −0 test/template.js
23 Cakefile
@@ -0,0 +1,23 @@
+fs = require('fs')
+sys = require('sys')
+{spawn, exec} = require('child_process')
+
+package = JSON.parse(fs.readFileSync('package.json', 'utf8'))
+
+execCmds = (cmds) ->
+ exec cmds.join(' && '), (err, stdout, stderr) ->
+ output = (stdout + stderr).trim()
+ console.log(output + '\n') if (output)
+ throw err if err
+
+task 'build', 'Build the library', ->
+ execCmds [
+ 'coffee --bare --output ./lib ./src/wings/*.coffee',
+ ]
+
+task 'test', 'Build and run the test suite', ->
+ execCmds [
+ 'coffee --bare --output ./test ./src/test/*.coffee',
+ 'npm install --dev',
+ 'node_modules/.bin/vows ./test/*.js'
+ ]
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Andrew McCollum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
0 README
No changes.
12 lib/ender.js
@@ -0,0 +1,12 @@
+!(function($) {
+ var renderTemplate;
+ renderTemplate = require('wings').renderTemplate;
+ $.ender({
+ renderTemplate: renderTemplate
+ });
+ return $.ender({
+ render: function(data, links) {
+ return renderTemplate(this.html(), data, links);
+ }
+ }, true);
+})(ender);
110 lib/template.js
@@ -0,0 +1,110 @@
+!(function(context) {
+ var escapeXML, isArray, parse_re, renderRawTemplate, replaceBraces, restoreBraces, _ref;
+ context['renderTemplate'] = function(template, data, links) {
+ template = replaceBraces(template);
+ template = renderRawTemplate(template, data, links);
+ template = restoreBraces(template);
+ return template;
+ };
+ replaceBraces = function(template) {
+ return template.replace(/\{\{/g, '\ufe5b').replace(/\}\}/g, '\ufe5d');
+ };
+ restoreBraces = function(template) {
+ return template.replace(/\ufe5b/g, '{').replace(/\ufe5d/g, '}');
+ };
+ isArray = (_ref = Array.isArray) != null ? _ref : (function(o) {
+ return Object.prototype.toString.call(o) === '[object Array]';
+ });
+ escapeXML = function(s) {
+ return (s || '').toString().replace(/&(?!\w+;)|["<>]/g, function(s) {
+ switch (s) {
+ case '&':
+ return '&amp';
+ case '"':
+ return '\"';
+ case '<':
+ return '&lt';
+ case '>':
+ return '&gt';
+ default:
+ return s;
+ }
+ });
+ };
+ parse_re = /\s*\{([!:])\s*([^}]*?)\s*\}([\S\s]+?)\s*\{\/\s*\2\s*\}|\{([#@&]?)\s*([^}]*?)\s*\}/mg;
+ return renderRawTemplate = function(template, data, links) {
+ return template.replace(parse_re, function(match, section_op, section_name, section_content, tag_op, tag_name) {
+ var content, link, name, op, part, v, value, _ref2;
+ op = section_op || tag_op;
+ name = section_name || tag_name;
+ content = section_content;
+ switch (op) {
+ case ':':
+ value = data[name];
+ if (!(value != null)) {
+ throw "Invalid section: " + data + ": " + name + ": " + value;
+ } else if (isArray(value)) {
+ return ((function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = value.length; _i < _len; _i++) {
+ v = value[_i];
+ _results.push(renderRawTemplate(content, v, links));
+ }
+ return _results;
+ })()).join('');
+ } else if (typeof value === 'object') {
+ return renderRawTemplate(content, value, links);
+ } else if (typeof value === 'function') {
+ return value.call(data, content);
+ } else if (value) {
+ return renderRawTemplate(content, data, links);
+ } else {
+ return "";
+ }
+ break;
+ case '!':
+ value = data[name];
+ if (!(value != null)) {
+ throw "Invalid inverted section: " + data + ": " + name + ": " + value;
+ } else if (!value || (isArray(value) && value.length === 0)) {
+ return renderRawTemplate(content, data, links);
+ } else {
+ return "";
+ }
+ break;
+ case '#':
+ return '';
+ case '@':
+ link = links ? links[name] : null;
+ if (!(link != null)) {
+ throw "Invalid link: " + links + ": " + name + ": " + link;
+ } else if (typeof link === 'function') {
+ link = link.call(data);
+ }
+ return renderRawTemplate(replaceBraces(link), data, links);
+ case '&':
+ case '':
+ value = data;
+ while (value && name) {
+ _ref2 = name.match(/^([^.]*)\.?(.*)$/).slice(1), part = _ref2[0], name = _ref2[1];
+ if (part in value) {
+ value = value[part];
+ } else {
+ value = null;
+ }
+ }
+ if (!(value != null)) {
+ throw "Invalid value: " + data + ": " + name + ": " + value;
+ } else if (typeof value === 'function') {
+ value = value.call(data);
+ }
+ if (op === '&') {
+ return value;
+ } else {
+ return escapeXML(value);
+ }
+ }
+ });
+ };
+})(exports || this);
110 lib/wings.js
@@ -0,0 +1,110 @@
+!(function(wings) {
+ var escapeXML, isArray, parse_re, renderRawTemplate, replaceBraces, restoreBraces, _ref;
+ wings.renderTemplate = function(template, data, links) {
+ template = replaceBraces(template);
+ template = renderRawTemplate(template, data, links);
+ template = restoreBraces(template);
+ return template;
+ };
+ replaceBraces = function(template) {
+ return template.replace(/\{\{/g, '\ufe5b').replace(/\}\}/g, '\ufe5d');
+ };
+ restoreBraces = function(template) {
+ return template.replace(/\ufe5b/g, '{').replace(/\ufe5d/g, '}');
+ };
+ isArray = (_ref = Array.isArray) != null ? _ref : (function(o) {
+ return Object.prototype.toString.call(o) === '[object Array]';
+ });
+ escapeXML = function(s) {
+ return (s || '').toString().replace(/&(?!\w+;)|["<>]/g, function(s) {
+ switch (s) {
+ case '&':
+ return '&amp';
+ case '"':
+ return '\"';
+ case '<':
+ return '&lt';
+ case '>':
+ return '&gt';
+ default:
+ return s;
+ }
+ });
+ };
+ parse_re = /\s*\{([!:])\s*([^}]*?)\s*\}([\S\s]+?)\s*\{\/\s*\2\s*\}|\{([#@&]?)\s*([^}]*?)\s*\}/mg;
+ return renderRawTemplate = function(template, data, links) {
+ return template.replace(parse_re, function(match, section_op, section_name, section_content, tag_op, tag_name) {
+ var content, link, name, op, part, v, value, _ref2;
+ op = section_op || tag_op;
+ name = section_name || tag_name;
+ content = section_content;
+ switch (op) {
+ case ':':
+ value = data[name];
+ if (!(value != null)) {
+ throw "Invalid section: " + data + ": " + name + ": " + value;
+ } else if (isArray(value)) {
+ return ((function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = value.length; _i < _len; _i++) {
+ v = value[_i];
+ _results.push(renderRawTemplate(content, v, links));
+ }
+ return _results;
+ })()).join('');
+ } else if (typeof value === 'object') {
+ return renderRawTemplate(content, value, links);
+ } else if (typeof value === 'function') {
+ return value.call(data, content);
+ } else if (value) {
+ return renderRawTemplate(content, data, links);
+ } else {
+ return "";
+ }
+ break;
+ case '!':
+ value = data[name];
+ if (!(value != null)) {
+ throw "Invalid inverted section: " + data + ": " + name + ": " + value;
+ } else if (!value || (isArray(value) && value.length === 0)) {
+ return renderRawTemplate(content, data, links);
+ } else {
+ return "";
+ }
+ break;
+ case '#':
+ return '';
+ case '@':
+ link = links ? links[name] : null;
+ if (!(link != null)) {
+ throw "Invalid link: " + links + ": " + name + ": " + link;
+ } else if (typeof link === 'function') {
+ link = link.call(data);
+ }
+ return renderRawTemplate(replaceBraces(link), data, links);
+ case '&':
+ case '':
+ value = data;
+ while (value && name) {
+ _ref2 = name.match(/^([^.]*)\.?(.*)$/).slice(1), part = _ref2[0], name = _ref2[1];
+ if (part in value) {
+ value = value[part];
+ } else {
+ value = null;
+ }
+ }
+ if (!(value != null)) {
+ throw "Invalid value: " + data + ": " + name + ": " + value;
+ } else if (typeof value === 'function') {
+ value = value.call(data);
+ }
+ if (op === '&') {
+ return value;
+ } else {
+ return escapeXML(value);
+ }
+ }
+ });
+ };
+})(typeof exports !== "undefined" && exports !== null ? exports : (this['wings'] = {}));
20 package.json
@@ -0,0 +1,20 @@
+{
+ "name": "wings",
+ "version": "0.1.0",
+ "description": "Templating library that works on the server and client closely modeled on Mustache",
+ "keywords": ["ender", "template", "mustache", "html"],
+ "author": "Andrew McCollum <amccollum@gmail.com>",
+ "url": "http://github.com/amccollum/wings",
+ "repositories": [{ "type": "git", "url": "http://github.com/amccollum/wings.git" }],
+ "licenses": [{ "type": "MIT", "url": "http://opensource.org/licenses/mit-license.php" }],
+
+ "main": "lib/template.js",
+ "ender": "lib/ender.js",
+ "directories": { "lib": "lib" },
+
+ "dependencies": { "bonzo": "*" },
+ "devDependencies": {
+ "coffee-script": "*",
+ "vows": "*"
+ }
+}
178 src/test/template.coffee
@@ -0,0 +1,178 @@
+assert = require('assert')
+vows = require('vows')
+template = require('../lib/template.js')
+
+t = template.renderTemplate
+equal = assert.equal
+
+vows.add 'templates'
+ 'basics:':
+ 'an empty template':
+ topic: t('')
+ 'should be equal to the empty string': (topic) ->
+ equal topic, ''
+
+ 'a template with no tags':
+ topic: t('I think, therefore I am.')
+ 'should be equal to the same string': (topic) ->
+ equal topic, 'I think, therefore I am.'
+
+ 'templates with escaped braces':
+ topic: [
+ t('I think, {{therefore I am.}}'),
+ t('I }}{{think, {{{{therefore I am.}}}}'),
+ t('nested {{ {:truthy} {{ braces }} {{ {/truthy} }}', {truthy: true}),
+ ]
+
+ 'should have double braces replaced with single braces': (topics) ->
+ equal topics[0], 'I think, {therefore I am.}'
+ equal topics[1], 'I }{think, {{therefore I am.}}'
+ equal topics[2], 'nested { { braces } { }'
+
+
+ 'tags:':
+ 'a template with a single tag':
+ topic: t('{test}', {test: 'blah'})
+ 'should be equal to the tag value': (topic) ->
+ equal topic, 'blah'
+
+ 'a template with multiple tags':
+ topic: t('The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.',
+ {adj1:'quick', adj2:'brown', adj3:'lazy', verb1:'jumped'})
+
+ 'should replace all the tags': (topic) ->
+ equal topic, 'The quick, brown fox jumped over the lazy dogs.'
+
+
+ 'a template with dotted tags':
+ topic: t('The {adjs.adj1}, {adjs.adj2} fox {verbs.verb1} over the {adjs.adj3} dogs.',
+ {adjs: {adj1:'quick', adj2:'brown', adj3:'lazy'}, verbs: {verb1:'jumped'}})
+
+ 'should replace the tags with the object properties': (topic) ->
+ equal topic, 'The quick, brown fox jumped over the lazy dogs.'
+
+ 'a template with a function tag':
+ topic: t('The result of the function is: "{fn1}".', {fn1: -> 'test'})
+
+ 'should replace the tag with the result of the function': (topic) ->
+ equal topic, 'The result of the function is: "test".'
+
+ 'a template with comment tags':
+ topic: t('There are comments{#comment} in this template{# longer comment }.')
+
+ 'should remove the comments when rendered': (topic) ->
+ equal topic, 'There are comments in this template.'
+
+ 'a template with escaped tags':
+ topic: t('This shouldn\'t produce html: {html}', {html: '<b>bolded</b>'})
+
+ 'should escape the html reserved characters': (topic) ->
+ equal topic, 'This shouldn\'t produce html: &ltb&gtbolded&lt/b&gt'
+
+ 'a template with unescaped tags':
+ topic: t('This should produce html: {&html}', {html: '<b>bolded</b>'})
+
+ 'should produce html': (topic) ->
+ equal topic, 'This should produce html: <b>bolded</b>'
+
+
+ 'links:':
+ 'a template with a normal link':
+ topic: t('{@foo}', {bar: 'baz'}, {foo:'{bar}'})
+
+ 'should follow the link': (topic) ->
+ equal topic, 'baz'
+
+ 'a template with a function link':
+ topic: t('{@foo}', {bar: 'baz'}, {foo: () -> '{bar}'})
+
+ 'should call the function to get the link': (topic) ->
+ equal topic, 'baz'
+
+
+ 'sections:':
+ 'templates with regular sections':
+ topic: [
+ t('{:untruthy}foo{/untruthy}bar', {untruthy: 0}),
+ t('{:untruthy}foo{/untruthy}bar', {untruthy: []}),
+ t('{:untruthy}foo{/untruthy}bar', {untruthy: false}),
+
+ t('{:truthy}foo{/truthy}bar', {truthy: 1}),
+ t('{:truthy}foo{/truthy}bar', {truthy: {}}),
+ t('{:truthy}foo{/truthy}bar', {truthy: true}),
+ ]
+
+ 'should only include those sections when the tag is truthy': (topics) ->
+ equal topics[0], 'bar'
+ equal topics[1], 'bar'
+ equal topics[2], 'bar'
+ equal topics[3], 'foobar'
+ equal topics[4], 'foobar'
+ equal topics[5], 'foobar'
+
+ 'templates with inverse sections':
+ topic: [
+ t('{!untruthy}foo{/untruthy}bar', {untruthy: 0}),
+ t('{!untruthy}foo{/untruthy}bar', {untruthy: []}),
+ t('{!untruthy}foo{/untruthy}bar', {untruthy: false}),
+
+ t('{!truthy}foo{/truthy}bar', {truthy: 1}),
+ t('{!truthy}foo{/truthy}bar', {truthy: {}}),
+ t('{!truthy}foo{/truthy}bar', {truthy: true}),
+ ]
+
+ 'should only include those sections when the tag is not truthy': (topics) ->
+ equal topics[0], 'foobar'
+ equal topics[1], 'foobar'
+ equal topics[2], 'foobar'
+ equal topics[3], 'bar'
+ equal topics[4], 'bar'
+ equal topics[5], 'bar'
+
+ 'templates with array sections':
+ topic: [
+ t('{:array}foo{/array}bar', {array: [1, 2, 3]}),
+ t('{:array}{}{/array}', {array: ['foo', 'bar', 'baz']}),
+ t('{:array}{}a{/array}', {array: [1, 2, 3, 4, 5]}),
+ t('{:array}{name}{/array}', {array: [{name:'foo'}, {name:'bar'}, {name:'baz'}]}),
+ t('{:array1}foo{/array1}bar{:array2}{}{/array2}{:array3}{}a{/array3}{:array4}{name}{/array4}', {
+ array1: [1, 2, 3],
+ array2: ['foo', 'bar', 'baz'],
+ array3: [1, 2, 3, 4, 5],
+ array4: [{name:'foo'}, {name:'bar'}, {name:'baz'}],
+ }),
+ ]
+
+ 'should render the section once for each item in the array': (topics) ->
+ equal topics[0], 'foofoofoobar'
+ equal topics[1], 'foobarbaz'
+ equal topics[2], '1a2a3a4a5a'
+ equal topics[3], 'foobarbaz'
+ equal topics[4], 'foofoofoobarfoobarbaz1a2a3a4a5afoobarbaz'
+
+ 'a template with an object section':
+ topic: t('{:obj}{foo}{bar}{baz}{/obj}', {obj: {foo: '1', bar: '2', baz: '3'}})
+
+ 'should use the object as the new environment': (topic) ->
+ equal topic, '123'
+
+ 'a template with a function section':
+ topic: t('{:fn}abcdef{/fn}', {fn: (str) -> str.split('').reverse().join('')})
+
+ 'should replace the section with the result of the function': (topic) ->
+ equal topic, 'fedcba'
+
+ 'a template with subtemplates':
+ topic: t('{:tmpls}{name}: \'{text}\',{/tmpls}', {
+ tmpls: [
+ { name: 'tmpl1', text: 'The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.' },
+ { name: 'tmpl2', text: '{:untrue}foo{/untrue}bar' },
+ { name: 'tmpl3', text: 'nested {{ {:truthy} {{ braces }} {{ {/truthy} }}' },
+ ]})
+
+ 'should insert the subtemplates unmodified': (topic) ->
+ equal topic, '''tmpl1: 'The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.',
+ tmpl2: '{:untrue}foo{/untrue}bar',
+ tmpl3: 'nested {{ {:truthy} {{ braces }} {{ {/truthy} }}',
+ '''.replace(/\n/g, '')
+
8 src/wings/ender.coffee
@@ -0,0 +1,8 @@
+!(($) ->
+ renderTemplate = require('wings').renderTemplate
+
+ $.ender({renderTemplate: renderTemplate})
+ $.ender({
+ render: (data, links) -> renderTemplate(@html(), data, links)
+ }, true)
+)(ender)
98 src/wings/wings.coffee
@@ -0,0 +1,98 @@
+!((wings) ->
+ wings.renderTemplate = (template, data, links) ->
+ # Replace escaped braces with an obscure unicode curly brace
+ template = replaceBraces(template)
+ template = renderRawTemplate(template, data, links)
+ template = restoreBraces(template)
+ return template
+
+
+ replaceBraces = (template) -> template.replace(/\{\{/g, '\ufe5b').replace(/\}\}/g, '\ufe5d')
+ restoreBraces = (template) -> template.replace(/\ufe5b/g, '{').replace(/\ufe5d/g, '}')
+
+ isArray = Array.isArray ? ((o) -> Object.prototype.toString.call(o) == '[object Array]')
+
+ escapeXML = (s) ->
+ return (s or '').toString().replace /&(?!\w+;)|["<>]/g, (s) ->
+ switch s
+ when '&' then return '&amp'
+ when '"' then return '\"'
+ when '<' then return '&lt'
+ when '>' then return '&gt'
+ else return s
+
+ parse_re = ///
+ \s* \{([!:]) \s* ([^}]*?) \s*\ } ([\S\s]+?) \s* \{/ \s* \2 \s*\} | # sections
+ \{([#@&]?) \s* ([^}]*?) \s* \} # tags
+ ///mg
+
+ renderRawTemplate = (template, data, links) ->
+ template.replace parse_re, (match, section_op, section_name, section_content, tag_op, tag_name) ->
+ op = section_op or tag_op
+ name = section_name or tag_name
+ content = section_content
+
+ switch op
+ when ':' # section
+ value = data[name]
+ if not value?
+ throw "Invalid section: #{data}: #{name}: #{value}"
+
+ else if isArray(value)
+ return (renderRawTemplate(content, v, links) for v in value).join('')
+
+ else if typeof value == 'object'
+ return renderRawTemplate(content, value, links)
+
+ else if typeof value == 'function'
+ return value.call(data, content)
+
+ else if value
+ return renderRawTemplate(content, data, links)
+
+ else
+ return ""
+
+ when '!' # inverted section
+ value = data[name]
+ if not value?
+ throw "Invalid inverted section: #{data}: #{name}: #{value}"
+
+ else if not value or (isArray(value) and value.length == 0)
+ return renderRawTemplate(content, data, links)
+
+ else
+ return ""
+
+ when '#' # comment tag
+ return ''
+
+ when '@' # link tag
+ link = if links then links[name] else null
+
+ if not link?
+ throw "Invalid link: #{links}: #{name}: #{link}"
+
+ else if typeof link == 'function'
+ link = link.call(data)
+
+ return renderRawTemplate(replaceBraces(link), data, links)
+
+ when '&', '' # value tag
+ value = data
+ while value and name
+ [part, name] = name.match(/^([^.]*)\.?(.*)$/)[1..]
+ if part of value
+ value = value[part]
+ else
+ value = null
+
+ if not value?
+ throw "Invalid value: #{data}: #{name}: #{value}"
+
+ else if typeof value == 'function'
+ value = value.call(data)
+
+ return (if op == '&' then value else escapeXML(value))
+
+)(exports ? (@['wings'] = {}))
260 test/template.js
@@ -0,0 +1,260 @@
+var assert, equal, t, template, vows;
+assert = require('assert');
+vows = require('vows');
+template = require('../lib/template.js');
+t = template.renderTemplate;
+equal = assert.equal;
+vows.add('templates', {
+ 'basics:': {
+ 'an empty template': {
+ topic: t(''),
+ 'should be equal to the empty string': function(topic) {
+ return equal(topic, '');
+ }
+ },
+ 'a template with no tags': {
+ topic: t('I think, therefore I am.'),
+ 'should be equal to the same string': function(topic) {
+ return equal(topic, 'I think, therefore I am.');
+ }
+ },
+ 'templates with escaped braces': {
+ topic: [
+ t('I think, {{therefore I am.}}'), t('I }}{{think, {{{{therefore I am.}}}}'), t('nested {{ {:truthy} {{ braces }} {{ {/truthy} }}', {
+ truthy: true
+ })
+ ],
+ 'should have double braces replaced with single braces': function(topics) {
+ equal(topics[0], 'I think, {therefore I am.}');
+ equal(topics[1], 'I }{think, {{therefore I am.}}');
+ return equal(topics[2], 'nested { { braces } { }');
+ }
+ }
+ },
+ 'tags:': {
+ 'a template with a single tag': {
+ topic: t('{test}', {
+ test: 'blah'
+ }),
+ 'should be equal to the tag value': function(topic) {
+ return equal(topic, 'blah');
+ }
+ },
+ 'a template with multiple tags': {
+ topic: t('The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.', {
+ adj1: 'quick',
+ adj2: 'brown',
+ adj3: 'lazy',
+ verb1: 'jumped'
+ }),
+ 'should replace all the tags': function(topic) {
+ return equal(topic, 'The quick, brown fox jumped over the lazy dogs.');
+ }
+ },
+ 'a template with dotted tags': {
+ topic: t('The {adjs.adj1}, {adjs.adj2} fox {verbs.verb1} over the {adjs.adj3} dogs.', {
+ adjs: {
+ adj1: 'quick',
+ adj2: 'brown',
+ adj3: 'lazy'
+ },
+ verbs: {
+ verb1: 'jumped'
+ }
+ }),
+ 'should replace the tags with the object properties': function(topic) {
+ return equal(topic, 'The quick, brown fox jumped over the lazy dogs.');
+ }
+ },
+ 'a template with a function tag': {
+ topic: t('The result of the function is: "{fn1}".', {
+ fn1: function() {
+ return 'test';
+ }
+ }),
+ 'should replace the tag with the result of the function': function(topic) {
+ return equal(topic, 'The result of the function is: "test".');
+ }
+ },
+ 'a template with comment tags': {
+ topic: t('There are comments{#comment} in this template{# longer comment }.'),
+ 'should remove the comments when rendered': function(topic) {
+ return equal(topic, 'There are comments in this template.');
+ }
+ },
+ 'a template with escaped tags': {
+ topic: t('This shouldn\'t produce html: {html}', {
+ html: '<b>bolded</b>'
+ }),
+ 'should escape the html reserved characters': function(topic) {
+ return equal(topic, 'This shouldn\'t produce html: &ltb&gtbolded&lt/b&gt');
+ }
+ },
+ 'a template with unescaped tags': {
+ topic: t('This should produce html: {&html}', {
+ html: '<b>bolded</b>'
+ }),
+ 'should produce html': function(topic) {
+ return equal(topic, 'This should produce html: <b>bolded</b>');
+ }
+ }
+ },
+ 'links:': {
+ 'a template with a normal link': {
+ topic: t('{@foo}', {
+ bar: 'baz'
+ }, {
+ foo: '{bar}'
+ }),
+ 'should follow the link': function(topic) {
+ return equal(topic, 'baz');
+ }
+ },
+ 'a template with a function link': {
+ topic: t('{@foo}', {
+ bar: 'baz'
+ }, {
+ foo: function() {
+ return '{bar}';
+ }
+ }),
+ 'should call the function to get the link': function(topic) {
+ return equal(topic, 'baz');
+ }
+ }
+ },
+ 'sections:': {
+ 'templates with regular sections': {
+ topic: [
+ t('{:untruthy}foo{/untruthy}bar', {
+ untruthy: 0
+ }), t('{:untruthy}foo{/untruthy}bar', {
+ untruthy: []
+ }), t('{:untruthy}foo{/untruthy}bar', {
+ untruthy: false
+ }), t('{:truthy}foo{/truthy}bar', {
+ truthy: 1
+ }), t('{:truthy}foo{/truthy}bar', {
+ truthy: {}
+ }), t('{:truthy}foo{/truthy}bar', {
+ truthy: true
+ })
+ ],
+ 'should only include those sections when the tag is truthy': function(topics) {
+ equal(topics[0], 'bar');
+ equal(topics[1], 'bar');
+ equal(topics[2], 'bar');
+ equal(topics[3], 'foobar');
+ equal(topics[4], 'foobar');
+ return equal(topics[5], 'foobar');
+ }
+ },
+ 'templates with inverse sections': {
+ topic: [
+ t('{!untruthy}foo{/untruthy}bar', {
+ untruthy: 0
+ }), t('{!untruthy}foo{/untruthy}bar', {
+ untruthy: []
+ }), t('{!untruthy}foo{/untruthy}bar', {
+ untruthy: false
+ }), t('{!truthy}foo{/truthy}bar', {
+ truthy: 1
+ }), t('{!truthy}foo{/truthy}bar', {
+ truthy: {}
+ }), t('{!truthy}foo{/truthy}bar', {
+ truthy: true
+ })
+ ],
+ 'should only include those sections when the tag is not truthy': function(topics) {
+ equal(topics[0], 'foobar');
+ equal(topics[1], 'foobar');
+ equal(topics[2], 'foobar');
+ equal(topics[3], 'bar');
+ equal(topics[4], 'bar');
+ return equal(topics[5], 'bar');
+ }
+ },
+ 'templates with array sections': {
+ topic: [
+ t('{:array}foo{/array}bar', {
+ array: [1, 2, 3]
+ }), t('{:array}{}{/array}', {
+ array: ['foo', 'bar', 'baz']
+ }), t('{:array}{}a{/array}', {
+ array: [1, 2, 3, 4, 5]
+ }), t('{:array}{name}{/array}', {
+ array: [
+ {
+ name: 'foo'
+ }, {
+ name: 'bar'
+ }, {
+ name: 'baz'
+ }
+ ]
+ }), t('{:array1}foo{/array1}bar{:array2}{}{/array2}{:array3}{}a{/array3}{:array4}{name}{/array4}', {
+ array1: [1, 2, 3],
+ array2: ['foo', 'bar', 'baz'],
+ array3: [1, 2, 3, 4, 5],
+ array4: [
+ {
+ name: 'foo'
+ }, {
+ name: 'bar'
+ }, {
+ name: 'baz'
+ }
+ ]
+ })
+ ],
+ 'should render the section once for each item in the array': function(topics) {
+ equal(topics[0], 'foofoofoobar');
+ equal(topics[1], 'foobarbaz');
+ equal(topics[2], '1a2a3a4a5a');
+ equal(topics[3], 'foobarbaz');
+ return equal(topics[4], 'foofoofoobarfoobarbaz1a2a3a4a5afoobarbaz');
+ }
+ },
+ 'a template with an object section': {
+ topic: t('{:obj}{foo}{bar}{baz}{/obj}', {
+ obj: {
+ foo: '1',
+ bar: '2',
+ baz: '3'
+ }
+ }),
+ 'should use the object as the new environment': function(topic) {
+ return equal(topic, '123');
+ }
+ },
+ 'a template with a function section': {
+ topic: t('{:fn}abcdef{/fn}', {
+ fn: function(str) {
+ return str.split('').reverse().join('');
+ }
+ }),
+ 'should replace the section with the result of the function': function(topic) {
+ return equal(topic, 'fedcba');
+ }
+ },
+ 'a template with subtemplates': {
+ topic: t('{:tmpls}{name}: \'{text}\',{/tmpls}', {
+ tmpls: [
+ {
+ name: 'tmpl1',
+ text: 'The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.'
+ }, {
+ name: 'tmpl2',
+ text: '{:untrue}foo{/untrue}bar'
+ }, {
+ name: 'tmpl3',
+ text: 'nested {{ {:truthy} {{ braces }} {{ {/truthy} }}'
+ }
+ ]
+ }),
+ 'should insert the subtemplates unmodified': function(topic) {
+ return equal(topic, 'tmpl1: \'The {adj1}, {adj2} fox {verb1} over the {adj3} dogs.\',\ntmpl2: \'{:untrue}foo{/untrue}bar\',\ntmpl3: \'nested {{ {:truthy} {{ braces }} {{ {/truthy} }}\','.replace(/\n/g, ''));
+ }
+ }
+ }
+});

0 comments on commit fe72382

Please sign in to comment.