diff --git a/mustache.js b/mustache.js index a3aac8f5e..6c7e0bc3f 100644 --- a/mustache.js +++ b/mustache.js @@ -76,6 +76,9 @@ var curlyRe = /\s*\}/; var tagRe = /#|\^|\/|>|\{|&|=|!/; + var customTagRe = ""; + var customTags = {}; // {tag1: callback1, tag2: callback2} + /** * Breaks up the given `template` string into a tree of tokens. If the `tags` * argument is given here it must be an array with two string values: the @@ -455,7 +458,7 @@ return self.render(template, context, partials); } - var token, value; + var token, value, out, scn, ctag; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; @@ -505,7 +508,20 @@ break; case 'name': value = context.lookup(token[1]); - if (value != null) buffer += mustache.escape(value); + if (value != null) { + buffer += mustache.escape(value); + } else { + // token may be a custom tag + scn = new Scanner(token[1]); + ctag = scn.scan(new RegExp(customTagRe)); + if (ctag) { + scn.scan(whiteRe); + out = customTags[ctag](scn.tail, context.lookup(scn.tail)); + if (typeof out === 'string') { + buffer += out; + } + } + } break; case 'text': buffer += token[1]; @@ -558,6 +574,44 @@ } }; + /** + * Adds a custom tag. + */ + mustache.addCustomTag = function (tag, callback) { + // allow only alphabetic characters + if (!tag.match(/^[A-z]+$/)) { + throw new Error("invalid tag name"); + } + if (typeof callback !== 'function') { + throw new Error("invalid callback"); + } + var customTagsArr; + if (!customTags[tag]) { + // tag does not exist + if (customTagRe === '') { + customTagsArr = []; + } else { + customTagsArr = customTagRe.split('|'); + } + customTagsArr.push(tag); + // sort customTagsArr to put the longest strings to the top + // in order to prevent regExp mistakes + customTagsArr.sort(function (str1, str2) { + if (str1.length === str2.length) { + if (str1 === str2) { + return 0; + } else { + return str1 < str2 ? 1 : -1; + } + } else { + return str1.length < str2.length ? 1 : -1; + } + }); + customTagRe = customTagsArr.join('|'); + } + customTags[tag] = callback; + } + // Export the escaping function so that the user may override it. // See https://github.com/janl/mustache.js/issues/244 mustache.escape = escapeHtml; diff --git a/test/custom-tag-test.js b/test/custom-tag-test.js new file mode 100644 index 000000000..3b3aabb42 --- /dev/null +++ b/test/custom-tag-test.js @@ -0,0 +1,86 @@ +require('./helper'); + +describe('Add custom tag function', function () { + describe('for an non-alphabetic tag string', function () { + it('throws an error', function () { + assert.throws(function() { + Mustache.addCustomTag('tag?', function () {}); + }, 'invalid tag name'); + assert.throws(function() { + Mustache.addCustomTag('0tag', function () {}); + }, 'invalid tag name'); + }) + }); + + describe('for an illegal function argument', function () { + it('throws an error', function () { + assert.throws(function() { + Mustache.addCustomTag('tag'); + }, 'invalid callback'); + }) + }); + + describe('callback', function () { + it('must receive contents of a tag', function() { + Mustache.addCustomTag('tag', function (contents, value) { + assert.equal(contents, 'some text'); + }); + + Mustache.render('here is {{tag some text}}'); + }); + + it('must receive a variable if it exists', function() { + Mustache.addCustomTag('tag', function (contents, value) { + assert.equal(value[1], 2); + }); + + Mustache.render('here is {{tag v}}', {v: [1,2,3]}); + }); + }); + + describe('for an alphababetic tag name and a valid callback', function () { + it('adds a new tag', function () { + Mustache.addCustomTag('dummy', function () { + return 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'; + }); + var text = Mustache.render('some dummy text: {{dummy}}'); + assert.equal(text, 'some dummy text: Lorem ipsum dolor sit amet, consectetur adipisicing elit'); + }); + + describe('if tag exists', function () { + it('updates a callback', function () { + Mustache.addCustomTag('dummy', function () { + return 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'; + }); + Mustache.addCustomTag('dummy', function () { + return 'updated'; + }); + var text = Mustache.render('some dummy text: {{dummy}}'); + assert.equal(text, 'some dummy text: updated'); + }); + }); + + describe('if callback does not return a string', function () { + it('ignores the tag', function () { + Mustache.addCustomTag('tag', function () { + return {}; + }); + var text = Mustache.render('some {{tag}} text'); + assert.equal(text, 'some text'); + }); + }); + + describe('every tag', function () { + it('must have it\'s own callback', function() { + Mustache.addCustomTag('som', function () { + return "this won\'t be displayed"; + }); + Mustache.addCustomTag('some', function () { + return "ok"; + }); + var text = Mustache.render('some {{some}} text'); + assert.equal(text, 'some ok text'); + }); + }); + }); +}); \ No newline at end of file