summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@fri.uni-lj.si>2016-09-08 20:33:55 +0200
committerTimotej Lazar <timotej.lazar@fri.uni-lj.si>2016-09-08 20:33:55 +0200
commit47cfa1b9c8abdd96e4aa9c02daf2e735c3a24024 (patch)
tree6c1117ba8b4474ce8af39ab6d3ed77d79becf6ea
parent792f685359449c84510e04a9f89070f5234a2307 (diff)
Fix and simplify template instantiation
-rw-r--r--js/codeq/template.js292
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) {