// 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 common metacharacters (note that JavaScript needs 2 \\ -- // no raw strings! var META_ESCAPE = { '{': '\\{', '}': '\\}', '{{': '\\{\\{', '}}': '\\}\\}', '[': '\\[', ']': '\\]' }; function _MakeTokenRegex(meta_left, meta_right) { // TODO: check errors return new RegExp( '(' + META_ESCAPE[meta_left] + '.+?' + META_ESCAPE[meta_right] + '\n?)', 'g'); // global for use with .exec() } // // Formatters // function HtmlEscape(s) { return s.replace(/&/g,'&'). replace(/>/g,'>'). replace(//g,'>'). replace(/ 1) { for (var i=1; i 0) { // 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); } if (pushed) { context.Pop(); } } var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/; // 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 = options.more_formatters || function (x) { return null; }; // 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 formatter = more_formatters(format_str) || DEFAULT_FORMATTERS[format_str]; if (formatter === undefined) { throw { name: 'BadFormatter', message: format_str + ' is not a valid formatter' }; } return formatter; } 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 section_match = token.match(_SECTION_RE); if (section_match) { var repeated = section_match[1]; var section_name = section_match[3]; var func = repeated ? _DoRepeatedSection : _DoSection; var new_block = _Section(section_name); current_block.Append([func, new_block]); stack.push(new_block); current_block = new_block; continue; } if (token == 'alternates with') { current_block.NewClause('alternate'); continue; } if (token == 'or') { current_block.NewClause('or'); 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, the default is the 'str' formatter, // which the user can define however they desire. formatters = [GetFormatter(default_formatter)]; name = token; } else { formatters = []; for (var j=1; j