/* 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 . */ (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("");\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; }, // instantiate a template 'process': function (template, templatePath, args) { var fn = templator(template, templatePath); return fn(args); }, // instantiate templates in a lang→template dictionary 'processDictionary': function (dict, templatePath, args) { var lang; for (lang in dict) { if (!dict.hasOwnProperty(lang)) continue; dict[lang] = codeq.template.process(dict[lang], templatePath, args); } } }; })();