diff options
Diffstat (limited to 'js/codeq/template.js')
-rw-r--r-- | js/codeq/template.js | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/js/codeq/template.js b/js/codeq/template.js new file mode 100644 index 0000000..361811b --- /dev/null +++ b/js/codeq/template.js @@ -0,0 +1,284 @@ +/* CodeQ: an online programming tutor. + Copyright (C) 2016 UL FRI + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License +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 = {}; + + var resolveResource = function (resourceName, resourceBranches) { + var traversedPath = ['data'], // top-level directory + branch = resources, + candidate = null, + i, fragment; + if (!resourceName) { + codeq.log.error('No resource name provided; path: "' + resourceBranches.join('/') + '"'); + return null; + } + if (branch[resourceName]) candidate = 'data/' + resourceName; // top-level match + for (i = 0; i < resourceBranches.length; i++) { + fragment = resourceBranches[i]; + branch = branch[fragment]; + if (!branch) { + 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 (candidate) return codeq.ajaxPrefix + candidate; + codeq.log.error('Resource ' + resourceName + ' was not found; path: "' + resourceBranches.join('/') + '"'); + return null; + }; + + var directiveHandlers = { + 'resource': function (code, tokens, templatePath) { + code.push('_result.push("', resolveResource(tokens[1], templatePath) || 'data/broken.png', '");\n'); + }, + + '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) { + case 'src': + attrs.push('src="' + resolveResource(token.value, templatePath) + '"'); + break; + case 'alt': + attrs.push('alt="' + codeq.escapeHtml(token.value) + '"'); + break; + case 'class': + attrs.push('class="' + token.value + '"'); + break; + } + } + code.push('_result.push("<img ', attrs.join(' ').replace(regexpQuote, '\\"'), '>");\n'); + } + }; + + var templator = function (str, templatePath) { + 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; + + 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]); + } + } + } + + // 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 + } + 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); + } + else { + handler = directiveHandlers[tokens[0]]; + if (!handler) { + codeq.log.error('An unknown directive in ' + templateName + ': ' + tokens[0]); + } + else { + handler(src, tokens, templatePath); + } + } + } + else { // javascript statement(s) + src.push(subpart, '\n'); + } + } + } + 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; + } + + 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; + } + }; + }; + + codeq.template = { + 'setResources': function (newResources) { + resources = newResources; + }, + + 'process': function (template, templatePath, args) { + var fn = templator(template, templatePath); + return fn(args); + } + }; +})(); |