diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/codeq/core.js | 254 | ||||
-rw-r--r-- | js/codeq/hint.js | 20 | ||||
-rw-r--r-- | js/codeq/init.js | 2 | ||||
-rw-r--r-- | js/codeq/template.js | 284 |
4 files changed, 290 insertions, 270 deletions
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, '&').replace(regexpLt, '<').replace(regexpGt, '>'); }, @@ -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, '&') - .replace(/</g, '<') - .replace(/>/g, '>'); - });*/ - }, - 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); + } + }; +})(); |