/* 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);
}
}
};
})();