summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html1
-rw-r--r--js/codeq/core.js254
-rw-r--r--js/codeq/hint.js20
-rw-r--r--js/codeq/init.js2
-rw-r--r--js/codeq/template.js284
5 files changed, 291 insertions, 270 deletions
diff --git a/index.html b/index.html
index 8bc8163..9b37a2f 100644
--- a/index.html
+++ b/index.html
@@ -546,6 +546,7 @@
<!-- codeq app cont. -->
<script src="js/codeq/init.js"></script>
<script src="js/codeq/misc.js"></script>
+ <script src="js/codeq/template.js"></script>
<script src="js/codeq/translation.js"></script>
<script src="js/codeq/navigation.js"></script>
<script src="js/codeq/comms.js"></script>
diff --git a/js/codeq/core.js b/js/codeq/core.js
index e86b396..50322c0 100644
--- a/js/codeq/core.js
+++ b/js/codeq/core.js
@@ -1,5 +1,5 @@
/* CodeQ: an online programming tutor.
- Copyright (C) 2015 UL FRI
+ Copyright (C) 2015,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
@@ -164,22 +164,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
// 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'),
regexpAmp = new RegExp('&', 'g'),
regexpLt = new RegExp('<', 'g'),
- regexpGt = new RegExp('>', 'g'),
- 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 resources = {}; // resource tree, loaded from data/resources.json in the boot sequence
+ regexpGt = new RegExp('>', 'g');
// event dispatch
var eventListeners = {}, // keyed by event name, value is an array of listeners
@@ -211,144 +198,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
}, 0);
};
- 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;
- };
-
- 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 makeActivityHandler = function (editor, problem_id) {
var lastActivityMillis = Date.now(),
deltaActivityMillis = function deltaActivityMillisFunc () {
@@ -439,10 +288,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
return codeq.settings['gui_lang'];
},
- 'setResources': function (newResources) {
- resources = newResources;
- },
-
'escapeHtml': function (s) {
return ('' + s).replace(regexpAmp, '&amp;').replace(regexpLt, '&lt;').replace(regexpGt, '&gt;');
},
@@ -484,101 +329,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
});
},
- 'templator': function (str, templatePath, templateName) {
- var f, parts, i, subparts, subpart,
- src = [ 'var _result = [], echo = function (s) { _result.push(s); };\n' ],
- atoms, j, atom, debugPrefix, s, r, tokens, handler;
-
- if (!templateName) templateName = 'unknown template';
- debugPrefix = '[' + templateName + ']';
- if (!templatePath) templatePath = [];
-
- // 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("");');
-// if (_internalDoLog_) codeq.log.debug('createTemplate(): ' + debugPrefix + ' creating templating function:\nfunction(_components_) {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str);
- 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 event handling
'fire': function (eventName, args) {
diff --git a/js/codeq/hint.js b/js/codeq/hint.js
index 05b9d2a..5b2f66e 100644
--- a/js/codeq/hint.js
+++ b/js/codeq/hint.js
@@ -38,7 +38,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
hintCommonTr = commonDef.hint,
planDef = problemDef.plan,
templatePath = [problemDef.language, problemDef.group, problemDef.problem],
- templateName = templatePath.join('/'),
guiDict = codeq.tr.getDictionary('gui'),
btnMoreTranslations = guiDict['btn_more'] || {}, // get the default translations for the "more..." button
@@ -65,19 +64,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
return result;
},
- processTemplate = function (template, args) {
- var fn = codeq.templator(template, templatePath, templateName);
- return fn(args);
-/* if (!args)
- return template;
- return template.replace(/\[%=(\w+)%\]/g, function(match, name) {
- return args[name].toString()
- .replace(/&/g, '&amp')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;');
- });*/
- },
-
prepareStaticHintContent = function (hintContent, indices, hintId) {
var content = {},
langs = codeq.availableLangs,
@@ -209,11 +195,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
try {
msg = content[lang][hintIndex];
if (typeof msg === 'string') {
- trContent[lang] = processTemplate(msg, args);
+ trContent[lang] = codeq.template.process(msg, templatePath, args);
trButton[lang] = btnMoreTranslations[lang] || 'More...';
}
else {
- trContent[lang] = processTemplate(msg.message, args);
+ trContent[lang] = codeq.template.process(msg.message, templatePath, args);
trButton[lang] = msg.linkText;
}
}
@@ -282,7 +268,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
lang = langs[i];
content = template[lang];
if (typeof content === 'string') {
- translations[lang] = processTemplate(content, args);
+ translations[lang] = codeq.template.process(content, templatePath, args);
}
else {
translations[lang] = 'No translation in ' + lang + ' available for ' + type + ' hint ' + serverHint.id;
diff --git a/js/codeq/init.js b/js/codeq/init.js
index f96fb95..6030f7c 100644
--- a/js/codeq/init.js
+++ b/js/codeq/init.js
@@ -82,7 +82,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */
loadGuiTranslations()
.then(codeq.comms.getResourceTree)
.then(function (resourceTree) {
- codeq.setResources(resourceTree); // save the loaded resource tree
+ codeq.template.setResources(resourceTree); // save the loaded resource tree
codeq.fire('init'); // tell any interested modules that we are now initialized, perhaps they want to initialize too
codeq.setLang(lang || 'en'); // initial language setting, this also translates the GUI
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);
+ }
+ };
+})();