| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 | // 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 metacharactersfunction 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,'<');}function HtmlTagEscape(s) {  return s.replace(/&/g,'&').           replace(/>/g,'>').           replace(/</g,'<').           replace(/"/g,'"');}// Default ToString can be changedfunction ToString(s) {  if (s === null) {    return 'null';  }  return s.toString();}// Formatter to pluralize wordsfunction _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_FORMATTERSvar 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" patternvar _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 Registryfunction 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 nonstandardvar 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    };}();
 |