prototype/src/lang/template.js

154 lines
6.1 KiB
JavaScript
Raw Normal View History

2009-03-05 19:56:01 +00:00
/** section: Language
2008-12-14 04:36:59 +00:00
* class Template
*
* A class for sophisticated string interpolation.
*
* Any time you have a group of similar objects and you need to produce
* formatted output for these objects, maybe inside a loop, you typically
2009-06-05 03:56:55 +00:00
* resort to concatenating string literals with the object's fields:
*
2009-06-05 03:56:55 +00:00
* "The TV show " + title + " was created by " + author + ".";
*
2009-06-05 03:56:55 +00:00
* There's nothing wrong with this approach, except that it is hard to
* visualize the output immediately just by glancing at the concatenation
* expression. The `Template` class provides a much nicer and clearer way of
* achieving this formatting.
*
* <h4>Straightforward templates</h4>
*
2009-06-05 03:56:55 +00:00
* The `Template` class uses a basic formatting syntax, similar to what is
* used in Ruby. The templates are created from strings that have embedded
* symbols in the form (e.g., `#{fieldName}`) that will be replaced by
2009-06-05 03:56:55 +00:00
* actual values when the template is applied (evaluated) to an object.
*
* // the template (our formatting expression)
* var myTemplate = new Template(
* 'The TV show #{title} was created by #{author}.');
*
* // our data to be formatted by the template
* var show = {
* title: 'The Simpsons',
* author: 'Matt Groening',
* network: 'FOX'
* };
*
* // let's format our data
* myTemplate.evaluate(show);
* // -> "The TV show The Simpsons was created by Matt Groening."
*
* <h4>Templates are meant to be reused</h4>
*
* As the example illustrates, `Template` objects are not tied to specific
* data. The data is bound to the template only during the evaluation of the
* template, without affecting the template itself. The next example shows the
* same template being used with a handful of distinct objects.
*
* // creating a few similar objects
* var conversion1 = { from: 'meters', to: 'feet', factor: 3.28 };
* var conversion2 = { from: 'kilojoules', to: 'BTUs', factor: 0.9478 };
* var conversion3 = { from: 'megabytes', to: 'gigabytes', factor: 1024 };
*
* // the template
* var templ = new Template(
* 'Multiply by #{factor} to convert from #{from} to #{to}.');
*
* // let's format each object
* [conversion1, conversion2, conversion3].each( function(conv){
* templ.evaluate(conv);
* });
* // -> Multiply by 3.28 to convert from meters to feet.
* // -> Multiply by 0.9478 to convert from kilojoules to BTUs.
* // -> Multiply by 1024 to convert from megabytes to gigabytes.
*
* <h4>Escape sequence</h4>
*
* There's always the chance that one day you'll need to have a literal in your
* template that looks like a symbol, but is not supposed to be replaced. For
2009-06-05 03:56:55 +00:00
* these situations there's an escape character: the backslash (<code>\\</code>).
*
* // NOTE: you're seeing two backslashes here because the backslash
* // is also an escape character in JavaScript strings, so a literal
* // backslash is represented by two backslashes.
* var t = new Template(
* 'in #{lang} we also use the \\#{variable} syntax for templates.');
* var data = { lang:'Ruby', variable: '(not used)' };
* t.evaluate(data);
* // -> in Ruby we also use the #{variable} syntax for templates.
*
* <h4>Custom syntaxes</h4>
*
* The default syntax of the template strings will probably be enough for most
* scenarios. In the rare occasion where the default Ruby-like syntax is
* inadequate, there's a provision for customization. `Template`'s
* constructor accepts an optional second argument that is a regular expression
* object to match the replaceable symbols in the template string. Let's put
* together a template that uses a syntax similar to the ubiquitous `<&#38;= %>`
* constructs:
*
* // matches symbols like '<&#38;= field %>'
* var syntax = /(^|.|\r|\n)(\<%=\s*(\w+)\s*%\>)/;
*
* var t = new Template(
* '<div>Name: <b><&#38;= name %></b>, Age: <b><&#38;=age%></b></div>',
* syntax);
* t.evaluate( {name: 'John Smith', age: 26} );
* // -> <div>Name: <b>John Smith</b>, Age: <b>26</b></div>
*
* 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.
*
2008-12-14 04:36:59 +00:00
**/
var Template = Class.create({
2008-12-14 04:36:59 +00:00
/**
* new Template(template[, pattern = Template.Pattern])
*
2009-02-24 02:35:49 +00:00
* Creates a Template object.
*
2009-02-24 02:35:49 +00:00
* The optional `pattern` argument expects a `RegExp` that defines a custom
* syntax for the replaceable symbols in `template`.
2008-12-14 04:36:59 +00:00
**/
initialize: function(template, pattern) {
this.template = template.toString();
this.pattern = pattern || Template.Pattern;
},
2008-12-14 04:36:59 +00:00
/**
* Template#evaluate(object) -> String
*
* Applies the template to `object`'s data, producing a formatted string
* with symbols replaced by `object`'s corresponding properties.
2008-12-14 04:36:59 +00:00
**/
evaluate: function(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];
var ctx = object, expr = match[3];
var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
match = pattern.exec(expr);
if (match == null) return before;
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 before + String.interpret(ctx);
});
}
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;