From e54aca8e3b054a769f5ab4fc295885be6b4b071a Mon Sep 17 00:00:00 2001 From: "subzey@gmail.com" Date: Wed, 29 Feb 2012 14:48:29 +0400 Subject: [PATCH 001/286] BOM as whitespace test case --- spec/_files/bom_as_whitespace.js | 1 + spec/_files/bom_as_whitespace.mustache | 1 + spec/_files/bom_as_whitespace.txt | 1 + 3 files changed, 3 insertions(+) create mode 100644 spec/_files/bom_as_whitespace.js create mode 100644 spec/_files/bom_as_whitespace.mustache create mode 100644 spec/_files/bom_as_whitespace.txt diff --git a/spec/_files/bom_as_whitespace.js b/spec/_files/bom_as_whitespace.js new file mode 100644 index 000000000..4bce3e105 --- /dev/null +++ b/spec/_files/bom_as_whitespace.js @@ -0,0 +1 @@ +var bom_as_whitespace = {'tag': 'Tag name w/o BOM', '\uFEFFtag': 'Tag name with BOM'}; \ No newline at end of file diff --git a/spec/_files/bom_as_whitespace.mustache b/spec/_files/bom_as_whitespace.mustache new file mode 100644 index 000000000..5189a1900 --- /dev/null +++ b/spec/_files/bom_as_whitespace.mustache @@ -0,0 +1 @@ +{{tag}} \ No newline at end of file diff --git a/spec/_files/bom_as_whitespace.txt b/spec/_files/bom_as_whitespace.txt new file mode 100644 index 000000000..76ef3f14a --- /dev/null +++ b/spec/_files/bom_as_whitespace.txt @@ -0,0 +1 @@ +Tag name w/o BOM \ No newline at end of file From d166547f01f8e24ac9897a73f792a0aafb9d84bf Mon Sep 17 00:00:00 2001 From: Alvaro Videla Date: Wed, 31 Jul 2013 22:13:47 +0200 Subject: [PATCH 002/286] an image is wroth a thousand words --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 95c92788b..866d7b166 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ You can use mustache.js to render mustache templates anywhere you can use JavaSc mustache.js ships with support for both the [CommonJS](http://www.commonjs.org/) module API and the [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) API, or AMD. +And this will be your templates after you use Mustache: + +!['stache](http://d24w6bsrhbeh9d.cloudfront.net/photo/aZPNGon_460sa.gif) + ## Who uses mustache.js? An updated list of mustache.js users is kept [on the Github wiki](http://wiki.github.com/janl/mustache.js/beard-competition). Add yourself or your company if you use mustache.js! From 84d9991e84f7d34d6a6fb22c96a4b4fecc25b6d6 Mon Sep 17 00:00:00 2001 From: Chris Berkhout Date: Fri, 1 Nov 2013 12:13:28 +1100 Subject: [PATCH 003/286] Fixed README to mention all falsy values tested in test/_files/falsy.*. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95c92788b..f49452123 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The behavior of the section is determined by the value of the key. #### False Values or Empty Lists -If the `person` key does not exist, or exists and has a value of `null`, `undefined`, or `false`, or is an empty list, the block will not be rendered. +If the `person` key does not exist, or exists and has a value of `null`, `undefined`, `false`, `0`, or `NaN`, or is an empty string or an empty list, the block will not be rendered. View: From 207b76a880913f9e3ffb2c9f4921d50226563139 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2013 10:06:26 -0800 Subject: [PATCH 004/286] Fix some Rake tasks, cleanup Rakefile --- Rakefile | 59 +++++++++++++++++++------------------- test/mustache-spec-test.js | 4 +-- test/spec | 1 - 3 files changed, 32 insertions(+), 32 deletions(-) delete mode 160000 test/spec diff --git a/Rakefile b/Rakefile index bc3217586..c01908732 100644 --- a/Rakefile +++ b/Rakefile @@ -3,66 +3,67 @@ require 'rake/clean' task :default => :test -ROOT = File.expand_path('..', __FILE__) -MUSTACHE_JS = File.read(File.join(ROOT, 'mustache.js')) +def minified_file + ENV['FILE'] || 'mustache.min.js' +end -def mustache_version - match = MUSTACHE_JS.match(/exports\.version = "([^"]+)";/) - match[1] +task :install_mocha do + sh "npm install -g mocha" if `which mocha`.empty? end -def minified_file - ENV['FILE'] || 'mustache.min.js' +task :install_uglify do + sh "npm install -g uglify-js" if `which uglifyjs`.empty? end -desc "Run all tests, requires vows (see http://vowsjs.org)" -task :test do - sh "vows --spec" +task :install_jshint do + sh "npm install -g jshint" if `which jshint`.empty? end -desc "Minify to #{minified_file}, requires UglifyJS (see http://marijnhaverbeke.nl/uglifyjs)" -task :minify do +desc "Run all tests" +task :test => :install_mocha do + sh "mocha test" +end + +desc "Make a compressed build in #{minified_file}" +task :minify => :install_uglify do sh "uglifyjs mustache.js > #{minified_file}" end -desc "Run JSHint, requires jshint (see http://www.jshint.com)" -task :lint do +desc "Run JSHint" +task :hint => :install_jshint do sh "jshint mustache.js" end # Creates a task that uses the various template wrappers to make a wrapped # output file. There is some extra complexity because Dojo and YUI use # different final locations. -def templated_build(name, opts={}) +def templated_build(name, final_location=nil) short = name.downcase source = File.join("wrappers", short) dependencies = ["mustache.js"] + Dir.glob("#{source}/*.tpl.*") - target_js = opts[:location] ? "mustache.js" : "#{short}.mustache.js" - - CLEAN.include(opts[:location] ? opts[:location] : target_js) + target_js = final_location.nil? ? "#{short}.mustache.js" : "mustache.js" desc "Package for #{name}" task short.to_sym => dependencies do puts "Packaging for #{name}" - mkdir_p opts[:location] if opts[:location] + mkdir_p final_location unless final_location.nil? - files = [ - "#{source}/mustache.js.pre", - 'mustache.js', - "#{source}/mustache.js.post" - ] + sources = [ "#{source}/mustache.js.pre", 'mustache.js', "#{source}/mustache.js.post" ] + relative_name = "#{final_location || '.'}/#{target_js}" - open("#{opts[:location] || '.'}/#{target_js}", 'w') do |f| - files.each {|file| f << File.read(file) } + open(relative_name, 'w') do |f| + sources.each {|source| f << File.read(source) } end - puts "Done, see #{opts[:location] || '.'}/#{target_js}" + puts "Done, see #{relative_name}" end + + CLEAN.include(final_location.nil? ? target_js : final_location) end templated_build "jQuery" templated_build "MooTools" -templated_build "Dojo", :location => "dojox/string" -templated_build "YUI3", :location => "yui3/mustache" +templated_build "Dojo", "dojox/string" +templated_build "YUI3", "yui3/mustache" templated_build "qooxdoo" diff --git a/test/mustache-spec-test.js b/test/mustache-spec-test.js index 1050ea43d..0c6e537ac 100644 --- a/test/mustache-spec-test.js +++ b/test/mustache-spec-test.js @@ -41,7 +41,7 @@ var noSkip = process.env.NOSKIP; // variable (e.g. TEST=interpolation mocha test/mustache-spec-test.js) var fileToRun = process.env.TEST; -// Mustache should work on node 0.6 that doesn't have fs.exisisSync +// Mustache should work on node 0.6 that doesn't have fs.existsSync function existsDir(path) { try { return fs.statSync(path).isDirectory(); @@ -86,4 +86,4 @@ describe('Mustache spec compliance', function() { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/spec b/test/spec deleted file mode 160000 index 72233f3ff..000000000 --- a/test/spec +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 72233f3ffda9e33915fd3022d0a9ebbcce265acd From 25243109da851bf0292bf044136670664f64480e Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2013 10:08:01 -0800 Subject: [PATCH 005/286] Ignore generated file for MooTools --- .gitignore | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 90007769a..a4c0df6aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ -.DS_Store -.rvmrc node_modules -runner.js jquery.mustache.js -qooxdoo.mustache.js +mootools.mustache.js dojox yui3 -requirejs.mustache.js +qooxdoo.mustache.js From 512b2e8df13f5cd2dc107ee6d39074a97337e874 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2013 10:08:19 -0800 Subject: [PATCH 006/286] Simplify some logic --- mustache.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/mustache.js b/mustache.js index bee26f965..360c80009 100644 --- a/mustache.js +++ b/mustache.js @@ -82,9 +82,10 @@ var match = this.tail.match(re); if (match && match.index === 0) { - this.tail = this.tail.substring(match[0].length); - this.pos += match[0].length; - return match[0]; + var string = match[0]; + this.tail = this.tail.substring(string.length); + this.pos += string.length; + return string; } return ""; @@ -95,23 +96,23 @@ * the skipped string, which is the entire tail if no match can be made. */ Scanner.prototype.scanUntil = function (re) { - var match, pos = this.tail.search(re); + var index = this.tail.search(re), match; - switch (pos) { + switch (index) { case -1: match = this.tail; - this.pos += this.tail.length; this.tail = ""; break; case 0: match = ""; break; default: - match = this.tail.substring(0, pos); - this.tail = this.tail.substring(pos); - this.pos += pos; + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); } + this.pos += match.length; + return match; }; From 397e56dabe78935f0b5014c967037badcbfd12cd Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2013 10:54:10 -0800 Subject: [PATCH 007/286] Fix JSHint errors --- mustache.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/mustache.js b/mustache.js index 360c80009..0076ce680 100644 --- a/mustache.js +++ b/mustache.js @@ -228,6 +228,12 @@ function renderTokens(tokens, writer, context, template) { var buffer = ''; + // This function is used to render an artbitrary template + // in the current context by higher-order functions. + function subRender(template) { + return writer.render(template, context); + } + var token, tokenValue, value; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; @@ -247,9 +253,7 @@ } } else if (typeof value === 'function') { var text = template == null ? null : template.slice(token[3], token[5]); - value = value.call(context.view, text, function (template) { - return writer.render(template, context); - }); + value = value.call(context.view, text, subRender); if (value != null) buffer += value; } else if (value) { buffer += renderTokens(token[4], writer, context, template); @@ -389,7 +393,7 @@ nonSpace = false; } - var start, type, value, chr, token; + var start, type, value, chr, token, openSection; while (!scanner.eos()) { start = scanner.pos; @@ -445,26 +449,32 @@ sections.push(token); } else if (type === '/') { // Check section nesting. - if (sections.length === 0) throw new Error('Unopened section "' + value + '" at ' + start); - var openSection = sections.pop(); - if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + openSection = sections.pop(); + if (!openSection) { + throw new Error('Unopened section "' + value + '" at ' + start); + } + if (openSection[1] !== value) { + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } } else if (type === 'name' || type === '{' || type === '&') { nonSpace = true; } else if (type === '=') { // Set the tags for the next time around. tags = value.split(spaceRe); - if (tags.length !== 2) throw new Error('Invalid tags at ' + start + ': ' + tags.join(', ')); + if (tags.length !== 2) { + throw new Error('Invalid tags at ' + start + ': ' + tags.join(', ')); + } tagRes = escapeTags(tags); } } // Make sure there are no open sections when we're done. - var openSection = sections.pop(); - if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - - tokens = squashTokens(tokens); + openSection = sections.pop(); + if (openSection) { + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + } - return nestTokens(tokens); + return nestTokens(squashTokens(tokens)); } mustache.name = "mustache.js"; From 257c235661b135ce783b7abd1020d0588ff3a838 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 4 Nov 2013 11:34:54 -0800 Subject: [PATCH 008/286] Add back mustache/spec submodule --- test/spec | 1 + 1 file changed, 1 insertion(+) create mode 160000 test/spec diff --git a/test/spec b/test/spec new file mode 160000 index 000000000..72233f3ff --- /dev/null +++ b/test/spec @@ -0,0 +1 @@ +Subproject commit 72233f3ffda9e33915fd3022d0a9ebbcce265acd From 48fb97b4efae2da79342c10570af3d202a16f673 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 06:09:57 -0800 Subject: [PATCH 009/286] Add isFunction helper --- mustache.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/mustache.js b/mustache.js index 0076ce680..81582eecc 100644 --- a/mustache.js +++ b/mustache.js @@ -38,10 +38,14 @@ } var Object_toString = Object.prototype.toString; - var isArray = Array.isArray || function (obj) { - return Object_toString.call(obj) === '[object Array]'; + var isArray = Array.isArray || function (object) { + return Object_toString.call(object) === '[object Array]'; }; + function isFunction(object) { + return typeof object === 'function'; + } + function escapeRegExp(string) { return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); } @@ -134,7 +138,7 @@ var value = this._cache[name]; if (!value) { - if (name == '.') { + if (name === '.') { value = this.view; } else { var context = this; @@ -142,6 +146,7 @@ while (context) { if (name.indexOf('.') > 0) { value = context.view; + var names = name.split('.'), i = 0; while (value && i < names.length) { value = value[names[i++]]; @@ -159,7 +164,9 @@ this._cache[name] = value; } - if (typeof value === 'function') value = value.call(this.view); + if (isFunction(value)) { + value = value.call(this.view); + } return value; }; @@ -202,7 +209,7 @@ var self = this; return function (view, partials) { if (partials) { - if (typeof partials === 'function') { + if (isFunction(partials)) { self._loadPartial = partials; } else { for (var name in partials) { @@ -251,7 +258,7 @@ } else if (value) { buffer += renderTokens(token[4], writer, context.push(value), template); } - } else if (typeof value === 'function') { + } else if (isFunction(value)) { var text = template == null ? null : template.slice(token[3], token[5]); value = value.call(context.view, text, subRender); if (value != null) buffer += value; @@ -272,7 +279,7 @@ break; case '>': value = writer.getPartial(tokenValue); - if (typeof value === 'function') buffer += value(context); + if (isFunction(value)) buffer += value(context); break; case '&': value = context.lookup(tokenValue); @@ -537,7 +544,7 @@ mustache.to_html = function (template, view, partials, send) { var result = mustache.render(template, view, partials); - if (typeof send === "function") { + if (isFunction(send)) { send(result); } else { return result; From 411edae06129af7816e37cd7904869e6e45618d2 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 06:40:25 -0800 Subject: [PATCH 010/286] Fix [object Object] errors Fixes #322 Fixes #330 Fixes #331 Fixes #334 --- mustache.js | 11 ++++++----- test/_files/zero_view.js | 1 + test/_files/zero_view.mustache | 1 + test/_files/zero_view.txt | 1 + test/context-test.js | 17 +++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 test/_files/zero_view.js create mode 100644 test/_files/zero_view.mustache create mode 100644 test/_files/zero_view.txt diff --git a/mustache.js b/mustache.js index 81582eecc..89e3aca22 100644 --- a/mustache.js +++ b/mustache.js @@ -121,7 +121,7 @@ }; function Context(view, parent) { - this.view = view || {}; + this.view = view == null ? {} : view; this.parent = parent; this._cache = {}; } @@ -135,9 +135,10 @@ }; Context.prototype.lookup = function (name) { - var value = this._cache[name]; - - if (!value) { + var value; + if (name in this._cache) { + value = this._cache[name]; + } else { if (name === '.') { value = this.view; } else { @@ -148,7 +149,7 @@ value = context.view; var names = name.split('.'), i = 0; - while (value && i < names.length) { + while (value != null && i < names.length) { value = value[names[i++]]; } } else { diff --git a/test/_files/zero_view.js b/test/_files/zero_view.js new file mode 100644 index 000000000..986460860 --- /dev/null +++ b/test/_files/zero_view.js @@ -0,0 +1 @@ +({ nums: [0, 1, 2] }) diff --git a/test/_files/zero_view.mustache b/test/_files/zero_view.mustache new file mode 100644 index 000000000..1cdc190c1 --- /dev/null +++ b/test/_files/zero_view.mustache @@ -0,0 +1 @@ +{{#nums}}{{.}},{{/nums}} \ No newline at end of file diff --git a/test/_files/zero_view.txt b/test/_files/zero_view.txt new file mode 100644 index 000000000..2aee585a0 --- /dev/null +++ b/test/_files/zero_view.txt @@ -0,0 +1 @@ +0,1,2, \ No newline at end of file diff --git a/test/context-test.js b/test/context-test.js index 752f74bc7..808b25377 100644 --- a/test/context-test.js +++ b/test/context-test.js @@ -43,6 +43,23 @@ describe('A new Mustache.Context', function () { }); }); +describe('A Mustache.Context', function () { + var context; + + describe('with an empty string in the lookup chain', function () { + var view, context; + beforeEach(function () { + view = { a: '' }; + view.a.b = 'value'; + context = new Context(view); + }); + + it('is able to lookup a nested property', function () { + assert.equal(context.lookup('a.b'), view.a.b); + }); + }); +}); + describe('Mustache.Context.make', function () { it('returns the same object when given a Context', function () { var context = new Context; From 5cc298e8af87c4521da33168ce438776103e96ed Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 07:03:02 -0800 Subject: [PATCH 011/286] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f49452123..4ce62fca8 100644 --- a/README.md +++ b/README.md @@ -378,7 +378,7 @@ The test suite consists of both unit and integration tests. If a template isn't Then, you can run the test with: - $ TEST=mytest mocha test/render_test.js + $ TEST=mytest mocha test/render-test.js ## Thanks From da16eada83dd0eb2771d3c388b132359ff42f7b5 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 07:04:03 -0800 Subject: [PATCH 012/286] Allow a string to act as context Fixes #321 --- mustache.js | 2 +- test/_files/nested_dot.js | 1 + test/_files/nested_dot.mustache | 1 + test/_files/nested_dot.txt | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 test/_files/nested_dot.js create mode 100644 test/_files/nested_dot.mustache create mode 100644 test/_files/nested_dot.txt diff --git a/mustache.js b/mustache.js index 89e3aca22..a7d42e08c 100644 --- a/mustache.js +++ b/mustache.js @@ -251,7 +251,7 @@ case '#': value = context.lookup(tokenValue); - if (typeof value === 'object') { + if (typeof value === 'object' || typeof value === 'string') { if (isArray(value)) { for (var j = 0, jlen = value.length; j < jlen; ++j) { buffer += renderTokens(token[4], writer, context.push(value[j]), template); diff --git a/test/_files/nested_dot.js b/test/_files/nested_dot.js new file mode 100644 index 000000000..2c22f8e38 --- /dev/null +++ b/test/_files/nested_dot.js @@ -0,0 +1 @@ +({ name: 'Bruno' }) diff --git a/test/_files/nested_dot.mustache b/test/_files/nested_dot.mustache new file mode 100644 index 000000000..12b072828 --- /dev/null +++ b/test/_files/nested_dot.mustache @@ -0,0 +1 @@ +{{#name}}Hello {{.}}{{/name}} \ No newline at end of file diff --git a/test/_files/nested_dot.txt b/test/_files/nested_dot.txt new file mode 100644 index 000000000..58df047cb --- /dev/null +++ b/test/_files/nested_dot.txt @@ -0,0 +1 @@ +Hello Bruno \ No newline at end of file From b60aa6f0f1a1f4ac347b47a3596fa1ba497113a9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 07:04:48 -0800 Subject: [PATCH 013/286] Pre-cache view for dot references --- mustache.js | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/mustache.js b/mustache.js index a7d42e08c..e27dc6a1a 100644 --- a/mustache.js +++ b/mustache.js @@ -123,7 +123,7 @@ function Context(view, parent) { this.view = view == null ? {} : view; this.parent = parent; - this._cache = {}; + this._cache = { '.': this.view }; } Context.make = function (view) { @@ -139,27 +139,23 @@ if (name in this._cache) { value = this._cache[name]; } else { - if (name === '.') { - value = this.view; - } else { - var context = this; + var context = this; - while (context) { - if (name.indexOf('.') > 0) { - value = context.view; + while (context) { + if (name.indexOf('.') > 0) { + value = context.view; - var names = name.split('.'), i = 0; - while (value != null && i < names.length) { - value = value[names[i++]]; - } - } else { - value = context.view[name]; + var names = name.split('.'), i = 0; + while (value != null && i < names.length) { + value = value[names[i++]]; } + } else { + value = context.view[name]; + } - if (value != null) break; + if (value != null) break; - context = context.parent; - } + context = context.parent; } this._cache[name] = value; From b626d1239557c616d053b101b2adfc33236470b8 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 5 Nov 2013 07:43:29 -0800 Subject: [PATCH 014/286] Version 0.7.3 --- CHANGES | 2 +- mustache.js | 2 +- mustache.js.nuspec | 2 +- package.json | 4 ++-- wrappers/qooxdoo/mustache.js.pre | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index ad80e95f4..fd72af89c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,4 @@ -= HEAD += 0.7.3 / 5 Nov 2013 * Don't require the original template to be passed to the rendering function when using compiled templates. This is still required when using higher-order diff --git a/mustache.js b/mustache.js index e27dc6a1a..4c65a9528 100644 --- a/mustache.js +++ b/mustache.js @@ -482,7 +482,7 @@ } mustache.name = "mustache.js"; - mustache.version = "0.7.2"; + mustache.version = "0.7.3"; mustache.tags = ["{{", "}}"]; mustache.Scanner = Scanner; diff --git a/mustache.js.nuspec b/mustache.js.nuspec index ec1b4b775..a103cd4aa 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 0.7.2 + 0.7.3 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/package.json b/package.json index 4abd6daf3..0d8019aa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "0.7.2", + "version": "0.7.3", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "repository": { @@ -13,7 +13,7 @@ "mocha": "1.5.0" }, "volo": { - "url": "https://raw.github.com/janl/mustache.js/0.7.2/mustache.js" + "url": "https://raw.github.com/janl/mustache.js/0.7.3/mustache.js" }, "scripts": { "test": "mocha test" diff --git a/wrappers/qooxdoo/mustache.js.pre b/wrappers/qooxdoo/mustache.js.pre index 22aca98c6..d4719c97a 100644 --- a/wrappers/qooxdoo/mustache.js.pre +++ b/wrappers/qooxdoo/mustache.js.pre @@ -19,7 +19,7 @@ This class contains code based on the following work: - * Mustache.js version 0.7.2 + * Mustache.js version 0.7.3 Code: https://github.com/janl/mustache.js From b1141207419f69068ba29064f02f9aea28842c21 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 6 Nov 2013 13:11:37 -0800 Subject: [PATCH 015/286] Major reorganization and cleanup --- CHANGES | 12 + mustache.js | 625 ++++++++++++++++++++++++------------------- test/context-test.js | 7 - test/parse-test.js | 2 +- test/writer-test.js | 47 ++-- 5 files changed, 378 insertions(+), 315 deletions(-) diff --git a/CHANGES b/CHANGES index fd72af89c..6b7702c10 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,15 @@ += HEAD + + * Remove compile* writer functions and replace them with cache* equivalents. This + reflects more clearly what the functions are actually doing, and also provides + a better basis for understanding what writer.clearCache() does. + * Do not cache dynamically loaded partials. + * Throw an error when rendering a template that contains higher-order sections and + the original template is not provided. + * Remove partials argument from low-level writer.render. + * Remove low-level Context.make function. + * Better code readability and inline documentation. + = 0.7.3 / 5 Nov 2013 * Don't require the original template to be passed to the rendering function diff --git a/mustache.js b/mustache.js index 4c65a9528..ce39f8faa 100644 --- a/mustache.js +++ b/mustache.js @@ -65,6 +65,216 @@ }); } + function escapeTags(tags) { + if (!isArray(tags) || tags.length !== 2) { + throw new Error('Invalid tags: ' + tags); + } + + return [ + new RegExp(escapeRegExp(tags[0]) + "\\s*"), + new RegExp("\\s*" + escapeRegExp(tags[1])) + ]; + } + + /** + * 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 + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all template text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices + * in the original template of the token, respectively. + * + * Tokens that are the root node of a subtree contain two more elements: an + * array of tokens in the subtree and the index in the original template at which + * the closing tag for that section begins. + */ + function parseTemplate(template, tags) { + tags = tags || mustache.tags; + template = template || ''; + + if (typeof tags === 'string') { + tags = tags.split(spaceRe); + } + + var tagRes = escapeTags(tags); + var scanner = new Scanner(template); + + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace() { + if (hasTag && !nonSpace) { + while (spaces.length) { + delete tokens[spaces.pop()]; + } + } else { + spaces = []; + } + + hasTag = false; + nonSpace = false; + } + + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; + + // Match any text between tags. + value = scanner.scanUntil(tagRes[0]); + if (value) { + for (var i = 0, len = value.length; i < len; ++i) { + chr = value.charAt(i); + + if (isWhitespace(chr)) { + spaces.push(tokens.length); + } else { + nonSpace = true; + } + + tokens.push(['text', chr, start, start + 1]); + start += 1; + + // Check for whitespace on the current line. + if (chr == '\n') stripSpace(); + } + } + + // Match the opening tag. + if (!scanner.scan(tagRes[0])) break; + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(eqRe); + scanner.scan(eqRe); + scanner.scanUntil(tagRes[1]); + } else if (type === '{') { + value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1]))); + scanner.scan(curlyRe); + scanner.scanUntil(tagRes[1]); + type = '&'; + } else { + value = scanner.scanUntil(tagRes[1]); + } + + // Match the closing tag. + if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos); + + token = [type, value, start, scanner.pos]; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) { + throw new Error('Unopened section "' + value + '" at ' + start); + } + if (openSection[1] !== value) { + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + tagRes = escapeTags(tags = value.split(spaceRe)); + } + } + + // Make sure there are no open sections when we're done. + openSection = sections.pop(); + if (openSection) { + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + } + + return nestTokens(squashTokens(tokens)); + } + + /** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ + function squashTokens(tokens) { + var squashedTokens = []; + + var token, lastToken; + for (var i = 0, len = tokens.length; i < len; ++i) { + token = tokens[i]; + + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + lastToken = token; + squashedTokens.push(token); + } + } + } + + return squashedTokens; + } + + /** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ + function nestTokens(tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token; + for (var i = 0, len = tokens.length; i < len; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + sections.push(token); + collector.push(token); + collector = token[4] = []; + break; + case '/': + var section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); + } + } + + return nestedTokens; + } + + /** + * A simple string scanner that is used by the mustache.parse to find + * tokens in template strings. + */ function Scanner(string) { this.string = string; this.tail = string; @@ -120,20 +330,28 @@ return match; }; + /** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ function Context(view, parent) { this.view = view == null ? {} : view; this.parent = parent; this._cache = { '.': this.view }; } - Context.make = function (view) { - return (view instanceof Context) ? view : new Context(view); - }; - + /** + * Creates a new context using the given view with this context + * as the parent. + */ Context.prototype.push = function (view) { return new Context(view, this); }; + /** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ Context.prototype.lookup = function (name) { var value; if (name in this._cache) { @@ -168,334 +386,169 @@ return value; }; - function Writer() { + /** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates and + * partials to avoid the need to parse the same template twice. + * + * The `partialLoader` argument may be a function that is used to load + * partials on the fly as they are needed if they are not found in cache. + * Partials loaded in this manner are not cached. + */ + function Writer(partialLoader) { this.clearCache(); + + if (isFunction(partialLoader)) { + this._loadPartial = partialLoader; + } } + /** + * Clears all cached templates and partials in this writer. + */ Writer.prototype.clearCache = function () { this._cache = {}; this._partialCache = {}; }; - Writer.prototype.compile = function (template, tags) { - var fn = this._cache[template]; - - if (!fn) { - var tokens = mustache.parse(template, tags); - fn = this._cache[template] = this.compileTokens(tokens, template); - } - - return fn; - }; - - Writer.prototype.compilePartial = function (name, template, tags) { - var fn = this.compile(template, tags); - this._partialCache[name] = fn; - return fn; - }; - + /** + * Gets an array of tokens for the partial with the given `name`, either + * from cache or from this writer's partial loading function. + */ Writer.prototype.getPartial = function (name) { if (!(name in this._partialCache) && this._loadPartial) { - this.compilePartial(name, this._loadPartial(name)); + var template = this._loadPartial(name); + if (template) { + // Intentionally do not cache dynamically-loaded templates. + return mustache.parse(template); + } } return this._partialCache[name]; }; - Writer.prototype.compileTokens = function (tokens, template) { - var self = this; - return function (view, partials) { - if (partials) { - if (isFunction(partials)) { - self._loadPartial = partials; - } else { - for (var name in partials) { - self.compilePartial(name, partials[name]); - } - } - } + /** + * Parses and caches the given `template` with the given `name` as a + * partial. This writer may then render other templates that reference + * that partial using the ">" tag and that partial's name. Returns the + * array of tokens that is generated from the parse. + */ + Writer.prototype.cachePartial = function (name, template, tags) { + return (this._partialCache[name] = this.cache(template, tags)); + }; + + /** + * Parses and caches the given `template` and returns the array of tokens + * that is generated from the parse. + */ + Writer.prototype.cache = function (template, tags) { + if (!(template in this._cache)) { + this._cache[template] = mustache.parse(template, tags); + } - return renderTokens(tokens, self, Context.make(view), template); - }; + return this._cache[template]; }; - Writer.prototype.render = function (template, view, partials) { - return this.compile(template)(view, partials); + /** + * High-level method that is used to render the given `template` with + * the given `view`. + */ + Writer.prototype.render = function (template, view) { + var tokens = this.cache(template); + var context = (view instanceof Context) ? view : new Context(view); + return this.renderTokens(tokens, context, template); }; /** - * Low-level function that renders the given `tokens` using the given `writer` - * and `context`. The `template` string is only needed for templates that use - * higher-order sections to extract the portion of the original template that - * was contained in that section. + * Low-level method that renders the given array of `tokens` using + * the given `context`. + * + * Note: The `template` string is only needed for templates that use + * higher-order sections to extract the portion of the original template + * that was contained in that section. */ - function renderTokens(tokens, writer, context, template) { + Writer.prototype.renderTokens = function (tokens, context, template) { var buffer = ''; - // This function is used to render an artbitrary template + // This function is used to render an arbitrary template // in the current context by higher-order functions. + var self = this; function subRender(template) { - return writer.render(template, context); + return self.render(template, context); } - var token, tokenValue, value; + var token, value; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; - tokenValue = token[1]; switch (token[0]) { case '#': - value = context.lookup(tokenValue); - - if (typeof value === 'object' || typeof value === 'string') { - if (isArray(value)) { - for (var j = 0, jlen = value.length; j < jlen; ++j) { - buffer += renderTokens(token[4], writer, context.push(value[j]), template); - } - } else if (value) { - buffer += renderTokens(token[4], writer, context.push(value), template); + value = context.lookup(token[1]); + if (!value) continue; + + if (isArray(value)) { + for (var j = 0, jlen = value.length; j < jlen; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), template); } + } else if (typeof value === 'object' || typeof value === 'string') { + buffer += this.renderTokens(token[4], context.push(value), template); } else if (isFunction(value)) { - var text = template == null ? null : template.slice(token[3], token[5]); - value = value.call(context.view, text, subRender); + if (typeof template !== 'string') { + throw new Error('Cannot use higher-order sections without the original template'); + } + + // Extract the portion of the original template that the section contains. + value = value.call(context.view, template.slice(token[3], token[5]), subRender); + if (value != null) buffer += value; - } else if (value) { - buffer += renderTokens(token[4], writer, context, template); + } else { + buffer += this.renderTokens(token[4], context, template); } break; case '^': - value = context.lookup(tokenValue); + value = context.lookup(token[1]); // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 if (!value || (isArray(value) && value.length === 0)) { - buffer += renderTokens(token[4], writer, context, template); + buffer += this.renderTokens(token[4], context, template); } break; case '>': - value = writer.getPartial(tokenValue); - if (isFunction(value)) buffer += value(context); + value = this.getPartial(token[1]); + if (value != null) buffer += this.renderTokens(value, context, template); break; case '&': - value = context.lookup(tokenValue); + value = context.lookup(token[1]); if (value != null) buffer += value; break; case 'name': - value = context.lookup(tokenValue); + value = context.lookup(token[1]); if (value != null) buffer += mustache.escape(value); break; case 'text': - buffer += tokenValue; + buffer += token[1]; break; } } return buffer; - } - - /** - * Forms the given array of `tokens` into a nested tree structure where - * tokens that represent a section have two additional items: 1) an array of - * all tokens that appear in that section and 2) the index in the original - * template that represents the end of that section. - */ - function nestTokens(tokens) { - var tree = []; - var collector = tree; - var sections = []; - - var token; - for (var i = 0, len = tokens.length; i < len; ++i) { - token = tokens[i]; - switch (token[0]) { - case '#': - case '^': - sections.push(token); - collector.push(token); - collector = token[4] = []; - break; - case '/': - var section = sections.pop(); - section[5] = token[2]; - collector = sections.length > 0 ? sections[sections.length - 1][4] : tree; - break; - default: - collector.push(token); - } - } - - return tree; - } - - /** - * Combines the values of consecutive text tokens in the given `tokens` array - * to a single token. - */ - function squashTokens(tokens) { - var squashedTokens = []; - - var token, lastToken; - for (var i = 0, len = tokens.length; i < len; ++i) { - token = tokens[i]; - if (token) { - if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { - lastToken[1] += token[1]; - lastToken[3] = token[3]; - } else { - lastToken = token; - squashedTokens.push(token); - } - } - } - - return squashedTokens; - } - - function escapeTags(tags) { - return [ - new RegExp(escapeRegExp(tags[0]) + "\\s*"), - new RegExp("\\s*" + escapeRegExp(tags[1])) - ]; - } - - /** - * Breaks up the given `template` string into a tree of token objects. If - * `tags` is given here it must be an array with two string values: the - * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of - * course, the default is to use mustaches (i.e. Mustache.tags). - */ - function parseTemplate(template, tags) { - template = template || ''; - tags = tags || mustache.tags; - - if (typeof tags === 'string') tags = tags.split(spaceRe); - if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', ')); - - var tagRes = escapeTags(tags); - var scanner = new Scanner(template); - - var sections = []; // Stack to hold section tokens - var tokens = []; // Buffer to hold the tokens - var spaces = []; // Indices of whitespace tokens on the current line - var hasTag = false; // Is there a {{tag}} on the current line? - var nonSpace = false; // Is there a non-space char on the current line? - - // Strips all whitespace tokens array for the current line - // if there was a {{#tag}} on it and otherwise only space. - function stripSpace() { - if (hasTag && !nonSpace) { - while (spaces.length) { - delete tokens[spaces.pop()]; - } - } else { - spaces = []; - } - - hasTag = false; - nonSpace = false; - } - - var start, type, value, chr, token, openSection; - while (!scanner.eos()) { - start = scanner.pos; - - // Match any text between tags. - value = scanner.scanUntil(tagRes[0]); - if (value) { - for (var i = 0, len = value.length; i < len; ++i) { - chr = value.charAt(i); - - if (isWhitespace(chr)) { - spaces.push(tokens.length); - } else { - nonSpace = true; - } - - tokens.push(['text', chr, start, start + 1]); - start += 1; - - // Check for whitespace on the current line. - if (chr == '\n') stripSpace(); - } - } - - // Match the opening tag. - if (!scanner.scan(tagRes[0])) break; - hasTag = true; - - // Get the tag type. - type = scanner.scan(tagRe) || 'name'; - scanner.scan(whiteRe); - - // Get the tag value. - if (type === '=') { - value = scanner.scanUntil(eqRe); - scanner.scan(eqRe); - scanner.scanUntil(tagRes[1]); - } else if (type === '{') { - value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1]))); - scanner.scan(curlyRe); - scanner.scanUntil(tagRes[1]); - type = '&'; - } else { - value = scanner.scanUntil(tagRes[1]); - } - - // Match the closing tag. - if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos); - - token = [type, value, start, scanner.pos]; - tokens.push(token); - - if (type === '#' || type === '^') { - sections.push(token); - } else if (type === '/') { - // Check section nesting. - openSection = sections.pop(); - if (!openSection) { - throw new Error('Unopened section "' + value + '" at ' + start); - } - if (openSection[1] !== value) { - throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); - } - } else if (type === 'name' || type === '{' || type === '&') { - nonSpace = true; - } else if (type === '=') { - // Set the tags for the next time around. - tags = value.split(spaceRe); - if (tags.length !== 2) { - throw new Error('Invalid tags at ' + start + ': ' + tags.join(', ')); - } - tagRes = escapeTags(tags); - } - } - - // Make sure there are no open sections when we're done. - openSection = sections.pop(); - if (openSection) { - throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - } - - return nestTokens(squashTokens(tokens)); - } + }; mustache.name = "mustache.js"; mustache.version = "0.7.3"; - mustache.tags = ["{{", "}}"]; - - mustache.Scanner = Scanner; - mustache.Context = Context; - mustache.Writer = Writer; + mustache.tags = [ "{{", "}}" ]; + // Export the parsing function. mustache.parse = parseTemplate; // Export the escaping function so that the user may override it. // See https://github.com/janl/mustache.js/issues/244 mustache.escape = escapeHtml; - // All Mustache.* functions use this writer. + // All high-level mustache.* functions use this writer. var defaultWriter = new Writer(); /** @@ -506,35 +559,42 @@ }; /** - * Compiles the given `template` to a reusable function using the default - * writer. + * Caches the given partial in the default writer and returns the + * array of tokens it contains. */ - mustache.compile = function (template, tags) { - return defaultWriter.compile(template, tags); + mustache.cachePartial = function (name, template, tags) { + return defaultWriter.cachePartial(name, template, tags); }; /** - * Compiles the partial with the given `name` and `template` to a reusable - * function using the default writer. + * Caches the given template in the default writer and returns the + * array of tokens it contains. */ - mustache.compilePartial = function (name, template, tags) { - return defaultWriter.compilePartial(name, template, tags); + mustache.cache = function (template, tags) { + return defaultWriter.cache(template, tags); }; /** - * Compiles the given array of tokens (the output of a parse) to a reusable - * function using the default writer. - */ - mustache.compileTokens = function (tokens, template) { - return defaultWriter.compileTokens(tokens, template); - }; - - /** - * Renders the `template` with the given `view` and `partials` using the - * default writer. + * Renders the `template` with the given `view` using the default writer. + * + * The optionals `partials` argument may either be a function that is used to load + * partials on the fly or an object containing names and templates of partials. If + * it is an object, the partials will be cached in the default writer. */ mustache.render = function (template, view, partials) { - return defaultWriter.render(template, view, partials); + if (partials) { + if (isFunction(partials)) { + defaultWriter._loadPartial = partials; + } else { + for (var name in partials) { + if (partials.hasOwnProperty(name)) { + defaultWriter.cachePartial(name, partials[name]); + } + } + } + } + + return defaultWriter.render(template, view); }; // This is here for backwards compatibility with 0.4.x. @@ -548,4 +608,9 @@ } }; + // Export these mainly for testing, but also for advanced usage. + mustache.Scanner = Scanner; + mustache.Context = Context; + mustache.Writer = Writer; + })); diff --git a/test/context-test.js b/test/context-test.js index 808b25377..9b9cbbeaf 100644 --- a/test/context-test.js +++ b/test/context-test.js @@ -59,10 +59,3 @@ describe('A Mustache.Context', function () { }); }); }); - -describe('Mustache.Context.make', function () { - it('returns the same object when given a Context', function () { - var context = new Context; - assert.strictEqual(Context.make(context), context); - }); -}); diff --git a/test/parse-test.js b/test/parse-test.js index 40d23a431..97c26db94 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -99,7 +99,7 @@ describe('Mustache.parse', function () { it('throws an error', function () { assert.throws(function () { Mustache.parse('A template {{=<%=}}'); - }, /invalid tags at 11/i); + }, /invalid tags/i); }); }); diff --git a/test/writer-test.js b/test/writer-test.js index db2813a5f..fc552dbff 100644 --- a/test/writer-test.js +++ b/test/writer-test.js @@ -3,41 +3,34 @@ var Writer = Mustache.Writer; describe('A new Mustache.Writer', function () { var writer; - beforeEach(function () { - writer = new Writer; - }); - it('loads partials correctly', function () { - var partial = 'The content of the partial.'; - var result = writer.render('{{>partial}}', {}, function (name) { - assert.equal(name, 'partial'); - return partial; + describe('with a cached partial', function () { + beforeEach(function () { + writer = new Writer; }); - assert.equal(result, partial); - }); + it('caches partials by content, not name', function () { + writer.cachePartial('partial', 'partial one'); + assert.equal(writer.render('{{>partial}}'), 'partial one'); - it('caches partials by content, not name', function () { - var result = writer.render('{{>partial}}', {}, { - partial: 'partial one' + writer.cachePartial('partial', 'partial two'); + assert.equal(writer.render('{{>partial}}'), 'partial two'); }); + }); - assert.equal(result, 'partial one'); - - result = writer.render('{{>partial}}', {}, { - partial: 'partial two' + describe('with a partial loader', function () { + var partial; + beforeEach(function () { + partial = 'The content of the partial.'; + writer = new Writer(function (name) { + assert.equal(name, 'partial'); + return partial; + }); }); - assert.equal(result, 'partial two'); + it('loads partials correctly', function () { + assert.equal(writer.render('{{>partial}}'), partial); + }); }); - it('can compile an array of tokens', function () { - var template = 'Hello {{name}}!'; - var tokens = Mustache.parse(template); - var render = writer.compileTokens(tokens, template); - - var result = render({ name: 'Michael' }); - - assert.equal(result, 'Hello Michael!'); - }); }); From c4b345f14c33d66a7e35606315a32fbf46721ea3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 7 Nov 2013 08:42:57 -0800 Subject: [PATCH 016/286] Smaller API surface area Removed cache* functions altogether. mustache.parse is all that's really needed. --- CHANGES | 6 +- mustache.js | 145 ++++++++++++++------------------------------ test/render-test.js | 2 +- test/writer-test.js | 36 ----------- 4 files changed, 49 insertions(+), 140 deletions(-) delete mode 100644 test/writer-test.js diff --git a/CHANGES b/CHANGES index 6b7702c10..4c9fca185 100644 --- a/CHANGES +++ b/CHANGES @@ -1,12 +1,8 @@ = HEAD - * Remove compile* writer functions and replace them with cache* equivalents. This - reflects more clearly what the functions are actually doing, and also provides - a better basis for understanding what writer.clearCache() does. - * Do not cache dynamically loaded partials. + * Remove compile* writer functions, use mustache.parse instead. Smaller API. * Throw an error when rendering a template that contains higher-order sections and the original template is not provided. - * Remove partials argument from low-level writer.render. * Remove low-level Context.make function. * Better code readability and inline documentation. diff --git a/mustache.js b/mustache.js index ce39f8faa..578431443 100644 --- a/mustache.js +++ b/mustache.js @@ -272,7 +272,7 @@ } /** - * A simple string scanner that is used by the mustache.parse to find + * A simple string scanner that is used by the template parser to find * tokens in template strings. */ function Scanner(string) { @@ -388,62 +388,27 @@ /** * A Writer knows how to take a stream of tokens and render them to a - * string, given a context. It also maintains a cache of templates and - * partials to avoid the need to parse the same template twice. - * - * The `partialLoader` argument may be a function that is used to load - * partials on the fly as they are needed if they are not found in cache. - * Partials loaded in this manner are not cached. + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. */ - function Writer(partialLoader) { - this.clearCache(); - - if (isFunction(partialLoader)) { - this._loadPartial = partialLoader; - } + function Writer() { + this._cache = {}; } /** - * Clears all cached templates and partials in this writer. + * Clears all cached templates in this writer. */ Writer.prototype.clearCache = function () { this._cache = {}; - this._partialCache = {}; - }; - - /** - * Gets an array of tokens for the partial with the given `name`, either - * from cache or from this writer's partial loading function. - */ - Writer.prototype.getPartial = function (name) { - if (!(name in this._partialCache) && this._loadPartial) { - var template = this._loadPartial(name); - if (template) { - // Intentionally do not cache dynamically-loaded templates. - return mustache.parse(template); - } - } - - return this._partialCache[name]; - }; - - /** - * Parses and caches the given `template` with the given `name` as a - * partial. This writer may then render other templates that reference - * that partial using the ">" tag and that partial's name. Returns the - * array of tokens that is generated from the parse. - */ - Writer.prototype.cachePartial = function (name, template, tags) { - return (this._partialCache[name] = this.cache(template, tags)); }; /** * Parses and caches the given `template` and returns the array of tokens * that is generated from the parse. */ - Writer.prototype.cache = function (template, tags) { + Writer.prototype.parse = function (template, tags) { if (!(template in this._cache)) { - this._cache[template] = mustache.parse(template, tags); + this._cache[template] = parseTemplate(template, tags); } return this._cache[template]; @@ -452,29 +417,35 @@ /** * High-level method that is used to render the given `template` with * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. */ - Writer.prototype.render = function (template, view) { - var tokens = this.cache(template); + Writer.prototype.render = function (template, view, partials) { + var tokens = this.parse(template); var context = (view instanceof Context) ? view : new Context(view); - return this.renderTokens(tokens, context, template); + return this.renderTokens(tokens, context, partials, template); }; /** * Low-level method that renders the given array of `tokens` using - * the given `context`. + * the given `context` and `partials`. * - * Note: The `template` string is only needed for templates that use - * higher-order sections to extract the portion of the original template - * that was contained in that section. + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. */ - Writer.prototype.renderTokens = function (tokens, context, template) { + Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) { var buffer = ''; // This function is used to render an arbitrary template - // in the current context by higher-order functions. + // in the current context by higher-order sections. var self = this; function subRender(template) { - return self.render(template, context); + return self.render(template, context, partials); } var token, value; @@ -488,21 +459,21 @@ if (isArray(value)) { for (var j = 0, jlen = value.length; j < jlen; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), template); + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } } else if (typeof value === 'object' || typeof value === 'string') { - buffer += this.renderTokens(token[4], context.push(value), template); + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); } else if (isFunction(value)) { - if (typeof template !== 'string') { + if (typeof originalTemplate !== 'string') { throw new Error('Cannot use higher-order sections without the original template'); } // Extract the portion of the original template that the section contains. - value = value.call(context.view, template.slice(token[3], token[5]), subRender); + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); if (value != null) buffer += value; } else { - buffer += this.renderTokens(token[4], context, template); + buffer += this.renderTokens(token[4], context, partials, originalTemplate); } break; @@ -512,13 +483,14 @@ // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 if (!value || (isArray(value) && value.length === 0)) { - buffer += this.renderTokens(token[4], context, template); + buffer += this.renderTokens(token[4], context, partials, originalTemplate); } break; case '>': - value = this.getPartial(token[1]); - if (value != null) buffer += this.renderTokens(value, context, template); + if (!partials) continue; + value = this.parse(isFunction(partials) ? partials(token[1]) : partials[token[1]]); + if (value != null) buffer += this.renderTokens(value, context, partials, originalTemplate); break; case '&': value = context.lookup(token[1]); @@ -541,60 +513,33 @@ mustache.version = "0.7.3"; mustache.tags = [ "{{", "}}" ]; - // Export the parsing function. - mustache.parse = parseTemplate; - - // Export the escaping function so that the user may override it. - // See https://github.com/janl/mustache.js/issues/244 - mustache.escape = escapeHtml; - // All high-level mustache.* functions use this writer. var defaultWriter = new Writer(); /** - * Clears all cached templates and partials in the default writer. + * Clears all cached templates in the default writer. */ mustache.clearCache = function () { return defaultWriter.clearCache(); }; /** - * Caches the given partial in the default writer and returns the - * array of tokens it contains. - */ - mustache.cachePartial = function (name, template, tags) { - return defaultWriter.cachePartial(name, template, tags); - }; - - /** - * Caches the given template in the default writer and returns the + * Parses and caches the given template in the default writer and returns the * array of tokens it contains. + * + * Pre-caching templates ahead of time avoids the need to parse templates + * on the fly as they are rendered. */ - mustache.cache = function (template, tags) { - return defaultWriter.cache(template, tags); + mustache.parse = function (template, tags) { + return defaultWriter.parse(template, tags); }; /** - * Renders the `template` with the given `view` using the default writer. - * - * The optionals `partials` argument may either be a function that is used to load - * partials on the fly or an object containing names and templates of partials. If - * it is an object, the partials will be cached in the default writer. + * Renders the `template` with the given `view` and `partials` using the + * default writer. */ mustache.render = function (template, view, partials) { - if (partials) { - if (isFunction(partials)) { - defaultWriter._loadPartial = partials; - } else { - for (var name in partials) { - if (partials.hasOwnProperty(name)) { - defaultWriter.cachePartial(name, partials[name]); - } - } - } - } - - return defaultWriter.render(template, view); + return defaultWriter.render(template, view, partials); }; // This is here for backwards compatibility with 0.4.x. @@ -608,6 +553,10 @@ } }; + // Export the escaping function so that the user may override it. + // See https://github.com/janl/mustache.js/issues/244 + mustache.escape = escapeHtml; + // Export these mainly for testing, but also for advanced usage. mustache.Scanner = Scanner; mustache.Context = Context; diff --git a/test/render-test.js b/test/render-test.js index acec47ffd..a0116787e 100644 --- a/test/render-test.js +++ b/test/render-test.js @@ -17,7 +17,7 @@ function getView(testName) { function getPartial(testName) { try { return getContents(testName, 'partial'); - } catch (e) { + } catch (error) { // No big deal. Not all tests need to test partial support. } } diff --git a/test/writer-test.js b/test/writer-test.js deleted file mode 100644 index fc552dbff..000000000 --- a/test/writer-test.js +++ /dev/null @@ -1,36 +0,0 @@ -require('./helper'); -var Writer = Mustache.Writer; - -describe('A new Mustache.Writer', function () { - var writer; - - describe('with a cached partial', function () { - beforeEach(function () { - writer = new Writer; - }); - - it('caches partials by content, not name', function () { - writer.cachePartial('partial', 'partial one'); - assert.equal(writer.render('{{>partial}}'), 'partial one'); - - writer.cachePartial('partial', 'partial two'); - assert.equal(writer.render('{{>partial}}'), 'partial two'); - }); - }); - - describe('with a partial loader', function () { - var partial; - beforeEach(function () { - partial = 'The content of the partial.'; - writer = new Writer(function (name) { - assert.equal(name, 'partial'); - return partial; - }); - }); - - it('loads partials correctly', function () { - assert.equal(writer.render('{{>partial}}'), partial); - }); - }); - -}); From ff6a093981a36df0a1a066dfbf137941ce2129d6 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 7 Nov 2013 13:52:48 -0800 Subject: [PATCH 017/286] Small tweaks --- mustache.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/mustache.js b/mustache.js index 578431443..59efb77f2 100644 --- a/mustache.js +++ b/mustache.js @@ -150,7 +150,9 @@ start += 1; // Check for whitespace on the current line. - if (chr == '\n') stripSpace(); + if (chr === '\n') { + stripSpace(); + } } } @@ -177,9 +179,11 @@ } // Match the closing tag. - if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos); + if (!scanner.scan(tagRes[1])) { + throw new Error('Unclosed tag at ' + scanner.pos); + } - token = [type, value, start, scanner.pos]; + token = [ type, value, start, scanner.pos ]; tokens.push(token); if (type === '#' || type === '^') { @@ -334,10 +338,10 @@ * Represents a rendering context by wrapping a view object and * maintaining a reference to the parent context. */ - function Context(view, parent) { + function Context(view, parentContext) { this.view = view == null ? {} : view; - this.parent = parent; - this._cache = { '.': this.view }; + this.cache = { '.': this.view }; + this.parent = parentContext; } /** @@ -354,8 +358,8 @@ */ Context.prototype.lookup = function (name) { var value; - if (name in this._cache) { - value = this._cache[name]; + if (name in this.cache) { + value = this.cache[name]; } else { var context = this; @@ -376,7 +380,7 @@ context = context.parent; } - this._cache[name] = value; + this.cache[name] = value; } if (isFunction(value)) { @@ -392,14 +396,14 @@ * avoid the need to parse the same template twice. */ function Writer() { - this._cache = {}; + this.cache = {}; } /** * Clears all cached templates in this writer. */ Writer.prototype.clearCache = function () { - this._cache = {}; + this.cache = {}; }; /** @@ -407,11 +411,11 @@ * that is generated from the parse. */ Writer.prototype.parse = function (template, tags) { - if (!(template in this._cache)) { - this._cache[template] = parseTemplate(template, tags); + if (!(template in this.cache)) { + this.cache[template] = parseTemplate(template, tags); } - return this._cache[template]; + return this.cache[template]; }; /** @@ -525,10 +529,8 @@ /** * Parses and caches the given template in the default writer and returns the - * array of tokens it contains. - * - * Pre-caching templates ahead of time avoids the need to parse templates - * on the fly as they are rendered. + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. */ mustache.parse = function (template, tags) { return defaultWriter.parse(template, tags); From 99c1149affebf347ce13775129c015c010046f07 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 8 Nov 2013 08:42:19 -0800 Subject: [PATCH 018/286] Small tweaks --- mustache.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mustache.js b/mustache.js index 59efb77f2..a82b23601 100644 --- a/mustache.js +++ b/mustache.js @@ -231,8 +231,8 @@ lastToken[1] += token[1]; lastToken[3] = token[3]; } else { - lastToken = token; squashedTokens.push(token); + lastToken = token; } } } @@ -251,19 +251,19 @@ var collector = nestedTokens; var sections = []; - var token; + var token, section; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; switch (token[0]) { case '#': case '^': - sections.push(token); collector.push(token); + sections.push(token); collector = token[4] = []; break; case '/': - var section = sections.pop(); + section = sections.pop(); section[5] = token[2]; collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; break; From e4a68c9e49f5f177e9488afde189710a835f035e Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 2 Dec 2013 15:22:32 -0800 Subject: [PATCH 019/286] Version 0.8.0 --- CHANGES | 3 ++- mustache.js | 2 +- mustache.js.nuspec | 2 +- package.json | 2 +- wrappers/qooxdoo/mustache.js.pre | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 4c9fca185..34e727085 100644 --- a/CHANGES +++ b/CHANGES @@ -1,10 +1,11 @@ -= HEAD += 0.8.0 / 2 Dec 2013 * Remove compile* writer functions, use mustache.parse instead. Smaller API. * Throw an error when rendering a template that contains higher-order sections and the original template is not provided. * Remove low-level Context.make function. * Better code readability and inline documentation. + * Stop caching templates by name. = 0.7.3 / 5 Nov 2013 diff --git a/mustache.js b/mustache.js index a82b23601..ef892a1ad 100644 --- a/mustache.js +++ b/mustache.js @@ -514,7 +514,7 @@ }; mustache.name = "mustache.js"; - mustache.version = "0.7.3"; + mustache.version = "0.8.0"; mustache.tags = [ "{{", "}}" ]; // All high-level mustache.* functions use this writer. diff --git a/mustache.js.nuspec b/mustache.js.nuspec index a103cd4aa..2da2411bc 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 0.7.3 + 0.8.0 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/package.json b/package.json index 0d8019aa5..a9b02e1fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "0.7.3", + "version": "0.8.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "repository": { diff --git a/wrappers/qooxdoo/mustache.js.pre b/wrappers/qooxdoo/mustache.js.pre index d4719c97a..8c3ac410d 100644 --- a/wrappers/qooxdoo/mustache.js.pre +++ b/wrappers/qooxdoo/mustache.js.pre @@ -19,7 +19,7 @@ This class contains code based on the following work: - * Mustache.js version 0.7.3 + * Mustache.js version 0.8.0 Code: https://github.com/janl/mustache.js From 5448386b2032d22dd5a2cd0ec93460de7cd42f21 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 2 Dec 2013 15:26:22 -0800 Subject: [PATCH 020/286] Ignore test files in npm package Fixes #341 --- .npmignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..76e579ae4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +test + From 625a5e1a7e0aea559c4f8a27ad5fe80e2831de29 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 3 Dec 2013 09:31:11 -0800 Subject: [PATCH 021/286] Update README Fixes #346 --- README.md | 315 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 184 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 4ce62fca8..b435bb744 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,16 @@ An updated list of mustache.js users is kept [on the Github wiki](http://wiki.gi Below is quick example how to use mustache.js: - var view = { - title: "Joe", - calc: function () { - return 2 + 4; - } - }; +```js +var view = { + title: "Joe", + calc: function () { + return 2 + 4; + } +}; - var output = Mustache.render("{{title}} spends {{calc}}", view); +var output = Mustache.render("{{title}} spends {{calc}}", view); +``` In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.com/) template and 2) a `view` object that contains the data and code needed to render the template. @@ -49,48 +51,60 @@ All variables are HTML-escaped by default. If you want to render unescaped HTML, View: - { - "name": "Chris", - "company": "GitHub" - } +```json +{ + "name": "Chris", + "company": "GitHub" +} +``` Template: - * {{name}} - * {{age}} - * {{company}} - * {{{company}}} - * {{&company}} +```html +* {{name}} +* {{age}} +* {{company}} +* {{{company}}} +* {{&company}} +``` Output: - * Chris - * - * <b>GitHub</b> - * GitHub - * GitHub +```html +* Chris +* +* <b>GitHub</b> +* GitHub +* GitHub +``` JavaScript's dot notation may be used to access keys that are properties of objects in a view. View: - { - "name": { - "first": "Michael", - "last": "Jackson" - }, - "age": "RIP" - } +```json +{ + "name": { + "first": "Michael", + "last": "Jackson" + }, + "age": "RIP" +} +``` Template: - * {{name.first}} {{name.last}} - * {{age}} +```html +* {{name.first}} {{name.last}} +* {{age}} +``` Output: - * Michael Jackson - * RIP +```html +* Michael Jackson +* RIP +``` ### Sections @@ -106,20 +120,26 @@ If the `person` key does not exist, or exists and has a value of `null`, `undefi View: - { - "person": false - } +```json +{ + "person": false +} +``` Template: - Shown. - {{#person}} - Never shown! - {{/person}} +```html +Shown. +{{#person}} +Never shown! +{{/person}} +``` Output: - Shown. +```html +Shown. +``` #### Non-Empty Lists @@ -129,75 +149,93 @@ When the value is a list, the block is rendered once for each item in the list. View: - { - "stooges": [ - { "name": "Moe" }, - { "name": "Larry" }, - { "name": "Curly" } - ] - } +```json +{ + "stooges": [ + { "name": "Moe" }, + { "name": "Larry" }, + { "name": "Curly" } + ] +} +``` Template: - {{#stooges}} - {{name}} - {{/stooges}} +```html +{{#stooges}} +{{name}} +{{/stooges}} +``` Output: - Moe - Larry - Curly +```html +Moe +Larry +Curly +``` When looping over an array of strings, a `.` can be used to refer to the current item in the list. View: - { - "musketeers": ["Athos", "Aramis", "Porthos", "D'Artagnan"] - } +```json +{ + "musketeers": ["Athos", "Aramis", "Porthos", "D'Artagnan"] +} +``` Template: - {{#musketeers}} - * {{.}} - {{/musketeers}} +```html +{{#musketeers}} +* {{.}} +{{/musketeers}} +``` Output: - * Athos - * Aramis - * Porthos - * D'Artagnan +```html +* Athos +* Aramis +* Porthos +* D'Artagnan +``` If the value of a section variable is a function, it will be called in the context of the current item in the list on each iteration. View: - { - "beatles": [ - { "firstName": "John", "lastName": "Lennon" }, - { "firstName": "Paul", "lastName": "McCartney" }, - { "firstName": "George", "lastName": "Harrison" }, - { "firstName": "Ringo", "lastName": "Starr" } - ], - "name": function () { - return this.firstName + " " + this.lastName; - } - } +```json +{ + "beatles": [ + { "firstName": "John", "lastName": "Lennon" }, + { "firstName": "Paul", "lastName": "McCartney" }, + { "firstName": "George", "lastName": "Harrison" }, + { "firstName": "Ringo", "lastName": "Starr" } + ], + "name": function () { + return this.firstName + " " + this.lastName; + } +} +``` Template: - {{#beatles}} - * {{name}} - {{/beatles}} +```html +{{#beatles}} +* {{name}} +{{/beatles}} +``` Output: - * John Lennon - * Paul McCartney - * George Harrison - * Ringo Starr +```html +* John Lennon +* Paul McCartney +* George Harrison +* Ringo Starr +``` #### Functions @@ -205,22 +243,28 @@ If the value of a section key is a function, it is called with the section's lit View: - { - "name": "Tater", - "bold": function () { - return function (text, render) { - return "" + render(text) + ""; - } - } +```js +{ + "name": "Tater", + "bold": function () { + return function (text, render) { + return "" + render(text) + ""; } + } +} +``` Template: - {{#bold}}Hi {{name}}.{{/bold}} +```html +{{#bold}}Hi {{name}}.{{/bold}} +``` Output: - Hi Tater. +```html +Hi Tater. +``` ### Inverted Sections @@ -228,28 +272,38 @@ An inverted section opens with `{{^section}}` instead of `{{#section}}`. The blo View: - { - "repos": [] - } +```json +{ + "repos": [] +} +``` Template: - {{#repos}}{{name}}{{/repos}} - {{^repos}}No repos :({{/repos}} +```html +{{#repos}}{{name}}{{/repos}} +{{^repos}}No repos :({{/repos}} +``` Output: - No repos :( +```html +No repos :( +``` ### Comments Comments begin with a bang and are ignored. The following template: -

Today{{! ignore me }}.

+```html +

Today{{! ignore me }}.

+``` Will render as follows: -

Today.

+```html +

Today.

+``` Comments may contain newlines. @@ -261,11 +315,15 @@ Partials are rendered at runtime (as opposed to compile time), so recursive part They also inherit the calling context. Whereas in ERB you may have this: - <%= partial :next_more, :start => start, :size => size %> +```html+erb +<%= partial :next_more, :start => start, :size => size %> +``` Mustache requires only this: - {{> next_more}} +```html +{{> next_more}} +``` Why? Because the `next_more.mustache` file will inherit the `size` and `start` variables from the calling context. In this way you may want to think of partials as includes, or template expansion, even though it's not literally true. @@ -282,24 +340,34 @@ For example, this template and partial: Can be thought of as a single, expanded template: -

Names

- {{#names}} - {{name}} - {{/names}} +```html +

Names

+{{#names}} + {{name}} +{{/names}} +``` In mustache.js an object of partials may be passed as the third argument to `Mustache.render`. The object should be keyed by the name of the partial, and its value should be the partial text. +```js +Mustache.render(template, view, { + user: userTemplate +}); +``` + ### Set Delimiter Set Delimiter tags start with an equals sign and change the tag delimiters from `{{` and `}}` to custom strings. Consider the following contrived example: - * {{ default_tags }} - {{=<% %>=}} - * <% erb_style_tags %> - <%={{ }}=%> - * {{ default_tags_again }} +```html +* {{ default_tags }} +{{=<% %>=}} +* <% erb_style_tags %> +<%={{ }}=%> +* {{ default_tags_again }} +``` Here we have a list with three items. The first item uses the default tag style, the second uses ERB style as defined by the Set Delimiter tag, and the third returns to the default style after yet another Set Delimiter declaration. @@ -307,31 +375,16 @@ According to [ctemplates](http://google-ctemplate.googlecode.com/svn/trunk/doc/h Custom delimiters may not contain whitespace or the equals sign. -### Compiled Templates - -Mustache templates can be compiled into JavaScript functions using `Mustache.compile` for improved rendering performance. - -If you have template views that are rendered multiple times, compiling your template into a JavaScript function will minimise the amount of work required for each re-render. - -Pre-compiled templates can also be generated server-side, for delivery to the browser as ready to use JavaScript functions, further reducing the amount of client side processing required for initialising templates. - -**Mustache.compile** - -Use `Mustache.compile` to compile standard Mustache string templates into reusable Mustache template functions. - - var compiledTemplate = Mustache.compile(stringTemplate); - -The function returned from `Mustache.compile` can then be called directly, passing in the template data as an argument (with an object of partials as an optional second parameter), to generate the final output. - - var templateOutput = compiledTemplate(templateData); - -**Mustache.compilePartial** +## Pre-parsing and Caching Templates -Template partials can also be compiled using the `Mustache.compilePartial` function. The first parameter of this function, is the name of the partial as it appears within parent templates. +By default, when mustache.js first parses a template it keeps the full parsed token tree in a cache. The next time it sees that same template it skips the parsing step and renders the template much more quickly. If you'd like, you can do this ahead of time using `mustache.parse`. - Mustache.compilePartial('partial-name', stringTemplate); +```js +Mustache.parse(template); -Compiled partials are then available to both `Mustache.render` and `Mustache.compile`. +// Then, sometime later. +Mustache.render(template, view); +``` ## Plugins for JavaScript Libraries From 32fc1976c0af44abaee2a3c7c5fde8321fda5aa2 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 3 Dec 2013 09:34:07 -0800 Subject: [PATCH 022/286] Small README tweaks --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b435bb744..729f15216 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ If the value of a section variable is a function, it will be called in the conte View: -```json +```js { "beatles": [ { "firstName": "John", "lastName": "Lennon" }, @@ -361,7 +361,7 @@ Set Delimiter tags start with an equals sign and change the tag delimiters from Consider the following contrived example: -```html +``` * {{ default_tags }} {{=<% %>=}} * <% erb_style_tags %> From 16ffa430a111dc293cd9ed899ecf9da3729f58bd Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 6 Dec 2013 13:19:54 -0800 Subject: [PATCH 023/286] Style tweaks --- mustache.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index ef892a1ad..a03e2e084 100644 --- a/mustache.js +++ b/mustache.js @@ -411,11 +411,14 @@ * that is generated from the parse. */ Writer.prototype.parse = function (template, tags) { - if (!(template in this.cache)) { - this.cache[template] = parseTemplate(template, tags); + var cache = this.cache; + var tokens = cache[template]; + + if (tokens == null) { + tokens = cache[template] = parseTemplate(template, tags); } - return this.cache[template]; + return tokens; }; /** From 42ec324c8fafb029f789eeae7f155c821b475ae5 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Thu, 2 Jan 2014 08:02:37 +0530 Subject: [PATCH 024/286] When rendering partials, pass the partial template instead of the original template. --- mustache.js | 5 +++-- test/_files/section_functions_in_partials.js | 7 +++++++ test/_files/section_functions_in_partials.mustache | 3 +++ test/_files/section_functions_in_partials.partial | 1 + test/_files/section_functions_in_partials.txt | 3 +++ 5 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 test/_files/section_functions_in_partials.js create mode 100644 test/_files/section_functions_in_partials.mustache create mode 100644 test/_files/section_functions_in_partials.partial create mode 100644 test/_files/section_functions_in_partials.txt diff --git a/mustache.js b/mustache.js index a03e2e084..e1099b057 100644 --- a/mustache.js +++ b/mustache.js @@ -496,8 +496,9 @@ break; case '>': if (!partials) continue; - value = this.parse(isFunction(partials) ? partials(token[1]) : partials[token[1]]); - if (value != null) buffer += this.renderTokens(value, context, partials, originalTemplate); + var partialTemplate = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + value = this.parse(partialTemplate); + if (value != null) buffer += this.renderTokens(value, context, partials, partialTemplate); break; case '&': value = context.lookup(token[1]); diff --git a/test/_files/section_functions_in_partials.js b/test/_files/section_functions_in_partials.js new file mode 100644 index 000000000..4672778bd --- /dev/null +++ b/test/_files/section_functions_in_partials.js @@ -0,0 +1,7 @@ +({ + bold: function(){ + return function(text, render) { + return "" + render(text) + ""; + } + } +}) diff --git a/test/_files/section_functions_in_partials.mustache b/test/_files/section_functions_in_partials.mustache new file mode 100644 index 000000000..816493254 --- /dev/null +++ b/test/_files/section_functions_in_partials.mustache @@ -0,0 +1,3 @@ +{{> partial}} + +

some more text

diff --git a/test/_files/section_functions_in_partials.partial b/test/_files/section_functions_in_partials.partial new file mode 100644 index 000000000..3e90b0040 --- /dev/null +++ b/test/_files/section_functions_in_partials.partial @@ -0,0 +1 @@ +{{#bold}}Hello There{{/bold}} diff --git a/test/_files/section_functions_in_partials.txt b/test/_files/section_functions_in_partials.txt new file mode 100644 index 000000000..2f5955c84 --- /dev/null +++ b/test/_files/section_functions_in_partials.txt @@ -0,0 +1,3 @@ +Hello There + +

some more text

From d6fa9285c16af6d63e2a4fe6b258dc41337a8b06 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 3 Jan 2014 06:56:27 -0800 Subject: [PATCH 025/286] Check for existence of partial template before parsing --- mustache.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index e1099b057..de8ec8f66 100644 --- a/mustache.js +++ b/mustache.js @@ -496,9 +496,8 @@ break; case '>': if (!partials) continue; - var partialTemplate = isFunction(partials) ? partials(token[1]) : partials[token[1]]; - value = this.parse(partialTemplate); - if (value != null) buffer += this.renderTokens(value, context, partials, partialTemplate); + value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) buffer += this.renderTokens(this.parse(value), context, partials, value); break; case '&': value = context.lookup(token[1]); From 58f77330ef9f092c3e2fc82a33cdff927031a1bd Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 3 Jan 2014 07:00:13 -0800 Subject: [PATCH 026/286] Version 0.8.1 --- CHANGES | 4 ++++ mustache.js | 2 +- mustache.js.nuspec | 2 +- package.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 34e727085..c3546e0e2 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ += 0.8.1 / 3 Jan 2014 + + * Fix usage of partial templates. + = 0.8.0 / 2 Dec 2013 * Remove compile* writer functions, use mustache.parse instead. Smaller API. diff --git a/mustache.js b/mustache.js index de8ec8f66..3a84b74d4 100644 --- a/mustache.js +++ b/mustache.js @@ -517,7 +517,7 @@ }; mustache.name = "mustache.js"; - mustache.version = "0.8.0"; + mustache.version = "0.8.1"; mustache.tags = [ "{{", "}}" ]; // All high-level mustache.* functions use this writer. diff --git a/mustache.js.nuspec b/mustache.js.nuspec index 2da2411bc..ae499a6df 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 0.8.0 + 0.8.1 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/package.json b/package.json index a9b02e1fc..105bbe733 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "0.8.0", + "version": "0.8.1", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "repository": { From 96c43e4c21df692f7d17a9cc4dedd171e583cd9b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 7 Jan 2014 22:15:05 -0800 Subject: [PATCH 027/286] Documentation tweaks --- mustache.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mustache.js b/mustache.js index 3a84b74d4..a3aac8f5e 100644 --- a/mustache.js +++ b/mustache.js @@ -19,13 +19,6 @@ } }(this, function (mustache) { - var whiteRe = /\s*/; - var spaceRe = /\s+/; - var nonSpaceRe = /\S/; - var eqRe = /\s*=/; - var curlyRe = /\s*\}/; - var tagRe = /#|\^|\/|>|\{|&|=|!/; - // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 // See https://github.com/janl/mustache.js/issues/189 var RegExp_test = RegExp.prototype.test; @@ -33,6 +26,7 @@ return RegExp_test.call(re, string); } + var nonSpaceRe = /\S/; function isWhitespace(string) { return !testRegExp(nonSpaceRe, string); } @@ -76,6 +70,12 @@ ]; } + var whiteRe = /\s*/; + var spaceRe = /\s+/; + var equalsRe = /\s*=/; + var curlyRe = /\s*\}/; + var tagRe = /#|\^|\/|>|\{|&|=|!/; + /** * 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 @@ -85,18 +85,18 @@ * A token is an array with at least 4 elements. The first element is the * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag * did not contain a symbol (i.e. {{myValue}}) this element is "name". For - * all template text that appears outside a symbol this element is "text". + * all text that appears outside a symbol this element is "text". * * The second element of a token is its "value". For mustache tags this is * whatever else was inside the tag besides the opening symbol. For text tokens * this is the text itself. * - * The third and fourth elements of the token are the start and end indices - * in the original template of the token, respectively. + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. * - * Tokens that are the root node of a subtree contain two more elements: an - * array of tokens in the subtree and the index in the original template at which - * the closing tag for that section begins. + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. */ function parseTemplate(template, tags) { tags = tags || mustache.tags; @@ -166,8 +166,8 @@ // Get the tag value. if (type === '=') { - value = scanner.scanUntil(eqRe); - scanner.scan(eqRe); + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); scanner.scanUntil(tagRes[1]); } else if (type === '{') { value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1]))); From 500fd5d825fb7a353b82e593ecd2719cf06e691e Mon Sep 17 00:00:00 2001 From: wittemann Date: Thu, 6 Feb 2014 13:00:44 +0100 Subject: [PATCH 028/286] Updated qooxdoo templates. --- wrappers/qooxdoo/mustache.js.pre | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wrappers/qooxdoo/mustache.js.pre b/wrappers/qooxdoo/mustache.js.pre index 8c3ac410d..1512a004b 100644 --- a/wrappers/qooxdoo/mustache.js.pre +++ b/wrappers/qooxdoo/mustache.js.pre @@ -154,6 +154,13 @@ qx.Bootstrap.define("qx.bom.Template", { }); (function() { +// prevent using CommonJS exports object, +// by shadowing global exports object +var exports; + +// prevent using AMD compatible loader, +// by shadowing global define function +var define; /** * Below is the original mustache.js code. Snapshot date is mentioned in @@ -161,4 +168,5 @@ qx.Bootstrap.define("qx.bom.Template", { * @ignore(exports) * @ignore(define.*) * @ignore(module.*) + * @lint ignoreNoLoopBlock() */ From 635667418b8cebf28851b2d31b1a109f30b258c6 Mon Sep 17 00:00:00 2001 From: Rob Graeber Date: Thu, 13 Mar 2014 21:28:34 -0700 Subject: [PATCH 029/286] Update copyright year Updated the copyright year to protect the changes you've made since 2010. :-) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6626848b3..aa1b83160 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License Copyright (c) 2009 Chris Wanstrath (Ruby) -Copyright (c) 2010 Jan Lehnardt (JavaScript) +Copyright (c) 2010-2014 Jan Lehnardt (JavaScript) 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: From c68b434eb438394d1b069e38ba9d582c8d1fd28f Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 17 Mar 2014 17:15:15 -0700 Subject: [PATCH 030/286] Adding a bower.json file The goal is to reduce the number of files downloaded by ignoring the test directory --- CHANGES | 4 ++++ bower.json | 21 +++++++++++++++++++++ mustache.js.nuspec | 2 +- package.json | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 bower.json diff --git a/CHANGES b/CHANGES index c3546e0e2..dfdec93dc 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ += 0.8.2 / 17 Mar 2014 + + * Supporting Bower through a bower.json file. + = 0.8.1 / 3 Jan 2014 * Fix usage of partial templates. diff --git a/bower.json b/bower.json new file mode 100644 index 000000000..c3af7d8c5 --- /dev/null +++ b/bower.json @@ -0,0 +1,21 @@ +{ + "name": "Mustache.js", + "main": "mustache.js", + "version": "0.8.2", + "homepage": "https://github.com/janl/mustache.js", + "authors": [ + "mustache.js Authors " + ], + "description": "Logic-less {{mustache}} templates with JavaScript", + "keywords": ["mustache", "template", "templates", "ejs"], + "moduleType": [ + "amd", + "globals", + "node" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "test" + ] +} diff --git a/mustache.js.nuspec b/mustache.js.nuspec index ae499a6df..7a47994a1 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 0.8.1 + 0.8.2 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/package.json b/package.json index 105bbe733..850e2ba17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "0.8.1", + "version": "0.8.2", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "repository": { From 3946a87ec70e37c5e53ae84e1aa788b0d8a90e47 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Thu, 20 Mar 2014 10:01:32 -0700 Subject: [PATCH 031/286] Lowercasing the package name Oops --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index c3af7d8c5..e8c9b1199 100644 --- a/bower.json +++ b/bower.json @@ -1,5 +1,5 @@ { - "name": "Mustache.js", + "name": "mustache.js", "main": "mustache.js", "version": "0.8.2", "homepage": "https://github.com/janl/mustache.js", From e70359981115777ecc7204255270ce87728bb1cb Mon Sep 17 00:00:00 2001 From: Pascal Pfiffner Date: Mon, 31 Mar 2014 16:18:05 -0400 Subject: [PATCH 032/286] Describe two techniques on how to load templates in the README --- README.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 729f15216..fae82d970 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,46 @@ In this example, the `Mustache.render` function takes two parameters: 1) the [mu ## Templates -A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. +A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. There are several types of tags available in mustache.js, described below. -There are several types of tags available in mustache.js. +There are several techniques that can be used to load templates and hand them to mustache.js, here are two of them: + +#### Include Templates + +If you need a template for a dynamic part in a static website, you can consider including the template in the static HTML file to avoid loading templates separately. Here's a small example using `jQuery`: + +```html + + +
Loading...
+ + + +``` + +```js +function loadUser() { + var template = $('#template').val(); + Mustache.parse(template); // optional, speeds up future uses + var rendered = Mustache.render(template, {name: "Luke"}); + $('#target').html(rendered); +} +``` + +#### Load External Templates + +If your templates reside in individual files, you can load them asynchronously and render them when they arrive. Another example using `jQuery`: + +```js +function loadUser() { + $.get('template.mst', function(template) { + var html = Mustache.render(template, {name: "Luke"}); + $('#target').html(rendered); + }); +} +``` ### Variables From 4184761b20ceb0ca343c11420a9feb43d92e1c18 Mon Sep 17 00:00:00 2001 From: Pascal Pfiffner Date: Mon, 31 Mar 2014 16:32:39 -0400 Subject: [PATCH 033/286] Change example to use `script` instead of `textarea` --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fae82d970..8d0ac8c2b 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,16 @@ If you need a template for a dynamic part in a static website, you can consider
Loading...
- + ``` ```js function loadUser() { - var template = $('#template').val(); + var template = $('#template').html(); Mustache.parse(template); // optional, speeds up future uses var rendered = Mustache.render(template, {name: "Luke"}); $('#target').html(rendered); From a7837898c7f4a3e6fc7d2129d7b25e0581410647 Mon Sep 17 00:00:00 2001 From: Kirill Maltsev Date: Sat, 5 Apr 2014 17:57:26 +0200 Subject: [PATCH 034/286] Correct mistake in the readme I've corrected the wrong variable name. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d0ac8c2b..ab01e0986 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If your templates reside in individual files, you can load them asynchronously a ```js function loadUser() { $.get('template.mst', function(template) { - var html = Mustache.render(template, {name: "Luke"}); + var rendered = Mustache.render(template, {name: "Luke"}); $('#target').html(rendered); }); } From ea3d4433cb26824ae86f28cdde5db78762063eaa Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 11 Apr 2014 05:42:22 -0700 Subject: [PATCH 035/286] Formatting tweaks --- mustache.js | 104 ++++++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/mustache.js b/mustache.js index a3aac8f5e..d14837ca5 100644 --- a/mustache.js +++ b/mustache.js @@ -10,7 +10,9 @@ factory(exports); // CommonJS } else { var mustache = {}; + factory(mustache); + if (typeof define === "function" && define.amd) { define(mustache); // AMD } else { @@ -60,9 +62,8 @@ } function escapeTags(tags) { - if (!isArray(tags) || tags.length !== 2) { + if (!isArray(tags) || tags.length !== 2) throw new Error('Invalid tags: ' + tags); - } return [ new RegExp(escapeRegExp(tags[0]) + "\\s*"), @@ -102,9 +103,8 @@ tags = tags || mustache.tags; template = template || ''; - if (typeof tags === 'string') { + if (typeof tags === 'string') tags = tags.split(spaceRe); - } var tagRes = escapeTags(tags); var scanner = new Scanner(template); @@ -119,9 +119,8 @@ // if there was a {{#tag}} on it and otherwise only space. function stripSpace() { if (hasTag && !nonSpace) { - while (spaces.length) { + while (spaces.length) delete tokens[spaces.pop()]; - } } else { spaces = []; } @@ -137,7 +136,7 @@ // Match any text between tags. value = scanner.scanUntil(tagRes[0]); if (value) { - for (var i = 0, len = value.length; i < len; ++i) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { chr = value.charAt(i); if (isWhitespace(chr)) { @@ -150,9 +149,8 @@ start += 1; // Check for whitespace on the current line. - if (chr === '\n') { + if (chr === '\n') stripSpace(); - } } } @@ -179,9 +177,8 @@ } // Match the closing tag. - if (!scanner.scan(tagRes[1])) { + if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos); - } token = [ type, value, start, scanner.pos ]; tokens.push(token); @@ -192,12 +189,11 @@ // Check section nesting. openSection = sections.pop(); - if (!openSection) { + if (!openSection) throw new Error('Unopened section "' + value + '" at ' + start); - } - if (openSection[1] !== value) { + + if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); - } } else if (type === 'name' || type === '{' || type === '&') { nonSpace = true; } else if (type === '=') { @@ -208,9 +204,9 @@ // Make sure there are no open sections when we're done. openSection = sections.pop(); - if (openSection) { + + if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - } return nestTokens(squashTokens(tokens)); } @@ -223,7 +219,7 @@ var squashedTokens = []; var token, lastToken; - for (var i = 0, len = tokens.length; i < len; ++i) { + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; if (token) { @@ -252,7 +248,7 @@ var sections = []; var token, section; - for (var i = 0, len = tokens.length; i < len; ++i) { + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; switch (token[0]) { @@ -299,14 +295,15 @@ Scanner.prototype.scan = function (re) { var match = this.tail.match(re); - if (match && match.index === 0) { - var string = match[0]; - this.tail = this.tail.substring(string.length); - this.pos += string.length; - return string; - } + if (!match || match.index !== 0) + return ''; + + var string = match[0]; + + this.tail = this.tail.substring(string.length); + this.pos += string.length; - return ""; + return string; }; /** @@ -361,21 +358,22 @@ if (name in this.cache) { value = this.cache[name]; } else { - var context = this; + var context = this, names, index; while (context) { if (name.indexOf('.') > 0) { value = context.view; + names = name.split('.'); + index = 0; - var names = name.split('.'), i = 0; - while (value != null && i < names.length) { - value = value[names[i++]]; - } + while (value != null && index < names.length) + value = value[names[index++]]; } else { value = context.view[name]; } - if (value != null) break; + if (value != null) + break; context = context.parent; } @@ -383,9 +381,8 @@ this.cache[name] = value; } - if (isFunction(value)) { + if (isFunction(value)) value = value.call(this.view); - } return value; }; @@ -414,9 +411,8 @@ var cache = this.cache; var tokens = cache[template]; - if (tokens == null) { + if (tokens == null) tokens = cache[template] = parseTemplate(template, tags); - } return tokens; }; @@ -456,29 +452,31 @@ } var token, value; - for (var i = 0, len = tokens.length; i < len; ++i) { + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; switch (token[0]) { case '#': value = context.lookup(token[1]); - if (!value) continue; + + if (!value) + continue; if (isArray(value)) { - for (var j = 0, jlen = value.length; j < jlen; ++j) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } } else if (typeof value === 'object' || typeof value === 'string') { buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); } else if (isFunction(value)) { - if (typeof originalTemplate !== 'string') { + if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); - } // Extract the portion of the original template that the section contains. value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); - if (value != null) buffer += value; + if (value != null) + buffer += value; } else { buffer += this.renderTokens(token[4], context, partials, originalTemplate); } @@ -489,23 +487,33 @@ // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 - if (!value || (isArray(value) && value.length === 0)) { + if (!value || (isArray(value) && value.length === 0)) buffer += this.renderTokens(token[4], context, partials, originalTemplate); - } break; case '>': - if (!partials) continue; + if (!partials) + continue; + value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; - if (value != null) buffer += this.renderTokens(this.parse(value), context, partials, value); + + if (value != null) + buffer += this.renderTokens(this.parse(value), context, partials, value); + break; case '&': value = context.lookup(token[1]); - if (value != null) buffer += value; + + if (value != null) + buffer += value; + break; case 'name': value = context.lookup(token[1]); - if (value != null) buffer += mustache.escape(value); + + if (value != null) + buffer += mustache.escape(value); + break; case 'text': buffer += token[1]; From 0295646b677b8c9e15527e8b63583ed6728b77d0 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 11 Apr 2014 05:53:49 -0700 Subject: [PATCH 036/286] More style tweaks --- mustache.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/mustache.js b/mustache.js index d14837ca5..6371f1a91 100644 --- a/mustache.js +++ b/mustache.js @@ -12,7 +12,7 @@ var mustache = {}; factory(mustache); - + if (typeof define === "function" && define.amd) { define(mustache); // AMD } else { @@ -21,18 +21,6 @@ } }(this, function (mustache) { - // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 - // See https://github.com/janl/mustache.js/issues/189 - var RegExp_test = RegExp.prototype.test; - function testRegExp(re, string) { - return RegExp_test.call(re, string); - } - - var nonSpaceRe = /\S/; - function isWhitespace(string) { - return !testRegExp(nonSpaceRe, string); - } - var Object_toString = Object.prototype.toString; var isArray = Array.isArray || function (object) { return Object_toString.call(object) === '[object Array]'; @@ -46,6 +34,18 @@ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); } + // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 + // See https://github.com/janl/mustache.js/issues/189 + var RegExp_test = RegExp.prototype.test; + function testRegExp(re, string) { + return RegExp_test.call(re, string); + } + + var nonSpaceRe = /\S/; + function isWhitespace(string) { + return !testRegExp(nonSpaceRe, string); + } + var entityMap = { "&": "&", "<": "<", @@ -100,8 +100,10 @@ * which the closing tag for that section begins. */ function parseTemplate(template, tags) { + if (!template) + return []; + tags = tags || mustache.tags; - template = template || ''; if (typeof tags === 'string') tags = tags.split(spaceRe); @@ -155,7 +157,9 @@ } // Match the opening tag. - if (!scanner.scan(tagRes[0])) break; + if (!scanner.scan(tagRes[0])) + break; + hasTag = true; // Get the tag type. From d56f23e769d239a76c5b31a7c2f49fa6d1027730 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 17 Apr 2014 07:33:45 -0700 Subject: [PATCH 037/286] Save local reference to cache --- mustache.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index 6371f1a91..46faf3e67 100644 --- a/mustache.js +++ b/mustache.js @@ -358,9 +358,11 @@ * up the context hierarchy if the value is absent in this context's view. */ Context.prototype.lookup = function (name) { + var cache = this.cache; + var value; - if (name in this.cache) { - value = this.cache[name]; + if (name in cache) { + value = cache[name]; } else { var context = this, names, index; @@ -382,7 +384,7 @@ context = context.parent; } - this.cache[name] = value; + cache[name] = value; } if (isFunction(value)) From 73017077034d61cb60f037cae354a9f050f30b03 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 17 Apr 2014 13:58:26 -0700 Subject: [PATCH 038/286] Inline tag compilation --- mustache.js | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/mustache.js b/mustache.js index 46faf3e67..f1bafa02e 100644 --- a/mustache.js +++ b/mustache.js @@ -61,16 +61,6 @@ }); } - function escapeTags(tags) { - if (!isArray(tags) || tags.length !== 2) - throw new Error('Invalid tags: ' + tags); - - return [ - new RegExp(escapeRegExp(tags[0]) + "\\s*"), - new RegExp("\\s*" + escapeRegExp(tags[1])) - ]; - } - var whiteRe = /\s*/; var spaceRe = /\s+/; var equalsRe = /\s*=/; @@ -103,14 +93,6 @@ if (!template) return []; - tags = tags || mustache.tags; - - if (typeof tags === 'string') - tags = tags.split(spaceRe); - - var tagRes = escapeTags(tags); - var scanner = new Scanner(template); - var sections = []; // Stack to hold section tokens var tokens = []; // Buffer to hold the tokens var spaces = []; // Indices of whitespace tokens on the current line @@ -131,12 +113,30 @@ nonSpace = false; } + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags(tags) { + if (typeof tags === 'string') + tags = tags.split(spaceRe, 2); + + if (!isArray(tags) || tags.length !== 2) + throw new Error('Invalid tags: ' + tags); + + openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1])); + } + + compileTags(tags || mustache.tags); + + var scanner = new Scanner(template); + var start, type, value, chr, token, openSection; while (!scanner.eos()) { start = scanner.pos; // Match any text between tags. - value = scanner.scanUntil(tagRes[0]); + value = scanner.scanUntil(openingTagRe); + if (value) { for (var i = 0, valueLength = value.length; i < valueLength; ++i) { chr = value.charAt(i); @@ -147,7 +147,7 @@ nonSpace = true; } - tokens.push(['text', chr, start, start + 1]); + tokens.push([ 'text', chr, start, start + 1 ]); start += 1; // Check for whitespace on the current line. @@ -157,7 +157,7 @@ } // Match the opening tag. - if (!scanner.scan(tagRes[0])) + if (!scanner.scan(openingTagRe)) break; hasTag = true; @@ -170,18 +170,18 @@ if (type === '=') { value = scanner.scanUntil(equalsRe); scanner.scan(equalsRe); - scanner.scanUntil(tagRes[1]); + scanner.scanUntil(closingTagRe); } else if (type === '{') { - value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1]))); + value = scanner.scanUntil(closingCurlyRe); scanner.scan(curlyRe); - scanner.scanUntil(tagRes[1]); + scanner.scanUntil(closingTagRe); type = '&'; } else { - value = scanner.scanUntil(tagRes[1]); + value = scanner.scanUntil(closingTagRe); } // Match the closing tag. - if (!scanner.scan(tagRes[1])) + if (!scanner.scan(closingTagRe)) throw new Error('Unclosed tag at ' + scanner.pos); token = [ type, value, start, scanner.pos ]; @@ -202,7 +202,7 @@ nonSpace = true; } else if (type === '=') { // Set the tags for the next time around. - tagRes = escapeTags(tags = value.split(spaceRe)); + compileTags(value); } } From 799f55d157c84faeb9f12816a316d4bd9b58f155 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 22 Apr 2014 09:40:57 -0700 Subject: [PATCH 039/286] Simplify UMD wrapper --- mustache.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/mustache.js b/mustache.js index f1bafa02e..700f536bf 100644 --- a/mustache.js +++ b/mustache.js @@ -5,19 +5,13 @@ /*global define: false*/ -(function (root, factory) { +(function (global, factory) { if (typeof exports === "object" && exports) { factory(exports); // CommonJS + } else if (typeof define === "function" && define.amd) { + define(factory({})); // AMD } else { - var mustache = {}; - - factory(mustache); - - if (typeof define === "function" && define.amd) { - define(mustache); // AMD - } else { - root.Mustache = mustache; // + +Text content to be overwritten + + diff --git a/test/module-systems/browser-test.js b/test/module-systems/browser-test.js new file mode 100644 index 000000000..05d4a71d1 --- /dev/null +++ b/test/module-systems/browser-test.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const path = require('path'); +const puppeteer = require('puppeteer'); +const chai = require('chai'); + +const mustache = fs.readFileSync(path.join(__dirname, '../../mustache.js'), 'utf8'); + +describe('Browser usage', () => { + + let browser; + let page; + + before(async function () { + this.timeout(10 * 1000); + + // Awkward .launch() options below are needed to avoid hitting timeouts + // when tests are run in GitHub Actions for some weird reason + // https://github.com/GoogleChrome/puppeteer/issues/4617 + browser = await puppeteer.launch({ignoreDefaultArgs: ['--disable-extensions']}); + + page = await browser.newPage(); + page.on('console', msg => console.log('PAGE LOG:', msg.text())); + page.on('pageerror', err => console.error('PAGE ERROR:', err)); + }); + + after(() => browser.close()); + + it('is exposed on the global scope as window.Mustache', async () => { + await page.goto(`file://${path.join(__dirname, '_fixtures/global-scope.html')}`); + + const bodyElement = await page.$('body'); + const textContentProperty = await bodyElement.getProperty('textContent'); + const value = await textContentProperty.jsonValue(); + + chai.assert.equal(value.trim(), 'Joe spends 6'); + }); +}); From 9452eafd642c7bf827fa0627bd493e1fdf48da94 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Wed, 30 Oct 2019 10:27:39 +0100 Subject: [PATCH 220/286] Add CI test verifying Mustache works in browser as AMD w/RequireJS This is a precursor to introducing a build step that will change what we expose from this package. Better off writing some tests to verify existing projects with different module systems continue to work as expected. --- test/module-systems/_fixtures/amd.html | 26 ++++++++++++++++++++++++++ test/module-systems/browser-test.js | 12 ++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/module-systems/_fixtures/amd.html diff --git a/test/module-systems/_fixtures/amd.html b/test/module-systems/_fixtures/amd.html new file mode 100644 index 000000000..5a5b268e9 --- /dev/null +++ b/test/module-systems/_fixtures/amd.html @@ -0,0 +1,26 @@ + + + + + + +Text content to be overwritten + diff --git a/test/module-systems/browser-test.js b/test/module-systems/browser-test.js index 05d4a71d1..c58c231f0 100644 --- a/test/module-systems/browser-test.js +++ b/test/module-systems/browser-test.js @@ -34,4 +34,16 @@ describe('Browser usage', () => { chai.assert.equal(value.trim(), 'Joe spends 6'); }); + + it('is exposed as AMD and consumable via RequireJS', async function () { + this.timeout(10 * 1000); + + await page.goto(`file://${path.join(__dirname, '_fixtures/amd.html')}`, { waitUntil: 'networkidle0' }); + + const bodyElement = await page.$('body'); + const textContentProperty = await bodyElement.getProperty('textContent'); + const value = await textContentProperty.jsonValue(); + + chai.assert.equal(value, 'Joe spends 6'); + }); }); From abc3984fadbb6d2a681c535b0e3d1401fd4ea941 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 7 Oct 2019 20:48:21 +0200 Subject: [PATCH 221/286] Renamed mustache.js -> mustache.mjs to transition to being an ES module In an effort of moving with recent changes in Node.js and its built-in support for ES modules, we're making the main source code reside inside `mustache.mjs`. This will also allow Deno users to use this package as an ES module directly by importing `mustache.mjs`. An ES3 / UMD version of the source code is still planned to exist in `mustache.js`, but will involve a build step to transform the ES module source into a plain .js version. By renaming the file *before* changing any contents, git will recognise the file has been renamed and therefore allows us to see all historical changes to the old .js source code with the `--follow` argument: ``` $ git log --follow -- mustache.mjs ``` Without ensuring git sees this is a file rename, we will in practise loose the old source code's history, or at least make it quite weird and less intuitive to find. That's not fair to the historical contributors and in general getting hold of a well documented log of changes to a file in git, is extremely valuable in those few scenarios where it's *really* needed. --- mustache.js => mustache.mjs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mustache.js => mustache.mjs (100%) diff --git a/mustache.js b/mustache.mjs similarity index 100% rename from mustache.js rename to mustache.mjs From b523b16517ab340a982863193313fda33a330e13 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 7 Oct 2019 15:00:58 +0200 Subject: [PATCH 222/286] Turn source code into a plain ES module without UMD wrapper As part of transition the source code to be a proper plain ES module, we're removing the handcrafted UMD wrapper around the source code. This means the UMD wrapper will have to come from elsewhere; a build step introduced later. --- mustache.mjs | 1270 +++++++++++++++++++++++++------------------------- 1 file changed, 629 insertions(+), 641 deletions(-) diff --git a/mustache.mjs b/mustache.mjs index 31825403f..2081c98e8 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -3,719 +3,707 @@ * http://github.com/janl/mustache.js */ -/*global define: false Mustache: true*/ +var objectToString = Object.prototype.toString; +var isArray = Array.isArray || function isArrayPolyfill (object) { + return objectToString.call(object) === '[object Array]'; +}; + +function isFunction (object) { + return typeof object === 'function'; +} + +/** + * More correct typeof string handling array + * which normally returns typeof 'object' + */ +function typeStr (obj) { + return isArray(obj) ? 'array' : typeof obj; +} -(function defineMustache (global, factory) { - if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { - factory(exports); // CommonJS - } else if (typeof define === 'function' && define.amd) { - define(['exports'], factory); // AMD - } else { - global.Mustache = {}; - factory(global.Mustache); // script, wsh, asp - } -}(this, function mustacheFactory (mustache) { +function escapeRegExp (string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); +} - var objectToString = Object.prototype.toString; - var isArray = Array.isArray || function isArrayPolyfill (object) { - return objectToString.call(object) === '[object Array]'; - }; +/** + * Null safe way of checking whether or not an object, + * including its prototype, has a given property + */ +function hasProperty (obj, propName) { + return obj != null && typeof obj === 'object' && (propName in obj); +} - function isFunction (object) { - return typeof object === 'function'; - } +/** + * Safe way of detecting whether or not the given thing is a primitive and + * whether it has the given property + */ +function primitiveHasOwnProperty (primitive, propName) { + return ( + primitive != null + && typeof primitive !== 'object' + && primitive.hasOwnProperty + && primitive.hasOwnProperty(propName) + ); +} + +// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 +// See https://github.com/janl/mustache.js/issues/189 +var regExpTest = RegExp.prototype.test; +function testRegExp (re, string) { + return regExpTest.call(re, string); +} + +var nonSpaceRe = /\S/; +function isWhitespace (string) { + return !testRegExp(nonSpaceRe, string); +} + +var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' +}; + +function escapeHtml (string) { + return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { + return entityMap[s]; + }); +} + +var whiteRe = /\s*/; +var spaceRe = /\s+/; +var equalsRe = /\s*=/; +var curlyRe = /\s*\}/; +var tagRe = /#|\^|\/|>|\{|&|=|!/; + +/** + * 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 + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. + * + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. + * + * Tokens for partials also contain two more elements: 1) a string value of + * indendation prior to that tag and 2) the index of that tag on that line - + * eg a value of 2 indicates the partial is the third tag on this line. + */ +function parseTemplate (template, tags) { + if (!template) + return []; + var lineHasNonSpace = false; + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + var indentation = ''; // Tracks indentation for tags that use it + var tagIndex = 0; // Stores a count of number of tags encountered on a line + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace () { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } - /** - * More correct typeof string handling array - * which normally returns typeof 'object' - */ - function typeStr (obj) { - return isArray(obj) ? 'array' : typeof obj; + hasTag = false; + nonSpace = false; } - function escapeRegExp (string) { - return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); - } + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags (tagsToCompile) { + if (typeof tagsToCompile === 'string') + tagsToCompile = tagsToCompile.split(spaceRe, 2); - /** - * Null safe way of checking whether or not an object, - * including its prototype, has a given property - */ - function hasProperty (obj, propName) { - return obj != null && typeof obj === 'object' && (propName in obj); - } + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error('Invalid tags: ' + tagsToCompile); - /** - * Safe way of detecting whether or not the given thing is a primitive and - * whether it has the given property - */ - function primitiveHasOwnProperty (primitive, propName) { - return ( - primitive != null - && typeof primitive !== 'object' - && primitive.hasOwnProperty - && primitive.hasOwnProperty(propName) - ); + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); } - // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 - // See https://github.com/janl/mustache.js/issues/189 - var regExpTest = RegExp.prototype.test; - function testRegExp (re, string) { - return regExpTest.call(re, string); - } + compileTags(tags || mustache.tags); - var nonSpaceRe = /\S/; - function isWhitespace (string) { - return !testRegExp(nonSpaceRe, string); - } + var scanner = new Scanner(template); - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; - - function escapeHtml (string) { - return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { - return entityMap[s]; - }); - } + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; - var whiteRe = /\s*/; - var spaceRe = /\s+/; - var equalsRe = /\s*=/; - var curlyRe = /\s*\}/; - var tagRe = /#|\^|\/|>|\{|&|=|!/; - - /** - * 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 - * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of - * course, the default is to use mustaches (i.e. mustache.tags). - * - * A token is an array with at least 4 elements. The first element is the - * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag - * did not contain a symbol (i.e. {{myValue}}) this element is "name". For - * all text that appears outside a symbol this element is "text". - * - * The second element of a token is its "value". For mustache tags this is - * whatever else was inside the tag besides the opening symbol. For text tokens - * this is the text itself. - * - * The third and fourth elements of the token are the start and end indices, - * respectively, of the token in the original template. - * - * Tokens that are the root node of a subtree contain two more elements: 1) an - * array of tokens in the subtree and 2) the index in the original template at - * which the closing tag for that section begins. - * - * Tokens for partials also contain two more elements: 1) a string value of - * indendation prior to that tag and 2) the index of that tag on that line - - * eg a value of 2 indicates the partial is the third tag on this line. - */ - function parseTemplate (template, tags) { - if (!template) - return []; - var lineHasNonSpace = false; - var sections = []; // Stack to hold section tokens - var tokens = []; // Buffer to hold the tokens - var spaces = []; // Indices of whitespace tokens on the current line - var hasTag = false; // Is there a {{tag}} on the current line? - var nonSpace = false; // Is there a non-space char on the current line? - var indentation = ''; // Tracks indentation for tags that use it - var tagIndex = 0; // Stores a count of number of tags encountered on a line - - // Strips all whitespace tokens array for the current line - // if there was a {{#tag}} on it and otherwise only space. - function stripSpace () { - if (hasTag && !nonSpace) { - while (spaces.length) - delete tokens[spaces.pop()]; - } else { - spaces = []; - } + // Match any text between tags. + value = scanner.scanUntil(openingTagRe); - hasTag = false; - nonSpace = false; - } + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); - var openingTagRe, closingTagRe, closingCurlyRe; - function compileTags (tagsToCompile) { - if (typeof tagsToCompile === 'string') - tagsToCompile = tagsToCompile.split(spaceRe, 2); + if (isWhitespace(chr)) { + spaces.push(tokens.length); + indentation += chr; + } else { + nonSpace = true; + lineHasNonSpace = true; + indentation += ' '; + } - if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) - throw new Error('Invalid tags: ' + tagsToCompile); + tokens.push([ 'text', chr, start, start + 1 ]); + start += 1; - openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); - closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); - closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); + // Check for whitespace on the current line. + if (chr === '\n') { + stripSpace(); + indentation = ''; + tagIndex = 0; + lineHasNonSpace = false; + } + } } - compileTags(tags || mustache.tags); + // Match the opening tag. + if (!scanner.scan(openingTagRe)) + break; + + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === '{') { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = '&'; + } else { + value = scanner.scanUntil(closingTagRe); + } - var scanner = new Scanner(template); + // Match the closing tag. + if (!scanner.scan(closingTagRe)) + throw new Error('Unclosed tag at ' + scanner.pos); - var start, type, value, chr, token, openSection; - while (!scanner.eos()) { - start = scanner.pos; + if (type == '>') { + token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; + } else { + token = [ type, value, start, scanner.pos ]; + } + tagIndex++; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + compileTags(value); + } + } - // Match any text between tags. - value = scanner.scanUntil(openingTagRe); + stripSpace(); - if (value) { - for (var i = 0, valueLength = value.length; i < valueLength; ++i) { - chr = value.charAt(i); + // Make sure there are no open sections when we're done. + openSection = sections.pop(); - if (isWhitespace(chr)) { - spaces.push(tokens.length); - indentation += chr; - } else { - nonSpace = true; - lineHasNonSpace = true; - indentation += ' '; - } + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - tokens.push([ 'text', chr, start, start + 1 ]); - start += 1; + return nestTokens(squashTokens(tokens)); +} - // Check for whitespace on the current line. - if (chr === '\n') { - stripSpace(); - indentation = ''; - tagIndex = 0; - lineHasNonSpace = false; - } - } - } +/** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ +function squashTokens (tokens) { + var squashedTokens = []; - // Match the opening tag. - if (!scanner.scan(openingTagRe)) - break; + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; - hasTag = true; - - // Get the tag type. - type = scanner.scan(tagRe) || 'name'; - scanner.scan(whiteRe); - - // Get the tag value. - if (type === '=') { - value = scanner.scanUntil(equalsRe); - scanner.scan(equalsRe); - scanner.scanUntil(closingTagRe); - } else if (type === '{') { - value = scanner.scanUntil(closingCurlyRe); - scanner.scan(curlyRe); - scanner.scanUntil(closingTagRe); - type = '&'; + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; } else { - value = scanner.scanUntil(closingTagRe); + squashedTokens.push(token); + lastToken = token; } + } + } - // Match the closing tag. - if (!scanner.scan(closingTagRe)) - throw new Error('Unclosed tag at ' + scanner.pos); - - if (type == '>') { - token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; - } else { - token = [ type, value, start, scanner.pos ]; - } - tagIndex++; - tokens.push(token); + return squashedTokens; +} - if (type === '#' || type === '^') { +/** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ +function nestTokens (tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + collector.push(token); sections.push(token); - } else if (type === '/') { - // Check section nesting. - openSection = sections.pop(); - - if (!openSection) - throw new Error('Unopened section "' + value + '" at ' + start); - - if (openSection[1] !== value) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); - } else if (type === 'name' || type === '{' || type === '&') { - nonSpace = true; - } else if (type === '=') { - // Set the tags for the next time around. - compileTags(value); - } + collector = token[4] = []; + break; + case '/': + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); } + } - stripSpace(); + return nestedTokens; +} - // Make sure there are no open sections when we're done. - openSection = sections.pop(); +/** + * A simple string scanner that is used by the template parser to find + * tokens in template strings. + */ +function Scanner (string) { + this.string = string; + this.tail = string; + this.pos = 0; +} + +/** + * Returns `true` if the tail is empty (end of string). + */ +Scanner.prototype.eos = function eos () { + return this.tail === ''; +}; - if (openSection) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); +/** + * Tries to match the given regular expression at the current position. + * Returns the matched text if it can match, the empty string otherwise. + */ +Scanner.prototype.scan = function scan (re) { + var match = this.tail.match(re); - return nestTokens(squashTokens(tokens)); - } + if (!match || match.index !== 0) + return ''; - /** - * Combines the values of consecutive text tokens in the given `tokens` array - * to a single token. - */ - function squashTokens (tokens) { - var squashedTokens = []; - - var token, lastToken; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; - - if (token) { - if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { - lastToken[1] += token[1]; - lastToken[3] = token[3]; - } else { - squashedTokens.push(token); - lastToken = token; - } - } - } + var string = match[0]; - return squashedTokens; - } + this.tail = this.tail.substring(string.length); + this.pos += string.length; - /** - * Forms the given array of `tokens` into a nested tree structure where - * tokens that represent a section have two additional items: 1) an array of - * all tokens that appear in that section and 2) the index in the original - * template that represents the end of that section. - */ - function nestTokens (tokens) { - var nestedTokens = []; - var collector = nestedTokens; - var sections = []; - - var token, section; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; - - switch (token[0]) { - case '#': - case '^': - collector.push(token); - sections.push(token); - collector = token[4] = []; - break; - case '/': - section = sections.pop(); - section[5] = token[2]; - collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; - break; - default: - collector.push(token); - } - } + return string; +}; - return nestedTokens; +/** + * Skips all text until the given regular expression can be matched. Returns + * the skipped string, which is the entire tail if no match can be made. + */ +Scanner.prototype.scanUntil = function scanUntil (re) { + var index = this.tail.search(re), match; + + switch (index) { + case -1: + match = this.tail; + this.tail = ''; + break; + case 0: + match = ''; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); } - /** - * A simple string scanner that is used by the template parser to find - * tokens in template strings. - */ - function Scanner (string) { - this.string = string; - this.tail = string; - this.pos = 0; - } + this.pos += match.length; - /** - * Returns `true` if the tail is empty (end of string). - */ - Scanner.prototype.eos = function eos () { - return this.tail === ''; - }; - - /** - * Tries to match the given regular expression at the current position. - * Returns the matched text if it can match, the empty string otherwise. - */ - Scanner.prototype.scan = function scan (re) { - var match = this.tail.match(re); - - if (!match || match.index !== 0) - return ''; - - var string = match[0]; - - this.tail = this.tail.substring(string.length); - this.pos += string.length; - - return string; - }; - - /** - * Skips all text until the given regular expression can be matched. Returns - * the skipped string, which is the entire tail if no match can be made. - */ - Scanner.prototype.scanUntil = function scanUntil (re) { - var index = this.tail.search(re), match; - - switch (index) { - case -1: - match = this.tail; - this.tail = ''; - break; - case 0: - match = ''; - break; - default: - match = this.tail.substring(0, index); - this.tail = this.tail.substring(index); - } - - this.pos += match.length; + return match; +}; - return match; - }; - - /** - * Represents a rendering context by wrapping a view object and - * maintaining a reference to the parent context. - */ - function Context (view, parentContext) { - this.view = view; - this.cache = { '.': this.view }; - this.parent = parentContext; - } +/** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ +function Context (view, parentContext) { + this.view = view; + this.cache = { '.': this.view }; + this.parent = parentContext; +} + +/** + * Creates a new context using the given view with this context + * as the parent. + */ +Context.prototype.push = function push (view) { + return new Context(view, this); +}; - /** - * Creates a new context using the given view with this context - * as the parent. - */ - Context.prototype.push = function push (view) { - return new Context(view, this); - }; - - /** - * Returns the value of the given name in this context, traversing - * up the context hierarchy if the value is absent in this context's view. - */ - Context.prototype.lookup = function lookup (name) { - var cache = this.cache; - - var value; - if (cache.hasOwnProperty(name)) { - value = cache[name]; - } else { - var context = this, intermediateValue, names, index, lookupHit = false; - - while (context) { - if (name.indexOf('.') > 0) { - intermediateValue = context.view; - names = name.split('.'); - index = 0; - - /** - * Using the dot notion path in `name`, we descend through the - * nested objects. - * - * To be certain that the lookup has been successful, we have to - * check if the last object in the path actually has the property - * we are looking for. We store the result in `lookupHit`. - * - * This is specially necessary for when the value has been set to - * `undefined` and we want to avoid looking up parent contexts. - * - * In the case where dot notation is used, we consider the lookup - * to be successful even if the last "object" in the path is - * not actually an object but a primitive (e.g., a string, or an - * integer), because it is sometimes useful to access a property - * of an autoboxed primitive, such as the length of a string. - **/ - while (intermediateValue != null && index < names.length) { - if (index === names.length - 1) - lookupHit = ( - hasProperty(intermediateValue, names[index]) - || primitiveHasOwnProperty(intermediateValue, names[index]) - ); - - intermediateValue = intermediateValue[names[index++]]; - } - } else { - intermediateValue = context.view[name]; - - /** - * Only checking against `hasProperty`, which always returns `false` if - * `context.view` is not an object. Deliberately omitting the check - * against `primitiveHasOwnProperty` if dot notation is not used. - * - * Consider this example: - * ``` - * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) - * ``` - * - * If we were to check also against `primitiveHasOwnProperty`, as we do - * in the dot notation case, then render call would return: - * - * "The length of a football field is 9." - * - * rather than the expected: - * - * "The length of a football field is 100 yards." - **/ - lookupHit = hasProperty(context.view, name); - } +/** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ +Context.prototype.lookup = function lookup (name) { + var cache = this.cache; - if (lookupHit) { - value = intermediateValue; - break; + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, intermediateValue, names, index, lookupHit = false; + + while (context) { + if (name.indexOf('.') > 0) { + intermediateValue = context.view; + names = name.split('.'); + index = 0; + + /** + * Using the dot notion path in `name`, we descend through the + * nested objects. + * + * To be certain that the lookup has been successful, we have to + * check if the last object in the path actually has the property + * we are looking for. We store the result in `lookupHit`. + * + * This is specially necessary for when the value has been set to + * `undefined` and we want to avoid looking up parent contexts. + * + * In the case where dot notation is used, we consider the lookup + * to be successful even if the last "object" in the path is + * not actually an object but a primitive (e.g., a string, or an + * integer), because it is sometimes useful to access a property + * of an autoboxed primitive, such as the length of a string. + **/ + while (intermediateValue != null && index < names.length) { + if (index === names.length - 1) + lookupHit = ( + hasProperty(intermediateValue, names[index]) + || primitiveHasOwnProperty(intermediateValue, names[index]) + ); + + intermediateValue = intermediateValue[names[index++]]; } + } else { + intermediateValue = context.view[name]; + + /** + * Only checking against `hasProperty`, which always returns `false` if + * `context.view` is not an object. Deliberately omitting the check + * against `primitiveHasOwnProperty` if dot notation is not used. + * + * Consider this example: + * ``` + * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) + * ``` + * + * If we were to check also against `primitiveHasOwnProperty`, as we do + * in the dot notation case, then render call would return: + * + * "The length of a football field is 9." + * + * rather than the expected: + * + * "The length of a football field is 100 yards." + **/ + lookupHit = hasProperty(context.view, name); + } - context = context.parent; + if (lookupHit) { + value = intermediateValue; + break; } - cache[name] = value; + context = context.parent; } - if (isFunction(value)) - value = value.call(this.view); - - return value; - }; - - /** - * A Writer knows how to take a stream of tokens and render them to a - * string, given a context. It also maintains a cache of templates to - * avoid the need to parse the same template twice. - */ - function Writer () { - this.cache = {}; + cache[name] = value; } - /** - * Clears all cached templates in this writer. - */ - Writer.prototype.clearCache = function clearCache () { - this.cache = {}; - }; - - /** - * Parses and caches the given `template` according to the given `tags` or - * `mustache.tags` if `tags` is omitted, and returns the array of tokens - * that is generated from the parse. - */ - Writer.prototype.parse = function parse (template, tags) { - var cache = this.cache; - var cacheKey = template + ':' + (tags || mustache.tags).join(':'); - var tokens = cache[cacheKey]; - - if (tokens == null) - tokens = cache[cacheKey] = parseTemplate(template, tags); - - return tokens; - }; - - /** - * High-level method that is used to render the given `template` with - * the given `view`. - * - * The optional `partials` argument may be an object that contains the - * names and templates of partials that are used in the template. It may - * also be a function that is used to load partial templates on the fly - * that takes a single argument: the name of the partial. - * - * If the optional `tags` argument is given here it must be an array with two - * string values: the opening and closing tags used in the template (e.g. - * [ "<%", "%>" ]). The default is to mustache.tags. - */ - Writer.prototype.render = function render (template, view, partials, tags) { - var tokens = this.parse(template, tags); - var context = (view instanceof Context) ? view : new Context(view); - return this.renderTokens(tokens, context, partials, template, tags); - }; - - /** - * Low-level method that renders the given array of `tokens` using - * the given `context` and `partials`. - * - * Note: The `originalTemplate` is only ever used to extract the portion - * of the original template that was contained in a higher-order section. - * If the template doesn't use higher-order sections, this argument may - * be omitted. - */ - Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { - var buffer = ''; - - var token, symbol, value; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - value = undefined; - token = tokens[i]; - symbol = token[0]; - - if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); - else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); - else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); - else if (symbol === '&') value = this.unescapedValue(token, context); - else if (symbol === 'name') value = this.escapedValue(token, context); - else if (symbol === 'text') value = this.rawValue(token); - - if (value !== undefined) - buffer += value; - } + if (isFunction(value)) + value = value.call(this.view); - return buffer; - }; + return value; +}; - Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { - var self = this; - var buffer = ''; - var value = context.lookup(token[1]); +/** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. + */ +function Writer () { + this.cache = {}; +} - // This function is used to render an arbitrary template - // in the current context by higher-order sections. - function subRender (template) { - return self.render(template, context, partials); - } +/** + * Clears all cached templates in this writer. + */ +Writer.prototype.clearCache = function clearCache () { + this.cache = {}; +}; + +/** + * Parses and caches the given `template` according to the given `tags` or + * `mustache.tags` if `tags` is omitted, and returns the array of tokens + * that is generated from the parse. + */ +Writer.prototype.parse = function parse (template, tags) { + var cache = this.cache; + var cacheKey = template + ':' + (tags || mustache.tags).join(':'); + var tokens = cache[cacheKey]; + + if (tokens == null) + tokens = cache[cacheKey] = parseTemplate(template, tags); + + return tokens; +}; + +/** + * High-level method that is used to render the given `template` with + * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. + * + * If the optional `tags` argument is given here it must be an array with two + * string values: the opening and closing tags used in the template (e.g. + * [ "<%", "%>" ]). The default is to mustache.tags. + */ +Writer.prototype.render = function render (template, view, partials, tags) { + var tokens = this.parse(template, tags); + var context = (view instanceof Context) ? view : new Context(view); + return this.renderTokens(tokens, context, partials, template, tags); +}; + +/** + * Low-level method that renders the given array of `tokens` using + * the given `context` and `partials`. + * + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. + */ +Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { + var buffer = ''; + + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = undefined; + token = tokens[i]; + symbol = token[0]; + + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); + else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + else if (symbol === '&') value = this.unescapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'text') value = this.rawValue(token); + + if (value !== undefined) + buffer += value; + } - if (!value) return; + return buffer; +}; - if (isArray(value)) { - for (var j = 0, valueLength = value.length; j < valueLength; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); - } - } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { - buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); - } else if (isFunction(value)) { - if (typeof originalTemplate !== 'string') - throw new Error('Cannot use higher-order sections without the original template'); +Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { + var self = this; + var buffer = ''; + var value = context.lookup(token[1]); - // Extract the portion of the original template that the section contains. - value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); + // This function is used to render an arbitrary template + // in the current context by higher-order sections. + function subRender (template) { + return self.render(template, context, partials); + } - if (value != null) - buffer += value; - } else { - buffer += this.renderTokens(token[4], context, partials, originalTemplate); - } - return buffer; - }; - - Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { - var value = context.lookup(token[1]); - - // Use JavaScript's definition of falsy. Include empty arrays. - // See https://github.com/janl/mustache.js/issues/186 - if (!value || (isArray(value) && value.length === 0)) - return this.renderTokens(token[4], context, partials, originalTemplate); - }; - - Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { - var filteredIndentation = indentation.replace(/[^ \t]/g, ''); - var partialByNl = partial.split('\n'); - for (var i = 0; i < partialByNl.length; i++) { - if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { - partialByNl[i] = filteredIndentation + partialByNl[i]; - } - } - return partialByNl.join('\n'); - }; - - Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { - if (!partials) return; - - var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; - if (value != null) { - var lineHasNonSpace = token[6]; - var tagIndex = token[5]; - var indentation = token[4]; - var indentedValue = value; - if (tagIndex == 0 && indentation) { - indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); - } - return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue); + if (!value) return; + + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } - }; + } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + } else if (isFunction(value)) { + if (typeof originalTemplate !== 'string') + throw new Error('Cannot use higher-order sections without the original template'); - Writer.prototype.unescapedValue = function unescapedValue (token, context) { - var value = context.lookup(token[1]); - if (value != null) - return value; - }; + // Extract the portion of the original template that the section contains. + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); - Writer.prototype.escapedValue = function escapedValue (token, context) { - var value = context.lookup(token[1]); if (value != null) - return mustache.escape(value); - }; - - Writer.prototype.rawValue = function rawValue (token) { - return token[1]; - }; - - mustache.name = 'mustache.js'; - mustache.version = '3.1.0'; - mustache.tags = [ '{{', '}}' ]; - - // All high-level mustache.* functions use this writer. - var defaultWriter = new Writer(); - - /** - * Clears all cached templates in the default writer. - */ - mustache.clearCache = function clearCache () { - return defaultWriter.clearCache(); - }; - - /** - * Parses and caches the given template in the default writer and returns the - * array of tokens it contains. Doing this ahead of time avoids the need to - * parse templates on the fly as they are rendered. - */ - mustache.parse = function parse (template, tags) { - return defaultWriter.parse(template, tags); - }; - - /** - * Renders the `template` with the given `view` and `partials` using the - * default writer. If the optional `tags` argument is given here it must be an - * array with two string values: the opening and closing tags used in the - * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. - */ - mustache.render = function render (template, view, partials, tags) { - if (typeof template !== 'string') { - throw new TypeError('Invalid template! Template should be a "string" ' + - 'but "' + typeStr(template) + '" was given as the first ' + - 'argument for mustache#render(template, view, partials)'); + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate); + } + return buffer; +}; + +Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { + var value = context.lookup(token[1]); + + // Use JavaScript's definition of falsy. Include empty arrays. + // See https://github.com/janl/mustache.js/issues/186 + if (!value || (isArray(value) && value.length === 0)) + return this.renderTokens(token[4], context, partials, originalTemplate); +}; + +Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { + var filteredIndentation = indentation.replace(/[^ \t]/g, ''); + var partialByNl = partial.split('\n'); + for (var i = 0; i < partialByNl.length; i++) { + if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { + partialByNl[i] = filteredIndentation + partialByNl[i]; + } + } + return partialByNl.join('\n'); +}; + +Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { + if (!partials) return; + + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) { + var lineHasNonSpace = token[6]; + var tagIndex = token[5]; + var indentation = token[4]; + var indentedValue = value; + if (tagIndex == 0 && indentation) { + indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } + return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue); + } +}; - return defaultWriter.render(template, view, partials, tags); - }; +Writer.prototype.unescapedValue = function unescapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; +}; - // This is here for backwards compatibility with 0.4.x., - /*eslint-disable */ // eslint wants camel cased function name - mustache.to_html = function to_html (template, view, partials, send) { - /*eslint-enable*/ +Writer.prototype.escapedValue = function escapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return mustache.escape(value); +}; - var result = mustache.render(template, view, partials); +Writer.prototype.rawValue = function rawValue (token) { + return token[1]; +}; - if (isFunction(send)) { - send(result); - } else { - return result; - } - }; +var mustache = { + name: 'mustache.js', + version: '3.1.0', + tags: [ '{{', '}}' ] +}; + +// All high-level mustache.* functions use this writer. +var defaultWriter = new Writer(); + +/** + * Clears all cached templates in the default writer. + */ +mustache.clearCache = function clearCache () { + return defaultWriter.clearCache(); +}; + +/** + * Parses and caches the given template in the default writer and returns the + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. + */ +mustache.parse = function parse (template, tags) { + return defaultWriter.parse(template, tags); +}; + +/** + * Renders the `template` with the given `view` and `partials` using the + * default writer. If the optional `tags` argument is given here it must be an + * array with two string values: the opening and closing tags used in the + * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. + */ +mustache.render = function render (template, view, partials, tags) { + if (typeof template !== 'string') { + throw new TypeError('Invalid template! Template should be a "string" ' + + 'but "' + typeStr(template) + '" was given as the first ' + + 'argument for mustache#render(template, view, partials)'); + } + + return defaultWriter.render(template, view, partials, tags); +}; + +// This is here for backwards compatibility with 0.4.x., +/*eslint-disable */ // eslint wants camel cased function name +mustache.to_html = function to_html (template, view, partials, send) { + /*eslint-enable*/ + + var result = mustache.render(template, view, partials); + + if (isFunction(send)) { + send(result); + } else { + return result; + } +}; - // Export the escaping function so that the user may override it. - // See https://github.com/janl/mustache.js/issues/244 - mustache.escape = escapeHtml; +// Export the escaping function so that the user may override it. +// See https://github.com/janl/mustache.js/issues/244 +mustache.escape = escapeHtml; - // Export these mainly for testing, but also for advanced usage. - mustache.Scanner = Scanner; - mustache.Context = Context; - mustache.Writer = Writer; +// Export these mainly for testing, but also for advanced usage. +mustache.Scanner = Scanner; +mustache.Context = Context; +mustache.Writer = Writer; - return mustache; -})); +export default mustache; \ No newline at end of file From f25abbea8b14bbc83caf81822279d0e708dba45d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 7 Oct 2019 15:01:33 +0200 Subject: [PATCH 223/286] Introduce build step to generate `.js | .min.js` from `.mjs` By making the main source code an ES module placed in `mustache.mjs`, what used to be in `mustache.js` is now built based off of the ES module source inside `mustache.mjs`. This is done primarily to avoid breaking existing projects already using mustache.js, by keeping the UMD-version in `mustache.js` part of the git repository in addition to the minified version in `mustache.min.js`. After experiment with several compilers; - Babel - TypeScript - Rollup and examining their build output, [Rollup](https://rollupjs.org/) was chosen because of the UMD output it creates which closely resembles what we've historically had in the previous `mustache.js` source code. The contents of `.js | .min.js` files has been generated by running the following npm script: ``` $ npm run build ``` Also change linting w/eslint to target the `.mjs` file, since the output of rollup that ends up in the old `mustache.js` file is not sensible to do linting on. --- .eslintrc | 11 +- mustache.js | 718 ++++++++++++++++++++++++++++++++++++++++++++++ mustache.min.js | 2 +- package-lock.json | 66 +++-- package.json | 5 +- 5 files changed, 769 insertions(+), 33 deletions(-) create mode 100644 mustache.js diff --git a/.eslintrc b/.eslintrc index 0e2d7ce3f..d4143d408 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,5 +16,14 @@ "no-use-before-define": 0, "no-process-exit": 0, "strict": 0 - } + }, + "overrides": [ + { + "files": ["mustache.mjs"], + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2015 + } + } + ] } \ No newline at end of file diff --git a/mustache.js b/mustache.js new file mode 100644 index 000000000..faad3dffa --- /dev/null +++ b/mustache.js @@ -0,0 +1,718 @@ +// This file has been generated from mustache.mjs +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Mustache = factory()); +}(this, function () { 'use strict'; + + /*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + */ + + var objectToString = Object.prototype.toString; + var isArray = Array.isArray || function isArrayPolyfill (object) { + return objectToString.call(object) === '[object Array]'; + }; + + function isFunction (object) { + return typeof object === 'function'; + } + + /** + * More correct typeof string handling array + * which normally returns typeof 'object' + */ + function typeStr (obj) { + return isArray(obj) ? 'array' : typeof obj; + } + + function escapeRegExp (string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + } + + /** + * Null safe way of checking whether or not an object, + * including its prototype, has a given property + */ + function hasProperty (obj, propName) { + return obj != null && typeof obj === 'object' && (propName in obj); + } + + /** + * Safe way of detecting whether or not the given thing is a primitive and + * whether it has the given property + */ + function primitiveHasOwnProperty (primitive, propName) { + return ( + primitive != null + && typeof primitive !== 'object' + && primitive.hasOwnProperty + && primitive.hasOwnProperty(propName) + ); + } + + // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 + // See https://github.com/janl/mustache.js/issues/189 + var regExpTest = RegExp.prototype.test; + function testRegExp (re, string) { + return regExpTest.call(re, string); + } + + var nonSpaceRe = /\S/; + function isWhitespace (string) { + return !testRegExp(nonSpaceRe, string); + } + + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; + + function escapeHtml (string) { + return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { + return entityMap[s]; + }); + } + + var whiteRe = /\s*/; + var spaceRe = /\s+/; + var equalsRe = /\s*=/; + var curlyRe = /\s*\}/; + var tagRe = /#|\^|\/|>|\{|&|=|!/; + + /** + * 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 + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. + * + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. + * + * Tokens for partials also contain two more elements: 1) a string value of + * indendation prior to that tag and 2) the index of that tag on that line - + * eg a value of 2 indicates the partial is the third tag on this line. + */ + function parseTemplate (template, tags) { + if (!template) + return []; + var lineHasNonSpace = false; + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + var indentation = ''; // Tracks indentation for tags that use it + var tagIndex = 0; // Stores a count of number of tags encountered on a line + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace () { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } + + hasTag = false; + nonSpace = false; + } + + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags (tagsToCompile) { + if (typeof tagsToCompile === 'string') + tagsToCompile = tagsToCompile.split(spaceRe, 2); + + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error('Invalid tags: ' + tagsToCompile); + + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); + } + + compileTags(tags || mustache.tags); + + var scanner = new Scanner(template); + + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; + + // Match any text between tags. + value = scanner.scanUntil(openingTagRe); + + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); + + if (isWhitespace(chr)) { + spaces.push(tokens.length); + indentation += chr; + } else { + nonSpace = true; + lineHasNonSpace = true; + indentation += ' '; + } + + tokens.push([ 'text', chr, start, start + 1 ]); + start += 1; + + // Check for whitespace on the current line. + if (chr === '\n') { + stripSpace(); + indentation = ''; + tagIndex = 0; + lineHasNonSpace = false; + } + } + } + + // Match the opening tag. + if (!scanner.scan(openingTagRe)) + break; + + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === '{') { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = '&'; + } else { + value = scanner.scanUntil(closingTagRe); + } + + // Match the closing tag. + if (!scanner.scan(closingTagRe)) + throw new Error('Unclosed tag at ' + scanner.pos); + + if (type == '>') { + token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; + } else { + token = [ type, value, start, scanner.pos ]; + } + tagIndex++; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + compileTags(value); + } + } + + stripSpace(); + + // Make sure there are no open sections when we're done. + openSection = sections.pop(); + + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + + return nestTokens(squashTokens(tokens)); + } + + /** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ + function squashTokens (tokens) { + var squashedTokens = []; + + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + squashedTokens.push(token); + lastToken = token; + } + } + } + + return squashedTokens; + } + + /** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ + function nestTokens (tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + collector.push(token); + sections.push(token); + collector = token[4] = []; + break; + case '/': + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); + } + } + + return nestedTokens; + } + + /** + * A simple string scanner that is used by the template parser to find + * tokens in template strings. + */ + function Scanner (string) { + this.string = string; + this.tail = string; + this.pos = 0; + } + + /** + * Returns `true` if the tail is empty (end of string). + */ + Scanner.prototype.eos = function eos () { + return this.tail === ''; + }; + + /** + * Tries to match the given regular expression at the current position. + * Returns the matched text if it can match, the empty string otherwise. + */ + Scanner.prototype.scan = function scan (re) { + var match = this.tail.match(re); + + if (!match || match.index !== 0) + return ''; + + var string = match[0]; + + this.tail = this.tail.substring(string.length); + this.pos += string.length; + + return string; + }; + + /** + * Skips all text until the given regular expression can be matched. Returns + * the skipped string, which is the entire tail if no match can be made. + */ + Scanner.prototype.scanUntil = function scanUntil (re) { + var index = this.tail.search(re), match; + + switch (index) { + case -1: + match = this.tail; + this.tail = ''; + break; + case 0: + match = ''; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); + } + + this.pos += match.length; + + return match; + }; + + /** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ + function Context (view, parentContext) { + this.view = view; + this.cache = { '.': this.view }; + this.parent = parentContext; + } + + /** + * Creates a new context using the given view with this context + * as the parent. + */ + Context.prototype.push = function push (view) { + return new Context(view, this); + }; + + /** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ + Context.prototype.lookup = function lookup (name) { + var cache = this.cache; + + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, intermediateValue, names, index, lookupHit = false; + + while (context) { + if (name.indexOf('.') > 0) { + intermediateValue = context.view; + names = name.split('.'); + index = 0; + + /** + * Using the dot notion path in `name`, we descend through the + * nested objects. + * + * To be certain that the lookup has been successful, we have to + * check if the last object in the path actually has the property + * we are looking for. We store the result in `lookupHit`. + * + * This is specially necessary for when the value has been set to + * `undefined` and we want to avoid looking up parent contexts. + * + * In the case where dot notation is used, we consider the lookup + * to be successful even if the last "object" in the path is + * not actually an object but a primitive (e.g., a string, or an + * integer), because it is sometimes useful to access a property + * of an autoboxed primitive, such as the length of a string. + **/ + while (intermediateValue != null && index < names.length) { + if (index === names.length - 1) + lookupHit = ( + hasProperty(intermediateValue, names[index]) + || primitiveHasOwnProperty(intermediateValue, names[index]) + ); + + intermediateValue = intermediateValue[names[index++]]; + } + } else { + intermediateValue = context.view[name]; + + /** + * Only checking against `hasProperty`, which always returns `false` if + * `context.view` is not an object. Deliberately omitting the check + * against `primitiveHasOwnProperty` if dot notation is not used. + * + * Consider this example: + * ``` + * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) + * ``` + * + * If we were to check also against `primitiveHasOwnProperty`, as we do + * in the dot notation case, then render call would return: + * + * "The length of a football field is 9." + * + * rather than the expected: + * + * "The length of a football field is 100 yards." + **/ + lookupHit = hasProperty(context.view, name); + } + + if (lookupHit) { + value = intermediateValue; + break; + } + + context = context.parent; + } + + cache[name] = value; + } + + if (isFunction(value)) + value = value.call(this.view); + + return value; + }; + + /** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. + */ + function Writer () { + this.cache = {}; + } + + /** + * Clears all cached templates in this writer. + */ + Writer.prototype.clearCache = function clearCache () { + this.cache = {}; + }; + + /** + * Parses and caches the given `template` according to the given `tags` or + * `mustache.tags` if `tags` is omitted, and returns the array of tokens + * that is generated from the parse. + */ + Writer.prototype.parse = function parse (template, tags) { + var cache = this.cache; + var cacheKey = template + ':' + (tags || mustache.tags).join(':'); + var tokens = cache[cacheKey]; + + if (tokens == null) + tokens = cache[cacheKey] = parseTemplate(template, tags); + + return tokens; + }; + + /** + * High-level method that is used to render the given `template` with + * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. + * + * If the optional `tags` argument is given here it must be an array with two + * string values: the opening and closing tags used in the template (e.g. + * [ "<%", "%>" ]). The default is to mustache.tags. + */ + Writer.prototype.render = function render (template, view, partials, tags) { + var tokens = this.parse(template, tags); + var context = (view instanceof Context) ? view : new Context(view); + return this.renderTokens(tokens, context, partials, template, tags); + }; + + /** + * Low-level method that renders the given array of `tokens` using + * the given `context` and `partials`. + * + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. + */ + Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { + var buffer = ''; + + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = undefined; + token = tokens[i]; + symbol = token[0]; + + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); + else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + else if (symbol === '&') value = this.unescapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'text') value = this.rawValue(token); + + if (value !== undefined) + buffer += value; + } + + return buffer; + }; + + Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { + var self = this; + var buffer = ''; + var value = context.lookup(token[1]); + + // This function is used to render an arbitrary template + // in the current context by higher-order sections. + function subRender (template) { + return self.render(template, context, partials); + } + + if (!value) return; + + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); + } + } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + } else if (isFunction(value)) { + if (typeof originalTemplate !== 'string') + throw new Error('Cannot use higher-order sections without the original template'); + + // Extract the portion of the original template that the section contains. + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); + + if (value != null) + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate); + } + return buffer; + }; + + Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { + var value = context.lookup(token[1]); + + // Use JavaScript's definition of falsy. Include empty arrays. + // See https://github.com/janl/mustache.js/issues/186 + if (!value || (isArray(value) && value.length === 0)) + return this.renderTokens(token[4], context, partials, originalTemplate); + }; + + Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { + var filteredIndentation = indentation.replace(/[^ \t]/g, ''); + var partialByNl = partial.split('\n'); + for (var i = 0; i < partialByNl.length; i++) { + if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { + partialByNl[i] = filteredIndentation + partialByNl[i]; + } + } + return partialByNl.join('\n'); + }; + + Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { + if (!partials) return; + + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) { + var lineHasNonSpace = token[6]; + var tagIndex = token[5]; + var indentation = token[4]; + var indentedValue = value; + if (tagIndex == 0 && indentation) { + indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); + } + return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue); + } + }; + + Writer.prototype.unescapedValue = function unescapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; + }; + + Writer.prototype.escapedValue = function escapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return mustache.escape(value); + }; + + Writer.prototype.rawValue = function rawValue (token) { + return token[1]; + }; + + var mustache = { + name: 'mustache.js', + version: '3.1.0', + tags: [ '{{', '}}' ] + }; + + // All high-level mustache.* functions use this writer. + var defaultWriter = new Writer(); + + /** + * Clears all cached templates in the default writer. + */ + mustache.clearCache = function clearCache () { + return defaultWriter.clearCache(); + }; + + /** + * Parses and caches the given template in the default writer and returns the + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. + */ + mustache.parse = function parse (template, tags) { + return defaultWriter.parse(template, tags); + }; + + /** + * Renders the `template` with the given `view` and `partials` using the + * default writer. If the optional `tags` argument is given here it must be an + * array with two string values: the opening and closing tags used in the + * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. + */ + mustache.render = function render (template, view, partials, tags) { + if (typeof template !== 'string') { + throw new TypeError('Invalid template! Template should be a "string" ' + + 'but "' + typeStr(template) + '" was given as the first ' + + 'argument for mustache#render(template, view, partials)'); + } + + return defaultWriter.render(template, view, partials, tags); + }; + + // This is here for backwards compatibility with 0.4.x., + /*eslint-disable */ // eslint wants camel cased function name + mustache.to_html = function to_html (template, view, partials, send) { + /*eslint-enable*/ + + var result = mustache.render(template, view, partials); + + if (isFunction(send)) { + send(result); + } else { + return result; + } + }; + + // Export the escaping function so that the user may override it. + // See https://github.com/janl/mustache.js/issues/244 + mustache.escape = escapeHtml; + + // Export these mainly for testing, but also for advanced usage. + mustache.Scanner = Scanner; + mustache.Context = Context; + mustache.Writer = Writer; + + return mustache; + +})); diff --git a/mustache.min.js b/mustache.min.js index 85b81b64a..4e360994d 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function defineMustache(global,factory){if(typeof exports==="object"&&exports&&typeof exports.nodeName!=="string"){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{global.Mustache={};factory(global.Mustache)}})(this,function mustacheFactory(mustache){var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};mustache.name="mustache.js";mustache.version="3.1.0";mustache.tags=["{{","}}"];var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.1.0",tags:["{{","}}"]};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/package-lock.json b/package-lock.json index 7e5ff855a..a81b7b3fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,12 @@ } } }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, "@types/node": { "version": "8.10.52", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.52.tgz", @@ -2899,8 +2905,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2921,14 +2926,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2943,20 +2946,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3073,8 +3073,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3086,7 +3085,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3101,7 +3099,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3109,14 +3106,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3135,7 +3130,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3216,8 +3210,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3229,7 +3222,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3315,8 +3307,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3352,7 +3343,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3372,7 +3362,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3416,14 +3405,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6771,6 +6758,25 @@ "inherits": "^2.0.1" } }, + "rollup": { + "version": "1.26.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.26.3.tgz", + "integrity": "sha512-8MhY/M8gnv3Q/pQQSWYWzbeJ5J1C5anCNY5BK1kV8Yzw9RFS0FF4lbLt+uyPO3wLKWXSXrhAL5pWL85TZAh+Sw==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/node": "*", + "acorn": "^7.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true + } + } + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", diff --git a/package.json b/package.json index 6785f38c8..04b576adb 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "files": [ "mustache.js", + "mustache.mjs", "mustache.min.js", "bin", "wrappers", @@ -32,8 +33,9 @@ "npm": ">=1.4.0" }, "scripts": { + "build": "rollup mustache.mjs --file mustache.js --format umd --name Mustache --banner '// This file has been generated from mustache.mjs' && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", - "test-lint": "eslint mustache.js bin/mustache test/**/*.js", + "test-lint": "eslint mustache.mjs bin/mustache test/**/*.js", "test-unit": "mocha --reporter spec test/*-test.js", "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", @@ -46,6 +48,7 @@ "jshint": "^2.9.5", "mocha": "^3.0.2", "puppeteer": "^2.0.0", + "rollup": "^1.26.3", "uglify-js": "^3.4.6", "zuul": "^3.11.0", "zuul-ngrok": "nolanlawson/zuul-ngrok#patch-1" From c28d73b53033d37d3a821f5856a2df45d43a0909 Mon Sep 17 00:00:00 2001 From: Vincent LE GOFF Date: Thu, 17 Oct 2019 22:01:36 +0200 Subject: [PATCH 224/286] Make mustache.mjs work with Deno Minor adjustments needed to make the TypeScript compiler that is built into Deno, be happy with how mustache.js' ES module source looks in terms of function parameters passed and object mutability. Refs https://github.com/phillipj/mustache.js/pull/1 --- mustache.js | 16 ++++++++++++---- mustache.min.js | 2 +- mustache.mjs | 14 +++++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/mustache.js b/mustache.js index faad3dffa..30c76e902 100644 --- a/mustache.js +++ b/mustache.js @@ -3,7 +3,7 @@ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Mustache = factory()); -}(this, function () { 'use strict'; +}(this, (function () { 'use strict'; /*! * mustache.js - Logic-less {{mustache}} templates with JavaScript @@ -527,7 +527,7 @@ */ Writer.prototype.render = function render (template, view, partials, tags) { var tokens = this.parse(template, tags); - var context = (view instanceof Context) ? view : new Context(view); + var context = (view instanceof Context) ? view : new Context(view, undefined); return this.renderTokens(tokens, context, partials, template, tags); }; @@ -652,7 +652,15 @@ var mustache = { name: 'mustache.js', version: '3.1.0', - tags: [ '{{', '}}' ] + tags: [ '{{', '}}' ], + clearCache: undefined, + escape: undefined, + parse: undefined, + render: undefined, + to_html: undefined, + Scanner: undefined, + Context: undefined, + Writer: undefined }; // All high-level mustache.* functions use this writer. @@ -715,4 +723,4 @@ return mustache; -})); +}))); diff --git a/mustache.min.js b/mustache.min.js index 4e360994d..c3b6643e2 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.1.0",tags:["{{","}}"]};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.1.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 2081c98e8..42a8b981b 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -520,7 +520,7 @@ Writer.prototype.parse = function parse (template, tags) { */ Writer.prototype.render = function render (template, view, partials, tags) { var tokens = this.parse(template, tags); - var context = (view instanceof Context) ? view : new Context(view); + var context = (view instanceof Context) ? view : new Context(view, undefined); return this.renderTokens(tokens, context, partials, template, tags); }; @@ -645,7 +645,15 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', version: '3.1.0', - tags: [ '{{', '}}' ] + tags: [ '{{', '}}' ], + clearCache: undefined, + escape: undefined, + parse: undefined, + render: undefined, + to_html: undefined, + Scanner: undefined, + Context: undefined, + Writer: undefined }; // All high-level mustache.* functions use this writer. @@ -706,4 +714,4 @@ mustache.Scanner = Scanner; mustache.Context = Context; mustache.Writer = Writer; -export default mustache; \ No newline at end of file +export default mustache; From b72d1a3b6cc3ecf7212c3c0c77515c40f8b5e9c6 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 21 Oct 2019 14:04:37 +0200 Subject: [PATCH 225/286] Add CI test verifying Mustache works with Deno To ensure the ES module and source exposed by this package is compatible with Deno going forward. Added benefit is we get some sanity checks of the source code for free due to Deno's built-in TypeScript compiler getting angry if it sees things that does not make sense, in terms of missing function arguments and so on. --- .github/workflows/verify.yml | 13 ++++++++++++- test/module-systems/deno-test.ts | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/module-systems/deno-test.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index f6d37898c..42e80d9d4 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,7 +2,7 @@ name: Verify changes on: [push, pull_request] -jobs: +jobs: tests: runs-on: ubuntu-latest @@ -71,3 +71,14 @@ jobs: run: | npm ci npx mocha test/module-systems/browser-test.js + + deno-usage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: denolib/setup-deno@v1 + with: + deno-version: 'v0.23.0' + - run: deno --version + - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts diff --git a/test/module-systems/deno-test.ts b/test/module-systems/deno-test.ts new file mode 100644 index 000000000..9325ae0b4 --- /dev/null +++ b/test/module-systems/deno-test.ts @@ -0,0 +1,17 @@ +import { test } from "https://deno.land/std@v0.21.0/testing/mod.ts"; +import { assertEquals } from "https://deno.land/std@v0.21.0/testing/asserts.ts"; +import mustache from "../../mustache.mjs"; + +const view = { + title: "Joe", + calc: function() { + return 2 + 4; + } +}; + +test(function canUseMustache() { + assertEquals( + mustache.render("{{title}} spends {{calc}}", view), + "Joe spends 6" + ); +}); From ddad1a71120d8bb48a181546d94a70cbb942ab04 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Fri, 22 Nov 2019 22:36:44 +0100 Subject: [PATCH 226/286] Add CI test to verify build output is in sync with source (.js vs .mjs) With the introduction of a build step, where we take the source in .mjs and build it into .js for non-ES Module systems, it's very important that we remember to keep those two in sync. That's what the CI job created in these changes ensure; 1. Run the build script 2. See if there are any pending git changes as a result As long as there are no pending git changes, we're all fine and dandy, or else we need to run the `npm run build` and commit the changes. --- .github/workflows/verify.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 42e80d9d4..b3cda17ff 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -82,3 +82,18 @@ jobs: deno-version: 'v0.23.0' - run: deno --version - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts + + build-output-sync: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Install, build and check git diff + run: | + npm ci + npm run build + git diff --quiet HEAD -- From e0a36313ce763550c3b56b45088daf489307b7db Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 23 Nov 2019 21:47:27 +0100 Subject: [PATCH 227/286] Add CI test verifying native ES Module usage for Node.js --- .github/workflows/verify.yml | 19 +++++++++++++++++++ test/module-systems/esm-test.mjs | 12 ++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 test/module-systems/esm-test.mjs diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b3cda17ff..8220851e8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -58,6 +58,25 @@ jobs: npm install $ARCHIVE_FILENAME node commonjs-test.js + esm-usage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '>=13.2.0' + - name: Package, install and test + run: | + export ARCHIVE_FILENAME=$(npm pack | tail -n 1) + export UNPACK_DESTINATION=$(mktemp -d) + mv $ARCHIVE_FILENAME $UNPACK_DESTINATION + cp test/module-systems/esm-test.mjs $UNPACK_DESTINATION + cd $UNPACK_DESTINATION + npm install $ARCHIVE_FILENAME + node esm-test.mjs + browser-usage: runs-on: ubuntu-latest diff --git a/test/module-systems/esm-test.mjs b/test/module-systems/esm-test.mjs new file mode 100644 index 000000000..050e4c04b --- /dev/null +++ b/test/module-systems/esm-test.mjs @@ -0,0 +1,12 @@ +import assert from 'assert'; +import mustache from 'mustache/mustache.mjs'; + +const view = { + title: 'Joe', + calc: () => 2 + 4 +}; + +assert.strictEqual( + mustache.render('{{title}} spends {{calc}}', view), + 'Joe spends 6' +); From 86fa37d39df79daa8a86a87cf0912cf9cc1f2aab Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Wed, 4 Dec 2019 20:56:39 +0100 Subject: [PATCH 228/286] Bump .version in mustache.js in git hook when version has changed This changes the pre-commit hook that we've used for years to keep the version value found in `package.json` in sync with the one in `mustache.js`, to change `mustache.mjs` instead since that's where the source code lives now. --- Rakefile | 5 ----- hooks/pre-commit | 8 ++++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Rakefile b/Rakefile index 261a05fd9..2540c2804 100644 --- a/Rakefile +++ b/Rakefile @@ -12,11 +12,6 @@ task :test do sh "./node_modules/.bin/mocha test" end -desc "Make a compressed build in #{minified_file}" -task :minify do - sh "./node_modules/.bin/uglifyjs mustache.js > #{minified_file}" -end - desc "Run JSHint" task :hint do sh "./node_modules/.bin/jshint mustache.js" diff --git a/hooks/pre-commit b/hooks/pre-commit index 07fedb749..368c135ed 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -34,9 +34,9 @@ class Bumper # if bumped, do extra stuff and notify the user if @bumped - # minify `mustache.js` using the Rakefile task - puts "> minifying `mustache.js`..." - `rake minify` + # keep .mjs & .js|.min.js in sync + puts "> building and minifying `mustache.mjs`..." + `npm run build` # stage files for commit `git add package.json` @@ -78,7 +78,7 @@ class Bumper end bumper = Bumper.new([ - Source.new('mustache.js', /mustache.version = '([\d\.]*)'/), + Source.new('mustache.mjs', /version: '([\d\.]*)'/), Source.new('mustache.js.nuspec', /([\d\.]*)<\/version>/), ]) bumper.start From f8d7a8df2ad9d0855b88819bab7ec1da9e4afd7d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 7 Dec 2019 11:41:31 +0100 Subject: [PATCH 229/286] Improve pre-commit hook keeping version in sync to handle beta versions The git pre-commit hook we've been using the last years to keeping .version found in `package.json` in sync with the .version field exposed by the source code, did not handle version numbers often used for pre-relases like `beta | new` etc. Therefore making sure it searches for versions that also contains characters, not only numbers separated by dots. That will make sure the following is also seen as valid versions: `3.2.0-beta.0`, whereas before the `-beta` part would cause trouble. --- hooks/pre-commit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/pre-commit b/hooks/pre-commit index 368c135ed..0fbd577c1 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -78,7 +78,7 @@ class Bumper end bumper = Bumper.new([ - Source.new('mustache.mjs', /version: '([\d\.]*)'/), - Source.new('mustache.js.nuspec', /([\d\.]*)<\/version>/), + Source.new('mustache.mjs', /version: '([^']+)'/), + Source.new('mustache.js.nuspec', /([^<]+)<\/version>/), ]) bumper.start From 492d6838a87b2d3bee79178f8e2f6c694dc4e162 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 7 Dec 2019 12:06:16 +0100 Subject: [PATCH 230/286] :ship: bump to version 3.2.0-beta.0 --- mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mustache.js b/mustache.js index 30c76e902..0fc40148e 100644 --- a/mustache.js +++ b/mustache.js @@ -651,7 +651,7 @@ var mustache = { name: 'mustache.js', - version: '3.1.0', + version: '3.2.0-beta.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index dc6733d72..d81423aac 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 3.1.0 + 3.2.0-beta.0 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index c3b6643e2..54d0bb852 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.1.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.0-beta.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 42a8b981b..2b98c3f2e 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -644,7 +644,7 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', - version: '3.1.0', + version: '3.2.0-beta.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index a81b7b3fa..b7fd5680a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.1.0", + "version": "3.2.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 04b576adb..fd4d6fcca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.1.0", + "version": "3.2.0-beta.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From 70d3e7ec4e54e315d8640fd8fc94d5c0f65ca208 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Wed, 18 Dec 2019 21:52:36 +0100 Subject: [PATCH 231/286] :ship: bump to version 3.2.0 --- CHANGELOG.md | 59 +++++++++++++++++++++++++++++++++++++++++++++- mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 7 files changed, 64 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ed83f1f..8a7cffbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,60 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [3.2.0] / 18 December 2019 + +### Added + +* [#728]: Expose ECMAScript Module in addition to UMD (CommonJS, AMD & global scope), by [@phillipj] and [@zekth]. + +### Using mustache.js as an ES module + +To stay backwards compatible with already using projects, the default exposed module format is still UMD. +That means projects using mustache.js as an CommonJS, AMD or global scope module, from npm or directly from github.com +can keep on doing that for now. + +For those projects who would rather want to use mustache.js as an ES module, the `mustache/mustache.mjs` file has to +be `import`ed directly. + +Below are some usage scenarios for different runtimes. + +#### Modern browser with ES module support + +```html + + +``` + +#### [Node.js](https://nodejs.org) (>= v13.2.0 or using --experimental-modules flag) + +```js +// index.mjs +import mustache from 'mustache/mustache.mjs' + +console.log(mustache.render('Hello {{name}}!', { name: 'Santa' })) +// Hello Santa! +``` + +ES Module support for Node.js will be improved in the future when [Conditional Exports](https://nodejs.org/api/esm.html#esm_conditional_exports) +is enabled by default rather than being behind an experimental flag. + +More info in [Node.js ECMAScript Modules docs](https://nodejs.org/api/esm.html). + +#### [Deno](https://deno.land/) + +```js +// index.ts +import mustache from 'https://unpkg.com/mustache@3.2.0/mustache.mjs' + +console.log(mustache.render('Hello {{name}}!', { name: 'Santa' })) +// Hello Santa! +``` + ## [3.1.0] / 13 September 2019 ### Added @@ -357,6 +411,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[3.2.0]: https://github.com/janl/mustache.js/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/janl/mustache.js/compare/v3.0.3...v3.1.0 [3.0.3]: https://github.com/janl/mustache.js/compare/v3.0.2...v3.0.3 [3.0.2]: https://github.com/janl/mustache.js/compare/v3.0.1...v3.0.2 @@ -424,6 +479,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#714]: https://github.com/janl/mustache.js/issues/714 [#716]: https://github.com/janl/mustache.js/issues/716 [#717]: https://github.com/janl/mustache.js/issues/717 +[#728]: https://github.com/janl/mustache.js/issues/728 [@afc163]: https://github.com/afc163 [@andersk]: https://github.com/andersk @@ -468,7 +524,8 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [@TiddoLangerak]: https://github.com/TiddoLangerak [@tomekwi]: https://github.com/tomekwi [@wizawu]: https://github.com/wizawu +[@wol-soft]: https://github.com/wol-soft [@Xcrucifier]: https://github.com/Xcrucifier [@yotammadem]: https://github.com/yotammadem [@yousefcisco]: https://github.com/yousefcisco -[@wol-soft]: https://github.com/wol-soft +[@zekth]: https://github.com/zekth diff --git a/mustache.js b/mustache.js index 0fc40148e..99dddb692 100644 --- a/mustache.js +++ b/mustache.js @@ -651,7 +651,7 @@ var mustache = { name: 'mustache.js', - version: '3.2.0-beta.0', + version: '3.2.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index d81423aac..43dd9c1b7 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 3.2.0-beta.0 + 3.2.0 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index 54d0bb852..b149af275 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.0-beta.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 2b98c3f2e..f6ea74c69 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -644,7 +644,7 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', - version: '3.2.0-beta.0', + version: '3.2.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index b7fd5680a..31ba30495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.0-beta.0", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fd4d6fcca..30007f5d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.0-beta.0", + "version": "3.2.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From aaaa94f4f30d38b5edaecccc7546a5cfc01f4690 Mon Sep 17 00:00:00 2001 From: Eamonn O'Brien-Strain Date: Sun, 29 Dec 2019 16:08:22 -0800 Subject: [PATCH 232/286] Allow JavaScript views to have the .cjs suffix. --- bin/mustache | 3 ++- test/_files/cli.cjs | 3 +++ test/_files/cli.js | 3 +++ test/cli-test.js | 18 ++++++++++++++++++ test/render-helper.js | 5 +++-- 5 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 test/_files/cli.cjs create mode 100644 test/_files/cli.js diff --git a/bin/mustache b/bin/mustache index 2b2250a25..6db073f5d 100755 --- a/bin/mustache +++ b/bin/mustache @@ -131,7 +131,8 @@ function isStdin (view) { } function isJsFile (view) { - return path.extname(view) === '.js'; + var extension = path.extname(view); + return extension === '.js' || extension === '.cjs'; } function wasNotFound (err) { diff --git a/test/_files/cli.cjs b/test/_files/cli.cjs new file mode 100644 index 000000000..dc090e577 --- /dev/null +++ b/test/_files/cli.cjs @@ -0,0 +1,3 @@ +module.exports = { + name: 'LeBron' +}; diff --git a/test/_files/cli.js b/test/_files/cli.js new file mode 100644 index 000000000..dc090e577 --- /dev/null +++ b/test/_files/cli.js @@ -0,0 +1,3 @@ +module.exports = { + name: 'LeBron' +}; diff --git a/test/cli-test.js b/test/cli-test.js index 28e25023e..b40917986 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -76,6 +76,24 @@ describe('Mustache CLI', function () { }); }); + it('can handle view written in JavaScript with .js suffix', function (done) { + exec('bin/mustache test/_files/cli.js test/_files/cli.mustache', function (err, stdout, stderr) { + assert.equal(err, null); + assert.equal(stderr, ''); + assert.equal(stdout, expectedOutput); + done(); + }); + }); + + it('can handle view written in JavaScript with .cjs suffix', function (done) { + exec('bin/mustache test/_files/cli.cjs test/_files/cli.mustache', function (err, stdout, stderr) { + assert.equal(err, null); + assert.equal(stderr, ''); + assert.equal(stdout, expectedOutput); + done(); + }); + }); + it('writes rendered template into the file specified by the third argument', function (done) { var outputFile = 'test/_files/cli_output.txt'; exec('bin/mustache test/_files/cli.json test/_files/cli.mustache ' + outputFile, function (err, stdout, stderr) { diff --git a/test/render-helper.js b/test/render-helper.js index 4b70480ac..1f25dcf1e 100644 --- a/test/render-helper.js +++ b/test/render-helper.js @@ -13,6 +13,7 @@ function getContents (testName, ext) { function getView (testName) { var view = getContents(testName, 'js'); + if (!view) view = getContents(testName, 'cjs'); if (!view) throw new Error('Cannot find view for test "' + testName + '"'); return view; } @@ -34,9 +35,9 @@ if (testToRun) { testNames = testToRun.split(','); } else { testNames = fs.readdirSync(_files).filter(function (file) { - return (/\.js$/).test(file); + return (/\.c?js$/).test(file); }).map(function (file) { - return path.basename(file).replace(/\.js$/, ''); + return path.basename(file).replace(/\.c?js$/, ''); }); } From 8e52a4ac6cf4ed86d0fedb3c4dae643fd7b56998 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 30 Dec 2019 09:16:22 +0100 Subject: [PATCH 233/286] :ship: bump to version 3.2.1 --- CHANGELOG.md | 9 +++++++++ mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 7 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7cffbfd..5ae09ab43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [3.2.1] / 30 December 2019 + +### Fixed + + * [#733]: Allow the CLI to use JavaScript views when the project has ES6 modules enabled, by [@eobrain]. + ## [3.2.0] / 18 December 2019 ### Added @@ -411,6 +417,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[3.2.1]: https://github.com/janl/mustache.js/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/janl/mustache.js/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/janl/mustache.js/compare/v3.0.3...v3.1.0 [3.0.3]: https://github.com/janl/mustache.js/compare/v3.0.2...v3.0.3 @@ -480,6 +487,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#716]: https://github.com/janl/mustache.js/issues/716 [#717]: https://github.com/janl/mustache.js/issues/717 [#728]: https://github.com/janl/mustache.js/issues/728 +[#733]: https://github.com/janl/mustache.js/issues/733 [@afc163]: https://github.com/afc163 [@andersk]: https://github.com/andersk @@ -490,6 +498,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [@cweider]: https://github.com/cweider [@dasilvacontin]: https://github.com/dasilvacontin [@djchie]: https://github.com/djchie +[@eobrain]: https://github.com/eobrain [@EvanLovely]: https://github.com/EvanLovely [@fallenice]: https://github.com/fallenice [@Flaque]: https://github.com/Flaque diff --git a/mustache.js b/mustache.js index 99dddb692..95f3f2176 100644 --- a/mustache.js +++ b/mustache.js @@ -651,7 +651,7 @@ var mustache = { name: 'mustache.js', - version: '3.2.0', + version: '3.2.1', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index 43dd9c1b7..82be2133f 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 3.2.0 + 3.2.1 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index b149af275..61a7aa1b4 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index f6ea74c69..abbbd6d8a 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -644,7 +644,7 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', - version: '3.2.0', + version: '3.2.1', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index 31ba30495..9aaab0e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 30007f5d7..2f5923ab0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.0", + "version": "3.2.1", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From e77fc7c7e893bbcc7f7a873ef32703a4eea32450 Mon Sep 17 00:00:00 2001 From: Andrew Leedham Date: Sat, 11 Jan 2020 18:03:37 +0000 Subject: [PATCH 234/286] Allow template caching to be customised (#731) These changes allows the internal template cache to be customised, either by disabling it complete or providing a custom implementation of how templates are cached. --- mustache.js | 45 ++++++++++++++++++++++++++++++++++-------- mustache.min.js | 2 +- mustache.mjs | 45 ++++++++++++++++++++++++++++++++++-------- test/parse-test.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) diff --git a/mustache.js b/mustache.js index 95f3f2176..da2a7fb48 100644 --- a/mustache.js +++ b/mustache.js @@ -486,14 +486,27 @@ * avoid the need to parse the same template twice. */ function Writer () { - this.cache = {}; + this.templateCache = { + _cache: {}, + set: function set (key, value) { + this._cache[key] = value; + }, + get: function get (key) { + return this._cache[key]; + }, + clear: function clear () { + this._cache = {}; + } + }; } /** * Clears all cached templates in this writer. */ Writer.prototype.clearCache = function clearCache () { - this.cache = {}; + if (typeof this.templateCache !== 'undefined') { + this.templateCache.clear(); + } }; /** @@ -502,13 +515,15 @@ * that is generated from the parse. */ Writer.prototype.parse = function parse (template, tags) { - var cache = this.cache; + var cache = this.templateCache; var cacheKey = template + ':' + (tags || mustache.tags).join(':'); - var tokens = cache[cacheKey]; - - if (tokens == null) - tokens = cache[cacheKey] = parseTemplate(template, tags); + var isCacheEnabled = typeof cache !== 'undefined'; + var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; + if (tokens == undefined) { + tokens = parseTemplate(template, tags); + isCacheEnabled && cache.set(cacheKey, tokens); + } return tokens; }; @@ -660,7 +675,21 @@ to_html: undefined, Scanner: undefined, Context: undefined, - Writer: undefined + Writer: undefined, + /** + * Allows a user to override the default caching strategy, by providing an + * object with set, get and clear methods. This can also be used to disable + * the cache by setting it to the literal `undefined`. + */ + set templateCache (cache) { + defaultWriter.templateCache = cache; + }, + /** + * Gets the default or overridden caching object from the default writer. + */ + get templateCache () { + return defaultWriter.templateCache; + } }; // All high-level mustache.* functions use this writer. diff --git a/mustache.min.js b/mustache.min.js index 61a7aa1b4..20fc2ab0d 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index abbbd6d8a..626d3f123 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -479,14 +479,27 @@ Context.prototype.lookup = function lookup (name) { * avoid the need to parse the same template twice. */ function Writer () { - this.cache = {}; + this.templateCache = { + _cache: {}, + set: function set (key, value) { + this._cache[key] = value; + }, + get: function get (key) { + return this._cache[key]; + }, + clear: function clear () { + this._cache = {}; + } + }; } /** * Clears all cached templates in this writer. */ Writer.prototype.clearCache = function clearCache () { - this.cache = {}; + if (typeof this.templateCache !== 'undefined') { + this.templateCache.clear(); + } }; /** @@ -495,13 +508,15 @@ Writer.prototype.clearCache = function clearCache () { * that is generated from the parse. */ Writer.prototype.parse = function parse (template, tags) { - var cache = this.cache; + var cache = this.templateCache; var cacheKey = template + ':' + (tags || mustache.tags).join(':'); - var tokens = cache[cacheKey]; - - if (tokens == null) - tokens = cache[cacheKey] = parseTemplate(template, tags); + var isCacheEnabled = typeof cache !== 'undefined'; + var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; + if (tokens == undefined) { + tokens = parseTemplate(template, tags); + isCacheEnabled && cache.set(cacheKey, tokens); + } return tokens; }; @@ -653,7 +668,21 @@ var mustache = { to_html: undefined, Scanner: undefined, Context: undefined, - Writer: undefined + Writer: undefined, + /** + * Allows a user to override the default caching strategy, by providing an + * object with set, get and clear methods. This can also be used to disable + * the cache by setting it to the literal `undefined`. + */ + set templateCache (cache) { + defaultWriter.templateCache = cache; + }, + /** + * Gets the default or overridden caching object from the default writer. + */ + get templateCache () { + return defaultWriter.templateCache; + } }; // All high-level mustache.* functions use this writer. diff --git a/test/parse-test.js b/test/parse-test.js index da225c8e5..1fb2cabe4 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -56,8 +56,14 @@ var expectations = { : [ [ '#', 'foo', 0, 8, [ [ '#', 'a', 11, 17, [ [ 'text', ' ', 18, 22 ], [ 'name', 'b', 22, 27 ], [ 'text', '\n', 27, 28 ] ], 30 ] ], 37 ] ] }; +var originalTemplateCache; +before(function () { + originalTemplateCache = Mustache.templateCache; +}); + beforeEach(function (){ Mustache.clearCache(); + Mustache.templateCache = originalTemplateCache; }); describe('Mustache.parse', function () { @@ -153,4 +159,47 @@ describe('Mustache.parse', function () { }); }); + describe('when parsing a template with caching disabled and the same tags second time, do not return the cached tokens', function () { + it('returns different tokens for the latter parse', function () { + Mustache.templateCache = undefined; + var template = '{{foo}}[bar]'; + var parsedResult1 = Mustache.parse(template); + var parsedResult2 = Mustache.parse(template); + + assert.deepEqual(parsedResult1, parsedResult2); + assert.ok(parsedResult1 !== parsedResult2); + }); + }); + + describe('when parsing a template with custom caching and the same tags second time, do not return the cached tokens', function () { + it('returns the same tokens for the latter parse', function () { + Mustache.templateCache = { + _cache: [], + set: function set (key, value) { + this._cache.push([key, value]); + }, + get: function get (key) { + var cacheLength = this._cache.length; + for (var i = 0; i < cacheLength; i++) { + var entry = this._cache[i]; + if (entry[0] === key) { + return entry[1]; + } + } + return undefined; + }, + clear: function clear () { + this._cache = []; + } + }; + + var template = '{{foo}}[bar]'; + var parsedResult1 = Mustache.parse(template); + var parsedResult2 = Mustache.parse(template); + + assert.deepEqual(parsedResult1, parsedResult2); + assert.ok(parsedResult1 === parsedResult2); + }); + }); + }); From 185fd6be94bc4e9bd9f795c29fc4250fc83c04c8 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 20:35:44 +0100 Subject: [PATCH 235/286] Update usage examples to not include jQuery Historically jQuery was seen as an absolute minimal to create anything with JavaScript. That time has past now that relatively modern browsers has excellent support for a lot of the things jQuery helped us with -- at least with the trivial examples shown in our README. Therefore removing jQuery usage from our examples as that should not be a necessary dependency in these examples. --- README.md | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1ce26d2ef..43c725f02 100644 --- a/README.md +++ b/README.md @@ -137,38 +137,42 @@ There are several techniques that can be used to load templates and hand them to #### Include Templates -If you need a template for a dynamic part in a static website, you can consider including the template in the static HTML file to avoid loading templates separately. Here's a small example using `jQuery`: +If you need a template for a dynamic part in a static website, you can consider including the template in the static HTML file to avoid loading templates separately. Here's a small example: + +```js +// file: render.js + +function renderHello() { + var template = document.getElementById('template').innerHTML; + var rendered = Mustache.render(template, { name: 'Luke' }); + document.getElementById('target').innerHTML = rendered; +} +``` ```html - - -
Loading...
- - + +
Loading...
+ + + + + ``` -```js -function loadUser() { - var template = $('#template').html(); - Mustache.parse(template); // optional, speeds up future uses - var rendered = Mustache.render(template, {name: "Luke"}); - $('#target').html(rendered); -} -``` - #### Load External Templates -If your templates reside in individual files, you can load them asynchronously and render them when they arrive. Another example using `jQuery`: +If your templates reside in individual files, you can load them asynchronously and render them when they arrive. Another example using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch): ```js -function loadUser() { - $.get('template.mst', function(template) { - var rendered = Mustache.render(template, {name: "Luke"}); - $('#target').html(rendered); +function renderHello() { + fetch('template.mustache').then(function (template) { + var template = document.getElementById('template').innerHTML; + var rendered = Mustache.render(template, { name: 'Luke' }); + document.getElementById('target').innerHTML = rendered; }); } ``` From bd742d5080f70008dc35e58a1d87152938d2f98b Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 20:41:17 +0100 Subject: [PATCH 236/286] Add response.text() from fetch() in README example When recently updating the usage examples in the README, it blooper was introduced where `response.text()` was forgotten when retrieving a mustache template with `window.fetch()`. --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 43c725f02..b16fd1344 100644 --- a/README.md +++ b/README.md @@ -169,11 +169,13 @@ If your templates reside in individual files, you can load them asynchronously a ```js function renderHello() { - fetch('template.mustache').then(function (template) { - var template = document.getElementById('template').innerHTML; - var rendered = Mustache.render(template, { name: 'Luke' }); - document.getElementById('target').innerHTML = rendered; - }); + fetch('template.mustache') + .then((response) => response.text()) + .then((template) => { + var template = document.getElementById('template').innerHTML; + var rendered = Mustache.render(template, { name: 'Luke' }); + document.getElementById('target').innerHTML = rendered; + }); } ``` From c41045baf8fcb14cc6b48db4b784dcef3e82cd26 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 21:07:08 +0100 Subject: [PATCH 237/286] Removing the rtype API definitions from README Primarily because [rtype](https://github.com/ericelliott/rtype) seems to be a stalled project that hasn't gotten updated for 4 years. Similar more up-to-date definitions can be found in the TypeScript definitions: [@types/mustache](https://www.npmjs.com/package/@types/mustache). --- README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/README.md b/README.md index b16fd1344..60bb97ef7 100644 --- a/README.md +++ b/README.md @@ -106,29 +106,6 @@ var output = Mustache.render("{{title}} spends {{calc}}", view); In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.com/) template and 2) a `view` object that contains the data and code needed to render the template. -## API - -Following is an [rtype](https://git.io/rtype) signature of the most commonly used functions. - -```js -Mustache.render( - template : String, - view : Object, - partials? : Object, - tags = ['{{', '}}'] : Tags, -) => String - -Mustache.parse( - template : String, - tags = ['{{', '}}'] : Tags, -) => Token[] - -interface Token [String, String, Number, Number, Token[]?, Number?] - -interface Tags [String, String] - -``` - ## Templates A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. There are several types of tags available in mustache.js, described below. From 39ee6ffc2c25ce8f89a906104498450bbfb31527 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 21:11:41 +0100 Subject: [PATCH 238/286] Point out it's a zero-dependency package in README More and more of the community seems to be encouraging use of zero-dependency packages. We might as well point that out early on in our README since that's how mustache.js always has been and planned to be going forward. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60bb97ef7..610ab0a73 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/janl/mustache.js.svg?branch=master)](https://travis-ci.org/janl/mustache.js) [![Gitter chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/janl/mustache.js) -[mustache.js](http://github.com/janl/mustache.js) is an implementation of the [mustache](http://mustache.github.com/) template system in JavaScript. +[mustache.js](http://github.com/janl/mustache.js) is a zero-dependency implementation of the [mustache](http://mustache.github.com/) template system in JavaScript. [Mustache](http://mustache.github.com/) is a logic-less template syntax. It can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object. From 7f94f138cbec5592529605cc8af02b58ed20c456 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 21:27:55 +0100 Subject: [PATCH 239/286] Move CLI and contribute section down in README Primarily because the these sections were very given a lot of attention, high up in the README, where ideally usage and basic syntax should have priority. --- README.md | 126 ++++++++++++++++++++++++++---------------------------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 610ab0a73..0695032c1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ For a language-agnostic overview of mustache's template syntax, see the `mustach You can use mustache.js to render mustache templates anywhere you can use JavaScript. This includes web browsers, server-side environments such as [Node.js](http://nodejs.org/), and [CouchDB](http://couchdb.apache.org/) views. -mustache.js ships with support for both the [CommonJS](http://www.commonjs.org/) module API and the [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) API (AMD). +mustache.js ships with support for the [CommonJS](http://www.commonjs.org/) module API, the [Asynchronous Module Definition](https://github.com/amdjs/amdjs-api/wiki/AMD) API (AMD) and [ECMAScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). + +In addition to being a package to be used programmatically, you can use it as a [command line tool](#command-line-tool). And this will be your templates after you use Mustache: @@ -30,65 +32,6 @@ You can get Mustache via [npm](http://npmjs.com). $ npm install mustache --save ``` -## Command line tool - -mustache.js is shipped with a Node.js based command line tool. It might be installed as a global tool on your computer to render a mustache template of some kind - -```bash -$ npm install -g mustache - -$ mustache dataView.json myTemplate.mustache > output.html -``` - -also supports stdin. - -```bash -$ cat dataView.json | mustache - myTemplate.mustache > output.html -``` - -or as a package.json `devDependency` in a build process maybe? - -```bash -$ npm install mustache --save-dev -``` - -```json -{ - "scripts": { - "build": "mustache dataView.json myTemplate.mustache > public/output.html" - } -} -``` -```bash -$ npm run build -``` - -The command line tool is basically a wrapper around `Mustache.render` so you get all the features. - -If your templates use partials you should pass paths to partials using `-p` flag: - -```bash -$ mustache -p path/to/partial1.mustache -p path/to/partial2.mustache dataView.json myTemplate.mustache -``` - -## Who uses mustache.js? - -An updated list of mustache.js users is kept [on the Github wiki](https://github.com/janl/mustache.js/wiki/Beard-Competition). Add yourself or your company if you use mustache.js! - -## Contributing - -mustache.js is a mature project, but it continues to actively invite maintainers. You can help out a high-profile project that is used in a lot of places on the web. There is [plenty](https://github.com/janl/mustache.js/issues) of [work](https://github.com/janl/mustache.js/pulls) to do. No big commitment required, if all you do is review a single [Pull Request](https://github.com/janl/mustache.js/pulls), you are a maintainer. And a hero. - -### Your First Contribution - -- review a [Pull Request](https://github.com/janl/mustache.js/pulls) -- fix an [Issue](https://github.com/janl/mustache.js/issues) -- update the [documentation](https://github.com/janl/mustache.js#usage) -- make a website -- write a tutorial - -* * * - ## Usage Below is a quick example how to use mustache.js: @@ -531,6 +474,47 @@ Mustache.parse(template); Mustache.render(template, view); ``` +## Command line tool + +mustache.js is shipped with a Node.js based command line tool. It might be installed as a global tool on your computer to render a mustache template of some kind + +```bash +$ npm install -g mustache + +$ mustache dataView.json myTemplate.mustache > output.html +``` + +also supports stdin. + +```bash +$ cat dataView.json | mustache - myTemplate.mustache > output.html +``` + +or as a package.json `devDependency` in a build process maybe? + +```bash +$ npm install mustache --save-dev +``` + +```json +{ + "scripts": { + "build": "mustache dataView.json myTemplate.mustache > public/output.html" + } +} +``` +```bash +$ npm run build +``` + +The command line tool is basically a wrapper around `Mustache.render` so you get all the features. + +If your templates use partials you should pass paths to partials using `-p` flag: + +```bash +$ mustache -p path/to/partial1.mustache -p path/to/partial2.mustache dataView.json myTemplate.mustache +``` + ## Plugins for JavaScript Libraries mustache.js may be built specifically for several different client libraries, including the following: @@ -549,6 +533,7 @@ $ rake dojo $ rake yui3 $ rake qooxdoo ``` + ## Testing In order to run the tests you'll need to install [Node.js](http://nodejs.org/). @@ -580,6 +565,7 @@ Then, you can run the test with: ```bash $ TEST=mytest npm run test-render ``` + ### Browser tests Browser tests are not included in `npm test` as they run for too long, although they are ran automatically on Travis when merged into master. Run browser tests locally in any browser: @@ -588,14 +574,22 @@ $ npm run test-browser-local ``` then point your browser to `http://localhost:8080/__zuul` -### Troubleshooting +## Who uses mustache.js? -#### npm install fails +An updated list of mustache.js users is kept [on the Github wiki](https://github.com/janl/mustache.js/wiki/Beard-Competition). Add yourself or your company if you use mustache.js! + +## Contributing + +mustache.js is a mature project, but it continues to actively invite maintainers. You can help out a high-profile project that is used in a lot of places on the web. No big commitment required, if all you do is review a single [Pull Request](https://github.com/janl/mustache.js/pulls), you are a maintainer. And a hero. + +### Your First Contribution + +- review a [Pull Request](https://github.com/janl/mustache.js/pulls) +- fix an [Issue](https://github.com/janl/mustache.js/issues) +- update the [documentation](https://github.com/janl/mustache.js#usage) +- make a website +- write a tutorial -Ensure to have a recent version of npm installed. While developing this project requires npm with support for `^` version ranges. -```bash -$ npm install -g npm -``` ## Thanks mustache.js wouldn't kick ass if it weren't for these fine souls: From 3bdd27c4e762f665232dc46186c7639e60dbd70e Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 11 Jan 2020 22:00:50 +0100 Subject: [PATCH 240/286] Add a section about TypeScript defs in README Since TypeScript usage has exploded the last years but this is package is written in JavaScript, we might at least reference the external DefinitelyTyped package @types/mustache that has a somewhat up-to-date set of type definitions for most of the package. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0695032c1..9c79082b7 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,11 @@ $ rake yui3 $ rake qooxdoo ``` +## TypeScript + +Since the source code of this package is written in JavaScript, we follow the [TypeScript publishing docs](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html) preferred approach +by having type definitions available via [@types/mustache](https://www.npmjs.com/package/@types/mustache). + ## Testing In order to run the tests you'll need to install [Node.js](http://nodejs.org/). From 5938104ba717a43dfd9e963dd67accdc1a526421 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 13 Jan 2020 21:02:27 +0100 Subject: [PATCH 241/286] Use fetched template in usage example Blooper introduced when making the usage examples in README.md more modern a couple of days ago, where the example fetching the mustache template over HTTP, didn't in fact use the fetched template. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9c79082b7..a3e09aa99 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ function renderHello() { fetch('template.mustache') .then((response) => response.text()) .then((template) => { - var template = document.getElementById('template').innerHTML; var rendered = Mustache.render(template, { name: 'Luke' }); document.getElementById('target').innerHTML = rendered; }); From f3012a2477a90ac98a1f8cbd2a53bafaeb4adc7d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Wed, 15 Jan 2020 21:45:00 +0100 Subject: [PATCH 242/286] Remove mustache.to_html() (#735) In the spirit of keeping the public API of mustache.js as small as possible for maintainence reasons, the undocumented and un-tested `.to_html()` method is removed. The functionality involved, where it can rather invoke an optional function provided with the result of `.render()`, instead of returning it as a string like `.render()` does, is something that using projects very easily can do themselfs -- it does not have to be provided by mustache.js. --- mustache.js | 15 --------------- mustache.min.js | 2 +- mustache.mjs | 15 --------------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/mustache.js b/mustache.js index da2a7fb48..289e14223 100644 --- a/mustache.js +++ b/mustache.js @@ -672,7 +672,6 @@ escape: undefined, parse: undefined, render: undefined, - to_html: undefined, Scanner: undefined, Context: undefined, Writer: undefined, @@ -727,20 +726,6 @@ return defaultWriter.render(template, view, partials, tags); }; - // This is here for backwards compatibility with 0.4.x., - /*eslint-disable */ // eslint wants camel cased function name - mustache.to_html = function to_html (template, view, partials, send) { - /*eslint-enable*/ - - var result = mustache.render(template, view, partials); - - if (isFunction(send)) { - send(result); - } else { - return result; - } - }; - // 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/mustache.min.js b/mustache.min.js index 20fc2ab0d..fb94303a8 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,to_html:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 626d3f123..29b000624 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -665,7 +665,6 @@ var mustache = { escape: undefined, parse: undefined, render: undefined, - to_html: undefined, Scanner: undefined, Context: undefined, Writer: undefined, @@ -720,20 +719,6 @@ mustache.render = function render (template, view, partials, tags) { return defaultWriter.render(template, view, partials, tags); }; -// This is here for backwards compatibility with 0.4.x., -/*eslint-disable */ // eslint wants camel cased function name -mustache.to_html = function to_html (template, view, partials, send) { - /*eslint-enable*/ - - var result = mustache.render(template, view, partials); - - if (isFunction(send)) { - send(result); - } else { - return result; - } -}; - // Export the escaping function so that the user may override it. // See https://github.com/janl/mustache.js/issues/244 mustache.escape = escapeHtml; From aca97b82c80e8fd1d36162e05e4b289380965d96 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Thu, 16 Jan 2020 14:31:18 +0100 Subject: [PATCH 243/286] :ship: bump to version 4.0.0 --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++ mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae09ab43..a5c343d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.0.0] / 16 January 2020 + +Majority of using projects don't have to worry by this being a new major version. + +**TLDR;** if your project manipulates `Writer.prototype.parse | Writer.cache` directly or uses `.to_html()`, you probably have to change that code. + +This release allows the internal template cache to be customised, either by disabling it completely +or provide a custom strategy deciding how the cache should behave when mustache.js parses templates. + +```js +const mustache = require('mustache'); + +// disable caching +Mustache.templateCache = undefined; + +// or use a built-in Map in modern environments +Mustache.templateCache = new Map(); +``` + +Projects that wanted to customise the caching behaviour in earlier versions of mustache.js were forced to +override internal method responsible for parsing templates; `Writer.prototype.parse`. In short, that was unfortunate +because there is more than caching happening in that method. + +We've improved that now by introducing a first class API that only affects template caching. + +The default template cache behaves as before and is still compatible with older JavaScript environments. +For those who wants to provide a custom more sopisiticated caching strategy, one can do that with an object that adheres to the following requirements: + +```ts +{ + set(cacheKey: string, value: string): void + get(cacheKey: string): string | undefined + clear(): void +} +``` + +### Added + +* [#731]: Allow template caching to be customised, by [@AndrewLeedham]. + +### Removed + +* [#735]: Remove `.to_html()`, by [@phillipj]. + ## [3.2.1] / 30 December 2019 ### Fixed @@ -417,6 +461,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[4.0.0]: https://github.com/janl/mustache.js/compare/v3.2.1...v4.0.0 [3.2.1]: https://github.com/janl/mustache.js/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/janl/mustache.js/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/janl/mustache.js/compare/v3.0.3...v3.1.0 @@ -488,10 +533,13 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#717]: https://github.com/janl/mustache.js/issues/717 [#728]: https://github.com/janl/mustache.js/issues/728 [#733]: https://github.com/janl/mustache.js/issues/733 +[#731]: https://github.com/janl/mustache.js/issues/731 +[#735]: https://github.com/janl/mustache.js/issues/735 [@afc163]: https://github.com/afc163 [@andersk]: https://github.com/andersk [@Andersos]: https://github.com/Andersos +[@AndrewLeedham]: https://github.com/AndrewLeedham [@bbrooks]: https://github.com/bbrooks [@calvinf]: https://github.com/calvinf [@cmbuckley]: https://github.com/cmbuckley diff --git a/mustache.js b/mustache.js index 289e14223..2f5e2427b 100644 --- a/mustache.js +++ b/mustache.js @@ -666,7 +666,7 @@ var mustache = { name: 'mustache.js', - version: '3.2.1', + version: '4.0.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index 82be2133f..09da8ddd0 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 3.2.1 + 4.0.0 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index fb94303a8..13b5a6354 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"3.2.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 29b000624..d5e19ade5 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -659,7 +659,7 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', - version: '3.2.1', + version: '4.0.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index 9aaab0e64..cbe73498b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.1", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2f5923ab0..1b796ff69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "3.2.1", + "version": "4.0.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From f3bd88837eda6f08bef5ace4b302f8d0e1cc8e14 Mon Sep 17 00:00:00 2001 From: Ricardo Aielo Date: Sun, 15 Mar 2020 12:46:34 -0700 Subject: [PATCH 244/286] Fix custom delimiters in nested partials (#739) Closes https://github.com/janl/mustache.js/pull/738 --- mustache.js | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- test/partial-test.js | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index 2f5e2427b..921c324bf 100644 --- a/mustache.js +++ b/mustache.js @@ -644,7 +644,7 @@ if (tagIndex == 0 && indentation) { indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } - return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue); + return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue, tags); } }; diff --git a/mustache.min.js b/mustache.min.js index 13b5a6354..dc1994605 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index d5e19ade5..9fe1b76c1 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -637,7 +637,7 @@ Writer.prototype.renderPartial = function renderPartial (token, context, partial if (tagIndex == 0 && indentation) { indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } - return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue); + return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue, tags); } }; diff --git a/test/partial-test.js b/test/partial-test.js index d1f1bb1e5..a1d629f6c 100644 --- a/test/partial-test.js +++ b/test/partial-test.js @@ -157,4 +157,19 @@ describe('Partials spec', function () { var renderResult = Mustache.render(template, data, partials); assert.equal(renderResult, expected); }); + + it('Nested partials should support custom delimiters.', function () { + var tags = ["[[", "]]"]; + var template = '[[> level1 ]]'; + var partials = { + level1: 'partial 1\n[[> level2]]', + level2: 'partial 2\n[[> level3]]', + level3: 'partial 3\n[[> level4]]', + level4: 'partial 4\n[[> level5]]', + level5: 'partial 5', + }; + var expected = 'partial 1\npartial 2\npartial 3\npartial 4\npartial 5'; + var renderResult = Mustache.render(template, {}, partials, tags); + assert.equal(renderResult, expected); + }); }); From 1de94bbdd3fe4b903cfbc084ebaaccfd1299dd3f Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 15 Mar 2020 21:15:41 +0100 Subject: [PATCH 245/286] :ship: bump to version 4.0.1 --- CHANGELOG.md | 9 +++++++++ mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 7 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c343d59..df3fcfe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.0.1] / 15 March 2020 + +### Fixed + + * [#739]: Fix custom delimiters in nested partials, by [@aielo]. + ## [4.0.0] / 16 January 2020 Majority of using projects don't have to worry by this being a new major version. @@ -461,6 +467,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[4.0.1]: https://github.com/janl/mustache.js/compare/v4.0.0...v4.0.1 [4.0.0]: https://github.com/janl/mustache.js/compare/v3.2.1...v4.0.0 [3.2.1]: https://github.com/janl/mustache.js/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/janl/mustache.js/compare/v3.1.0...v3.2.0 @@ -535,8 +542,10 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#733]: https://github.com/janl/mustache.js/issues/733 [#731]: https://github.com/janl/mustache.js/issues/731 [#735]: https://github.com/janl/mustache.js/issues/735 +[#739]: https://github.com/janl/mustache.js/issues/739 [@afc163]: https://github.com/afc163 +[@aielo]: https://github.com/aielo [@andersk]: https://github.com/andersk [@Andersos]: https://github.com/Andersos [@AndrewLeedham]: https://github.com/AndrewLeedham diff --git a/mustache.js b/mustache.js index 921c324bf..6ed26c7be 100644 --- a/mustache.js +++ b/mustache.js @@ -666,7 +666,7 @@ var mustache = { name: 'mustache.js', - version: '4.0.0', + version: '4.0.1', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index 09da8ddd0..bffc3f4bd 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 4.0.0 + 4.0.1 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index dc1994605..173c85af5 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 9fe1b76c1..5f31ace48 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -659,7 +659,7 @@ Writer.prototype.rawValue = function rawValue (token) { var mustache = { name: 'mustache.js', - version: '4.0.0', + version: '4.0.1', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index cbe73498b..bf36f8e8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.0.0", + "version": "4.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1b796ff69..c7cf56b3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.0.0", + "version": "4.0.1", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From b747b922df087d2dae749cdc61cd568f7f98ecb6 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 8 Apr 2020 19:51:25 +1000 Subject: [PATCH 246/286] docs: Fix simple typo, skiped -> skipped (#749) There is a small typo in test/mustache-spec-test.js. Should read `skipped` rather than `skiped`. --- test/mustache-spec-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mustache-spec-test.js b/test/mustache-spec-test.js index 181c0e7fd..0e5001899 100644 --- a/test/mustache-spec-test.js +++ b/test/mustache-spec-test.js @@ -32,7 +32,7 @@ var skipTests = { ] }; -// You can run the skiped tests by setting the NOSKIP environment variable to +// You can run the skipped tests by setting the NOSKIP environment variable to // true (e.g. NOSKIP=true mocha test/mustache-spec-test.js) var noSkip = process.env.NOSKIP; From 65af14d1e01c74fc94337b9241909d7c153f5cfc Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 16 May 2020 23:38:36 +0200 Subject: [PATCH 247/286] Update deno version from v0.21 -> v1.0.0 in usage tests Because it seems the standard library for v0.21 that was used in the usage tests before has been removed, it returns 404 now.. --- .github/workflows/verify.yml | 4 ++-- test/module-systems/deno-test.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8220851e8..5edf66e5f 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -96,9 +96,9 @@ jobs: steps: - uses: actions/checkout@v1 - - uses: denolib/setup-deno@v1 + - uses: denolib/setup-deno@master with: - deno-version: 'v0.23.0' + deno-version: 'v1.0.0' - run: deno --version - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts diff --git a/test/module-systems/deno-test.ts b/test/module-systems/deno-test.ts index 9325ae0b4..282a97dc2 100644 --- a/test/module-systems/deno-test.ts +++ b/test/module-systems/deno-test.ts @@ -1,5 +1,4 @@ -import { test } from "https://deno.land/std@v0.21.0/testing/mod.ts"; -import { assertEquals } from "https://deno.land/std@v0.21.0/testing/asserts.ts"; +import { assertEquals } from "https://deno.land/std@v0.51.0/testing/asserts.ts"; import mustache from "../../mustache.mjs"; const view = { @@ -9,7 +8,7 @@ const view = { } }; -test(function canUseMustache() { +Deno.test("can use mustache", () => { assertEquals( mustache.render("{{title}} spends {{calc}}", view), "Joe spends 6" From 4dc00b8a9c0857476bd41303477a89d249276373 Mon Sep 17 00:00:00 2001 From: urain39 <16981964+urain39@users.noreply.github.com> Date: Sat, 16 May 2020 17:06:36 -0500 Subject: [PATCH 248/286] Optimize `Writer.prototype.escapedValue` for numbers (#754) These changes optimizes the performance of escaping number values, by not passing them through the ordinary HTML escaping that is done on string values. --- mustache.js | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index 6ed26c7be..c2c57ad26 100644 --- a/mustache.js +++ b/mustache.js @@ -657,7 +657,7 @@ Writer.prototype.escapedValue = function escapedValue (token, context) { var value = context.lookup(token[1]); if (value != null) - return mustache.escape(value); + return typeof value === 'number' ? String(value) : mustache.escape(value); }; Writer.prototype.rawValue = function rawValue (token) { diff --git a/mustache.min.js b/mustache.min.js index 173c85af5..6a7fde016 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"?String(value):mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 5f31ace48..2344a25dc 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -650,7 +650,7 @@ Writer.prototype.unescapedValue = function unescapedValue (token, context) { Writer.prototype.escapedValue = function escapedValue (token, context) { var value = context.lookup(token[1]); if (value != null) - return mustache.escape(value); + return typeof value === 'number' ? String(value) : mustache.escape(value); }; Writer.prototype.rawValue = function rawValue (token) { From 3182bd1dc62750b8b378e2c260eb2feea249d4a8 Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Sat, 30 May 2020 23:53:54 -0700 Subject: [PATCH 249/286] Add .idea to .gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index efad455ca..76def77e9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,5 @@ qooxdoo.mustache.js test/render-test-browser.js npm-debug.log .DS_Store - -test/render-test-browser.js \ No newline at end of file +test/render-test-browser.js +.idea/ From a7f1c3dde9999a044284d34001de392351de0a01 Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Sun, 31 May 2020 00:18:46 -0700 Subject: [PATCH 250/286] Update and simplify package.json --- package.json | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c7cf56b3c..99b95a2cc 100644 --- a/package.json +++ b/package.json @@ -14,24 +14,19 @@ "templates", "ejs" ], - "main": "./mustache.js", + "main": "mustache.js", "bin": { "mustache": "./bin/mustache" }, "files": [ - "mustache.js", + "bin/", "mustache.mjs", "mustache.min.js", - "bin", - "wrappers", - "LICENSE" + "wrappers/" ], "volo": { "url": "https://raw.github.com/janl/mustache.js/{version}/mustache.js" }, - "engines": { - "npm": ">=1.4.0" - }, "scripts": { "build": "rollup mustache.mjs --file mustache.js --format umd --name Mustache --banner '// This file has been generated from mustache.mjs' && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", From 05a83453408b3fded93db66288f09191989dc7a0 Mon Sep 17 00:00:00 2001 From: Pierre Carru Date: Fri, 3 Feb 2017 17:25:01 +0100 Subject: [PATCH 251/286] readme: sections can be rendered 0 times If the key is false or empty --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3e09aa99..cbfadf2a3 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Output: ### Sections -Sections render blocks of text one or more times, depending on the value of the key in the current context. +Sections render blocks of text 0 or more times, depending on the value of the key in the current context. A section begins with a pound and ends with a slash. That is, `{{#person}}` begins a `person` section, while `{{/person}}` ends it. The text between the two tags is referred to as that section's "block". From 4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 22 Jun 2020 22:26:59 +0200 Subject: [PATCH 252/286] Use zero instead of 0 in blocks README example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbfadf2a3..5c25dd470 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Output: ### Sections -Sections render blocks of text 0 or more times, depending on the value of the key in the current context. +Sections render blocks of text zero or more times, depending on the value of the key in the current context. A section begins with a pound and ends with a slash. That is, `{{#person}}` begins a `person` section, while `{{/person}}` ends it. The text between the two tags is referred to as that section's "block". From eb523bdfa17be07121d43b35fa1287fd53e7affa Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Fri, 9 Oct 2020 10:48:23 +0300 Subject: [PATCH 253/286] Modify build command so that mustache.js can be built on Windows Windows shell does not recognize 'single quotes' the same way as bash. The 'single quotes' in the build command have been changed to "double quotes" instead. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99b95a2cc..e7fee427c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "url": "https://raw.github.com/janl/mustache.js/{version}/mustache.js" }, "scripts": { - "build": "rollup mustache.mjs --file mustache.js --format umd --name Mustache --banner '// This file has been generated from mustache.mjs' && uglifyjs mustache.js > mustache.min.js", + "build": "rollup mustache.mjs --file mustache.js --format umd --name Mustache --banner \"// This file has been generated from mustache.mjs\" && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", "test-lint": "eslint mustache.mjs bin/mustache test/**/*.js", "test-unit": "mocha --reporter spec test/*-test.js", From 67eb95cf5f46673ec6962b5f67c57a590b39539b Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Fri, 9 Oct 2020 10:51:36 +0300 Subject: [PATCH 254/286] Fix eslint failure in test/partial-test.js The linter enforces 'single quotes' but two strings in this test file were written using "double quotes". This issue was causing tests to fail. --- test/partial-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/partial-test.js b/test/partial-test.js index a1d629f6c..54d62b581 100644 --- a/test/partial-test.js +++ b/test/partial-test.js @@ -159,7 +159,7 @@ describe('Partials spec', function () { }); it('Nested partials should support custom delimiters.', function () { - var tags = ["[[", "]]"]; + var tags = ['[[', ']]']; var template = '[[> level1 ]]'; var partials = { level1: 'partial 1\n[[> level2]]', From 9891d4f40a2c4b4728308b2fc06380ac166a6e6b Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Fri, 9 Oct 2020 11:11:29 +0300 Subject: [PATCH 255/286] Render function now recognizes a config object argument If an object is provided for what used to be the `tags` argument instead of an array, then the object is treated as a "config" object. A config object can have a `tags` attribute which behaves the same as the `tags` argument, and it can also optionally have an `escape` argument that is used for providing a custom escape function for that render call. The rationale for this addition is that providing an escape function here, when rendering a template, is a safer pattern for specifying an escape function than modifying the global mustache.escape attribute when a project may need to use mustache to render more than just one kind of content, e.g. both HTML and Latex templates in the same project. --- mustache.js | 75 +++++++++++++++++++++++++++++++++------------ mustache.min.js | 2 +- mustache.mjs | 81 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 113 insertions(+), 45 deletions(-) diff --git a/mustache.js b/mustache.js index c2c57ad26..7b6786630 100644 --- a/mustache.js +++ b/mustache.js @@ -536,14 +536,25 @@ * also be a function that is used to load partial templates on the fly * that takes a single argument: the name of the partial. * - * If the optional `tags` argument is given here it must be an array with two + * If the optional `config` argument is given here, then it should be an + * object with a `tags` attribute or an `escape` attribute or both. + * If an array is passed, then it will be interpreted the same way as + * a `tags` attribute on a `config` object. + * + * The `tags` attribute of a `config` object must be an array with two * string values: the opening and closing tags used in the template (e.g. * [ "<%", "%>" ]). The default is to mustache.tags. + * + * The `escape` attribute of a `config` object must be a function which + * accepts a string as input and outputs a safely escaped string. + * If an `escape` function is not provided, then an HTML-safe string + * escaping function is used as the default. */ - Writer.prototype.render = function render (template, view, partials, tags) { + Writer.prototype.render = function render (template, view, partials, config) { + var tags = this.getConfigTags(config); var tokens = this.parse(template, tags); var context = (view instanceof Context) ? view : new Context(view, undefined); - return this.renderTokens(tokens, context, partials, template, tags); + return this.renderTokens(tokens, context, partials, template, config); }; /** @@ -555,7 +566,7 @@ * If the template doesn't use higher-order sections, this argument may * be omitted. */ - Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { + Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) { var buffer = ''; var token, symbol, value; @@ -564,11 +575,11 @@ token = tokens[i]; symbol = token[0]; - if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); - else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); - else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config); + else if (symbol === '>') value = this.renderPartial(token, context, partials, config); else if (symbol === '&') value = this.unescapedValue(token, context); - else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context, config); else if (symbol === 'text') value = this.rawValue(token); if (value !== undefined) @@ -578,7 +589,7 @@ return buffer; }; - Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { + Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) { var self = this; var buffer = ''; var value = context.lookup(token[1]); @@ -593,10 +604,10 @@ if (isArray(value)) { for (var j = 0, valueLength = value.length; j < valueLength; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); } } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { - buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); } else if (isFunction(value)) { if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); @@ -607,18 +618,18 @@ if (value != null) buffer += value; } else { - buffer += this.renderTokens(token[4], context, partials, originalTemplate); + buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); } return buffer; }; - Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { + Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) { var value = context.lookup(token[1]); // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 if (!value || (isArray(value) && value.length === 0)) - return this.renderTokens(token[4], context, partials, originalTemplate); + return this.renderTokens(token[4], context, partials, originalTemplate, config); }; Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { @@ -632,8 +643,9 @@ return partialByNl.join('\n'); }; - Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { + Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) { if (!partials) return; + var tags = this.getConfigTags(config); var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; if (value != null) { @@ -644,7 +656,8 @@ if (tagIndex == 0 && indentation) { indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } - return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue, tags); + var tokens = this.parse(indentedValue, tags); + return this.renderTokens(tokens, context, partials, indentedValue, config); } }; @@ -654,16 +667,38 @@ return value; }; - Writer.prototype.escapedValue = function escapedValue (token, context) { + Writer.prototype.escapedValue = function escapedValue (token, context, config) { + var escape = this.getConfigEscape(config) || mustache.escape; var value = context.lookup(token[1]); if (value != null) - return typeof value === 'number' ? String(value) : mustache.escape(value); + return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value); }; Writer.prototype.rawValue = function rawValue (token) { return token[1]; }; + Writer.prototype.getConfigTags = function getConfigTags (config) { + if (Array.isArray(config)) { + return config; + } + else if (config && typeof config === 'object') { + return config.tags; + } + else { + return undefined; + } + }; + + Writer.prototype.getConfigEscape = function getConfigEscape (config) { + if (config && typeof config === 'object' && !Array.isArray(config)) { + return config.escape; + } + else { + return undefined; + } + }; + var mustache = { name: 'mustache.js', version: '4.0.1', @@ -716,14 +751,14 @@ * array with two string values: the opening and closing tags used in the * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. */ - mustache.render = function render (template, view, partials, tags) { + mustache.render = function render (template, view, partials, config) { if (typeof template !== 'string') { throw new TypeError('Invalid template! Template should be a "string" ' + 'but "' + typeStr(template) + '" was given as the first ' + 'argument for mustache#render(template, view, partials)'); } - return defaultWriter.render(template, view, partials, tags); + return defaultWriter.render(template, view, partials, config); }; // Export the escaping function so that the user may override it. diff --git a/mustache.min.js b/mustache.min.js index 6a7fde016..0f2387ad2 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,tags);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,tags){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}return this.renderTokens(this.parse(indentedValue,tags),context,partials,indentedValue,tags)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"?String(value):mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,tags){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,tags)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(Array.isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!Array.isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 2344a25dc..96d75b2aa 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -529,14 +529,25 @@ Writer.prototype.parse = function parse (template, tags) { * also be a function that is used to load partial templates on the fly * that takes a single argument: the name of the partial. * - * If the optional `tags` argument is given here it must be an array with two + * If the optional `config` argument is given here, then it should be an + * object with a `tags` attribute or an `escape` attribute or both. + * If an array is passed, then it will be interpreted the same way as + * a `tags` attribute on a `config` object. + * + * The `tags` attribute of a `config` object must be an array with two * string values: the opening and closing tags used in the template (e.g. * [ "<%", "%>" ]). The default is to mustache.tags. + * + * The `escape` attribute of a `config` object must be a function which + * accepts a string as input and outputs a safely escaped string. + * If an `escape` function is not provided, then an HTML-safe string + * escaping function is used as the default. */ -Writer.prototype.render = function render (template, view, partials, tags) { +Writer.prototype.render = function render (template, view, partials, config) { + var tags = this.getConfigTags(config); var tokens = this.parse(template, tags); var context = (view instanceof Context) ? view : new Context(view, undefined); - return this.renderTokens(tokens, context, partials, template, tags); + return this.renderTokens(tokens, context, partials, template, config); }; /** @@ -548,7 +559,7 @@ Writer.prototype.render = function render (template, view, partials, tags) { * If the template doesn't use higher-order sections, this argument may * be omitted. */ -Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { +Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) { var buffer = ''; var token, symbol, value; @@ -557,11 +568,11 @@ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials token = tokens[i]; symbol = token[0]; - if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); - else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); - else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config); + else if (symbol === '>') value = this.renderPartial(token, context, partials, config); else if (symbol === '&') value = this.unescapedValue(token, context); - else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context, config); else if (symbol === 'text') value = this.rawValue(token); if (value !== undefined) @@ -571,7 +582,7 @@ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials return buffer; }; -Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { +Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) { var self = this; var buffer = ''; var value = context.lookup(token[1]); @@ -586,10 +597,10 @@ Writer.prototype.renderSection = function renderSection (token, context, partial if (isArray(value)) { for (var j = 0, valueLength = value.length; j < valueLength; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); } } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { - buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); } else if (isFunction(value)) { if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); @@ -600,18 +611,18 @@ Writer.prototype.renderSection = function renderSection (token, context, partial if (value != null) buffer += value; } else { - buffer += this.renderTokens(token[4], context, partials, originalTemplate); + buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); } return buffer; }; -Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { +Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) { var value = context.lookup(token[1]); // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 if (!value || (isArray(value) && value.length === 0)) - return this.renderTokens(token[4], context, partials, originalTemplate); + return this.renderTokens(token[4], context, partials, originalTemplate, config); }; Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { @@ -625,8 +636,9 @@ Writer.prototype.indentPartial = function indentPartial (partial, indentation, l return partialByNl.join('\n'); }; -Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { +Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) { if (!partials) return; + var tags = this.getConfigTags(config); var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; if (value != null) { @@ -637,7 +649,8 @@ Writer.prototype.renderPartial = function renderPartial (token, context, partial if (tagIndex == 0 && indentation) { indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } - return this.renderTokens(this.parse(indentedValue, tags), context, partials, indentedValue, tags); + var tokens = this.parse(indentedValue, tags); + return this.renderTokens(tokens, context, partials, indentedValue, config); } }; @@ -647,16 +660,38 @@ Writer.prototype.unescapedValue = function unescapedValue (token, context) { return value; }; -Writer.prototype.escapedValue = function escapedValue (token, context) { +Writer.prototype.escapedValue = function escapedValue (token, context, config) { + var escape = this.getConfigEscape(config) || mustache.escape; var value = context.lookup(token[1]); if (value != null) - return typeof value === 'number' ? String(value) : mustache.escape(value); + return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value); }; Writer.prototype.rawValue = function rawValue (token) { return token[1]; }; +Writer.prototype.getConfigTags = function getConfigTags (config) { + if (Array.isArray(config)) { + return config; + } + else if (config && typeof config === 'object') { + return config.tags; + } + else { + return undefined; + } +}; + +Writer.prototype.getConfigEscape = function getConfigEscape (config) { + if (config && typeof config === 'object' && !Array.isArray(config)) { + return config.escape; + } + else { + return undefined; + } +}; + var mustache = { name: 'mustache.js', version: '4.0.1', @@ -704,19 +739,17 @@ mustache.parse = function parse (template, tags) { }; /** - * Renders the `template` with the given `view` and `partials` using the - * default writer. If the optional `tags` argument is given here it must be an - * array with two string values: the opening and closing tags used in the - * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. + * Renders the `template` with the given `view`, `partials`, and `config` + * using the default writer. */ -mustache.render = function render (template, view, partials, tags) { +mustache.render = function render (template, view, partials, config) { if (typeof template !== 'string') { throw new TypeError('Invalid template! Template should be a "string" ' + 'but "' + typeStr(template) + '" was given as the first ' + 'argument for mustache#render(template, view, partials)'); } - return defaultWriter.render(template, view, partials, tags); + return defaultWriter.render(template, view, partials, config); }; // Export the escaping function so that the user may override it. From 37fdf8163c5b5cb47d1859be04e772ebadb791fb Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Fri, 9 Oct 2020 11:12:00 +0300 Subject: [PATCH 256/286] Add test coverage for new render config object parameter --- mustache.js | 6 +- test/render-test.js | 158 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/mustache.js b/mustache.js index 7b6786630..213f31310 100644 --- a/mustache.js +++ b/mustache.js @@ -746,10 +746,8 @@ }; /** - * Renders the `template` with the given `view` and `partials` using the - * default writer. If the optional `tags` argument is given here it must be an - * array with two string values: the opening and closing tags used in the - * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags. + * Renders the `template` with the given `view`, `partials`, and `config` + * using the default writer. */ mustache.render = function render (template, view, partials, config) { if (typeof template !== 'string') { diff --git a/test/render-test.js b/test/render-test.js index 008852101..a21d6c396 100644 --- a/test/render-test.js +++ b/test/render-test.js @@ -24,21 +24,43 @@ describe('Mustache.render', function () { Mustache.tags = ['{{', '}}']; assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, ['<<', '>>']), 'foobar{{placeholder}}'); }); + + it('uses config.tags argument instead of Mustache.tags when given', function () { + var template = '<>bar{{placeholder}}'; + + Mustache.tags = ['{{', '}}']; + assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, { tags: ['<<', '>>']}), 'foobar{{placeholder}}'); + }); - it('uses tags argument instead of Mustache.tags when given, even when it previous rendered the template using Mustache.tags', function () { + it('uses tags argument instead of Mustache.tags when given, even when it previously rendered the template using Mustache.tags', function () { var template = '((placeholder))bar{{placeholder}}'; Mustache.tags = ['{{', '}}']; Mustache.render(template, { placeholder: 'foo' }); assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, ['((', '))']), 'foobar{{placeholder}}'); }); + + it('uses config.tags argument instead of Mustache.tags when given, even when it previously rendered the template using Mustache.tags', function () { + var template = '((placeholder))bar{{placeholder}}'; + + Mustache.tags = ['{{', '}}']; + Mustache.render(template, { placeholder: 'foo' }); + assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, { tags: ['((', '))'] }), 'foobar{{placeholder}}'); + }); - it('uses tags argument instead of Mustache.tags when given, even when it previous rendered the template using different tags', function () { + it('uses tags argument instead of Mustache.tags when given, even when it previously rendered the template using different tags', function () { var template = '[[placeholder]]bar<>'; Mustache.render(template, { placeholder: 'foo' }, {}, ['<<', '>>']); assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, ['[[', ']]']), 'foobar<>'); }); + + it('uses config.tags argument instead of Mustache.tags when given, even when it previously rendered the template using different tags', function () { + var template = '[[placeholder]]bar<>'; + + Mustache.render(template, { placeholder: 'foo' }, {}, ['<<', '>>']); + assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, { tags: ['[[', ']]'] }), 'foobar<>'); + }); it('does not mutate Mustache.tags when given tags argument', function () { var correctMustacheTags = ['{{', '}}']; @@ -49,6 +71,16 @@ describe('Mustache.render', function () { assert.equal(Mustache.tags, correctMustacheTags); assert.deepEqual(Mustache.tags, ['{{', '}}']); }); + + it('does not mutate Mustache.tags when given config.tags argument', function () { + var correctMustacheTags = ['{{', '}}']; + Mustache.tags = correctMustacheTags; + + Mustache.render('((placeholder))', { placeholder: 'foo' }, {}, { tags: ['((', '))'] }); + + assert.equal(Mustache.tags, correctMustacheTags); + assert.deepEqual(Mustache.tags, ['{{', '}}']); + }); it('uses provided tags when rendering partials', function () { var output = Mustache.render('<%> partial %>', { name: 'Santa Claus' }, { @@ -57,6 +89,128 @@ describe('Mustache.render', function () { assert.equal(output, 'Santa Claus'); }); + + it('uses provided config.tags when rendering partials', function () { + var output = Mustache.render('<%> partial %>', { name: 'Santa Claus' }, { + partial: '<% name %>' + }, { tags: ['<%', '%>'] }); + + assert.equal(output, 'Santa Claus'); + }); + + it('uses config.escape argument instead of Mustache.escape when given', function () { + var template = 'Hello, {{placeholder}}'; + + function escapeBang (text) { + return text + '!'; + } + assert.equal(Mustache.render(template, { placeholder: 'world' }, {}, { escape: escapeBang }), 'Hello, world!'); + }); + + it('uses config.escape argument instead of Mustache.escape when given, even when it previously rendered the template using Mustache.escape', function () { + var template = 'Hello, {{placeholder}}'; + + function escapeQuestion (text) { + return text + '?'; + } + Mustache.render(template, { placeholder: 'world' }); + assert.equal(Mustache.render(template, { placeholder: 'world' }, {}, { escape: escapeQuestion }), 'Hello, world?'); + }); + + it('uses config.escape argument instead of Mustache.escape when given, even when it previously rendered the template using a different escape function', function () { + var template = 'Hello, {{placeholder}}'; + + function escapeQuestion (text) { + return text + '?'; + } + function escapeBang (text) { + return text + '!'; + } + Mustache.render(template, { placeholder: 'foo' }, {}, { escape: escapeQuestion }); + assert.equal(Mustache.render(template, { placeholder: 'foo' }, {}, { escape: escapeBang }), 'Hello, foo!'); + }); + + it('does not mutate Mustache.escape when given config.escape argument', function () { + var correctMustacheEscape = Mustache.escape; + + function escapeNone (text) { + return text; + } + Mustache.render('((placeholder))', { placeholder: 'foo' }, {}, { escape: escapeNone }); + + assert.equal(Mustache.escape, correctMustacheEscape); + assert.equal(Mustache.escape('>&'), '>&'); + }); + + it('uses provided config.escape when rendering partials', function () { + function escapeDoubleAmpersand (text) { + return text.replace('&', '&&'); + } + var output = Mustache.render('{{> partial }}', { name: 'Ampersand &' }, { + partial: '{{ name }}' + }, { escape: escapeDoubleAmpersand }); + + assert.equal(output, 'Ampersand &&'); + }); + + it('uses config.tags and config.escape arguments instead of Mustache.tags and Mustache.escape when given', function () { + var template = 'Hello, {{placeholder}} [[placeholder]]'; + + function escapeTwoBangs (text) { + return text + '!!'; + } + var config = { + tags: ['[[', ']]'], + escape: escapeTwoBangs, + }; + assert.equal(Mustache.render(template, { placeholder: 'world' }, {}, config), 'Hello, {{placeholder}} world!!'); + }); + + it('uses provided config.tags and config.escape when rendering partials', function () { + function escapeDoubleAmpersand (text) { + return text.replace('&', '&&'); + } + var config = { + tags: ['[[', ']]'], + escape: escapeDoubleAmpersand + }; + var output = Mustache.render('[[> partial ]]', { name: 'Ampersand &' }, { + partial: '[[ name ]]' + }, config); + + assert.equal(output, 'Ampersand &&'); + }); + + it('uses provided config.tags and config.escape when rendering sections', function () { + var template = '<[[&value-raw]] [[#test-1]][[value-1]][[/test-1]][[^test-2]][[value-2]][[/test-2]]>'; + + function escapeQuotes (text) { + return '"' + text + '"'; + } + var config = { + tags: ['[[', ']]'], + escape: escapeQuotes + }; + var viewTestTrue = { + 'value-raw': 'foo', + 'test-1': true, + 'value-1': 'abc', + 'test-2': true, + 'value-2': '123' + }; + var viewTestFalse = { + 'value-raw': 'foo', + 'test-1': false, + 'value-1': 'abc', + 'test-2': false, + 'value-2': '123' + }; + var outputTrue = Mustache.render(template, viewTestTrue, {}, config); + var outputFalse = Mustache.render(template, viewTestFalse, {}, config); + + assert.equal(outputTrue, ''); + assert.equal(outputFalse, ''); + }); }); tests.forEach(function (test) { From 862e497c25af978ec852d483b723685cc3bb4ece Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Fri, 9 Oct 2020 11:38:05 +0300 Subject: [PATCH 257/286] Fix behavior when rendering lambda sections & add regression test The `render` argument passed to a `function (text, render)` lambda function was not accounting for custom tags or other configuration. This issue has been fixed and a regression test has been added for this case. --- mustache.js | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- test/render-test.js | 24 +++++++++++++++++++----- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mustache.js b/mustache.js index 213f31310..1333cec5f 100644 --- a/mustache.js +++ b/mustache.js @@ -597,7 +597,7 @@ // This function is used to render an arbitrary template // in the current context by higher-order sections. function subRender (template) { - return self.render(template, context, partials); + return self.render(template, context, partials, config); } if (!value) return; diff --git a/mustache.min.js b/mustache.min.js index 0f2387ad2..c56b332f0 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(Array.isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!Array.isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(Array.isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!Array.isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index 96d75b2aa..f4a27dc2a 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -590,7 +590,7 @@ Writer.prototype.renderSection = function renderSection (token, context, partial // This function is used to render an arbitrary template // in the current context by higher-order sections. function subRender (template) { - return self.render(template, context, partials); + return self.render(template, context, partials, config); } if (!value) return; diff --git a/test/render-test.js b/test/render-test.js index a21d6c396..d05552314 100644 --- a/test/render-test.js +++ b/test/render-test.js @@ -182,7 +182,13 @@ describe('Mustache.render', function () { }); it('uses provided config.tags and config.escape when rendering sections', function () { - var template = '<[[&value-raw]] [[#test-1]][[value-1]][[/test-1]][[^test-2]][[value-2]][[/test-2]]>'; + var template = ( + '<[[&value-raw]]: ' + + '[[#test-1]][[value-1]][[/test-1]]' + + '[[^test-2]][[value-2]][[/test-2]], ' + + '[[#test-lambda]][[value-lambda]][[/test-lambda]]' + + '>' + ); function escapeQuotes (text) { return '"' + text + '"'; @@ -196,20 +202,28 @@ describe('Mustache.render', function () { 'test-1': true, 'value-1': 'abc', 'test-2': true, - 'value-2': '123' + 'value-2': '123', + 'test-lambda': function () { + return function (text, render) { return 'lambda: ' + render(text); }; + }, + 'value-lambda': 'bar' }; var viewTestFalse = { 'value-raw': 'foo', 'test-1': false, 'value-1': 'abc', 'test-2': false, - 'value-2': '123' + 'value-2': '123', + 'test-lambda': function () { + return function (text, render) { return 'lambda: ' + render(text); }; + }, + 'value-lambda': 'bar' }; var outputTrue = Mustache.render(template, viewTestTrue, {}, config); var outputFalse = Mustache.render(template, viewTestFalse, {}, config); - assert.equal(outputTrue, ''); - assert.equal(outputFalse, ''); + assert.equal(outputTrue, ''); + assert.equal(outputFalse, ''); }); }); From 224fe3a23e605e2901c8bf3a09d99e518463cb61 Mon Sep 17 00:00:00 2001 From: Sophie Kirschner Date: Sat, 10 Oct 2020 15:16:25 +0300 Subject: [PATCH 258/286] Use outstanding isArray function in new render config object code --- mustache.js | 4 ++-- mustache.min.js | 2 +- mustache.mjs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mustache.js b/mustache.js index 1333cec5f..a157f6272 100644 --- a/mustache.js +++ b/mustache.js @@ -679,7 +679,7 @@ }; Writer.prototype.getConfigTags = function getConfigTags (config) { - if (Array.isArray(config)) { + if (isArray(config)) { return config; } else if (config && typeof config === 'object') { @@ -691,7 +691,7 @@ }; Writer.prototype.getConfigEscape = function getConfigEscape (config) { - if (config && typeof config === 'object' && !Array.isArray(config)) { + if (config && typeof config === 'object' && !isArray(config)) { return config.escape; } else { diff --git a/mustache.min.js b/mustache.min.js index c56b332f0..615c23cd5 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(Array.isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!Array.isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index f4a27dc2a..bb4085775 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -672,7 +672,7 @@ Writer.prototype.rawValue = function rawValue (token) { }; Writer.prototype.getConfigTags = function getConfigTags (config) { - if (Array.isArray(config)) { + if (isArray(config)) { return config; } else if (config && typeof config === 'object') { @@ -684,7 +684,7 @@ Writer.prototype.getConfigTags = function getConfigTags (config) { }; Writer.prototype.getConfigEscape = function getConfigEscape (config) { - if (config && typeof config === 'object' && !Array.isArray(config)) { + if (config && typeof config === 'object' && !isArray(config)) { return config.escape; } else { From 67c39b89af494141fc2ec2b279aacf986b86a8c3 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 6 Dec 2020 00:50:45 +0100 Subject: [PATCH 259/286] :ship: bump to version 4.1.0 --- CHANGELOG.md | 13 +++++++++++++ mustache.js | 2 +- mustache.js.nuspec | 2 +- mustache.min.js | 2 +- mustache.mjs | 2 +- package-lock.json | 2 +- package.json | 2 +- 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3fcfe00..cf5c7ce89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.1.0] / 6 December 2020 + +### Added + +* [#764]: `render()` now recognizes a config object argument, by [@pineapplemachine]. + +### Fixed + +* [#764]: Ask custom `escape` functions to escape all types of values (including `number`s), by [@pineapplemachine]. + ## [4.0.1] / 15 March 2020 ### Fixed @@ -467,6 +477,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[4.1.0]: https://github.com/janl/mustache.js/compare/v4.0.1...v4.1.0 [4.0.1]: https://github.com/janl/mustache.js/compare/v4.0.0...v4.0.1 [4.0.0]: https://github.com/janl/mustache.js/compare/v3.2.1...v4.0.0 [3.2.1]: https://github.com/janl/mustache.js/compare/v3.2.0...v3.2.1 @@ -543,6 +554,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#731]: https://github.com/janl/mustache.js/issues/731 [#735]: https://github.com/janl/mustache.js/issues/735 [#739]: https://github.com/janl/mustache.js/issues/739 +[#764]: https://github.com/janl/mustache.js/issues/764 [@afc163]: https://github.com/afc163 [@aielo]: https://github.com/aielo @@ -581,6 +593,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [@paultopia]: https://github.com/paultopia [@pgilad]: https://github.com/pgilad [@phillipj]: https://github.com/phillipj +[@pineapplemachine]: https://github.com/pineapplemachine [@pra85]: https://github.com/pra85 [@raymond-lam]: https://github.com/raymond-lam [@seminaoki]: https://github.com/seminaoki diff --git a/mustache.js b/mustache.js index a157f6272..33b83a94b 100644 --- a/mustache.js +++ b/mustache.js @@ -701,7 +701,7 @@ var mustache = { name: 'mustache.js', - version: '4.0.1', + version: '4.1.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/mustache.js.nuspec b/mustache.js.nuspec index bffc3f4bd..e77395a77 100644 --- a/mustache.js.nuspec +++ b/mustache.js.nuspec @@ -2,7 +2,7 @@ mustache.js - 4.0.1 + 4.1.0 mustache.js Authors https://github.com/janl/mustache.js/blob/master/LICENSE http://mustache.github.com/ diff --git a/mustache.min.js b/mustache.min.js index 615c23cd5..9dbc77b4d 100644 --- a/mustache.min.js +++ b/mustache.min.js @@ -1 +1 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.0.1",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); +(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.1.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); diff --git a/mustache.mjs b/mustache.mjs index bb4085775..8cad81638 100644 --- a/mustache.mjs +++ b/mustache.mjs @@ -694,7 +694,7 @@ Writer.prototype.getConfigEscape = function getConfigEscape (config) { var mustache = { name: 'mustache.js', - version: '4.0.1', + version: '4.1.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index bf36f8e8f..a3438bd89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.0.1", + "version": "4.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e7fee427c..e938a83b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.0.1", + "version": "4.1.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From 2502fdfadd47489e4659173bd0dd19b2f8636435 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 6 Dec 2020 00:58:08 +0100 Subject: [PATCH 260/286] Updated deno usage test to avoid use of deprecated URL As seen from the test run log: ``` $ deno test --allow-net=deno.land test/module-systems/deno-test.ts Compile file:///home/runner/work/mustache.js/mustache.js/.deno.test.ts Download deno.land/std@v0.51.0/testing/asserts.ts Warning std versions prefixed with 'v' were deprecated recently. Please change your import to deno.land/std@0.51.0/testing/asserts.ts (at deno.land/std@v0.51.0/testing/asserts.ts) error: Uncaught Error: Import 'deno.land/std@v0.51.0/testing/asserts.ts' failed: 404 Not Found at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11) at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10) at async processImports ($deno$/compiler.ts:736:23) ``` --- test/module-systems/deno-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/module-systems/deno-test.ts b/test/module-systems/deno-test.ts index 282a97dc2..cd991b485 100644 --- a/test/module-systems/deno-test.ts +++ b/test/module-systems/deno-test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "https://deno.land/std@v0.51.0/testing/asserts.ts"; +import { assertEquals } from "https://deno.land/std@0.51.0/testing/asserts.ts"; import mustache from "../../mustache.mjs"; const view = { From 36edf6874bbfb6dc5e48e64da514fa88d72df9eb Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 6 Dec 2020 01:16:08 +0100 Subject: [PATCH 261/286] Only keep Node.js 8 with browser usage tests on Travis CI Removes running Node.js 10 & 12 from Travis CI as we do that with GitHub Actions as well. The only reason we still keep Node.js 8 running on Travis CI is due to the browser usage testing we've setup, with encrypted secrets and all. Ideally we'd move those browser tests to run via GitHub Actions for consistency with the rest of our tests, but postponing that work for now as it's not critical. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8ed59c8c5..33fd94e6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: node_js node_js: - 8 - - 10 - - 12 script: - npm test - "test $TRAVIS_PULL_REQUEST != 'false' || test $TRAVIS_NODE_VERSION != '8' || npm run test-browser" From de09ecaa56c87ff9afe8a52943fa9e19b207d994 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 6 Dec 2020 01:19:51 +0100 Subject: [PATCH 262/286] Run tests on Node.js 14 & 15 as well --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5edf66e5f..3c6b13737 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [8.x, 10.x, 12.x] + node-version: [8.x, 10.x, 12.x, 14.x, 15.x] steps: - uses: actions/checkout@v1 From 1ff17aae7ca4bce12e8dc0c081c949ad9b990d2a Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 12 Dec 2020 21:56:10 +0100 Subject: [PATCH 263/286] Include mustache spec tests in CI by checking out submodules Our test suite already has support for running mustache specs, but looks like we've forgotten about them in CI for many years now. --- .github/workflows/verify.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3c6b13737..c24c97344 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -11,7 +11,8 @@ jobs: node-version: [8.x, 10.x, 12.x, 14.x, 15.x] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + submodules: recursive - name: Setup Node.js uses: actions/setup-node@v1 with: @@ -29,7 +30,8 @@ jobs: node-version: [0.10.x, 0.12.x, 4.x, 6.x] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + submodules: recursive - name: Setup Node.js uses: actions/setup-node@v1 with: From b9e113f45039f5371370ca034746eb628a6be882 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 13 Dec 2020 00:12:36 +0100 Subject: [PATCH 264/286] Fix GitHub checkout action configuration to pull submodules ..blooper introduced in the latest commit where submodules were to be pulled to make sure mustache spec tests were included in test runs. --- .github/workflows/verify.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c24c97344..53399565c 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -12,7 +12,8 @@ jobs: steps: - uses: actions/checkout@v2 - submodules: recursive + with: + submodules: recursive - name: Setup Node.js uses: actions/setup-node@v1 with: @@ -31,7 +32,8 @@ jobs: steps: - uses: actions/checkout@v2 - submodules: recursive + with: + submodules: recursive - name: Setup Node.js uses: actions/setup-node@v1 with: From a11bfc8f7ca18b9b80273c58c71d2604f39f30f3 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 6 Feb 2021 19:35:43 +0100 Subject: [PATCH 265/286] Move Node.js 8 alongside other legacy Node.js versions in CI Now that Node.js 8 is end-of-life / no longer maintained, we might as well treat it as a legacy Node.js version too. --- .github/workflows/verify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 53399565c..5a1a5fe35 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [8.x, 10.x, 12.x, 14.x, 15.x] + node-version: [10.x, 12.x, 14.x, 15.x] steps: - uses: actions/checkout@v2 @@ -28,7 +28,7 @@ jobs: strategy: matrix: - node-version: [0.10.x, 0.12.x, 4.x, 6.x] + node-version: [0.10.x, 0.12.x, 4.x, 6.x, 8.x] steps: - uses: actions/checkout@v2 From 4dbc88deb7e5b55f0f31647f558876c193fa50e3 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 7 Feb 2021 13:42:22 +0100 Subject: [PATCH 266/286] Extract usage tests into separate GitHub Actions workflow Primarily to group similar kinds of "usage" tests together, which are all either verifying the use of mustache.js from consuming party' point of view. --- .github/workflows/usage.yml | 94 ++++++++++++++++++++++++++++++++++++ .github/workflows/verify.yml | 63 ------------------------ 2 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/usage.yml diff --git a/.github/workflows/usage.yml b/.github/workflows/usage.yml new file mode 100644 index 000000000..5608b2eba --- /dev/null +++ b/.github/workflows/usage.yml @@ -0,0 +1,94 @@ +name: Package usage + +on: [push, pull_request] + +jobs: + package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Create package tarball + run: | + export ARCHIVE_FILENAME=$(npm pack | tail -n 1) + mv $ARCHIVE_FILENAME mustache.tgz + - name: Store package tarball for later + uses: actions/upload-artifact@v2 + with: + name: package-output + path: mustache.tgz + + common-js-usage: + runs-on: ubuntu-latest + + needs: package + steps: + - uses: actions/checkout@v1 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get package tarball from package step + uses: actions/download-artifact@v2 + with: + name: package-output + - name: Package, install and test + run: | + export UNPACK_DESTINATION=$(mktemp -d) + mv mustache.tgz $UNPACK_DESTINATION + cp test/module-systems/commonjs-test.js $UNPACK_DESTINATION + cd $UNPACK_DESTINATION + npm install mustache.tgz + node commonjs-test.js + + esm-usage: + runs-on: ubuntu-latest + + needs: package + steps: + - uses: actions/checkout@v1 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '>=13.2.0' + - name: Get package tarball from package step + uses: actions/download-artifact@v2 + with: + name: package-output + - name: Package, install and test + run: | + export UNPACK_DESTINATION=$(mktemp -d) + mv mustache.tgz $UNPACK_DESTINATION + cp test/module-systems/esm-test.mjs $UNPACK_DESTINATION + cd $UNPACK_DESTINATION + npm install mustache.tgz + node esm-test.mjs + + browser-usage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Install and test + run: | + npm ci + npx mocha test/module-systems/browser-test.js + + deno-usage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: denolib/setup-deno@master + with: + deno-version: 'v1.0.0' + - run: deno --version + - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5a1a5fe35..6406f5e2d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -43,69 +43,6 @@ jobs: npm install mocha@3 chai@3 npm run test-unit - common-js-usage: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: 12.x - - name: Package, install and test - run: | - export ARCHIVE_FILENAME=$(npm pack | tail -n 1) - export UNPACK_DESTINATION=$(mktemp -d) - mv $ARCHIVE_FILENAME $UNPACK_DESTINATION - cp test/module-systems/commonjs-test.js $UNPACK_DESTINATION - cd $UNPACK_DESTINATION - npm install $ARCHIVE_FILENAME - node commonjs-test.js - - esm-usage: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: '>=13.2.0' - - name: Package, install and test - run: | - export ARCHIVE_FILENAME=$(npm pack | tail -n 1) - export UNPACK_DESTINATION=$(mktemp -d) - mv $ARCHIVE_FILENAME $UNPACK_DESTINATION - cp test/module-systems/esm-test.mjs $UNPACK_DESTINATION - cd $UNPACK_DESTINATION - npm install $ARCHIVE_FILENAME - node esm-test.mjs - - browser-usage: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: 12.x - - name: Install and test - run: | - npm ci - npx mocha test/module-systems/browser-test.js - - deno-usage: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - uses: denolib/setup-deno@master - with: - deno-version: 'v1.0.0' - - run: deno --version - - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts - build-output-sync: runs-on: ubuntu-latest From 042fbbe5da95ade5c7acec608beb4280da539c06 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 30 Jan 2021 13:07:03 +0100 Subject: [PATCH 267/286] Add separate CI job for linting No need for X amount of different Node.js version to all run the linting as part of their CI rutine. Easier to just run that once, completely separate from the unit tests. --- .github/workflows/verify.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6406f5e2d..b397edaf9 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -3,6 +3,18 @@ name: Verify changes on: [push, pull_request] jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - run: npm install + - run: npm run test-lint + tests: runs-on: ubuntu-latest @@ -21,7 +33,7 @@ jobs: - name: npm install and test run: | npm install - npm test + npm run test-unit tests-on-legacy: runs-on: ubuntu-latest From dd74683da0be48a8c503573c0c513627bf0f6c40 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 8 Feb 2021 20:31:22 +0100 Subject: [PATCH 268/286] Remove nuget.org spec from the repository None of the recent maintainers has remembered trying to keep the nuget.org package up to date. As of writing this, it's over 7 years since the last mustache.js version got published on nuget (v0.7.2). Adding to that, there has been no questions asked from the nuget community about outdated versions. With the above in mind, we'll remove the nuget spec from our repo since it only adds to confusion at this point. Refs https://www.nuget.org/packages/mustache.js/ --- hooks/pre-commit | 1 - mustache.js.nuspec | 14 -------------- 2 files changed, 15 deletions(-) delete mode 100644 mustache.js.nuspec diff --git a/hooks/pre-commit b/hooks/pre-commit index 0fbd577c1..7d0034d59 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -79,6 +79,5 @@ end bumper = Bumper.new([ Source.new('mustache.mjs', /version: '([^']+)'/), - Source.new('mustache.js.nuspec', /([^<]+)<\/version>/), ]) bumper.start diff --git a/mustache.js.nuspec b/mustache.js.nuspec deleted file mode 100644 index e77395a77..000000000 --- a/mustache.js.nuspec +++ /dev/null @@ -1,14 +0,0 @@ - - - - mustache.js - 4.1.0 - mustache.js Authors - https://github.com/janl/mustache.js/blob/master/LICENSE - http://mustache.github.com/ - false - Logic-less templates in JavaScript. - - mustache template templates javascript - - \ No newline at end of file From 9faa18e44130b88891cb91e21c8ba0befd9547a7 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 22 Feb 2021 21:27:39 +0100 Subject: [PATCH 269/286] Remove gitter badge from README.md There has not been activity there for several years. No point in actively inviting potential using developers in there, instead of just using what we have on GitHub. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c25dd470..127dfe106 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > What could be more logical awesome than no logic at all? -[![Build Status](https://travis-ci.org/janl/mustache.js.svg?branch=master)](https://travis-ci.org/janl/mustache.js) [![Gitter chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/janl/mustache.js) +[![Build Status](https://travis-ci.org/janl/mustache.js.svg?branch=master)](https://travis-ci.org/janl/mustache.js) [mustache.js](http://github.com/janl/mustache.js) is a zero-dependency implementation of the [mustache](http://mustache.github.com/) template system in JavaScript. From cc979e0419e7a1aae6c14c1a59763324855a54f3 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 26 Dec 2020 00:58:32 +0100 Subject: [PATCH 270/286] Rename .mjs -> .js to make it ESM and not have build output in git Although we'll now end up with only `mustache.js` in the git repo, being written in ESM syntax, the npm package will still be as before: - `mustache.js`: UMD, meaning CommonJS, AMD or global scope - `mustache.mjs`: ESM --- .eslintrc | 2 +- .github/workflows/verify.yml | 15 - .gitignore | 1 + hooks/pre-commit | 2 +- mustache.js | 1399 +++++++++++++++++----------------- mustache.mjs | 764 ------------------- package.json | 4 +- 7 files changed, 700 insertions(+), 1487 deletions(-) delete mode 100644 mustache.mjs diff --git a/.eslintrc b/.eslintrc index d4143d408..21f5e1634 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,7 +19,7 @@ }, "overrides": [ { - "files": ["mustache.mjs"], + "files": ["mustache.js"], "parserOptions": { "sourceType": "module", "ecmaVersion": 2015 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b397edaf9..8a4258914 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -54,18 +54,3 @@ jobs: run: | npm install mocha@3 chai@3 npm run test-unit - - build-output-sync: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: 12.x - - name: Install, build and check git diff - run: | - npm ci - npm run build - git diff --quiet HEAD -- diff --git a/.gitignore b/.gitignore index 76def77e9..0b968161b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ npm-debug.log .DS_Store test/render-test-browser.js .idea/ +mustache.mjs diff --git a/hooks/pre-commit b/hooks/pre-commit index 7d0034d59..db8ad4528 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -78,6 +78,6 @@ class Bumper end bumper = Bumper.new([ - Source.new('mustache.mjs', /version: '([^']+)'/), + Source.new('mustache.js', /version: '([^']+)'/), ]) bumper.start diff --git a/mustache.js b/mustache.js index 33b83a94b..8cad81638 100644 --- a/mustache.js +++ b/mustache.js @@ -1,773 +1,764 @@ -// This file has been generated from mustache.mjs -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = global || self, global.Mustache = factory()); -}(this, (function () { 'use strict'; - - /*! - * mustache.js - Logic-less {{mustache}} templates with JavaScript - * http://github.com/janl/mustache.js - */ - - var objectToString = Object.prototype.toString; - var isArray = Array.isArray || function isArrayPolyfill (object) { - return objectToString.call(object) === '[object Array]'; - }; - - function isFunction (object) { - return typeof object === 'function'; - } - - /** - * More correct typeof string handling array - * which normally returns typeof 'object' - */ - function typeStr (obj) { - return isArray(obj) ? 'array' : typeof obj; - } +/*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + */ + +var objectToString = Object.prototype.toString; +var isArray = Array.isArray || function isArrayPolyfill (object) { + return objectToString.call(object) === '[object Array]'; +}; + +function isFunction (object) { + return typeof object === 'function'; +} + +/** + * More correct typeof string handling array + * which normally returns typeof 'object' + */ +function typeStr (obj) { + return isArray(obj) ? 'array' : typeof obj; +} + +function escapeRegExp (string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); +} + +/** + * Null safe way of checking whether or not an object, + * including its prototype, has a given property + */ +function hasProperty (obj, propName) { + return obj != null && typeof obj === 'object' && (propName in obj); +} + +/** + * Safe way of detecting whether or not the given thing is a primitive and + * whether it has the given property + */ +function primitiveHasOwnProperty (primitive, propName) { + return ( + primitive != null + && typeof primitive !== 'object' + && primitive.hasOwnProperty + && primitive.hasOwnProperty(propName) + ); +} + +// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 +// See https://github.com/janl/mustache.js/issues/189 +var regExpTest = RegExp.prototype.test; +function testRegExp (re, string) { + return regExpTest.call(re, string); +} + +var nonSpaceRe = /\S/; +function isWhitespace (string) { + return !testRegExp(nonSpaceRe, string); +} + +var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' +}; + +function escapeHtml (string) { + return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { + return entityMap[s]; + }); +} + +var whiteRe = /\s*/; +var spaceRe = /\s+/; +var equalsRe = /\s*=/; +var curlyRe = /\s*\}/; +var tagRe = /#|\^|\/|>|\{|&|=|!/; + +/** + * 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 + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. + * + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. + * + * Tokens for partials also contain two more elements: 1) a string value of + * indendation prior to that tag and 2) the index of that tag on that line - + * eg a value of 2 indicates the partial is the third tag on this line. + */ +function parseTemplate (template, tags) { + if (!template) + return []; + var lineHasNonSpace = false; + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + var indentation = ''; // Tracks indentation for tags that use it + var tagIndex = 0; // Stores a count of number of tags encountered on a line + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace () { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } - function escapeRegExp (string) { - return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + hasTag = false; + nonSpace = false; } - /** - * Null safe way of checking whether or not an object, - * including its prototype, has a given property - */ - function hasProperty (obj, propName) { - return obj != null && typeof obj === 'object' && (propName in obj); - } + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags (tagsToCompile) { + if (typeof tagsToCompile === 'string') + tagsToCompile = tagsToCompile.split(spaceRe, 2); - /** - * Safe way of detecting whether or not the given thing is a primitive and - * whether it has the given property - */ - function primitiveHasOwnProperty (primitive, propName) { - return ( - primitive != null - && typeof primitive !== 'object' - && primitive.hasOwnProperty - && primitive.hasOwnProperty(propName) - ); - } + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error('Invalid tags: ' + tagsToCompile); - // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 - // See https://github.com/janl/mustache.js/issues/189 - var regExpTest = RegExp.prototype.test; - function testRegExp (re, string) { - return regExpTest.call(re, string); + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); } - var nonSpaceRe = /\S/; - function isWhitespace (string) { - return !testRegExp(nonSpaceRe, string); - } + compileTags(tags || mustache.tags); - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; + var scanner = new Scanner(template); - function escapeHtml (string) { - return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { - return entityMap[s]; - }); - } + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; - var whiteRe = /\s*/; - var spaceRe = /\s+/; - var equalsRe = /\s*=/; - var curlyRe = /\s*\}/; - var tagRe = /#|\^|\/|>|\{|&|=|!/; + // Match any text between tags. + value = scanner.scanUntil(openingTagRe); - /** - * 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 - * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of - * course, the default is to use mustaches (i.e. mustache.tags). - * - * A token is an array with at least 4 elements. The first element is the - * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag - * did not contain a symbol (i.e. {{myValue}}) this element is "name". For - * all text that appears outside a symbol this element is "text". - * - * The second element of a token is its "value". For mustache tags this is - * whatever else was inside the tag besides the opening symbol. For text tokens - * this is the text itself. - * - * The third and fourth elements of the token are the start and end indices, - * respectively, of the token in the original template. - * - * Tokens that are the root node of a subtree contain two more elements: 1) an - * array of tokens in the subtree and 2) the index in the original template at - * which the closing tag for that section begins. - * - * Tokens for partials also contain two more elements: 1) a string value of - * indendation prior to that tag and 2) the index of that tag on that line - - * eg a value of 2 indicates the partial is the third tag on this line. - */ - function parseTemplate (template, tags) { - if (!template) - return []; - var lineHasNonSpace = false; - var sections = []; // Stack to hold section tokens - var tokens = []; // Buffer to hold the tokens - var spaces = []; // Indices of whitespace tokens on the current line - var hasTag = false; // Is there a {{tag}} on the current line? - var nonSpace = false; // Is there a non-space char on the current line? - var indentation = ''; // Tracks indentation for tags that use it - var tagIndex = 0; // Stores a count of number of tags encountered on a line - - // Strips all whitespace tokens array for the current line - // if there was a {{#tag}} on it and otherwise only space. - function stripSpace () { - if (hasTag && !nonSpace) { - while (spaces.length) - delete tokens[spaces.pop()]; - } else { - spaces = []; - } + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); - hasTag = false; - nonSpace = false; - } - - var openingTagRe, closingTagRe, closingCurlyRe; - function compileTags (tagsToCompile) { - if (typeof tagsToCompile === 'string') - tagsToCompile = tagsToCompile.split(spaceRe, 2); - - if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) - throw new Error('Invalid tags: ' + tagsToCompile); - - openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); - closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); - closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); - } - - compileTags(tags || mustache.tags); - - var scanner = new Scanner(template); - - var start, type, value, chr, token, openSection; - while (!scanner.eos()) { - start = scanner.pos; - - // Match any text between tags. - value = scanner.scanUntil(openingTagRe); - - if (value) { - for (var i = 0, valueLength = value.length; i < valueLength; ++i) { - chr = value.charAt(i); - - if (isWhitespace(chr)) { - spaces.push(tokens.length); - indentation += chr; - } else { - nonSpace = true; - lineHasNonSpace = true; - indentation += ' '; - } - - tokens.push([ 'text', chr, start, start + 1 ]); - start += 1; - - // Check for whitespace on the current line. - if (chr === '\n') { - stripSpace(); - indentation = ''; - tagIndex = 0; - lineHasNonSpace = false; - } + if (isWhitespace(chr)) { + spaces.push(tokens.length); + indentation += chr; + } else { + nonSpace = true; + lineHasNonSpace = true; + indentation += ' '; } - } - - // Match the opening tag. - if (!scanner.scan(openingTagRe)) - break; - - hasTag = true; - - // Get the tag type. - type = scanner.scan(tagRe) || 'name'; - scanner.scan(whiteRe); - - // Get the tag value. - if (type === '=') { - value = scanner.scanUntil(equalsRe); - scanner.scan(equalsRe); - scanner.scanUntil(closingTagRe); - } else if (type === '{') { - value = scanner.scanUntil(closingCurlyRe); - scanner.scan(curlyRe); - scanner.scanUntil(closingTagRe); - type = '&'; - } else { - value = scanner.scanUntil(closingTagRe); - } - - // Match the closing tag. - if (!scanner.scan(closingTagRe)) - throw new Error('Unclosed tag at ' + scanner.pos); - - if (type == '>') { - token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; - } else { - token = [ type, value, start, scanner.pos ]; - } - tagIndex++; - tokens.push(token); - - if (type === '#' || type === '^') { - sections.push(token); - } else if (type === '/') { - // Check section nesting. - openSection = sections.pop(); - - if (!openSection) - throw new Error('Unopened section "' + value + '" at ' + start); - - if (openSection[1] !== value) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); - } else if (type === 'name' || type === '{' || type === '&') { - nonSpace = true; - } else if (type === '=') { - // Set the tags for the next time around. - compileTags(value); - } - } - - stripSpace(); - - // Make sure there are no open sections when we're done. - openSection = sections.pop(); - - if (openSection) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - - return nestTokens(squashTokens(tokens)); - } - - /** - * Combines the values of consecutive text tokens in the given `tokens` array - * to a single token. - */ - function squashTokens (tokens) { - var squashedTokens = []; - var token, lastToken; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; + tokens.push([ 'text', chr, start, start + 1 ]); + start += 1; - if (token) { - if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { - lastToken[1] += token[1]; - lastToken[3] = token[3]; - } else { - squashedTokens.push(token); - lastToken = token; + // Check for whitespace on the current line. + if (chr === '\n') { + stripSpace(); + indentation = ''; + tagIndex = 0; + lineHasNonSpace = false; } } } - return squashedTokens; - } - - /** - * Forms the given array of `tokens` into a nested tree structure where - * tokens that represent a section have two additional items: 1) an array of - * all tokens that appear in that section and 2) the index in the original - * template that represents the end of that section. - */ - function nestTokens (tokens) { - var nestedTokens = []; - var collector = nestedTokens; - var sections = []; - - var token, section; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; - - switch (token[0]) { - case '#': - case '^': - collector.push(token); - sections.push(token); - collector = token[4] = []; - break; - case '/': - section = sections.pop(); - section[5] = token[2]; - collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; - break; - default: - collector.push(token); - } + // Match the opening tag. + if (!scanner.scan(openingTagRe)) + break; + + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === '{') { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = '&'; + } else { + value = scanner.scanUntil(closingTagRe); } - return nestedTokens; - } + // Match the closing tag. + if (!scanner.scan(closingTagRe)) + throw new Error('Unclosed tag at ' + scanner.pos); - /** - * A simple string scanner that is used by the template parser to find - * tokens in template strings. - */ - function Scanner (string) { - this.string = string; - this.tail = string; - this.pos = 0; + if (type == '>') { + token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; + } else { + token = [ type, value, start, scanner.pos ]; + } + tagIndex++; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + compileTags(value); + } } - /** - * Returns `true` if the tail is empty (end of string). - */ - Scanner.prototype.eos = function eos () { - return this.tail === ''; - }; + stripSpace(); - /** - * Tries to match the given regular expression at the current position. - * Returns the matched text if it can match, the empty string otherwise. - */ - Scanner.prototype.scan = function scan (re) { - var match = this.tail.match(re); + // Make sure there are no open sections when we're done. + openSection = sections.pop(); - if (!match || match.index !== 0) - return ''; + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - var string = match[0]; + return nestTokens(squashTokens(tokens)); +} - this.tail = this.tail.substring(string.length); - this.pos += string.length; +/** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ +function squashTokens (tokens) { + var squashedTokens = []; - return string; - }; + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; - /** - * Skips all text until the given regular expression can be matched. Returns - * the skipped string, which is the entire tail if no match can be made. - */ - Scanner.prototype.scanUntil = function scanUntil (re) { - var index = this.tail.search(re), match; + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + squashedTokens.push(token); + lastToken = token; + } + } + } - switch (index) { - case -1: - match = this.tail; - this.tail = ''; + return squashedTokens; +} + +/** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ +function nestTokens (tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + collector.push(token); + sections.push(token); + collector = token[4] = []; break; - case 0: - match = ''; + case '/': + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; break; default: - match = this.tail.substring(0, index); - this.tail = this.tail.substring(index); + collector.push(token); } - - this.pos += match.length; - - return match; - }; - - /** - * Represents a rendering context by wrapping a view object and - * maintaining a reference to the parent context. - */ - function Context (view, parentContext) { - this.view = view; - this.cache = { '.': this.view }; - this.parent = parentContext; } - /** - * Creates a new context using the given view with this context - * as the parent. - */ - Context.prototype.push = function push (view) { - return new Context(view, this); - }; - - /** - * Returns the value of the given name in this context, traversing - * up the context hierarchy if the value is absent in this context's view. - */ - Context.prototype.lookup = function lookup (name) { - var cache = this.cache; - - var value; - if (cache.hasOwnProperty(name)) { - value = cache[name]; - } else { - var context = this, intermediateValue, names, index, lookupHit = false; - - while (context) { - if (name.indexOf('.') > 0) { - intermediateValue = context.view; - names = name.split('.'); - index = 0; - - /** - * Using the dot notion path in `name`, we descend through the - * nested objects. - * - * To be certain that the lookup has been successful, we have to - * check if the last object in the path actually has the property - * we are looking for. We store the result in `lookupHit`. - * - * This is specially necessary for when the value has been set to - * `undefined` and we want to avoid looking up parent contexts. - * - * In the case where dot notation is used, we consider the lookup - * to be successful even if the last "object" in the path is - * not actually an object but a primitive (e.g., a string, or an - * integer), because it is sometimes useful to access a property - * of an autoboxed primitive, such as the length of a string. - **/ - while (intermediateValue != null && index < names.length) { - if (index === names.length - 1) - lookupHit = ( - hasProperty(intermediateValue, names[index]) - || primitiveHasOwnProperty(intermediateValue, names[index]) - ); - - intermediateValue = intermediateValue[names[index++]]; - } - } else { - intermediateValue = context.view[name]; - - /** - * Only checking against `hasProperty`, which always returns `false` if - * `context.view` is not an object. Deliberately omitting the check - * against `primitiveHasOwnProperty` if dot notation is not used. - * - * Consider this example: - * ``` - * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) - * ``` - * - * If we were to check also against `primitiveHasOwnProperty`, as we do - * in the dot notation case, then render call would return: - * - * "The length of a football field is 9." - * - * rather than the expected: - * - * "The length of a football field is 100 yards." - **/ - lookupHit = hasProperty(context.view, name); - } + return nestedTokens; +} + +/** + * A simple string scanner that is used by the template parser to find + * tokens in template strings. + */ +function Scanner (string) { + this.string = string; + this.tail = string; + this.pos = 0; +} + +/** + * Returns `true` if the tail is empty (end of string). + */ +Scanner.prototype.eos = function eos () { + return this.tail === ''; +}; + +/** + * Tries to match the given regular expression at the current position. + * Returns the matched text if it can match, the empty string otherwise. + */ +Scanner.prototype.scan = function scan (re) { + var match = this.tail.match(re); + + if (!match || match.index !== 0) + return ''; + + var string = match[0]; + + this.tail = this.tail.substring(string.length); + this.pos += string.length; + + return string; +}; + +/** + * Skips all text until the given regular expression can be matched. Returns + * the skipped string, which is the entire tail if no match can be made. + */ +Scanner.prototype.scanUntil = function scanUntil (re) { + var index = this.tail.search(re), match; + + switch (index) { + case -1: + match = this.tail; + this.tail = ''; + break; + case 0: + match = ''; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); + } - if (lookupHit) { - value = intermediateValue; - break; + this.pos += match.length; + + return match; +}; + +/** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ +function Context (view, parentContext) { + this.view = view; + this.cache = { '.': this.view }; + this.parent = parentContext; +} + +/** + * Creates a new context using the given view with this context + * as the parent. + */ +Context.prototype.push = function push (view) { + return new Context(view, this); +}; + +/** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ +Context.prototype.lookup = function lookup (name) { + var cache = this.cache; + + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, intermediateValue, names, index, lookupHit = false; + + while (context) { + if (name.indexOf('.') > 0) { + intermediateValue = context.view; + names = name.split('.'); + index = 0; + + /** + * Using the dot notion path in `name`, we descend through the + * nested objects. + * + * To be certain that the lookup has been successful, we have to + * check if the last object in the path actually has the property + * we are looking for. We store the result in `lookupHit`. + * + * This is specially necessary for when the value has been set to + * `undefined` and we want to avoid looking up parent contexts. + * + * In the case where dot notation is used, we consider the lookup + * to be successful even if the last "object" in the path is + * not actually an object but a primitive (e.g., a string, or an + * integer), because it is sometimes useful to access a property + * of an autoboxed primitive, such as the length of a string. + **/ + while (intermediateValue != null && index < names.length) { + if (index === names.length - 1) + lookupHit = ( + hasProperty(intermediateValue, names[index]) + || primitiveHasOwnProperty(intermediateValue, names[index]) + ); + + intermediateValue = intermediateValue[names[index++]]; } - - context = context.parent; + } else { + intermediateValue = context.view[name]; + + /** + * Only checking against `hasProperty`, which always returns `false` if + * `context.view` is not an object. Deliberately omitting the check + * against `primitiveHasOwnProperty` if dot notation is not used. + * + * Consider this example: + * ``` + * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) + * ``` + * + * If we were to check also against `primitiveHasOwnProperty`, as we do + * in the dot notation case, then render call would return: + * + * "The length of a football field is 9." + * + * rather than the expected: + * + * "The length of a football field is 100 yards." + **/ + lookupHit = hasProperty(context.view, name); } - cache[name] = value; - } - - if (isFunction(value)) - value = value.call(this.view); - - return value; - }; - - /** - * A Writer knows how to take a stream of tokens and render them to a - * string, given a context. It also maintains a cache of templates to - * avoid the need to parse the same template twice. - */ - function Writer () { - this.templateCache = { - _cache: {}, - set: function set (key, value) { - this._cache[key] = value; - }, - get: function get (key) { - return this._cache[key]; - }, - clear: function clear () { - this._cache = {}; + if (lookupHit) { + value = intermediateValue; + break; } - }; - } - /** - * Clears all cached templates in this writer. - */ - Writer.prototype.clearCache = function clearCache () { - if (typeof this.templateCache !== 'undefined') { - this.templateCache.clear(); + context = context.parent; } - }; - /** - * Parses and caches the given `template` according to the given `tags` or - * `mustache.tags` if `tags` is omitted, and returns the array of tokens - * that is generated from the parse. - */ - Writer.prototype.parse = function parse (template, tags) { - var cache = this.templateCache; - var cacheKey = template + ':' + (tags || mustache.tags).join(':'); - var isCacheEnabled = typeof cache !== 'undefined'; - var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; - - if (tokens == undefined) { - tokens = parseTemplate(template, tags); - isCacheEnabled && cache.set(cacheKey, tokens); - } - return tokens; - }; - - /** - * High-level method that is used to render the given `template` with - * the given `view`. - * - * The optional `partials` argument may be an object that contains the - * names and templates of partials that are used in the template. It may - * also be a function that is used to load partial templates on the fly - * that takes a single argument: the name of the partial. - * - * If the optional `config` argument is given here, then it should be an - * object with a `tags` attribute or an `escape` attribute or both. - * If an array is passed, then it will be interpreted the same way as - * a `tags` attribute on a `config` object. - * - * The `tags` attribute of a `config` object must be an array with two - * string values: the opening and closing tags used in the template (e.g. - * [ "<%", "%>" ]). The default is to mustache.tags. - * - * The `escape` attribute of a `config` object must be a function which - * accepts a string as input and outputs a safely escaped string. - * If an `escape` function is not provided, then an HTML-safe string - * escaping function is used as the default. - */ - Writer.prototype.render = function render (template, view, partials, config) { - var tags = this.getConfigTags(config); - var tokens = this.parse(template, tags); - var context = (view instanceof Context) ? view : new Context(view, undefined); - return this.renderTokens(tokens, context, partials, template, config); - }; + cache[name] = value; + } - /** - * Low-level method that renders the given array of `tokens` using - * the given `context` and `partials`. - * - * Note: The `originalTemplate` is only ever used to extract the portion - * of the original template that was contained in a higher-order section. - * If the template doesn't use higher-order sections, this argument may - * be omitted. - */ - Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) { - var buffer = ''; - - var token, symbol, value; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - value = undefined; - token = tokens[i]; - symbol = token[0]; - - if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config); - else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config); - else if (symbol === '>') value = this.renderPartial(token, context, partials, config); - else if (symbol === '&') value = this.unescapedValue(token, context); - else if (symbol === 'name') value = this.escapedValue(token, context, config); - else if (symbol === 'text') value = this.rawValue(token); - - if (value !== undefined) - buffer += value; + if (isFunction(value)) + value = value.call(this.view); + + return value; +}; + +/** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. + */ +function Writer () { + this.templateCache = { + _cache: {}, + set: function set (key, value) { + this._cache[key] = value; + }, + get: function get (key) { + return this._cache[key]; + }, + clear: function clear () { + this._cache = {}; } - - return buffer; }; +} + +/** + * Clears all cached templates in this writer. + */ +Writer.prototype.clearCache = function clearCache () { + if (typeof this.templateCache !== 'undefined') { + this.templateCache.clear(); + } +}; + +/** + * Parses and caches the given `template` according to the given `tags` or + * `mustache.tags` if `tags` is omitted, and returns the array of tokens + * that is generated from the parse. + */ +Writer.prototype.parse = function parse (template, tags) { + var cache = this.templateCache; + var cacheKey = template + ':' + (tags || mustache.tags).join(':'); + var isCacheEnabled = typeof cache !== 'undefined'; + var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; + + if (tokens == undefined) { + tokens = parseTemplate(template, tags); + isCacheEnabled && cache.set(cacheKey, tokens); + } + return tokens; +}; + +/** + * High-level method that is used to render the given `template` with + * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. + * + * If the optional `config` argument is given here, then it should be an + * object with a `tags` attribute or an `escape` attribute or both. + * If an array is passed, then it will be interpreted the same way as + * a `tags` attribute on a `config` object. + * + * The `tags` attribute of a `config` object must be an array with two + * string values: the opening and closing tags used in the template (e.g. + * [ "<%", "%>" ]). The default is to mustache.tags. + * + * The `escape` attribute of a `config` object must be a function which + * accepts a string as input and outputs a safely escaped string. + * If an `escape` function is not provided, then an HTML-safe string + * escaping function is used as the default. + */ +Writer.prototype.render = function render (template, view, partials, config) { + var tags = this.getConfigTags(config); + var tokens = this.parse(template, tags); + var context = (view instanceof Context) ? view : new Context(view, undefined); + return this.renderTokens(tokens, context, partials, template, config); +}; + +/** + * Low-level method that renders the given array of `tokens` using + * the given `context` and `partials`. + * + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. + */ +Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) { + var buffer = ''; + + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = undefined; + token = tokens[i]; + symbol = token[0]; + + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config); + else if (symbol === '>') value = this.renderPartial(token, context, partials, config); + else if (symbol === '&') value = this.unescapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context, config); + else if (symbol === 'text') value = this.rawValue(token); + + if (value !== undefined) + buffer += value; + } - Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) { - var self = this; - var buffer = ''; - var value = context.lookup(token[1]); - - // This function is used to render an arbitrary template - // in the current context by higher-order sections. - function subRender (template) { - return self.render(template, context, partials, config); - } - - if (!value) return; - - if (isArray(value)) { - for (var j = 0, valueLength = value.length; j < valueLength; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); - } - } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { - buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); - } else if (isFunction(value)) { - if (typeof originalTemplate !== 'string') - throw new Error('Cannot use higher-order sections without the original template'); - - // Extract the portion of the original template that the section contains. - value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); - - if (value != null) - buffer += value; - } else { - buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); - } - return buffer; - }; + return buffer; +}; - Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) { - var value = context.lookup(token[1]); +Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) { + var self = this; + var buffer = ''; + var value = context.lookup(token[1]); - // Use JavaScript's definition of falsy. Include empty arrays. - // See https://github.com/janl/mustache.js/issues/186 - if (!value || (isArray(value) && value.length === 0)) - return this.renderTokens(token[4], context, partials, originalTemplate, config); - }; + // This function is used to render an arbitrary template + // in the current context by higher-order sections. + function subRender (template) { + return self.render(template, context, partials, config); + } - Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { - var filteredIndentation = indentation.replace(/[^ \t]/g, ''); - var partialByNl = partial.split('\n'); - for (var i = 0; i < partialByNl.length; i++) { - if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { - partialByNl[i] = filteredIndentation + partialByNl[i]; - } - } - return partialByNl.join('\n'); - }; + if (!value) return; - Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) { - if (!partials) return; - var tags = this.getConfigTags(config); - - var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; - if (value != null) { - var lineHasNonSpace = token[6]; - var tagIndex = token[5]; - var indentation = token[4]; - var indentedValue = value; - if (tagIndex == 0 && indentation) { - indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); - } - var tokens = this.parse(indentedValue, tags); - return this.renderTokens(tokens, context, partials, indentedValue, config); + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); } - }; + } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); + } else if (isFunction(value)) { + if (typeof originalTemplate !== 'string') + throw new Error('Cannot use higher-order sections without the original template'); - Writer.prototype.unescapedValue = function unescapedValue (token, context) { - var value = context.lookup(token[1]); - if (value != null) - return value; - }; + // Extract the portion of the original template that the section contains. + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); - Writer.prototype.escapedValue = function escapedValue (token, context, config) { - var escape = this.getConfigEscape(config) || mustache.escape; - var value = context.lookup(token[1]); if (value != null) - return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value); - }; - - Writer.prototype.rawValue = function rawValue (token) { - return token[1]; - }; - - Writer.prototype.getConfigTags = function getConfigTags (config) { - if (isArray(config)) { - return config; - } - else if (config && typeof config === 'object') { - return config.tags; - } - else { - return undefined; - } - }; - - Writer.prototype.getConfigEscape = function getConfigEscape (config) { - if (config && typeof config === 'object' && !isArray(config)) { - return config.escape; - } - else { - return undefined; + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); + } + return buffer; +}; + +Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) { + var value = context.lookup(token[1]); + + // Use JavaScript's definition of falsy. Include empty arrays. + // See https://github.com/janl/mustache.js/issues/186 + if (!value || (isArray(value) && value.length === 0)) + return this.renderTokens(token[4], context, partials, originalTemplate, config); +}; + +Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { + var filteredIndentation = indentation.replace(/[^ \t]/g, ''); + var partialByNl = partial.split('\n'); + for (var i = 0; i < partialByNl.length; i++) { + if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { + partialByNl[i] = filteredIndentation + partialByNl[i]; } - }; - - var mustache = { - name: 'mustache.js', - version: '4.1.0', - tags: [ '{{', '}}' ], - clearCache: undefined, - escape: undefined, - parse: undefined, - render: undefined, - Scanner: undefined, - Context: undefined, - Writer: undefined, - /** - * Allows a user to override the default caching strategy, by providing an - * object with set, get and clear methods. This can also be used to disable - * the cache by setting it to the literal `undefined`. - */ - set templateCache (cache) { - defaultWriter.templateCache = cache; - }, - /** - * Gets the default or overridden caching object from the default writer. - */ - get templateCache () { - return defaultWriter.templateCache; + } + return partialByNl.join('\n'); +}; + +Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) { + if (!partials) return; + var tags = this.getConfigTags(config); + + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) { + var lineHasNonSpace = token[6]; + var tagIndex = token[5]; + var indentation = token[4]; + var indentedValue = value; + if (tagIndex == 0 && indentation) { + indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); } - }; - - // All high-level mustache.* functions use this writer. - var defaultWriter = new Writer(); + var tokens = this.parse(indentedValue, tags); + return this.renderTokens(tokens, context, partials, indentedValue, config); + } +}; - /** - * Clears all cached templates in the default writer. - */ - mustache.clearCache = function clearCache () { - return defaultWriter.clearCache(); - }; +Writer.prototype.unescapedValue = function unescapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; +}; + +Writer.prototype.escapedValue = function escapedValue (token, context, config) { + var escape = this.getConfigEscape(config) || mustache.escape; + var value = context.lookup(token[1]); + if (value != null) + return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value); +}; + +Writer.prototype.rawValue = function rawValue (token) { + return token[1]; +}; + +Writer.prototype.getConfigTags = function getConfigTags (config) { + if (isArray(config)) { + return config; + } + else if (config && typeof config === 'object') { + return config.tags; + } + else { + return undefined; + } +}; +Writer.prototype.getConfigEscape = function getConfigEscape (config) { + if (config && typeof config === 'object' && !isArray(config)) { + return config.escape; + } + else { + return undefined; + } +}; + +var mustache = { + name: 'mustache.js', + version: '4.1.0', + tags: [ '{{', '}}' ], + clearCache: undefined, + escape: undefined, + parse: undefined, + render: undefined, + Scanner: undefined, + Context: undefined, + Writer: undefined, /** - * Parses and caches the given template in the default writer and returns the - * array of tokens it contains. Doing this ahead of time avoids the need to - * parse templates on the fly as they are rendered. + * Allows a user to override the default caching strategy, by providing an + * object with set, get and clear methods. This can also be used to disable + * the cache by setting it to the literal `undefined`. */ - mustache.parse = function parse (template, tags) { - return defaultWriter.parse(template, tags); - }; - + set templateCache (cache) { + defaultWriter.templateCache = cache; + }, /** - * Renders the `template` with the given `view`, `partials`, and `config` - * using the default writer. + * Gets the default or overridden caching object from the default writer. */ - mustache.render = function render (template, view, partials, config) { - if (typeof template !== 'string') { - throw new TypeError('Invalid template! Template should be a "string" ' + - 'but "' + typeStr(template) + '" was given as the first ' + - 'argument for mustache#render(template, view, partials)'); - } - - return defaultWriter.render(template, view, partials, config); - }; + get templateCache () { + return defaultWriter.templateCache; + } +}; + +// All high-level mustache.* functions use this writer. +var defaultWriter = new Writer(); + +/** + * Clears all cached templates in the default writer. + */ +mustache.clearCache = function clearCache () { + return defaultWriter.clearCache(); +}; + +/** + * Parses and caches the given template in the default writer and returns the + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. + */ +mustache.parse = function parse (template, tags) { + return defaultWriter.parse(template, tags); +}; + +/** + * Renders the `template` with the given `view`, `partials`, and `config` + * using the default writer. + */ +mustache.render = function render (template, view, partials, config) { + if (typeof template !== 'string') { + throw new TypeError('Invalid template! Template should be a "string" ' + + 'but "' + typeStr(template) + '" was given as the first ' + + 'argument for mustache#render(template, view, partials)'); + } - // Export the escaping function so that the user may override it. - // See https://github.com/janl/mustache.js/issues/244 - mustache.escape = escapeHtml; + return defaultWriter.render(template, view, partials, config); +}; - // Export these mainly for testing, but also for advanced usage. - mustache.Scanner = Scanner; - mustache.Context = Context; - mustache.Writer = Writer; +// Export the escaping function so that the user may override it. +// See https://github.com/janl/mustache.js/issues/244 +mustache.escape = escapeHtml; - return mustache; +// Export these mainly for testing, but also for advanced usage. +mustache.Scanner = Scanner; +mustache.Context = Context; +mustache.Writer = Writer; -}))); +export default mustache; diff --git a/mustache.mjs b/mustache.mjs deleted file mode 100644 index 8cad81638..000000000 --- a/mustache.mjs +++ /dev/null @@ -1,764 +0,0 @@ -/*! - * mustache.js - Logic-less {{mustache}} templates with JavaScript - * http://github.com/janl/mustache.js - */ - -var objectToString = Object.prototype.toString; -var isArray = Array.isArray || function isArrayPolyfill (object) { - return objectToString.call(object) === '[object Array]'; -}; - -function isFunction (object) { - return typeof object === 'function'; -} - -/** - * More correct typeof string handling array - * which normally returns typeof 'object' - */ -function typeStr (obj) { - return isArray(obj) ? 'array' : typeof obj; -} - -function escapeRegExp (string) { - return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); -} - -/** - * Null safe way of checking whether or not an object, - * including its prototype, has a given property - */ -function hasProperty (obj, propName) { - return obj != null && typeof obj === 'object' && (propName in obj); -} - -/** - * Safe way of detecting whether or not the given thing is a primitive and - * whether it has the given property - */ -function primitiveHasOwnProperty (primitive, propName) { - return ( - primitive != null - && typeof primitive !== 'object' - && primitive.hasOwnProperty - && primitive.hasOwnProperty(propName) - ); -} - -// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 -// See https://github.com/janl/mustache.js/issues/189 -var regExpTest = RegExp.prototype.test; -function testRegExp (re, string) { - return regExpTest.call(re, string); -} - -var nonSpaceRe = /\S/; -function isWhitespace (string) { - return !testRegExp(nonSpaceRe, string); -} - -var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' -}; - -function escapeHtml (string) { - return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { - return entityMap[s]; - }); -} - -var whiteRe = /\s*/; -var spaceRe = /\s+/; -var equalsRe = /\s*=/; -var curlyRe = /\s*\}/; -var tagRe = /#|\^|\/|>|\{|&|=|!/; - -/** - * 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 - * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of - * course, the default is to use mustaches (i.e. mustache.tags). - * - * A token is an array with at least 4 elements. The first element is the - * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag - * did not contain a symbol (i.e. {{myValue}}) this element is "name". For - * all text that appears outside a symbol this element is "text". - * - * The second element of a token is its "value". For mustache tags this is - * whatever else was inside the tag besides the opening symbol. For text tokens - * this is the text itself. - * - * The third and fourth elements of the token are the start and end indices, - * respectively, of the token in the original template. - * - * Tokens that are the root node of a subtree contain two more elements: 1) an - * array of tokens in the subtree and 2) the index in the original template at - * which the closing tag for that section begins. - * - * Tokens for partials also contain two more elements: 1) a string value of - * indendation prior to that tag and 2) the index of that tag on that line - - * eg a value of 2 indicates the partial is the third tag on this line. - */ -function parseTemplate (template, tags) { - if (!template) - return []; - var lineHasNonSpace = false; - var sections = []; // Stack to hold section tokens - var tokens = []; // Buffer to hold the tokens - var spaces = []; // Indices of whitespace tokens on the current line - var hasTag = false; // Is there a {{tag}} on the current line? - var nonSpace = false; // Is there a non-space char on the current line? - var indentation = ''; // Tracks indentation for tags that use it - var tagIndex = 0; // Stores a count of number of tags encountered on a line - - // Strips all whitespace tokens array for the current line - // if there was a {{#tag}} on it and otherwise only space. - function stripSpace () { - if (hasTag && !nonSpace) { - while (spaces.length) - delete tokens[spaces.pop()]; - } else { - spaces = []; - } - - hasTag = false; - nonSpace = false; - } - - var openingTagRe, closingTagRe, closingCurlyRe; - function compileTags (tagsToCompile) { - if (typeof tagsToCompile === 'string') - tagsToCompile = tagsToCompile.split(spaceRe, 2); - - if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) - throw new Error('Invalid tags: ' + tagsToCompile); - - openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); - closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); - closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); - } - - compileTags(tags || mustache.tags); - - var scanner = new Scanner(template); - - var start, type, value, chr, token, openSection; - while (!scanner.eos()) { - start = scanner.pos; - - // Match any text between tags. - value = scanner.scanUntil(openingTagRe); - - if (value) { - for (var i = 0, valueLength = value.length; i < valueLength; ++i) { - chr = value.charAt(i); - - if (isWhitespace(chr)) { - spaces.push(tokens.length); - indentation += chr; - } else { - nonSpace = true; - lineHasNonSpace = true; - indentation += ' '; - } - - tokens.push([ 'text', chr, start, start + 1 ]); - start += 1; - - // Check for whitespace on the current line. - if (chr === '\n') { - stripSpace(); - indentation = ''; - tagIndex = 0; - lineHasNonSpace = false; - } - } - } - - // Match the opening tag. - if (!scanner.scan(openingTagRe)) - break; - - hasTag = true; - - // Get the tag type. - type = scanner.scan(tagRe) || 'name'; - scanner.scan(whiteRe); - - // Get the tag value. - if (type === '=') { - value = scanner.scanUntil(equalsRe); - scanner.scan(equalsRe); - scanner.scanUntil(closingTagRe); - } else if (type === '{') { - value = scanner.scanUntil(closingCurlyRe); - scanner.scan(curlyRe); - scanner.scanUntil(closingTagRe); - type = '&'; - } else { - value = scanner.scanUntil(closingTagRe); - } - - // Match the closing tag. - if (!scanner.scan(closingTagRe)) - throw new Error('Unclosed tag at ' + scanner.pos); - - if (type == '>') { - token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ]; - } else { - token = [ type, value, start, scanner.pos ]; - } - tagIndex++; - tokens.push(token); - - if (type === '#' || type === '^') { - sections.push(token); - } else if (type === '/') { - // Check section nesting. - openSection = sections.pop(); - - if (!openSection) - throw new Error('Unopened section "' + value + '" at ' + start); - - if (openSection[1] !== value) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); - } else if (type === 'name' || type === '{' || type === '&') { - nonSpace = true; - } else if (type === '=') { - // Set the tags for the next time around. - compileTags(value); - } - } - - stripSpace(); - - // Make sure there are no open sections when we're done. - openSection = sections.pop(); - - if (openSection) - throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - - return nestTokens(squashTokens(tokens)); -} - -/** - * Combines the values of consecutive text tokens in the given `tokens` array - * to a single token. - */ -function squashTokens (tokens) { - var squashedTokens = []; - - var token, lastToken; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; - - if (token) { - if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { - lastToken[1] += token[1]; - lastToken[3] = token[3]; - } else { - squashedTokens.push(token); - lastToken = token; - } - } - } - - return squashedTokens; -} - -/** - * Forms the given array of `tokens` into a nested tree structure where - * tokens that represent a section have two additional items: 1) an array of - * all tokens that appear in that section and 2) the index in the original - * template that represents the end of that section. - */ -function nestTokens (tokens) { - var nestedTokens = []; - var collector = nestedTokens; - var sections = []; - - var token, section; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - token = tokens[i]; - - switch (token[0]) { - case '#': - case '^': - collector.push(token); - sections.push(token); - collector = token[4] = []; - break; - case '/': - section = sections.pop(); - section[5] = token[2]; - collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; - break; - default: - collector.push(token); - } - } - - return nestedTokens; -} - -/** - * A simple string scanner that is used by the template parser to find - * tokens in template strings. - */ -function Scanner (string) { - this.string = string; - this.tail = string; - this.pos = 0; -} - -/** - * Returns `true` if the tail is empty (end of string). - */ -Scanner.prototype.eos = function eos () { - return this.tail === ''; -}; - -/** - * Tries to match the given regular expression at the current position. - * Returns the matched text if it can match, the empty string otherwise. - */ -Scanner.prototype.scan = function scan (re) { - var match = this.tail.match(re); - - if (!match || match.index !== 0) - return ''; - - var string = match[0]; - - this.tail = this.tail.substring(string.length); - this.pos += string.length; - - return string; -}; - -/** - * Skips all text until the given regular expression can be matched. Returns - * the skipped string, which is the entire tail if no match can be made. - */ -Scanner.prototype.scanUntil = function scanUntil (re) { - var index = this.tail.search(re), match; - - switch (index) { - case -1: - match = this.tail; - this.tail = ''; - break; - case 0: - match = ''; - break; - default: - match = this.tail.substring(0, index); - this.tail = this.tail.substring(index); - } - - this.pos += match.length; - - return match; -}; - -/** - * Represents a rendering context by wrapping a view object and - * maintaining a reference to the parent context. - */ -function Context (view, parentContext) { - this.view = view; - this.cache = { '.': this.view }; - this.parent = parentContext; -} - -/** - * Creates a new context using the given view with this context - * as the parent. - */ -Context.prototype.push = function push (view) { - return new Context(view, this); -}; - -/** - * Returns the value of the given name in this context, traversing - * up the context hierarchy if the value is absent in this context's view. - */ -Context.prototype.lookup = function lookup (name) { - var cache = this.cache; - - var value; - if (cache.hasOwnProperty(name)) { - value = cache[name]; - } else { - var context = this, intermediateValue, names, index, lookupHit = false; - - while (context) { - if (name.indexOf('.') > 0) { - intermediateValue = context.view; - names = name.split('.'); - index = 0; - - /** - * Using the dot notion path in `name`, we descend through the - * nested objects. - * - * To be certain that the lookup has been successful, we have to - * check if the last object in the path actually has the property - * we are looking for. We store the result in `lookupHit`. - * - * This is specially necessary for when the value has been set to - * `undefined` and we want to avoid looking up parent contexts. - * - * In the case where dot notation is used, we consider the lookup - * to be successful even if the last "object" in the path is - * not actually an object but a primitive (e.g., a string, or an - * integer), because it is sometimes useful to access a property - * of an autoboxed primitive, such as the length of a string. - **/ - while (intermediateValue != null && index < names.length) { - if (index === names.length - 1) - lookupHit = ( - hasProperty(intermediateValue, names[index]) - || primitiveHasOwnProperty(intermediateValue, names[index]) - ); - - intermediateValue = intermediateValue[names[index++]]; - } - } else { - intermediateValue = context.view[name]; - - /** - * Only checking against `hasProperty`, which always returns `false` if - * `context.view` is not an object. Deliberately omitting the check - * against `primitiveHasOwnProperty` if dot notation is not used. - * - * Consider this example: - * ``` - * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"}) - * ``` - * - * If we were to check also against `primitiveHasOwnProperty`, as we do - * in the dot notation case, then render call would return: - * - * "The length of a football field is 9." - * - * rather than the expected: - * - * "The length of a football field is 100 yards." - **/ - lookupHit = hasProperty(context.view, name); - } - - if (lookupHit) { - value = intermediateValue; - break; - } - - context = context.parent; - } - - cache[name] = value; - } - - if (isFunction(value)) - value = value.call(this.view); - - return value; -}; - -/** - * A Writer knows how to take a stream of tokens and render them to a - * string, given a context. It also maintains a cache of templates to - * avoid the need to parse the same template twice. - */ -function Writer () { - this.templateCache = { - _cache: {}, - set: function set (key, value) { - this._cache[key] = value; - }, - get: function get (key) { - return this._cache[key]; - }, - clear: function clear () { - this._cache = {}; - } - }; -} - -/** - * Clears all cached templates in this writer. - */ -Writer.prototype.clearCache = function clearCache () { - if (typeof this.templateCache !== 'undefined') { - this.templateCache.clear(); - } -}; - -/** - * Parses and caches the given `template` according to the given `tags` or - * `mustache.tags` if `tags` is omitted, and returns the array of tokens - * that is generated from the parse. - */ -Writer.prototype.parse = function parse (template, tags) { - var cache = this.templateCache; - var cacheKey = template + ':' + (tags || mustache.tags).join(':'); - var isCacheEnabled = typeof cache !== 'undefined'; - var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined; - - if (tokens == undefined) { - tokens = parseTemplate(template, tags); - isCacheEnabled && cache.set(cacheKey, tokens); - } - return tokens; -}; - -/** - * High-level method that is used to render the given `template` with - * the given `view`. - * - * The optional `partials` argument may be an object that contains the - * names and templates of partials that are used in the template. It may - * also be a function that is used to load partial templates on the fly - * that takes a single argument: the name of the partial. - * - * If the optional `config` argument is given here, then it should be an - * object with a `tags` attribute or an `escape` attribute or both. - * If an array is passed, then it will be interpreted the same way as - * a `tags` attribute on a `config` object. - * - * The `tags` attribute of a `config` object must be an array with two - * string values: the opening and closing tags used in the template (e.g. - * [ "<%", "%>" ]). The default is to mustache.tags. - * - * The `escape` attribute of a `config` object must be a function which - * accepts a string as input and outputs a safely escaped string. - * If an `escape` function is not provided, then an HTML-safe string - * escaping function is used as the default. - */ -Writer.prototype.render = function render (template, view, partials, config) { - var tags = this.getConfigTags(config); - var tokens = this.parse(template, tags); - var context = (view instanceof Context) ? view : new Context(view, undefined); - return this.renderTokens(tokens, context, partials, template, config); -}; - -/** - * Low-level method that renders the given array of `tokens` using - * the given `context` and `partials`. - * - * Note: The `originalTemplate` is only ever used to extract the portion - * of the original template that was contained in a higher-order section. - * If the template doesn't use higher-order sections, this argument may - * be omitted. - */ -Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) { - var buffer = ''; - - var token, symbol, value; - for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { - value = undefined; - token = tokens[i]; - symbol = token[0]; - - if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config); - else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config); - else if (symbol === '>') value = this.renderPartial(token, context, partials, config); - else if (symbol === '&') value = this.unescapedValue(token, context); - else if (symbol === 'name') value = this.escapedValue(token, context, config); - else if (symbol === 'text') value = this.rawValue(token); - - if (value !== undefined) - buffer += value; - } - - return buffer; -}; - -Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) { - var self = this; - var buffer = ''; - var value = context.lookup(token[1]); - - // This function is used to render an arbitrary template - // in the current context by higher-order sections. - function subRender (template) { - return self.render(template, context, partials, config); - } - - if (!value) return; - - if (isArray(value)) { - for (var j = 0, valueLength = value.length; j < valueLength; ++j) { - buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); - } - } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { - buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); - } else if (isFunction(value)) { - if (typeof originalTemplate !== 'string') - throw new Error('Cannot use higher-order sections without the original template'); - - // Extract the portion of the original template that the section contains. - value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); - - if (value != null) - buffer += value; - } else { - buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); - } - return buffer; -}; - -Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) { - var value = context.lookup(token[1]); - - // Use JavaScript's definition of falsy. Include empty arrays. - // See https://github.com/janl/mustache.js/issues/186 - if (!value || (isArray(value) && value.length === 0)) - return this.renderTokens(token[4], context, partials, originalTemplate, config); -}; - -Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) { - var filteredIndentation = indentation.replace(/[^ \t]/g, ''); - var partialByNl = partial.split('\n'); - for (var i = 0; i < partialByNl.length; i++) { - if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { - partialByNl[i] = filteredIndentation + partialByNl[i]; - } - } - return partialByNl.join('\n'); -}; - -Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) { - if (!partials) return; - var tags = this.getConfigTags(config); - - var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; - if (value != null) { - var lineHasNonSpace = token[6]; - var tagIndex = token[5]; - var indentation = token[4]; - var indentedValue = value; - if (tagIndex == 0 && indentation) { - indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); - } - var tokens = this.parse(indentedValue, tags); - return this.renderTokens(tokens, context, partials, indentedValue, config); - } -}; - -Writer.prototype.unescapedValue = function unescapedValue (token, context) { - var value = context.lookup(token[1]); - if (value != null) - return value; -}; - -Writer.prototype.escapedValue = function escapedValue (token, context, config) { - var escape = this.getConfigEscape(config) || mustache.escape; - var value = context.lookup(token[1]); - if (value != null) - return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value); -}; - -Writer.prototype.rawValue = function rawValue (token) { - return token[1]; -}; - -Writer.prototype.getConfigTags = function getConfigTags (config) { - if (isArray(config)) { - return config; - } - else if (config && typeof config === 'object') { - return config.tags; - } - else { - return undefined; - } -}; - -Writer.prototype.getConfigEscape = function getConfigEscape (config) { - if (config && typeof config === 'object' && !isArray(config)) { - return config.escape; - } - else { - return undefined; - } -}; - -var mustache = { - name: 'mustache.js', - version: '4.1.0', - tags: [ '{{', '}}' ], - clearCache: undefined, - escape: undefined, - parse: undefined, - render: undefined, - Scanner: undefined, - Context: undefined, - Writer: undefined, - /** - * Allows a user to override the default caching strategy, by providing an - * object with set, get and clear methods. This can also be used to disable - * the cache by setting it to the literal `undefined`. - */ - set templateCache (cache) { - defaultWriter.templateCache = cache; - }, - /** - * Gets the default or overridden caching object from the default writer. - */ - get templateCache () { - return defaultWriter.templateCache; - } -}; - -// All high-level mustache.* functions use this writer. -var defaultWriter = new Writer(); - -/** - * Clears all cached templates in the default writer. - */ -mustache.clearCache = function clearCache () { - return defaultWriter.clearCache(); -}; - -/** - * Parses and caches the given template in the default writer and returns the - * array of tokens it contains. Doing this ahead of time avoids the need to - * parse templates on the fly as they are rendered. - */ -mustache.parse = function parse (template, tags) { - return defaultWriter.parse(template, tags); -}; - -/** - * Renders the `template` with the given `view`, `partials`, and `config` - * using the default writer. - */ -mustache.render = function render (template, view, partials, config) { - if (typeof template !== 'string') { - throw new TypeError('Invalid template! Template should be a "string" ' + - 'but "' + typeStr(template) + '" was given as the first ' + - 'argument for mustache#render(template, view, partials)'); - } - - return defaultWriter.render(template, view, partials, config); -}; - -// Export the escaping function so that the user may override it. -// See https://github.com/janl/mustache.js/issues/244 -mustache.escape = escapeHtml; - -// Export these mainly for testing, but also for advanced usage. -mustache.Scanner = Scanner; -mustache.Context = Context; -mustache.Writer = Writer; - -export default mustache; diff --git a/package.json b/package.json index e938a83b2..938cb2194 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "url": "https://raw.github.com/janl/mustache.js/{version}/mustache.js" }, "scripts": { - "build": "rollup mustache.mjs --file mustache.js --format umd --name Mustache --banner \"// This file has been generated from mustache.mjs\" && uglifyjs mustache.js > mustache.min.js", + "build": "cp mustache.js mustache.mjs && rollup mustache.mjs --file mustache.js --format umd --name Mustache && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", - "test-lint": "eslint mustache.mjs bin/mustache test/**/*.js", + "test-lint": "eslint mustache.js bin/mustache test/**/*.js", "test-unit": "mocha --reporter spec test/*-test.js", "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", From 2061046686c46fd5b2b54611f4eb55f56991a210 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 26 Dec 2020 01:05:29 +0100 Subject: [PATCH 271/286] Remove .min.js from git repository, will be kept in npm package --- .gitignore | 1 + mustache.min.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 mustache.min.js diff --git a/.gitignore b/.gitignore index 0b968161b..adaaa819c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ npm-debug.log test/render-test-browser.js .idea/ mustache.mjs +mustache.min.js diff --git a/mustache.min.js b/mustache.min.js deleted file mode 100644 index 9dbc77b4d..000000000 --- a/mustache.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.1.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); From a93c39eef286bdbaa3562902d7870b1d6444ecad Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 22 Feb 2021 13:11:01 +0100 Subject: [PATCH 272/286] Bump `mustache.js` version via npm script instead of git pre-commit hook Primarily because installing `git` hooks are easy to forget for maintainers, whilst telling `npm` to do something as a consequence of `$ npm version ` is seamless. Also worth mentioning we what we used to do in the pre-commit hook script is now greatly simplified as we don't want to have the build output and/or the `.min.js` in git anymore. Therefore, as long as we've bumped the version number in the source code, we're basically done, after having amended that change into the git commit the `npm` CLI creates. --- hooks/install-hooks.sh | 22 ------------------- package.json | 3 ++- .../bump-version-in-source | 14 ++---------- 3 files changed, 4 insertions(+), 35 deletions(-) delete mode 100755 hooks/install-hooks.sh rename hooks/pre-commit => scripts/bump-version-in-source (78%) diff --git a/hooks/install-hooks.sh b/hooks/install-hooks.sh deleted file mode 100755 index 422d24d22..000000000 --- a/hooks/install-hooks.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -HOOK_NAMES="pre-commit" -HOOK_DIR=$(git rev-parse --show-toplevel)/.git/hooks -INSTALL_DIR=$(git rev-parse --show-toplevel)/hooks -COLOR_GREEN=`tput setaf 2` -COLOR_RESET=`tput sgr0` - -for hook in $HOOK_NAMES; do - echo -n "Installing $hook hook..." - # If the hook already exists, is executable, and is not a symlink - if [ ! -h $HOOK_DIR/$hook -a -x $HOOK_DIR/$hook ]; then - echo -n " Hook already exists, saving old hook backup at $HOOK_DIR/$hook.local..." - mv $HOOK_DIR/$hook $HOOK_DIR/$hook.local - fi - # create the symlink, overwriting the file if it exists - # probably the only way this would happen is if you're using an old version of git - # -- back when the sample hooks were not executable, instead of being named ____.sample - echo -n " Creating symlink..." - ln -s -f $INSTALL_DIR/$hook $HOOK_DIR - echo "${COLOR_GREEN} Done! ✓${COLOR_RESET}" -done \ No newline at end of file diff --git a/package.json b/package.json index 938cb2194..48c4a5f94 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", "test-browser": "npm run pre-test-browser && zuul -- test/context-test.js test/parse-test.js test/scanner-test.js test/render-test-browser.js", - "test-browser-local": "npm run pre-test-browser && zuul --local 8080 -- test/context-test.js test/scanner-test.js test/parse-test.js test/render-test-browser.js" + "test-browser-local": "npm run pre-test-browser && zuul --local 8080 -- test/context-test.js test/scanner-test.js test/parse-test.js test/render-test-browser.js", + "postversion": "scripts/bump-version-in-source" }, "devDependencies": { "chai": "^3.4.0", diff --git a/hooks/pre-commit b/scripts/bump-version-in-source similarity index 78% rename from hooks/pre-commit rename to scripts/bump-version-in-source index db8ad4528..2574d201f 100755 --- a/hooks/pre-commit +++ b/scripts/bump-version-in-source @@ -33,19 +33,9 @@ class Bumper # if bumped, do extra stuff and notify the user if @bumped + `git add mustache.js` + `git commit --amend --no-edit` - # keep .mjs & .js|.min.js in sync - puts "> building and minifying `mustache.mjs`..." - `npm run build` - - # stage files for commit - `git add package.json` - @sources.each {|source| `git add #{source.path}`} - `git add mustache.min.js` - `git commit -m ":ship: bump to version #{@target_v}"` - - # notify codemonkey - puts "staged bumped files and created commit" puts_c 32, "successfully bumped version to #{@target_v}!" puts_c 33, "don't forget to `npm publish`!" end From d4a50420c5fc7da546ec8653a0def48a81d77dd5 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 1 Feb 2021 19:29:24 +0100 Subject: [PATCH 273/286] Use `esm` package locally when testing to use ESM syntax from CJS code These changes are primarily aiming for having an acceptable developer experience when working on changes locally and want to run the test suite. By using `esm` as a required package to be executed *before* the individual test files are executed, `esm` allows CommonJS/`require()` based code to use code written with ES Modules syntax. In practise that means our existing CommonJS based test suite that is `require()`ing the source code witten in ES Modules, is seamless and totally transparent. It just works without any build or transpiling pipeline like `babel` involved. Caveat: the `esm` package only support Node.js 6.x and above. That is more than okey, as we've got continous integration to verify how our changes works on different versions of Node.js, browsers & deno. Refs https://www.npmjs.com/package/esm --- .esmrc | 8 ++++++++ package-lock.json | 47 ++++++++++++++++++++++++++++++++++++----------- package.json | 3 ++- test/cli-test.js | 2 +- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 .esmrc diff --git a/.esmrc b/.esmrc new file mode 100644 index 000000000..c40474328 --- /dev/null +++ b/.esmrc @@ -0,0 +1,8 @@ +{ + cjs: { + // Ensure ESM `export default` ends up as the root, e.g. `module.exports` when + // being `require()`d from CJS code. This is not spec compliant, but that does + // not matter because only use this `esm` package trickery locally while testing + dedefault: true + } +} diff --git a/package-lock.json b/package-lock.json index a3438bd89..2a09067e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2178,6 +2178,12 @@ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, "espree": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", @@ -2905,7 +2911,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2926,12 +2933,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2946,17 +2955,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3073,7 +3085,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3085,6 +3098,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3099,6 +3113,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3106,12 +3121,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3130,6 +3147,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3210,7 +3228,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3222,6 +3241,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3307,7 +3327,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3343,6 +3364,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3362,6 +3384,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3405,12 +3428,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 48c4a5f94..71312e0fd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build": "cp mustache.js mustache.mjs && rollup mustache.mjs --file mustache.js --format umd --name Mustache && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", "test-lint": "eslint mustache.js bin/mustache test/**/*.js", - "test-unit": "mocha --reporter spec test/*-test.js", + "test-unit": "mocha --reporter spec --require esm test/*-test.js", "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", "test-browser": "npm run pre-test-browser && zuul -- test/context-test.js test/parse-test.js test/scanner-test.js test/render-test-browser.js", @@ -41,6 +41,7 @@ "devDependencies": { "chai": "^3.4.0", "eslint": "^6.5.1", + "esm": "^3.2.25", "jshint": "^2.9.5", "mocha": "^3.0.2", "puppeteer": "^2.0.0", diff --git a/test/cli-test.js b/test/cli-test.js index b40917986..6f7d2f872 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -9,10 +9,10 @@ var cliPartialsTxt = path.resolve(_files, 'cli_with_partials.txt'); var moduleVersion = require('../package').version; function changeForOS (command) { + command = command.replace('bin/mustache', 'node --require esm bin/mustache') if (process.platform === 'win32') { return command - .replace(/bin\/mustache/g, 'node bin\\mustache') .replace(/\bcat\b/g, 'type') .replace(/\//g, '\\'); } From 69bf4bd47f3c2b98330e18fcd1cfaa9e0f4b469d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 6 Feb 2021 19:30:53 +0100 Subject: [PATCH 274/286] Avoid use of `esm` when running tests on legacy Node.js versions Because `esm` does not support legacy versions of Node.js, at least not below Node.js 6 as of writing this, we have to avoid using that whenever tests are running on legacy versions. But now that the source code (`mustache.js`) is written in ESM syntax, how on earth are we going to run tests on these legacy versions of Node.js? We gotta run the build step first, so that we end up with a `mustache.js` file in CJS, or strictly speaking it will be UMD. That's kinda pain in the backside isn't it? Yes, but running tests on legacy versions are not meant to be done locally, but rather in CI. That means we can easily automate the flow of (1) building the source code before (2) starting the test suite. For our futureselves, if we want to stop running tests on legacy versions of Node.js; the changes introduces in this commit, could be removed completely. --- package.json | 2 +- test/cli-test.js | 3 ++- test/helper.js | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 71312e0fd..48d672c4b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build": "cp mustache.js mustache.mjs && rollup mustache.mjs --file mustache.js --format umd --name Mustache && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", "test-lint": "eslint mustache.js bin/mustache test/**/*.js", - "test-unit": "mocha --reporter spec --require esm test/*-test.js", + "test-unit": "mocha --reporter spec test/*-test.js", "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", "test-browser": "npm run pre-test-browser && zuul -- test/context-test.js test/parse-test.js test/scanner-test.js test/render-test-browser.js", diff --git a/test/cli-test.js b/test/cli-test.js index 6f7d2f872..cfaabddfb 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -9,7 +9,8 @@ var cliPartialsTxt = path.resolve(_files, 'cli_with_partials.txt'); var moduleVersion = require('../package').version; function changeForOS (command) { - command = command.replace('bin/mustache', 'node --require esm bin/mustache') + var requireFlag = !isLegacyNodeVersion ? '--require esm' : ''; + command = command.replace('bin/mustache', 'node ' + requireFlag + ' bin/mustache') if (process.platform === 'win32') { return command diff --git a/test/helper.js b/test/helper.js index 298876621..f89a5f6f7 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1,4 +1,12 @@ var chai = require('chai'); +var nodejsMajorVersion = Number(process.versions.node.split(".")[0]); + +isLegacyNodeVersion = !(nodejsMajorVersion >= 10); + +if (!isLegacyNodeVersion) { + require = require("esm")(module); +} + assert = chai.assert; chai.should(); Mustache = require('../mustache'); From 3e29d677561cff9924580e67abd61820144c28e5 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 3 Jan 2021 12:18:56 +0100 Subject: [PATCH 275/286] Build ESM -> CJS before running legacy Node.js tests and packaging tests Because the source code is written in ESM syntax and we cannot use the `esm` package to make `require()` ESM compatible, since that package does not support legacy versions of Node.js. Also in our usage tests in CI, we need to mimck the npm packaging behaviour where building is involved. --- .github/workflows/usage.yml | 36 ++++++++++++++++++++++++++++++++++++ .github/workflows/verify.yml | 26 ++++++++++++++++++++++++++ package.json | 3 ++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/.github/workflows/usage.yml b/.github/workflows/usage.yml index 5608b2eba..75d68ff7e 100644 --- a/.github/workflows/usage.yml +++ b/.github/workflows/usage.yml @@ -3,15 +3,41 @@ name: Package usage on: [push, pull_request] jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: npm install and build + run: | + npm install + npm run build + - name: Store build-output for later + uses: actions/upload-artifact@v2 + with: + name: build-output + path: | + mustache.js + mustache.mjs + package: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v1 with: node-version: 14.x + - name: Get build-output from build step + uses: actions/download-artifact@v2 + with: + name: build-output - name: Create package tarball run: | export ARCHIVE_FILENAME=$(npm pack | tail -n 1) @@ -71,12 +97,17 @@ jobs: browser-usage: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v1 - name: Setup Node.js uses: actions/setup-node@v1 with: node-version: 12.x + - name: Get build-output from build step + uses: actions/download-artifact@v2 + with: + name: build-output - name: Install and test run: | npm ci @@ -85,10 +116,15 @@ jobs: deno-usage: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v1 - uses: denolib/setup-deno@master with: deno-version: 'v1.0.0' + - name: Get build-output from build step + uses: actions/download-artifact@v2 + with: + name: build-output - run: deno --version - run: deno test --allow-net=deno.land test/module-systems/deno-test.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8a4258914..0a86a51db 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -35,6 +35,27 @@ jobs: npm install npm run test-unit + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: npm install and build + run: | + npm install + npm run build + - name: Store build-output for later + uses: actions/upload-artifact@v2 + with: + name: build-output + path: | + mustache.js + mustache.mjs + tests-on-legacy: runs-on: ubuntu-latest @@ -42,6 +63,7 @@ jobs: matrix: node-version: [0.10.x, 0.12.x, 4.x, 6.x, 8.x] + needs: build steps: - uses: actions/checkout@v2 with: @@ -50,6 +72,10 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Get build-output from build step + uses: actions/download-artifact@v2 + with: + name: build-output - name: npm install and test run: | npm install mocha@3 chai@3 diff --git a/package.json b/package.json index 48d672c4b..f5dda0608 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "pre-test-browser": "node test/create-browser-suite.js", "test-browser": "npm run pre-test-browser && zuul -- test/context-test.js test/parse-test.js test/scanner-test.js test/render-test-browser.js", "test-browser-local": "npm run pre-test-browser && zuul --local 8080 -- test/context-test.js test/scanner-test.js test/parse-test.js test/render-test-browser.js", - "postversion": "scripts/bump-version-in-source" + "postversion": "scripts/bump-version-in-source", + "prepublishOnly": "npm run build" }, "devDependencies": { "chai": "^3.4.0", From f15befd50f3d3d371f692d41815f68311082390d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sat, 6 Feb 2021 20:51:45 +0100 Subject: [PATCH 276/286] Build ESM -> CJS before running tests in browsers via Saucelabs Noteworthy tweak is the fact that it seems `zuul` are eagerly loading all `require('whatever-package-or-path')` it sees before pushing the contents to Saucelabs. That was troublesome because we don't want the `esm` package to be loaded when running browser tests. But it was, even though we loaded `esm` conditionally after checking the current Node.js version used. The trick was to not use `require('esm')` but `module.require('esm')` instead, to fool the assumed look-out for `require` somewhere inside `zuul`'s code. --- .travis.yml | 2 +- test/helper.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 33fd94e6a..a81a06dcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - 8 script: - - npm test + - npm run build - "test $TRAVIS_PULL_REQUEST != 'false' || test $TRAVIS_NODE_VERSION != '8' || npm run test-browser" env: global: diff --git a/test/helper.js b/test/helper.js index f89a5f6f7..aa9f83dd3 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1,12 +1,18 @@ var chai = require('chai'); -var nodejsMajorVersion = Number(process.versions.node.split(".")[0]); +var isRunningInNode = process !== undefined && process.versions.node !== undefined; -isLegacyNodeVersion = !(nodejsMajorVersion >= 10); +if (isRunningInNode) { + var nodejsMajorVersion = Number(process.versions.node.split('.')[0]); + isLegacyNodeVersion = !(nodejsMajorVersion >= 10); -if (!isLegacyNodeVersion) { - require = require("esm")(module); + if (!isLegacyNodeVersion) { + // The `zuul` package we use to run tests in browsers via Saucelabs eagerly loads all + // packages it sees being used via `require()`. Because we don't want the `esm` package + // to be loaded when running browser tests, we refer to `require()` via `module.require()` + // because that avoid the mentioned eager loading + module.require = module.require('esm')(module); + } } - assert = chai.assert; chai.should(); Mustache = require('../mustache'); From ea3adcfc93fed6d7060b514599d7ff3e4bd1ab6f Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Sun, 14 Mar 2021 06:04:30 -0400 Subject: [PATCH 277/286] Add package.json `exports` field (#773) These changes adds an `exports` field to package.json with the main goal of allowing the root/main export to be our ESM syntax version of the package, as opposed to the UMD/CommonJS version. This will make it easier for using projects written with ESM to use mustache.js out of the box, without having to figure out that they'd have to `import mustache/mustache.mjs` to get the ESM version. It is also assumed to be beneficial for tools like http://skypack.dev to choose the correct ESM version when appropriate. Refs https://nodejs.org/api/packages.html#packages_package_entry_points --- .github/workflows/usage.yml | 2 ++ package.json | 7 +++++++ test/module-systems/esm-test-exports.mjs | 12 ++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 test/module-systems/esm-test-exports.mjs diff --git a/.github/workflows/usage.yml b/.github/workflows/usage.yml index 75d68ff7e..f22ede85c 100644 --- a/.github/workflows/usage.yml +++ b/.github/workflows/usage.yml @@ -90,9 +90,11 @@ jobs: export UNPACK_DESTINATION=$(mktemp -d) mv mustache.tgz $UNPACK_DESTINATION cp test/module-systems/esm-test.mjs $UNPACK_DESTINATION + cp test/module-systems/esm-test-exports.mjs $UNPACK_DESTINATION cd $UNPACK_DESTINATION npm install mustache.tgz node esm-test.mjs + node esm-test-exports.mjs browser-usage: runs-on: ubuntu-latest diff --git a/package.json b/package.json index f5dda0608..bec0f7691 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,13 @@ "mustache.min.js", "wrappers/" ], + "exports": { + ".": { + "import": "./mustache.mjs", + "require": "./mustache.js" + }, + "./*": "./*" + }, "volo": { "url": "https://raw.github.com/janl/mustache.js/{version}/mustache.js" }, diff --git a/test/module-systems/esm-test-exports.mjs b/test/module-systems/esm-test-exports.mjs new file mode 100644 index 000000000..d79feb629 --- /dev/null +++ b/test/module-systems/esm-test-exports.mjs @@ -0,0 +1,12 @@ +import assert from 'assert'; +import mustache from 'mustache'; + +const view = { + title: 'Joe', + calc: () => 2 + 4 +}; + +assert.strictEqual( + mustache.render('{{title}} spends {{calc}}', view), + 'Joe spends 6' +); From cc66a7084a1dbd31d4e9ff2fa00db59582639e34 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 28 Mar 2021 21:26:21 +0200 Subject: [PATCH 278/286] Preparing CHANGELOG for v4.2.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5c7ce89..b1f72d0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.2.0] / 28 March 2021 + +### Added + +* [#773]: Add package.json `exports` field, by [@manzt]. + ## [4.1.0] / 6 December 2020 ### Added @@ -555,6 +561,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [#735]: https://github.com/janl/mustache.js/issues/735 [#739]: https://github.com/janl/mustache.js/issues/739 [#764]: https://github.com/janl/mustache.js/issues/764 +[#773]: https://github.com/janl/mustache.js/issues/773 [@afc163]: https://github.com/afc163 [@aielo]: https://github.com/aielo @@ -582,6 +589,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec [@kookookchoozeus]: https://github.com/kookookchoozeus [@kristijanmatic]: https://github.com/kristijanmatic [@kevindew]: https://github.com/kevindew +[@manzt]: https://github.com/manzt [@mateusortiz]: https://github.com/mateusortiz [@mightyplow]: https://github.com/mightyplow [@mikesherov]: https://github.com/mikesherov From 813e273a658677852ab37e6f47c98a9d9352ccde Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 28 Mar 2021 21:26:48 +0200 Subject: [PATCH 279/286] 4.2.0 --- mustache.js | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mustache.js b/mustache.js index 8cad81638..ed0cd6d71 100644 --- a/mustache.js +++ b/mustache.js @@ -694,7 +694,7 @@ Writer.prototype.getConfigEscape = function getConfigEscape (config) { var mustache = { name: 'mustache.js', - version: '4.1.0', + version: '4.2.0', tags: [ '{{', '}}' ], clearCache: undefined, escape: undefined, diff --git a/package-lock.json b/package-lock.json index 2a09067e9..3e688ca86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.1.0", + "version": "4.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index bec0f7691..81ce69d7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mustache", - "version": "4.1.0", + "version": "4.2.0", "description": "Logic-less {{mustache}} templates with JavaScript", "author": "mustache.js Authors ", "homepage": "https://github.com/janl/mustache.js", From bd29972ab8a0f4c592f35483615ab9a274396300 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Sun, 28 Mar 2021 21:35:41 +0200 Subject: [PATCH 280/286] Added 4.2.0 ref in CHANGELOG ..simply forgot that when preparing the v4.2.0 release --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f72d0a3..9f6f89e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -483,6 +483,7 @@ This release is made to revert changes introduced in [2.3.1] that caused unexpec * Fixed a bug that clashed with QUnit (thanks [@kannix]). * Added volo support (thanks [@guybedford]). +[4.2.0]: https://github.com/janl/mustache.js/compare/v4.1.0...v4.2.0 [4.1.0]: https://github.com/janl/mustache.js/compare/v4.0.1...v4.1.0 [4.0.1]: https://github.com/janl/mustache.js/compare/v4.0.0...v4.0.1 [4.0.0]: https://github.com/janl/mustache.js/compare/v3.2.1...v4.0.0 From edc988b7d5dade30aa44e3c4a2285619c72d46e9 Mon Sep 17 00:00:00 2001 From: Nate Sire Date: Sun, 11 Apr 2021 16:17:42 -0400 Subject: [PATCH 281/286] Improve usage example in README.md (#778) By showing explicitly how/where `Mustache` comes from in that specific scenario, where CommonJS is assume to be available, either provided by Node.js or a build tool like webpack etc. For the record, it could also be `import` if the using project uses ES Modules, as we've recently exposed native ES Modules support as well. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 127dfe106..afe4bd63f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ $ npm install mustache --save Below is a quick example how to use mustache.js: ```js +var Mustache = require('mustache'); + var view = { title: "Joe", calc: function () { From af216b0ce7ec6e4e384a2c43f0f65e2a31900ca0 Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 12 Apr 2021 19:51:44 +0200 Subject: [PATCH 282/286] Include linting of `test/*.js` in `npm run test-lint` script Had obviously been forgotten, but those test files should have been part of the ordinary lint phase. The fact that we had forgotten to lint these files, allowed a missing semi-colon to sneak into `./test/cli-test.js` file. Refs https://github.com/janl/mustache.js/pull/777 --- package.json | 2 +- test/render-test-browser-tmpl.mustache | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 81ce69d7e..89e2781f7 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "scripts": { "build": "cp mustache.js mustache.mjs && rollup mustache.mjs --file mustache.js --format umd --name Mustache && uglifyjs mustache.js > mustache.min.js", "test": "npm run test-lint && npm run test-unit", - "test-lint": "eslint mustache.js bin/mustache test/**/*.js", + "test-lint": "eslint mustache.js bin/mustache test/*.js test/**/*.js", "test-unit": "mocha --reporter spec test/*-test.js", "test-render": "mocha --reporter spec test/render-test", "pre-test-browser": "node test/create-browser-suite.js", diff --git a/test/render-test-browser-tmpl.mustache b/test/render-test-browser-tmpl.mustache index 808389121..b9b14ae45 100644 --- a/test/render-test-browser-tmpl.mustache +++ b/test/render-test-browser-tmpl.mustache @@ -1,3 +1,4 @@ +/* eslint-disable */ require('./helper'); describe('Mustache.render', function () { From e53a6e8cd2ea9cb44d63af561f936def7c921c30 Mon Sep 17 00:00:00 2001 From: Mejia1994 Date: Mon, 12 Apr 2021 11:59:53 -0600 Subject: [PATCH 283/286] Fix missing semicolon in `test/cli-test.js` (#777) Co-authored-by: JUAN RAFAEL MEJIA LAGUNA --- test/cli-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli-test.js b/test/cli-test.js index cfaabddfb..c01395c65 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -10,7 +10,7 @@ var moduleVersion = require('../package').version; function changeForOS (command) { var requireFlag = !isLegacyNodeVersion ? '--require esm' : ''; - command = command.replace('bin/mustache', 'node ' + requireFlag + ' bin/mustache') + command = command.replace('bin/mustache', 'node ' + requireFlag + ' bin/mustache'); if (process.platform === 'win32') { return command From e90b2a9aeb945d7cee2b1cd3578d2089c5d3125d Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Mon, 12 Apr 2021 21:40:41 +0200 Subject: [PATCH 284/286] Run CommonJS/ESM usage tests on more Node.js versions (#779) As it's very interesting to see how the package works for different major versions of Node.js, not just 12.x or 13.x as before. --- .github/workflows/usage.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/usage.yml b/.github/workflows/usage.yml index f22ede85c..c4c493313 100644 --- a/.github/workflows/usage.yml +++ b/.github/workflows/usage.yml @@ -51,13 +51,17 @@ jobs: common-js-usage: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [10.x, 12.x, 14.x, 15.x] + needs: package steps: - uses: actions/checkout@v1 - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: 12.x + node-version: ${{ matrix.node-version }} - name: Get package tarball from package step uses: actions/download-artifact@v2 with: @@ -74,13 +78,17 @@ jobs: esm-usage: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x, 15.x] + needs: package steps: - uses: actions/checkout@v1 - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: '>=13.2.0' + node-version: ${{ matrix.node-version }} - name: Get package tarball from package step uses: actions/download-artifact@v2 with: From 550d1da9e3f322649d04b4795f5356914f6fd7e8 Mon Sep 17 00:00:00 2001 From: Pietro Menna Date: Tue, 27 Apr 2021 09:45:52 -0300 Subject: [PATCH 285/286] Fix broken link in README.md (#782) Currently links to https://mustache.github.com/ lead the user to a 404 page. With this correction they will be linked to https://mustache.github.io that is the expected page. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index afe4bd63f..f6ac3358c 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ [![Build Status](https://travis-ci.org/janl/mustache.js.svg?branch=master)](https://travis-ci.org/janl/mustache.js) -[mustache.js](http://github.com/janl/mustache.js) is a zero-dependency implementation of the [mustache](http://mustache.github.com/) template system in JavaScript. +[mustache.js](http://github.com/janl/mustache.js) is a zero-dependency implementation of the [mustache](http://mustache.github.io/) template system in JavaScript. -[Mustache](http://mustache.github.com/) is a logic-less template syntax. It can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object. +[Mustache](http://mustache.github.io/) is a logic-less template syntax. It can be used for HTML, config files, source code - anything. It works by expanding tags in a template using values provided in a hash or object. We call it "logic-less" because there are no if statements, else clauses, or for loops. Instead there are only tags. Some tags are replaced with a value, some nothing, and others a series of values. -For a language-agnostic overview of mustache's template syntax, see the `mustache(5)` [manpage](http://mustache.github.com/mustache.5.html). +For a language-agnostic overview of mustache's template syntax, see the `mustache(5)` [manpage](http://mustache.github.io/mustache.5.html). ## Where to use mustache.js? @@ -49,11 +49,11 @@ var view = { var output = Mustache.render("{{title}} spends {{calc}}", view); ``` -In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.com/) template and 2) a `view` object that contains the data and code needed to render the template. +In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.io/) template and 2) a `view` object that contains the data and code needed to render the template. ## Templates -A [mustache](http://mustache.github.com/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. There are several types of tags available in mustache.js, described below. +A [mustache](http://mustache.github.io/) template is a string that contains any number of mustache tags. Tags are indicated by the double mustaches that surround them. `{{person}}` is a tag, as is `{{#person}}`. In both examples we refer to `person` as the tag's key. There are several types of tags available in mustache.js, described below. There are several techniques that can be used to load templates and hand them to mustache.js, here are two of them: From 972fd2b27a036888acfcb60d6119317744fac7ee Mon Sep 17 00:00:00 2001 From: Taha Shieenavaz Date: Sat, 21 Jan 2023 18:51:41 +0100 Subject: [PATCH 286/286] Makes README.md examples more convenient (#812) --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f6ac3358c..ea74fc830 100644 --- a/README.md +++ b/README.md @@ -37,16 +37,14 @@ $ npm install mustache --save Below is a quick example how to use mustache.js: ```js -var Mustache = require('mustache'); +const Mustache = require('mustache'); -var view = { +const view = { title: "Joe", - calc: function () { - return 2 + 4; - } + calc: () => ( 2 + 4 ) }; -var output = Mustache.render("{{title}} spends {{calc}}", view); +const output = Mustache.render("{{title}} spends {{calc}}", view); ``` In this example, the `Mustache.render` function takes two parameters: 1) the [mustache](http://mustache.github.io/) template and 2) a `view` object that contains the data and code needed to render the template. @@ -65,8 +63,8 @@ If you need a template for a dynamic part in a static website, you can consider // file: render.js function renderHello() { - var template = document.getElementById('template').innerHTML; - var rendered = Mustache.render(template, { name: 'Luke' }); + const template = document.getElementById('template').innerHTML; + const rendered = Mustache.render(template, { name: 'Luke' }); document.getElementById('target').innerHTML = rendered; } ``` @@ -94,7 +92,7 @@ function renderHello() { fetch('template.mustache') .then((response) => response.text()) .then((template) => { - var rendered = Mustache.render(template, { name: 'Luke' }); + const rendered = Mustache.render(template, { name: 'Luke' }); document.getElementById('target').innerHTML = rendered; }); } @@ -430,7 +428,7 @@ Custom delimiters can be used in place of `{{` and `}}` by setting the new value The `Mustache.tags` property holds an array consisting of the opening and closing tag values. Set custom values by passing a new array of tags to `render()`, which gets honored over the default values, or by overriding the `Mustache.tags` property itself: ```js -var customTags = [ '<%', '%>' ]; +const customTags = [ '<%', '%>' ]; ``` ##### Pass Value into Render Method