/* 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";
// 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;
};
// 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 (str, templatePath) {
return resolveResource(str, templatePath) || 'data/broken.png';
},
'img': function (str, templatePath) {
var attrs = parseKeyval(str),
output, key;
output = '';
return output;
}
};
// convert latex math notation to MathML
var renderMath = function (str) {
// inline math delimited by \( \)
str = str.replace(/\\\((.*?)\\\)/g, function (match, expression) {
return katex.renderToString(expression);
});
// display math delimited by \[ \]
str = str.replace(/\\\[(.*?)\\\]/g, function (match, expression) {
return katex.renderToString(expression, {'displayMode': true});
});
return str;
};
// instantiate template with args
var process = function (template, templatePath, args) {
var templateName,
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('/');
end = 0;
while ((start = template.indexOf('[%', end)) != -1) {
// add remaining text from previous iteration
output += template.substring(end, start);
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
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 {
codeq.log.error('Invalid directive in ' + templateName + ': ' + match[2]);
}
break;
default:
codeq.log.error('Invalid template in ' + templateName + ': ' + subpart);
}
}
}
// add any remaining text
output += template.substr(end == -1 ? start : end);
// render latex formulas
return renderMath(output);
};
codeq.template = {
'setResources': function (newResources) {
resources = newResources;
},
// instantiate a template
'process': process,
// 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);
}
}
};
})();