0
0
mirror of https://github.com/phpv8/v8js.git synced 2024-12-22 19:51:51 +00:00
phpv8/js/json-template.js
2010-12-30 14:04:51 +00:00

840 lines
22 KiB
JavaScript

// Copyright (C) 2009 Andy Chu
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// $Id$
//
// JavaScript implementation of json-template.
//
// This is predefined in tests, shouldn't be defined anywhere else. TODO: Do
// something nicer.
var log = log || function() {};
var repr = repr || function() {};
// The "module" exported by this script is called "jsontemplate":
var jsontemplate = function() {
// Regex escaping for metacharacters
function EscapeMeta(meta) {
return meta.replace(/([\{\}\(\)\[\]\|\^\$\-\+\?])/g, '\\$1');
}
var token_re_cache = {};
function _MakeTokenRegex(meta_left, meta_right) {
var key = meta_left + meta_right;
var regex = token_re_cache[key];
if (regex === undefined) {
var str = '(' + EscapeMeta(meta_left) + '.*?' + EscapeMeta(meta_right) +
'\n?)';
regex = new RegExp(str, 'g');
}
return regex;
}
//
// Formatters
//
function HtmlEscape(s) {
return s.replace(/&/g,'&').
replace(/>/g,'>').
replace(/</g,'&lt;');
}
function HtmlTagEscape(s) {
return s.replace(/&/g,'&amp;').
replace(/>/g,'&gt;').
replace(/</g,'&lt;').
replace(/"/g,'&quot;');
}
// Default ToString can be changed
function ToString(s) {
if (s === null) {
return 'null';
}
return s.toString();
}
// Formatter to pluralize words
function _Pluralize(value, unused_context, args) {
var s, p;
switch (args.length) {
case 0:
s = ''; p = 's';
break;
case 1:
s = ''; p = args[0];
break;
case 2:
s = args[0]; p = args[1];
break;
default:
// Should have been checked at compile time
throw {
name: 'EvaluationError', message: 'pluralize got too many args'
};
}
return (value > 1) ? p : s;
}
function _Cycle(value, unused_context, args) {
// Cycle between various values on consecutive integers.
// @index starts from 1, so use 1-based indexing.
return args[(value - 1) % args.length];
}
var DEFAULT_FORMATTERS = {
'html': HtmlEscape,
'htmltag': HtmlTagEscape,
'html-attr-value': HtmlTagEscape,
'str': ToString,
'raw': function(x) { return x; },
'AbsUrl': function(value, context) {
// TODO: Normalize leading/trailing slashes
return context.get('base-url') + '/' + value;
}
};
var DEFAULT_PREDICATES = {
'singular?': function(x) { return x == 1; },
'plural?': function(x) { return x > 1; },
'Debug?': function(unused, context) {
try {
return context.get('debug');
} catch(err) {
if (err.name == 'UndefinedVariable') {
return false;
} else {
throw err;
}
}
}
};
var FunctionRegistry = function() {
return {
lookup: function(user_str) {
return [null, null];
}
};
};
var SimpleRegistry = function(obj) {
return {
lookup: function(user_str) {
var func = obj[user_str] || null;
return [func, null];
}
};
};
var CallableRegistry = function(callable) {
return {
lookup: function(user_str) {
var func = callable(user_str);
return [func, null];
}
};
};
// Default formatters which can't be expressed in DEFAULT_FORMATTERS
var PrefixRegistry = function(functions) {
return {
lookup: function(user_str) {
for (var i = 0; i < functions.length; i++) {
var name = functions[i].name, func = functions[i].func;
if (user_str.slice(0, name.length) == name) {
// Delimiter is usually a space, but could be something else
var args;
var splitchar = user_str.charAt(name.length);
if (splitchar === '') {
args = []; // No arguments
} else {
args = user_str.split(splitchar).slice(1);
}
return [func, args];
}
}
return [null, null]; // No formatter
}
};
};
var ChainedRegistry = function(registries) {
return {
lookup: function(user_str) {
for (var i=0; i<registries.length; i++) {
var result = registries[i].lookup(user_str);
if (result[0]) {
return result;
}
}
return [null, null]; // Nothing found
}
};
};
//
// Template implementation
//
function _ScopedContext(context, undefined_str) {
// The stack contains:
// The current context (an object).
// An iteration index. -1 means we're NOT iterating.
var stack = [{context: context, index: -1}];
return {
PushSection: function(name) {
if (name === undefined || name === null) {
return null;
}
var new_context;
if (name == '@') {
new_context = stack[stack.length-1].context;
} else {
new_context = stack[stack.length-1].context[name] || null;
}
stack.push({context: new_context, index: -1});
return new_context;
},
Pop: function() {
stack.pop();
},
next: function() {
var stacktop = stack[stack.length-1];
// Now we're iterating -- push a new mutable object onto the stack
if (stacktop.index == -1) {
stacktop = {context: null, index: 0};
stack.push(stacktop);
}
// The thing we're iterating over
var context_array = stack[stack.length-2].context;
// We're already done
if (stacktop.index == context_array.length) {
stack.pop();
return undefined; // sentinel to say that we're done
}
stacktop.context = context_array[stacktop.index++];
return true; // OK, we mutated the stack
},
_Undefined: function(name) {
if (undefined_str === undefined) {
throw {
name: 'UndefinedVariable', message: name + ' is not defined'
};
} else {
return undefined_str;
}
},
_LookUpStack: function(name) {
var i = stack.length - 1;
while (true) {
var frame = stack[i];
if (name == '@index') {
if (frame.index != -1) { // -1 is undefined
return frame.index;
}
} else {
var context = frame.context;
if (typeof context === 'object') {
var value = context[name];
if (value !== undefined) {
return value;
}
}
}
i--;
if (i <= -1) {
return this._Undefined(name);
}
}
},
get: function(name) {
if (name == '@') {
return stack[stack.length-1].context;
}
var parts = name.split('.');
var value = this._LookUpStack(parts[0]);
if (parts.length > 1) {
for (var i=1; i<parts.length; i++) {
value = value[parts[i]];
if (value === undefined) {
return this._Undefined(parts[i]);
}
}
}
return value;
}
};
}
// Crockford's "functional inheritance" pattern
var _AbstractSection = function(spec) {
var that = {};
that.current_clause = [];
that.Append = function(statement) {
that.current_clause.push(statement);
};
that.AlternatesWith = function() {
throw {
name: 'TemplateSyntaxError',
message:
'{.alternates with} can only appear with in {.repeated section ...}'
};
};
that.NewOrClause = function(pred) {
throw { name: 'NotImplemented' }; // "Abstract"
};
return that;
};
var _Section = function(spec) {
var that = _AbstractSection(spec);
that.statements = {'default': that.current_clause};
that.section_name = spec.section_name;
that.Statements = function(clause) {
clause = clause || 'default';
return that.statements[clause] || [];
};
that.NewOrClause = function(pred) {
if (pred) {
throw {
name: 'TemplateSyntaxError',
message: '{.or} clause only takes a predicate inside predicate blocks'
};
}
that.current_clause = [];
that.statements['or'] = that.current_clause;
};
return that;
};
// Repeated section is like section, but it supports {.alternates with}
var _RepeatedSection = function(spec) {
var that = _Section(spec);
that.AlternatesWith = function() {
that.current_clause = [];
that.statements['alternate'] = that.current_clause;
};
return that;
};
// Represents a sequence of predicate clauses.
var _PredicateSection = function(spec) {
var that = _AbstractSection(spec);
// Array of func, statements
that.clauses = [];
that.NewOrClause = function(pred) {
// {.or} always executes if reached, so use identity func with no args
pred = pred || [function(x) { return true; }, null];
that.current_clause = [];
that.clauses.push([pred, that.current_clause]);
};
return that;
};
function _Execute(statements, context, callback) {
for (var i=0; i<statements.length; i++) {
var statement = statements[i];
if (typeof(statement) == 'string') {
callback(statement);
} else {
var func = statement[0];
var args = statement[1];
func(args, context, callback);
}
}
}
function _DoSubstitute(statement, context, callback) {
var value;
value = context.get(statement.name);
// Format values
for (var i=0; i<statement.formatters.length; i++) {
var pair = statement.formatters[i];
var formatter = pair[0];
var args = pair[1];
value = formatter(value, context, args);
}
callback(value);
}
// for [section foo]
function _DoSection(args, context, callback) {
var block = args;
var value = context.PushSection(block.section_name);
var do_section = false;
// "truthy" values should have their sections executed.
if (value) {
do_section = true;
}
// Except: if the value is a zero-length array (which is "truthy")
if (value && value.length === 0) {
do_section = false;
}
if (do_section) {
_Execute(block.Statements(), context, callback);
context.Pop();
} else { // Empty list, None, False, etc.
context.Pop();
_Execute(block.Statements('or'), context, callback);
}
}
// {.pred1?} A {.or pred2?} B ... {.or} Z {.end}
function _DoPredicates(args, context, callback) {
// Here we execute the first clause that evaluates to true, and then stop.
var block = args;
var value = context.get('@');
for (var i=0; i<block.clauses.length; i++) {
var clause = block.clauses[i];
var predicate = clause[0][0];
var pred_args = clause[0][1];
var statements = clause[1];
var do_clause = predicate(value, context, pred_args);
if (do_clause) {
_Execute(statements, context, callback);
break;
}
}
}
function _DoRepeatedSection(args, context, callback) {
var block = args;
items = context.PushSection(block.section_name);
pushed = true;
if (items && items.length > 0) {
// TODO: check that items is an array; apparently this is hard in JavaScript
//if type(items) is not list:
// raise EvaluationError('Expected a list; got %s' % type(items))
// Execute the statements in the block for every item in the list.
// Execute the alternate block on every iteration except the last. Each
// item could be an atom (string, integer, etc.) or a dictionary.
var last_index = items.length - 1;
var statements = block.Statements();
var alt_statements = block.Statements('alternate');
for (var i=0; context.next() !== undefined; i++) {
_Execute(statements, context, callback);
if (i != last_index) {
_Execute(alt_statements, context, callback);
}
}
} else {
_Execute(block.Statements('or'), context, callback);
}
context.Pop();
}
var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/;
var _OR_RE = /or(?:\s+(.+))?/;
var _IF_RE = /if(?:\s+(.+))?/;
// Turn a object literal, function, or Registry into a Registry
function MakeRegistry(obj) {
if (!obj) {
// if null/undefined, use a totally empty FunctionRegistry
return new FunctionRegistry();
} else if (typeof obj === 'function') {
return new CallableRegistry(obj);
} else if (obj.lookup !== undefined) {
// TODO: Is this a good pattern? There is a namespace conflict where get
// could be either a formatter or a method on a FunctionRegistry.
// instanceof might be more robust.
return obj;
} else if (typeof obj === 'object') {
return new SimpleRegistry(obj);
}
}
// TODO: The compile function could be in a different module, in case we want to
// compile on the server side.
function _Compile(template_str, options) {
var more_formatters = MakeRegistry(options.more_formatters);
// default formatters with arguments
var default_formatters = PrefixRegistry([
{name: 'pluralize', func: _Pluralize},
{name: 'cycle', func: _Cycle}
]);
var all_formatters = new ChainedRegistry([
more_formatters,
SimpleRegistry(DEFAULT_FORMATTERS),
default_formatters
]);
var more_predicates = MakeRegistry(options.more_predicates);
// TODO: Add defaults
var all_predicates = new ChainedRegistry([
more_predicates, SimpleRegistry(DEFAULT_PREDICATES)
]);
// We want to allow an explicit null value for default_formatter, which means
// that an error is raised if no formatter is specified.
var default_formatter;
if (options.default_formatter === undefined) {
default_formatter = 'str';
} else {
default_formatter = options.default_formatter;
}
function GetFormatter(format_str) {
var pair = all_formatters.lookup(format_str);
if (!pair[0]) {
throw {
name: 'BadFormatter',
message: format_str + ' is not a valid formatter'
};
}
return pair;
}
function GetPredicate(pred_str) {
var pair = all_predicates.lookup(pred_str);
if (!pair[0]) {
throw {
name: 'BadPredicate',
message: pred_str + ' is not a valid predicate'
};
}
return pair;
}
var format_char = options.format_char || '|';
if (format_char != ':' && format_char != '|') {
throw {
name: 'ConfigurationError',
message: 'Only format characters : and | are accepted'
};
}
var meta = options.meta || '{}';
var n = meta.length;
if (n % 2 == 1) {
throw {
name: 'ConfigurationError',
message: meta + ' has an odd number of metacharacters'
};
}
var meta_left = meta.substring(0, n/2);
var meta_right = meta.substring(n/2, n);
var token_re = _MakeTokenRegex(meta_left, meta_right);
var current_block = _Section({});
var stack = [current_block];
var strip_num = meta_left.length; // assume they're the same length
var token_match;
var last_index = 0;
while (true) {
token_match = token_re.exec(template_str);
if (token_match === null) {
break;
} else {
var token = token_match[0];
}
// Add the previous literal to the program
if (token_match.index > last_index) {
var tok = template_str.slice(last_index, token_match.index);
current_block.Append(tok);
}
last_index = token_re.lastIndex;
var had_newline = false;
if (token.slice(-1) == '\n') {
token = token.slice(null, -1);
had_newline = true;
}
token = token.slice(strip_num, -strip_num);
if (token.charAt(0) == '#') {
continue; // comment
}
if (token.charAt(0) == '.') { // Keyword
token = token.substring(1, token.length);
var literal = {
'meta-left': meta_left,
'meta-right': meta_right,
'space': ' ',
'tab': '\t',
'newline': '\n'
}[token];
if (literal !== undefined) {
current_block.Append(literal);
continue;
}
var new_block, func;
var section_match = token.match(_SECTION_RE);
if (section_match) {
var repeated = section_match[1];
var section_name = section_match[3];
if (repeated) {
func = _DoRepeatedSection;
new_block = _RepeatedSection({section_name: section_name});
} else {
func = _DoSection;
new_block = _Section({section_name: section_name});
}
current_block.Append([func, new_block]);
stack.push(new_block);
current_block = new_block;
continue;
}
var pred_str, pred;
// Check {.or pred?} before {.pred?}
var or_match = token.match(_OR_RE);
if (or_match) {
pred_str = or_match[1];
pred = pred_str ? GetPredicate(pred_str) : null;
current_block.NewOrClause(pred);
continue;
}
// Match either {.pred?} or {.if pred?}
var matched = false;
var if_match = token.match(_IF_RE);
if (if_match) {
pred_str = if_match[1];
matched = true;
} else if (token.charAt(token.length-1) == '?') {
pred_str = token;
matched = true;
}
if (matched) {
pred = pred_str ? GetPredicate(pred_str) : null;
new_block = _PredicateSection();
new_block.NewOrClause(pred);
current_block.Append([_DoPredicates, new_block]);
stack.push(new_block);
current_block = new_block;
continue;
}
if (token == 'alternates with') {
current_block.AlternatesWith();
continue;
}
if (token == 'end') {
// End the block
stack.pop();
if (stack.length > 0) {
current_block = stack[stack.length-1];
} else {
throw {
name: 'TemplateSyntaxError',
message: 'Got too many {end} statements'
};
}
continue;
}
}
// A variable substitution
var parts = token.split(format_char);
var formatters;
var name;
if (parts.length == 1) {
if (default_formatter === null) {
throw {
name: 'MissingFormatter',
message: 'This template requires explicit formatters.'
};
}
// If no formatter is specified, use the default.
formatters = [GetFormatter(default_formatter)];
name = token;
} else {
formatters = [];
for (var j=1; j<parts.length; j++) {
formatters.push(GetFormatter(parts[j]));
}
name = parts[0];
}
current_block.Append([_DoSubstitute, {name: name, formatters: formatters}]);
if (had_newline) {
current_block.Append('\n');
}
}
// Add the trailing literal
current_block.Append(template_str.slice(last_index));
if (stack.length !== 1) {
throw {
name: 'TemplateSyntaxError',
message: 'Got too few {end} statements'
};
}
return current_block;
}
// The Template class is defined in the traditional style so that users can add
// methods by mutating the prototype attribute. TODO: Need a good idiom for
// inheritance without mutating globals.
function Template(template_str, options) {
// Add 'new' if we were not called with 'new', so prototyping works.
if(!(this instanceof Template)) {
return new Template(template_str, options);
}
this._options = options || {};
this._program = _Compile(template_str, this._options);
}
Template.prototype.render = function(data_dict, callback) {
// options.undefined_str can either be a string or undefined
var context = _ScopedContext(data_dict, this._options.undefined_str);
_Execute(this._program.Statements(), context, callback);
};
Template.prototype.expand = function(data_dict) {
var tokens = [];
this.render(data_dict, function(x) { tokens.push(x); });
return tokens.join('');
};
// fromString is a construction method that allows metadata to be written at the
// beginning of the template string. See Python's FromFile for a detailed
// description of the format.
//
// The argument 'options' takes precedence over the options in the template, and
// can be used for non-serializable options like template formatters.
var OPTION_RE = /^([a-zA-Z\-]+):\s*(.*)/;
var OPTION_NAMES = [
'meta', 'format-char', 'default-formatter', 'undefined-str'];
// Use this "linear search" instead of Array.indexOf, which is nonstandard
var OPTION_NAMES_RE = new RegExp(OPTION_NAMES.join('|'));
function fromString(s, options) {
var parsed = {};
var begin = 0, end = 0;
while (true) {
var parsedOption = false;
end = s.indexOf('\n', begin);
if (end == -1) {
break;
}
var line = s.slice(begin, end);
begin = end+1;
var match = line.match(OPTION_RE);
if (match !== null) {
var name = match[1].toLowerCase(), value = match[2];
if (name.match(OPTION_NAMES_RE)) {
name = name.replace('-', '_');
value = value.replace(/^\s+/, '').replace(/\s+$/, '');
if (name == 'default_formatter' && value.toLowerCase() == 'none') {
value = null;
}
parsed[name] = value;
parsedOption = true;
}
}
if (!parsedOption) {
break;
}
}
// TODO: This doesn't enforce the blank line between options and template, but
// that might be more trouble than it's worth
if (parsed !== {}) {
body = s.slice(begin);
} else {
body = s;
}
for (var o in options) {
parsed[o] = options[o];
}
return Template(body, parsed);
}
// We just export one name for now, the Template "class".
// We need HtmlEscape in the browser tests, so might as well export it.
return {
Template: Template, HtmlEscape: HtmlEscape,
FunctionRegistry: FunctionRegistry, SimpleRegistry: SimpleRegistry,
CallableRegistry: CallableRegistry, ChainedRegistry: ChainedRegistry,
fromString: fromString,
// Private but exposed for testing
_Section: _Section
};
}();