diff --git a/src/lang/template.js b/src/lang/template.js
index 435a3e7a8..d097e3c35 100644
--- a/src/lang/template.js
+++ b/src/lang/template.js
@@ -86,7 +86,7 @@
* constructs:
*
* // matches symbols like '<&= field %>'
- * var syntax = /(^|.|\r|\n)(\<%=\s*(\w+)\s*%\>)/;
+ * var syntax = /\\?\<%=\s*(\w+)\s*%\>/g;
*
* var t = new Template(
* '
Name: <&= name %>, Age: <&=age%>
',
@@ -94,16 +94,20 @@
* t.evaluate( {name: 'John Smith', age: 26} );
* // -> Name: John Smith, Age: 26
*
- * There are important constraints to any custom syntax. Any syntax must
- * provide at least three groupings in the regular expression. The first
- * grouping is to capture what comes before the symbol, to detect the backslash
- * escape character (no, you cannot use a different character). The second
- * grouping captures the entire symbol and will be completely replaced upon
- * evaluation. Lastly, the third required grouping captures the name of the
- * field inside the symbol.
+ * There are important constraints to any custom syntax. Any syntax must be a
+ * global regular expression starting with an eventual backslash escape character
+ * (no, you cannot use a different character) and containing a group capturing the
+ * name of the field inside the symbol.
+ *
+ * Backward compatilibility
+ *
+ * Old style syntaxes are still supported but deprecated and will be removed in
+ * future releases:
+ *
+ * var oldSyntax = /(^|.|\r|\n)(\<%=\s*(\w+)\s*%\>)/;
*
**/
-var Template = Class.create({
+var Template = Class.create((function() {
/**
* new Template(template[, pattern = Template.Pattern])
*
@@ -112,10 +116,10 @@ var Template = Class.create({
* The optional `pattern` argument expects a `RegExp` that defines a custom
* syntax for the replaceable symbols in `template`.
**/
- initialize: function(template, pattern) {
+ function initialize(template, pattern) {
this.template = template.toString();
- this.pattern = pattern || Template.Pattern;
- },
+ this.pattern = formatPattern(pattern || Template.Pattern);
+ }
/**
* Template#evaluate(object) -> String
@@ -123,31 +127,33 @@ var Template = Class.create({
* Applies the template to `object`'s data, producing a formatted string
* with symbols replaced by `object`'s corresponding properties.
**/
- evaluate: function(object) {
+ function evaluate(object) {
if (object && Object.isFunction(object.toTemplateReplacements))
object = object.toTemplateReplacements();
- return this.template.gsub(this.pattern, function(match) {
- if (object == null) return (match[1] + '');
-
- var before = match[1] || '';
- if (before == '\\') return match[2];
+ return this.template.replace(this.pattern, function(interpolation, expr) {
+ if (interpolation.charAt(0) === '\\') return interpolation.slice(1);
+ if (object == null) return '';
+
+ var result = expr.split(/\.|\[|\]/).inject(object, function(ctx, property) {
+ return property.length ? ctx[property] : ctx;
+ });
+
+ return result === object ? '' : String.interpret(result);
+ });
+ }
- var ctx = object, expr = match[3];
- var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
- match = pattern.exec(expr);
- if (match == null) return before;
+ // Backward-compatibility with 1.6 syntax
+ function formatPattern(pattern) {
+ if (pattern.global) return pattern;
+ var source = pattern.source.replace('(^|.|\\r|\\n)', '\\\\?').replace(/\((.*)\)/, '$1');
+ return new RegExp(source, 'g');
+ }
- while (match != null) {
- var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : 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 {
+ initialize: initialize,
+ evaluate: evaluate
+ };
+})());
- return before + String.interpret(ctx);
- });
- }
-});
-Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.Pattern = /\\?#\{(.*?)\}/g;
diff --git a/test/unit/string_test.js b/test/unit/string_test.js
index a7ef84197..f23848c77 100644
--- a/test/unit/string_test.js
+++ b/test/unit/string_test.js
@@ -324,21 +324,16 @@ new Test.Unit.Runner({
},
testTemplateEvaluationWithIndexing: function() {
- var source = '#{0} = #{[0]} - #{1} = #{[1]} - #{[2][0]} - #{[2].name} - #{first[0]} - #{[first][0]} - #{[\]]} - #{first[\]]}';
+ var source = '#{0} = #{[0]} - #{1} = #{[1]} - #{[2][0]} - #{[2].name} - #{first[0]} - #{[first][0]}';
var subject = [ 'zero', 'one', [ 'two-zero' ] ];
subject[2].name = 'two-zero-name';
subject.first = subject[2];
- subject[']'] = '\\';
- subject.first[']'] = 'first\\';
this.assertEqual('zero', new Template('#{[0]}').evaluate(subject), "#{[0]}");
this.assertEqual('one', new Template('#{[1]}').evaluate(subject), "#{[1]}");
this.assertEqual('two-zero', new Template('#{[2][0]}').evaluate(subject), '#{[2][0]}');
this.assertEqual('two-zero-name', new Template('#{[2].name}').evaluate(subject), '#{[2].name}');
this.assertEqual('two-zero', new Template('#{first[0]}').evaluate(subject), '#{first[0]}');
- this.assertEqual('\\', new Template('#{[\]]}').evaluate(subject), '#{[\]]}');
- this.assertEqual('first\\', new Template('#{first[\]]}').evaluate(subject), '#{first[\]]}');
- this.assertEqual('empty - empty2', new Template('#{[]} - #{m[]}').evaluate({ '': 'empty', m: {'': 'empty2'}}), '#{[]} - #{m[]}');
- this.assertEqual('zero = zero - one = one - two-zero - two-zero-name - two-zero - two-zero - \\ - first\\', new Template(source).evaluate(subject));
+ this.assertEqual('zero = zero - one = one - two-zero - two-zero-name - two-zero - two-zero', new Template(source).evaluate(subject));
},
testTemplateToTemplateReplacements: function() {
@@ -372,9 +367,17 @@ new Test.Unit.Runner({
testInterpolate: function() {
var subject = { name: 'Stephan' };
- var pattern = /(^|.|\r|\n)(#\((.*?)\))/;
+ var pattern = /\\?#\((.*?)\)/g;
+
this.assertEqual('#{name}: Stephan', '\\#{name}: #{name}'.interpolate(subject));
this.assertEqual('#(name): Stephan', '\\#(name): #(name)'.interpolate(subject, pattern));
+
+ this.assertEqual('foobar', '#{a}#{b}'.interpolate({a: 'foo', b: 'bar'}), "adjacent regexps");
+ this.assertEqual('#{b}', '#{a}\\#{b}'.interpolate(), "null object");
+
+ // 1.6 non-global syntax backward compatibility test
+ pattern = /(^|.|\r|\n)(#\((.*?)\))/;
+ this.assertEqual('#(name): Stephan', '\\#(name): #(name)'.interpolate(subject, pattern));
},
testToQueryParams: function() {