From ff45622e39f752d50c9b56584e9b9796a1fc9409 Mon Sep 17 00:00:00 2001 From: Sam Stephenson Date: Tue, 24 Jul 2007 17:24:25 +0000 Subject: [PATCH] prototype: Template enhancements. Closes #8166. --- CHANGELOG | 10 ++++++- src/string.js | 32 ++++++++++++++++++---- test/unit/string.html | 64 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 176fbfd..90fdd7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ *SVN* -* Extended grep semantics. The first argument to Enumerable#grep is now a "filter" (an object with a match() method) so you can now e.g. filter an array of DOM nodes by CSS selector. RegExp#match is now an alias to RegExp#test, so grep can still be used to filter an array of strings with a regular expression. Closes #7596. [Christophe Porteneuve, sam] +* Template enhancements. Closes #8166. [Christophe Porteneuve] + - Added String#interpolate as a shortcut for new Template(...).evaluate(...). + - If you pass String#interpolate or Template#evaluate an object with a toTemplateReplacements() method, the return value of that method will be used as the replacement object. + - You can now substitute properties of template replacement values in template strings, using dot or bracket notation (or both). Example: + "#{name.last}, #{name.first[0]}. (#{location})".interpolate({ + name: { first: "Christophe", last: "Porteneuve" }, location: "Paris" + }) // "Porteneuve, C. (Paris)" + +* Extended grep semantics. The first argument to Enumerable#grep is now a "filter" (an object with a match() method) so you can now e.g. filter an array of DOM nodes by CSS selector. RegExp#match is now an alias to RegExp#test, so grep can still be used to filter an array of strings with a regular expression. Closes #7596. [Christophe Porteneuve, sam] * Make String#scan explicitly return a string. This prevents possible issues with methods expecting input data that is typeof == 'string'. Closes #6350. [AndrewRev, Tobie Langel] diff --git a/src/string.js b/src/string.js index 2c7ff8e..bbd8770 100644 --- a/src/string.js +++ b/src/string.js @@ -199,6 +199,10 @@ Object.extend(String.prototype, { blank: function() { return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); } }); @@ -231,14 +235,32 @@ Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; Template.prototype = { initialize: function(template, pattern) { this.template = template.toString(); - this.pattern = pattern || Template.Pattern; + this.pattern = pattern || Template.Pattern; }, evaluate: function(object) { + if (typeof object.toTemplateReplacements == 'function') + object = object.toTemplateReplacements(); + return this.template.gsub(this.pattern, function(match) { - var before = match[1]; + if (object == null) return ''; + + var before = match[1] || ''; if (before == '\\') return match[2]; - return before + String.interpret(object[match[3]]); - }); + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr); + if (match == null) return ''; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); } -} +}; diff --git a/test/unit/string.html b/test/unit/string.html index 7632754..52ff8e1 100644 --- a/test/unit/string.html +++ b/test/unit/string.html @@ -146,7 +146,7 @@ assertEnumEqual(['a','b'],'ab'.toArray()); assertEnumEqual(['f','o','o'],'foo'.toArray()); }}, - + /* Note that camelize() differs from its Rails counterpart, as it is optimized for dealing with JavaScript object @@ -313,7 +313,67 @@ assertEqual('0false', template.evaluate(falses)); }}, - + + testTemplateEvaluationWithNested: function() {with(this) { + var source = '#{name} #{manager.name} #{manager.age} #{manager.undef} #{manager.age.undef} #{colleagues.first.name}'; + var subject = { manager: { name: 'John', age: 29 }, name: 'Stephan', age: 22, colleagues: { first: { name: 'Mark' } } }; + assertEqual('Stephan', new Template('#{name}').evaluate(subject)); + assertEqual('John', new Template('#{manager.name}').evaluate(subject)); + assertEqual('29', new Template('#{manager.age}').evaluate(subject)); + assertEqual('', new Template('#{manager.undef}').evaluate(subject)); + assertEqual('', new Template('#{manager.age.undef}').evaluate(subject)); + assertEqual('Mark', new Template('#{colleagues.first.name}').evaluate(subject)); + assertEqual('Stephan John 29 Mark', new Template(source).evaluate(subject)); + }}, + + testTemplateEvaluationWithIndexing: function() {with(this) { + var source = '#{0} = #{[0]} - #{1} = #{[1]} - #{[2][0]} - #{[2].name} - #{first[0]} - #{[first][0]} - #{[\\]]} - #{first[\\]]}'; + var subject = [ 'zero', 'one', [ 'two-zero' ] ]; + subject[2].name = 'two-zero-name'; + subject.first = subject[2]; + subject[']'] = '\\'; + subject.first[']'] = 'first\\'; + assertEqual('zero', new Template('#{[0]}').evaluate(subject)); + assertEqual('one', new Template('#{[1]}').evaluate(subject)); + assertEqual('two-zero', new Template('#{[2][0]}').evaluate(subject)); + assertEqual('two-zero-name', new Template('#{[2].name}').evaluate(subject)); + assertEqual('two-zero', new Template('#{first[0]}').evaluate(subject)); + assertEqual('\\', new Template('#{[\\]]}').evaluate(subject)); + assertEqual('first\\', new Template('#{first[\\]]}').evaluate(subject)); + assertEqual('empty - empty2', new Template('#{[]} - #{m[]}').evaluate({ '': 'empty', m: {'': 'empty2'}})); + assertEqual('zero = zero - one = one - two-zero - two-zero-name - two-zero - two-zero - \\ - first\\', new Template(source).evaluate(subject)); + }}, + + testTemplateToTemplateReplacements: function() {with(this) { + var source = 'My name is #{name}, my job is #{job}'; + var subject = { + name: 'Stephan', + getJob: function() { return 'Web developer'; }, + toTemplateReplacements: function() { return { name: this.name, job: this.getJob() } } + }; + assertEqual('My name is Stephan, my job is Web developer', new Template(source).evaluate(subject)); + }}, + + testTemplateEvaluationCombined: function() {with(this) { + var source = '#{name} is #{age} years old, managed by #{manager.name}, #{manager.age}.\n' + + 'Colleagues include #{colleagues[0].name} and #{colleagues[1].name}.'; + var subject = { + name: 'Stephan', age: 22, + manager: { name: 'John', age: 29 }, + colleagues: [ { name: 'Mark' }, { name: 'Indy' } ] + }; + assertEqual('Stephan is 22 years old, managed by John, 29.\n' + + 'Colleagues include Mark and Indy.', + new Template(source).evaluate(subject)); + }}, + + testInterpolate: function() {with(this) { + var subject = { name: 'Stephan' }; + var pattern = /(^|.|\r|\n)(#\((.*?)\))/; + assertEqual('#{name}: Stephan', '\\#{name}: #{name}'.interpolate(subject)); + assertEqual('#(name): Stephan', '\\#(name): #(name)'.interpolate(subject, pattern)); + }}, + testToQueryParams: function() {with(this) { // only the query part var result = {a:undefined, b:'c'};