summaryrefslogtreecommitdiff
path: root/js/codeq/template.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/codeq/template.js')
-rw-r--r--js/codeq/template.js284
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);
+ }
+ };
+})();