diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/codeq/template.js | 292 |
1 files changed, 89 insertions, 203 deletions
diff --git a/js/codeq/template.js b/js/codeq/template.js index 90b4db3..85f1596 100644 --- a/js/codeq/template.js +++ b/js/codeq/template.js @@ -16,106 +16,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ (function () { "use strict"; - // regular expressions for the templating function, etc. - var regexpQuote = new RegExp('"', 'g'), - regexpBackslash = new RegExp('\\\\', 'g'), - regexpWhiteSpaceStart = new RegExp('^[ \r\n\t]+'), - regexpWhiteSpaceEnd = new RegExp('[ \r\n\t]+$'), - regexpWhiteSpaceTrim = new RegExp('^[ \\t\\r\\n]*(.*[^ \\t\\r\\n])[ \\t\\r\\n]*$', 'm'), - regexpCR = new RegExp('\\r', 'g'), - regexpLF = new RegExp('\\n', 'g'), - regexpTab = new RegExp('\\t', 'g'); - - // convert a string into its definition (javascript literal) - var stringToDef = function (str) { - return str.replace(regexpBackslash, '\\\\') - .replace(regexpQuote, '\\"') - .replace(regexpCR, '\\r') - .replace(regexpLF, '\\n') - .replace(regexpTab, '\\t'); - }; - - var tokenize = function (line) { - var result = [], - N = line.length, - i = 0, // character index into line - storedAtom = null, // the previous atom, to decide whether to store a string or an object {key:'', value:''} - atom = [], // currently parsed content - isKeyValue = false, - storeAtom = function () { - if (atom.length == 0) return; - if (storedAtom === null) storedAtom = atom.join(''); - else if (isKeyValue) { - result.push({'key': storedAtom, 'value': atom.join('')}); - storedAtom = null; - isKeyValue = false; - } - else { - result.push(storedAtom); - storedAtom = atom.join(''); - } - atom.length = 0; - }, - c, q; - - while (i < N) { - c = line[i++]; - switch (c) { - // end-of-atom characters - case ' ': - case '\r': - case '\n': - case '\t': - storeAtom(); - break; - - // escape character - case '\\': - if (i < N) atom.push(line[i++]); - break; - - // key-value delimiter character - case '=': - if (storedAtom) { - if (isKeyValue) atom.push(c); - else if (atom.length == 0) isKeyValue = true; // must have been a whitespace before "=" - else { - storeAtom(); - isKeyValue = true; - } - } - else atom.push(c); // "=" is delimiter only before the value, not before the key - break; - - // quoted string - case '"': - case "'": - stringConsume: - while (i < N) { - q = line[i++]; - switch (q) { - case '\\': - if (i < N) atom.push(line[i++]); - break; - case c: - break stringConsume; - default: - atom.push(q); - break; - } - } - break; - - default: - atom.push(c); - break; - } - } - // purge anything in cache - storeAtom(); - if (storedAtom !== null) result.push(storedAtom); - return result; - }; // resource tree, loaded from data/resources.json in the boot sequence var resources = {}; @@ -126,7 +26,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ candidate = null, i, fragment; if (!resourceName) { - codeq.log.error('No resource name provided; path: "' + resourceBranches.join('/') + '"'); + codeq.log.error('No resource name provided; ' + + 'path: "' + resourceBranches.join('/') + '"'); return null; } if (branch[resourceName]) candidate = 'data/' + resourceName; // top-level match @@ -134,141 +35,129 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ fragment = resourceBranches[i]; branch = branch[fragment]; if (!branch) { - codeq.log.error('Resource sub-branch ' + fragment + ' does not exist; resource: "' + resourceName + '", path: "' + resourceBranches.join('/') + '"'); + codeq.log.error('Resource sub-branch ' + fragment + ' does not exist; ' + + 'resource: "' + resourceName + '", ' + + 'path: "' + resourceBranches.join('/') + '"'); break; } traversedPath.push(fragment); - if (branch[resourceName]) candidate = traversedPath.join('/') + '/' + resourceName; + if (branch[resourceName]) { + candidate = traversedPath.join('/') + '/' + resourceName; + } } if (candidate) return codeq.ajaxPrefix + candidate; - codeq.log.error('Resource ' + resourceName + ' was not found; path: "' + resourceBranches.join('/') + '"'); + codeq.log.error('Resource ' + resourceName + ' was not found; ' + + 'path: "' + resourceBranches.join('/') + '"'); return null; }; + // parse a key-value string of the form: a=1 b='2 + 3' c="4" + var parseKeyval = function (str) { + // match foo=bar, foo="bar", foo = 'bar baz' etc. + var regex = /\s*([^=\s]*)\s*=\s*([^'"]\S*|"[^"]*"|'[^']*')/g, + dict = {}, + match, key, val; + + while ((match = regex.exec(str)) !== null) { + key = match[1]; + val = match[2]; + if (val[0] == val[val.length-1] && (val[0] === '"' || val[0] === "'")) { + val = val.slice(1, -1); + } + dict[key] = val; + } + return dict; + }; + var directiveHandlers = { - 'resource': function (code, tokens, templatePath) { - code.push('_result.push("', resolveResource(tokens[1], templatePath) || 'data/broken.png', '");\n'); + 'resource': function (str, templatePath) { + return resolveResource(str, templatePath) || 'data/broken.png'; }, - 'img': function (code, tokens, templatePath) { - var N = tokens.length, - attrs = [], - token, i; - for (i = 1; i < N; i++) { - token = tokens[i]; - if (!token || typeof token !== 'object') { - codeq.log.error('Invalid token at position ' + i + ' in @img'); - continue; - } - switch (token.key) { + 'img': function (str, templatePath) { + var attrs = parseKeyval(str), + output, key; + + output = '<img '; + for (key in attrs) { + if (!attrs.hasOwnProperty(key)) continue; + switch (key) { case 'src': - attrs.push('src="' + resolveResource(token.value, templatePath) + '"'); + output += 'src="' + resolveResource(attrs[key], templatePath) + '" '; break; case 'alt': - attrs.push('alt="' + codeq.escapeHtml(token.value) + '"'); + output += 'alt="' + codeq.escapeHtml(attrs[key]) + '" '; break; case 'class': - attrs.push('class="' + token.value + '"'); + output += 'class="' + attrs[key] + '" '; break; } } - code.push('_result.push("<img ', attrs.join(' ').replace(regexpQuote, '\\"'), '>");\n'); + output += '/>'; + return output; } }; - var templator = function (str, templatePath) { + // instantiate template with args + var process = function (template, templatePath, args) { var templateName, - f, parts, i, subparts, subpart, - src = [ 'var _result = [], echo = function (s) { _result.push(s); };\n' ], - atoms, j, atom, debugPrefix, s, r, tokens, handler; + key, subpart, start, end, match, handler, + output = ''; + + // escape arguments + args = args || {}; + for (key in args) { + if (!args.hasOwnProperty(key)) continue; + args[key] = codeq.escapeHtml(args[key]); + } if (!templatePath) templatePath = []; templateName = templatePath.join('/'); - debugPrefix = '[' + templateName + ']'; - // remove comments - parts = str.split('[%--'); // break on start-of-comment - atoms = [ parts[0] ]; // first part is not a comment - for (i = 1; i < parts.length; i++) { // iterate over start-of-comments - atom = parts[i].split('--%]'); // break on end-of-comment - if (atom.length > 1) { // if end-of-comment was encountered - atoms.push(atom[1]); // add whatever is trailing it - for (j = 2; j < atom.length; j++) { // re-add even dangling end-of-comments - atoms.push('--%]'); - atoms.push(atom[j]); - } - } - } + end = 0; + while ((start = template.indexOf('[%', end)) != -1) { + // add remaining text from previous iteration + output += template.substring(end, start); - // start processing - parts = atoms.join('').split('[%'); - - if (parts[0].length > 0) src.push('_result.push("', stringToDef(parts[0]), '");\n'); // the first part that doesn't begin with '[%' - for (i = 1; i < parts.length; i++) { // for every part that begins with '[%' - if (parts[i].slice(0, 2) === '--') { // a comment start - subparts = parts[i].split('--%]'); // split at comment end + if (template.substr(start+2, 2) === '--') { // a comment start + if ((end = template.indexOf('--%]', start+4)) == -1) { + break; + } + end += 4; } else { // a start of a statement or of a value reference - subparts = parts[i].split('%]'); // there should be only one terminating '%]', find it - subpart = subparts[0].replace(regexpWhiteSpaceStart, '').replace(regexpWhiteSpaceEnd, ''); // trim the white space - if (subpart.length > 0) { - if (subpart[0] === '=') { // a value reference - s = subpart.slice(1); - r = regexpWhiteSpaceTrim.exec(s); - r = r && r[1] || s; - src.push('_result.push(typeof this.', r, ' === \'undefined\' ? \'', r, ' missing\' : this.', r, ');\n'); - } - else if (subpart[0] === '@') { // a directive - tokens = tokenize(subpart.slice(1)); - if (tokens.length === 0) { - codeq.log.error('An empty directive in ' + templateName); + if ((end = template.indexOf('%]', start+2)) == -1) { + break; + } + subpart = template.substring(start+2, end).trim(); + end += 2; + + if ((match = subpart.match(/(.)(\S+)(.*)/)) === null) { + codeq.log.error('Invalide template in ' + templateName + ': ' + subpart); + continue; + } + switch (match[1]) { + case '=': // a value reference + output += String(args[match[2]]); + break; + case '@': // a directive + handler = directiveHandlers[match[2]]; + if (handler) { + output += handler(match[3].trim(), templatePath); } else { - handler = directiveHandlers[tokens[0]]; - if (!handler) { - codeq.log.error('An unknown directive in ' + templateName + ': ' + tokens[0]); - } - else { - handler(src, tokens, templatePath); - } + codeq.log.error('Invalid directive in ' + templateName + ': ' + match[2]); } - } - else { // javascript statement(s) - src.push(subpart, '\n'); - } + break; + default: + codeq.log.error('Invalid template in ' + templateName + ': ' + subpart); } } - if ((subparts.length > 1) && (subparts[1].length > 0)) { // there's a trailing text - src.push('_result.push("', stringToDef(subparts[1]), '");\n'); - } - } - src.push('return _result.join("");'); - try { - f = new Function(src.join('')); // create a function based on the given template - } - catch (e) { - codeq.log.error('createTemplate(): ' + debugPrefix + ' Failed to instantiate template function: ' + e + '\nfunction() {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str, e); - throw e; } + // add any remaining text + output += template.substr(end == -1 ? start : end); - return function (args) { - var esc = codeq.escapeHtml, - escArgs = {}, - key; - if ((typeof args === 'object') && (args !== null)) { - for (key in args) { - if (!args.hasOwnProperty(key)) continue; - escArgs[key] = esc(args[key]); - } - } - try { - return f.apply(escArgs); - } - catch (e) { - codeq.log.error('Error evaluating template ' + templateName + ' function: ' + e + '\nfunction() {\n' + src.join('') + '\nCreated from template:\n' + str, e); - throw e; - } - }; + return output; }; codeq.template = { @@ -277,10 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ }, // instantiate a template - 'process': function (template, templatePath, args) { - var fn = templator(template, templatePath); - return fn(args); - }, + 'process': process, // instantiate templates in a lang→template dictionary 'processDictionary': function (dict, templatePath, args) { |