diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/codeq/hint.js | 238 | ||||
-rw-r--r-- | js/codeq/problem.js | 38 | ||||
-rw-r--r-- | js/codeq/python.js | 2 | ||||
-rw-r--r-- | js/codeq/translation.js | 1 |
4 files changed, 225 insertions, 54 deletions
diff --git a/js/codeq/hint.js b/js/codeq/hint.js index d30f934..6f54717 100644 --- a/js/codeq/hint.js +++ b/js/codeq/hint.js @@ -12,6 +12,7 @@ hintCleaners = [], planIdx = 0, dictionary = [], + jqHintsContainer = jqHints.parent(), clearHints = function () { var i; @@ -44,45 +45,192 @@ }); }, - typeHandlers = { - 'static': function (type, template, serverHint) { - var args = serverHint ? serverHint.args : null, - jqContainer, jqButton, i, N, tmpl, tmplIsObject; - if (template instanceof Array) { // unwrap the template if there's only one - if (template.length == 0) template = ''; - else if (template.length == 1) template = template[0] + ''; // it must be a string + prepareStaticHintContent = function (hintContent, indices, hintId) { + var content = {}, + langs = codeq.availableLangs, + skippedLangs = [], + Nhints = 0, // count hints and remember the count + haveIndices = indices instanceof Array, + i, lang, j, node, tmpContent; + langLoop: + for (j = langs.length - 1; j >= 0; j--) { + lang = langs[j]; + node = hintContent[lang]; + if (!node) { + // no translation for this language, mark for later + skippedLangs.push(lang); + } + else { + if (haveIndices) { + // traverse node's indices + for (i = 0; i < indices.length; i++) { + node = node[indices[i]]; + if (!node) { + // index out of bounds, mark this language for later + codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + indices); + skippedLangs.push(lang); + continue langLoop; + } + } + } + if (typeof node === 'string') { + // we have a single string (= a simplex message), put it into an array + content[lang] = [node]; + if (Nhints < 1) Nhints = 1; + } + else if (node instanceof Array) { + // we already have an array + if (node.length == 0) { + if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' for language ' + lang + ' contains an empty array'); + else codeq.log.error('Hint ' + hintId + ' for language ' + lang + ' contains an empty array'); + skippedLangs.push(lang); + continue; + } + // verify that each array element is a string or an object with a message + for (i = node.length - 1; i >= 0; i--) { + tmpContent = node[i]; + if (typeof tmpContent === 'string') { + // this is okay + } + else if (tmpContent && (typeof tmpContent === 'object')) { + if (typeof tmpContent.message !== 'string') { + if (haveIndices) tmpContent.message = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i; + else tmpContent.message = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i; + codeq.log.error(tmpContent.message); + } + } + else { + // not a string or an object with a message + if (haveIndices) tmpContent = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i; + else tmpContent = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i; + node[i] = tmpContent; + codeq.log.error(tmpContent); + } + } + content[lang] = node; + if (Nhints < node.length) Nhints = node.length; + } + else if (node && (typeof node === 'object') && (typeof node.message === 'string')) { + // we have a single object with a message (= a complex message), put it into an array + content[lang] = [node]; + if (Nhints < 1) Nhints = 1; + } + else { + if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' did not result in a terminal node for language ' + lang + ', but: ' + node); + else codeq.log.error('Hint ' + hintId + ' probably needs indices because it does not have a terminal node for language ' + lang + ', but: ' + node); + skippedLangs.push(lang); + } } - if (template instanceof Array) { - codeq.log.debug('Processing an array of static hints'); + } + if (Nhints === 0) { + // provide error feedback on display when there is no hint translation available in any language + if (haveIndices) tmpContent = ['No hints found for hint ' + hintId + ' with indices ' + indices]; + else tmpContent = ['No hints found for hint ' + hintId]; + codeq.log.error(tmpContent[0]); + for (j = langs.length - 1; j >= 0; j--) { + content[langs[j]] = tmpContent; + } + Nhints = 1; + } + else if (skippedLangs.length > 0) { + // choose a default content and assign it to skipped languages + lang = 'en'; // try English first + tmpContent = content[lang]; + if (!tmpContent) { + // if no English exists, find one that does + for (lang in content) { + if (!content.hasOwnProperty(lang)) continue; + tmpContent = content[lang]; + if (tmpContent) break; + } + } + codeq.log.error('Translations in languages ' + skippedLangs.join(', ') + ' are missing or erroneous for hint ' + hintId + ', replacing their content with translation for ' + lang); + // assign the default content to skipped languages + for (j = skippedLangs.length - 1; j >= 0; j--) { + content[skippedLangs[j]] = tmpContent; + } + } + content.hintLength = Nhints; + return content; + }, + + ta = function (trObj) { // an object of the form: {'en': 'english content', 'sl': 'slovenska vsebina'} + var result = ['data-dict="', trNamespace, '" data-tkey="', dictionary.length, '"'].join(''); + dictionary.push(trObj); + return result; + }, + + typeHandlers = { + 'static': function (type, template, serverHint, hintId) { + var content = prepareStaticHintContent(template, serverHint.indices, hintId), + args = serverHint ? serverHint.args : null, + hintIndex = 0, + trButton = {}, + Nhints = content.hintLength, + nextJqHint = function () { + var trContent = {}, + langs = codeq.availableLangs, + lang, i, msg, jq, deltaHeight; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + try { + msg = content[lang][hintIndex]; + if (typeof msg === 'string') { + trContent[lang] = processTemplate(msg, args); + trButton[lang] = 'More...'; + } + else { + trContent[lang] = processTemplate(msg.message, args); + trButton[lang] = msg.linkText; + } + } + catch (e) { + msg = 'Error processing hint ' + hintId + ' at index ' + hintIndex + ' for language ' + lang + ': ' + e; + codeq.log.error(msg, e); + trContent[lang] = msg; + } + } + jq = $('<div class="hint-static translatable" ' + ta(trContent) + '></div>'); + hintIndex++; + if (jqButton) { + if (hintIndex < Nhints) { + jqButton.before(jq); + codeq.tr.translateDom(jqButton); + } + else { + jqButton.remove(); + jqContainer.append(jq); + } + } + else { + jqContainer.append(jq); + } + codeq.tr.translateDom(jq); + // scroll into view if overflowing + deltaHeight = jqHints.height() - jqHintsContainer.height(); + if (deltaHeight > 0) { + jqHintsContainer.scrollTop(deltaHeight); + } + }, + jqContainer, jqButton; + + if (Nhints > 1) { + // hint sequence jqContainer = $('<div class="hint-static-group"></div>'); - jqButton = $('<a class="hint-static-link"></a>'); + jqButton = $('<a class="hint-static-link translatable" ' + ta(trButton) + '></a>'); jqHints.append(jqContainer); - N = template.length; - tmpl = template[0]; - tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null); - jqContainer.append('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>'); jqContainer.append(jqButton); - jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" - i = 1; jqButton.on('click', function () { - var tmpl = template[i], - tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null), - jqNext = $('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>'); - i++; - if (i < N) { - jqButton.before(jqNext); - jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" - } - else { - jqButton.remove(); - jqContainer.append(jqNext); - } + nextJqHint(); }); } else { - codeq.log.debug('Processing a single static hint'); - jqHints.append('<div class="hint-static">' + processTemplate(template, args) + '</div>'); + // a single hint + jqContainer = jqHints; + jqButton = null; } + nextJqHint(); + // no hint cleaner here, a static hint remains on the screen }, @@ -137,7 +285,7 @@ hintCommonDefs = commonDef.hint_type, hintProblemTr = problemDef.hint, hintCommonTr = commonDef.hint, - planDef = problemDef.plan.sl; + planDef = problemDef.plan; return { /** @@ -146,12 +294,16 @@ */ 'planNext': function () { if (planIdx < planDef.length) { - typeHandlers['static']('static', planDef[planIdx], null); + typeHandlers.static('static', planDef[planIdx], {}, 'plan'); planIdx++; } return planIdx < planDef.length; }, + 'hasNextPlan': function () { + return planIdx < planDef.length; + }, + /** * Processes and display appropriately the server hints. * TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last @@ -160,7 +312,8 @@ */ 'handle': function (serverHints) { var n = serverHints.length, - i, serverHint, hintId, hintDef, hintContent, hintType, hintTemplate, t, fn, indices; + i, serverHint, hintId, hintDef, hintContent, hintType, t, fn, indices; + clearHints(); mainLoop: for (i = 0; i < n; i++) { @@ -182,28 +335,18 @@ codeq.log.error('Hint without content: ' + hintId); continue; } - if (serverHint.indices) { - indices = serverHint.indices; - for (i = 0; i < indices.length; i++) { - hintContent = hintContent[indices[i]]; - if (!hintContent) { - codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + serverHint.indices); - continue mainLoop; - } - } - } t = typeof hintDef; - if (t === 'string') hintType = hintDef; - else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; + if (t === 'string') hintType = hintDef; // currently a hint type is a string + else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; // but in future we may use an object, if a definition becomes more complex else { - codeq.log.error('Cannot determine type of hint ' + hintId + ' from: ' + hintDef); + codeq.log.error('Cannot determine the type of hint ' + hintId + ' from: ' + hintDef); continue; } fn = typeHandlers[hintType]; if (!fn) codeq.log.error('Unsupported hint type: ' + hintType); - else fn(hintType, hintContent.sl, serverHint); + else fn(hintType, hintContent, serverHint, hintId); } }, @@ -213,6 +356,7 @@ jqHints.empty(); jqHints = null; jqEditor = null; + jqHintsContainer = null; editor = null; } }; diff --git a/js/codeq/problem.js b/js/codeq/problem.js index d07e5fa..f6331b3 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -83,25 +83,51 @@ if (!translation || !(translation instanceof Array)) return false; return translation.length > 0; }, + /** + * Structurally converts the plan definition into something that the + * hint module can work with. + * The input is the translations object from problem.json in the form + * {'sl': { ..., 'plan': [hint1_sl, hint2_sl, ...]}, 'en': { ..., 'plan': [hint1_en, hint2_en, ...]}, ...} + * The output is a list of hints in the plan, translations are in each hint: + * [{'sl': hint1_sl, 'en': hint1_en, ...}, {'sl': hint2_sl, 'en': hint2_en}, ...] + */ processPlans = function (rawTranslations) { // find the default plan translation var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan', defaultPlanCondition) || [], - allPlans = {}, // the result - i, lang, tr; - // create all translations for plan + result = [], + i, lang, tr, plan, j, fragment; + if (defaultPlan.length == 0) return result; // no plan + // copy all translations from plan for (i = langs.length - 1; i >= 0; i--) { lang = langs[i]; tr = rawTranslations[lang]; // set up plan if (tr && defaultPlanCondition(tr.plan)) { - allPlans[lang] = tr.plan; + plan = tr.plan; } else { // there's no plan in the current language, copy the default plan - allPlans[lang] = defaultPlan; + plan = defaultPlan; + } + if (!(plan instanceof Array)) plan = [plan]; + for (j = 0; j < plan.length; j++) { + if (j < result.length) fragment = result[j]; + else { + fragment = {}; + result.push(fragment); + } + fragment[lang] = plan[j]; } } - return allPlans; + // ensure each plan element has all translations + for (j = result.length - 1; j >= 0; j--) { + fragment = result[j]; + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + if (!fragment[lang]) fragment[lang] = 'Missing plan for language ' + lang + ' at index ' + j; + } + } + return result; }, // ================================================================================ diff --git a/js/codeq/python.js b/js/codeq/python.js index 8a9e92f..59e010a 100644 --- a/js/codeq/python.js +++ b/js/codeq/python.js @@ -233,7 +233,7 @@ if (currentSolution) editor.setValue(currentSolution); // $('#screen_python .title').text(problem.slug); // jqDescriptionContent.html(problem.description); - jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.sl) || []).length == 0); + jqBtnPlan.prop('disabled', !hinter.hasNextPlan()); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), diff --git a/js/codeq/translation.js b/js/codeq/translation.js index d4278ac..f33199d 100644 --- a/js/codeq/translation.js +++ b/js/codeq/translation.js @@ -70,6 +70,7 @@ 'translateDom': function (jqTopElt) { var lang = codeq.getLang(); + if (jqTopElt.hasClass('translatable')) translateElement(jqTopElt, lang); jqTopElt.find('.translatable').each(function () { translateElement($(this), lang); }); |