// introduce the namespace object for codeq window.codeq = {}; window.siteDefinition = { logLevel: 'debug' }; // for debug purposes window.phandler = null; // TODO: this is for debug only // type definitions, so the IDE and coders have easier work /** * @typedef {Object} ServerHint a hint sent by the server * @property {string} id the hint ID * @property {Object} args optional arguments for the hint in case of a pop-up or static hint * @property {string[]} choices array of choices in case of a drop-down hint * @property {number} start the starting position of the highlighted code, in case of a pop-up or drop-down hint * @property {number} end the ending position of the highlighted code, in case of a pop-up or drop-down hint */ (function () { // regular expressions for the templating function, the logging system, etc. var regexpQuote = new RegExp('"', 'g'), regexpBackslash = new RegExp('\\\\', 'g'), regexpWhiteSpaceStart = new RegExp('^[ \r\n\t]+'), regexpWhiteSpaceEnd = new RegExp('[ \r\n\t]+$'), regexpWhiteSpaceNonPrintable = new RegExp('[\r\n\t]', 'g'), regexpWhiteSpaceBeforeTag = new RegExp('[ \r\n\t]+(?=<)', 'g'), regexpWhiteSpaceAfterTag = new RegExp('>[ \r\n\t]+', 'g'), regexpIKeyMarker = new RegExp('(?:^|\\s)(ikey-marker.*)(?:$|\\s)'), regexpWhiteSpace = new RegExp('[ \\r\\n\\t]+'); // ================================================================================ // The log module: contains methods for logging, sending logs to the server // ================================================================================ var jsonize; // JSONization function if (JSON && JSON.stringify) { jsonize = JSON.stringify; } else { jsonize = function (obj) { var t, buffer, i, isFirst; if (null === obj) return 'null'; t = typeof obj; if (t === 'string') { return '"' + obj.replace(regexpBackslash, '\\\\').replace(regexpQuote, '\\"') + '"'; } if (t === 'number') { if (isFinite(obj)) return obj.toString(); throw new Error('Cannot jsonize a non-finite number: ' + obj.toString()); } if (t === 'boolean') { if (obj) return 'true'; return 'false'; } if (t === 'object') { if (obj instanceof String) return jsonize(obj.valueOf()); if (obj instanceof Number) return jsonize(obj.valueOf()); if (obj instanceof Boolean) return jsonize(obj.valueOf()); if (obj instanceof Array) { buffer = [ '[' ]; isFirst = true; for (i = 0; i < obj.length; i++) { if (isFirst) isFirst = false; else buffer.push(','); buffer.push(jsonize(obj[i])); } buffer.push(']'); return buffer.join(''); } buffer = [ '{' ]; isFirst = true; for (i in obj) { if (isFirst) isFirst = false; else buffer.push(','); buffer.push(jsonize(i), ':', jsonize(obj[i])); } buffer.push('}'); return buffer.join(''); } throw new Error('Cannot jsonize ' + t); }; } codeq.jsonize = jsonize; codeq.log = {}; (function () { var assembleOutput = function (stuff, e) { var lines = [ stuff ]; if (e && e.stack) lines.push(e.stack); return lines.join('\n'); }; if (window.siteDefinition && window.siteDefinition.logService) { var url = window.siteDefinition.logService, logs = [], storeLog = function (level, stuff, e) { logs.push({ 't': Date.now(), 'l': level, 'm': assembleOutput(stuff, e) }); }; if (window.siteDefinition && (window.siteDefinition.logLevel == 'debug')) { codeq.log.debug = function (stuff, e) { storeLog('debug', stuff, e); }; } else codeq.log.debug = function () {}; if (window.siteDefinition && ((window.siteDefinition.logLevel == 'info') || (window.siteDefinition.logLevel == 'debug'))) { codeq.log.info = function (stuff, e) { storeLog('info', stuff, e); }; } else codeq.log.info = function () {}; codeq.log.error = function (stuff, e) { storeLog('error', stuff, e); }; setInterval(function () { var copyOfLogs; if (logs.length < 1) return; copyOfLogs = jsonize({'logs': logs}); logs = []; $.ajax({ contentType: 'application/json', dataType: 'application/json', type: 'POST', url: url, data: copyOfLogs, error: function (jqXHR, textStatus, errorThrown) { if (window.console && console.log) console.log(assembleOutput('Posting of logs to ' + url + ' failed: ' + textStatus, errorThrown)); else dump(assembleOutput('Posting of logs to ' + url + ' failed: ' + textStatus, errorThrown)); }, success: function (data, textStatus, jqXHR) { } }); }, 1000); } else if (window.console && console.log) { if (window.siteDefinition && (window.siteDefinition.logLevel == 'debug')) { codeq.log.debug = function (stuff, e) { console.log(assembleOutput('DEBUG: ' + stuff, e)); }; } else codeq.log.debug = function () {}; if (window.siteDefinition && ((window.siteDefinition.logLevel == 'info') || (window.siteDefinition.logLevel == 'debug'))) { codeq.log.info = function (stuff, e) { console.log(assembleOutput('INFO: ' + stuff, e)); }; } else codeq.log.info = function () {}; codeq.log.error = function (stuff, e) { console.log(assembleOutput('ERROR: ' + stuff, e)); }; } else { if (window.siteDefinition && (window.siteDefinition.logLevel == 'debug')) { codeq.log.debug = function (stuff, e) { dump(assembleOutput('DEBUG: ' + stuff, e)); }; } else codeq.log.debug = function () {}; if (window.siteDefinition && ((window.siteDefinition.logLevel == 'info') || (window.siteDefinition.logLevel == 'debug'))) { codeq.log.info = function (stuff, e) { dump(assembleOutput('INFO: ' + stuff, e)); }; } else codeq.log.info = function () {}; codeq.log.error = function (stuff, e) { dump(assembleOutput('ERROR: ' + stuff, e)); }; } })(); // ================================================================================ // The system module: contains essential methods for the operation of the app // ================================================================================ // -------------------------------------------------------------------------------- // The templating part: the createTemplate() and its utility functions // -------------------------------------------------------------------------------- // jQuery extension jQuery.fn.makeUnselectable = function () { this.attr("unselectable", "on").attr("draggable", "false"); this.find("*").attr("unselectable", "on").attr("draggable", "false"); return this; }; var emptyConstObject = {}; // for use as a default read-only parameter in various methods, so we don't instantiate empty objects for no reason // convert a string into its definition var stringToDef = function (str) { return str.replace(regexpBackslash, '\\\\').replace(regexpQuote, '\\"').replace(regexpWhiteSpaceNonPrintable, ' '); }; // given a HTML source, remove whitespace among tags var cleanHtml = function (html) { // JavaScript doesn't support lookbehind, so we use the split-join trick return html.replace(regexpWhiteSpaceBeforeTag, '').split(regexpWhiteSpaceAfterTag).join('>'); }; var trailingBackslashCount = function (s) { var n = 0, i = s.length - 1; while ((i >= 0) && (s.charAt(i) === '\\')) { n++; i--; } return n; }; /** * Takes a string of "argName=argValue" arguments separated with a comma. * Creates and returns an array of arguments, where each argument is * represented as an object in the form { name: "argName", value: "argValue" }. * Heading and trailing whitespace is auto-removed. */ var splitComponentArguments = function (s) { var commaParts = s.split(','), args = [], // array of arguments, an argument is an object with the properties name and value (atomL and atomR) doubleParts, singleParts, i, atom, j, k, l, doublePart, singlePart, doubleLastIndex, singleLastIndex, equalsParts, atomL, atomR, singleOpen = false, doubleOpen = false; atom = []; // substrings of the currently assembling atom atomL = false; // the left-side atom atomR = false; // the right-side atom, between them is an equals sign for (i = 0; i < commaParts.length; i++) { doubleParts = commaParts[i].split('"'); // first split after double quotes doubleLastIndex = doubleParts.length - 1; for (j = 0; j < doubleParts.length; j++) { // and process the parts doublePart = doubleParts[j]; singleParts = doublePart.split("'"); // split after single quotes singleLastIndex = singleParts.length - 1; for (k = 0; k < singleParts.length; k++) { // process the parts delimited by single quotes if (atomL || singleOpen || doubleOpen) { // left side of the assignment was already found, or quotes are active atom.push(singleParts[k]); // don't search for the equals sign } else { // we don't have the left side of the assignment yet and no quotes are open equalsParts = singleParts[k].split('='); // search for the equals sign atom.push(equalsParts[0]); if (equalsParts.length > 1) { // equals sign was found: we have the left side of the assignment atomL = jQuery.trim(atom.join('')); // store the left side of the assignment if (atomL === '') throw new Error('splitComponentArguments(): left side of the argument at index ' + args.length + ' is empty: ' + s); atom = []; // and reset the atom buffer atom.push(equalsParts[1]); // save the rest into the atom buffer for (l = 2; l < equalsParts.length; l++) { // there may be more than 1 equals sign, store them, too atom.push('='); atom.push(equalsParts[l]); } } } if (k < singleLastIndex) { // the last item does not have a quote attached atom.push("'"); if (!doubleOpen) { if ((trailingBackslashCount(singleParts[k]) % 2) == 0) singleOpen = !singleOpen; } } } if (j < doubleLastIndex) { // the last item does not have a quote attached atom.push('"'); if (!singleOpen) { if ((trailingBackslashCount(doublePart) % 2) == 0) doubleOpen = !doubleOpen; } } } if (singleOpen || doubleOpen) { // a quote is still open: include the comma atom.push(','); } else { // no open quotes, and a comma or end-of-line encoutered: means end of an atom if (!atomL) throw new Error('splitComponentArguments(): the argument at index ' + args.length + ' does not contain a parameter assignment: ' + s); atomR = jQuery.trim(atom.join('')); args.push({ 'name': atomL, 'value': atomR }); atomL = false; atomR = false; atom = []; } } if (atomL || (atom.length > 0)) throw new Error('splitComponentArguments(): premature end of the last argument: ' + s); return args; }; var QuotesWalker = function (s, isLongQuotes) { var doubleQuotes, singleQuotes, quoteLen, iteration = 0, n = s.length, startCharPos = 0, unescapedIndexOf = function (s, quotes, startPos) { var n = s.length, pos; while (startPos < n) { pos = s.indexOf(quotes, startPos); if (pos <= 0) return pos; if (s.charAt(pos-1) != '\\') return pos; startPos = pos + 1; } return -1; }; if (isLongQuotes) { doubleQuotes= '"""'; singleQuotes = "'''"; quoteLen = 3; } else { doubleQuotes= '"'; singleQuotes = "'"; quoteLen = 1; } this.prefixString = ''; this.quotedString = ''; this.quoteType = ''; this.next = function () { var doubleQuotePos, singleQuotePos, quotePos, quoteType; iteration++; if (startCharPos >= n) { this.prefixString = ''; this.quotedString = ''; this.quoteType = ''; codeq.log.debug("QuotesWalker #" + iteration + ': no more data, returning false'); return false; } // with which quotes to start, single or double? doubleQuotePos = unescapedIndexOf(s, doubleQuotes, startCharPos); singleQuotePos = unescapedIndexOf(s, singleQuotes, startCharPos); if (doubleQuotePos < 0) { if (singleQuotePos < 0) { // the end this.prefixString = s.slice(startCharPos, n); this.quotedString = ''; this.quoteType = ''; startCharPos = n; codeq.log.debug("QuotesWalker #" + iteration + ': last data, remaining string: ' + this.prefixString); return true; } quotePos = singleQuotePos; quoteType = singleQuotes; } else if (singleQuotePos < 0) { quotePos = doubleQuotePos; quoteType = doubleQuotes; } else if (doubleQuotePos < singleQuotePos) { quotePos = doubleQuotePos; quoteType = doubleQuotes; } else { quotePos = singleQuotePos; quoteType = singleQuotes; } this.quoteType = quoteType; this.prefixString = s.slice(startCharPos, quotePos); startCharPos = quotePos + quoteLen; quotePos = unescapedIndexOf(s, quoteType, startCharPos); this.quotedString = s.slice(startCharPos, quotePos); startCharPos = quotePos + quoteLen; codeq.log.debug('QuotesWalker #' + iteration + ': quoteType=' + this.quoteType + ', data:\nprefix=' + this.prefixString + '\nquoted=' + this.quotedString); return true; }; }; var escapePythonicLongString = function (s, output) { var pos = 0, parts = s.split("'"), n = parts.length, i, part, previousPart; output.push("'"); // starting quote previousPart = parts[0]; output.push(previousPart.split('\r').join('\\r').split('\t').join('\\t').split('\n').join('\\n')); for (i = 1; i < n; i++) { part = parts[i]; // first escape the single quote, if required if (previousPart.charAt(previousPart.length - 1) != '\\') output.push('\\'); // escape \r, \n, \t output.push(part.split('\r').join('\\r').split('\t').join('\\t').split('\n').join('\\n')); previousPart = part; } output.push("'"); // ending quote }; // the "engine" of codeq codeq.system = { // define XML namespaces, for use in generating HTML content ns: { svg: 'http://www.w3.org/2000/svg' }, // the method for creating a templating function from the given string template createTemplate: function (str, templateName, _internalDoLog_) { var f, parts, i, subparts, subpart, isSvg = str.substring(0, 4) === ' 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(cleanHtml(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 src.push('_result.push(', subpart.slice(1), ');\n'); } else if (subpart[0] === '@') { // a component directive, this gets processed in two phases atoms = subpart.slice(1).replace(regexpWhiteSpaceStart, ''); // get the whole directive, remove any heading whitespace componentName = atoms.split(regexpWhiteSpace, 1)[0]; // extract the component name atoms = splitComponentArguments(atoms.slice(componentName.length)); // extract the atoms src.push('_tmp={"name":"', stringToDef(componentName), '","params":{},"key":"image"+_counter};\n'); src.push('_components_.push(_tmp);\n'); src.push('_tmp=_tmp.params;\n'); for (j = 0; j < atoms.length; j++) { atom = atoms[j]; src.push('_tmp["', stringToDef(atom.name), '"]=', atom.value, ';\n'); } // src.push('_result.push(\'<', nodeName, ' class="image\', _counter, \'">\');\n'); src.push('_result.push(\'\');\n'); src.push('_counter++;\n'); } else { // a statement src.push(subpart, '\n'); } } } if ((subparts.length > 1) && (subparts[1].length > 0)) { // there's a trailing text src.push('_result.push("', stringToDef(cleanHtml(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('_components_', src.join('')); // create a function that takes the single argument named _components_ (an array) } catch (e) { codeq.log.error('createTemplate(): ' + debugPrefix + ' Failed to instantiate template function: ' + e + '\nfunction(_components_) {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str, e); throw e; } return function () { // takes params, callback var param, templateParams, callback, html, components = [], i, jq, jqContainer, processDom; // parse arguments // first set defaults templateParams = false; callback = false; jqContainer = false; for (i = arguments.length - 1; i >= 0; i--) { param = arguments[i]; if (param instanceof Function) { // set the optional callback that will be invoked with a jQuery argument representing the "compiled" template, after the template is all set up if (callback) throw new Error(debugPrefix + ' More than one callback provided to a template function'); callback = param; } else if (param instanceof jQuery) { // set the container that will receive the template content, the container is mandatory for SVG if (jqContainer) throw new Error(debugPrefix + ' More than one jQuery container provided to a template function'); jqContainer = param; } else if (param instanceof Object) { // BEWARE: this one catches all Objects, so do testing for descendants (e.g.: Function, jQuery) before testing for Object! // set the parameters to the template function, they are optional if (templateParams) throw new Error(debugPrefix + ' More than one object with template parameters provided to a template function'); templateParams = param; } else throw new Error(debugPrefix + ' Unknown parameter provided to a template function: ' + typeof param); } // we need at least an empty object for the template function if (!templateParams) templateParams = emptyConstObject; // the container is mandatory for SVG //if (isSvg && !jqContainer) throw new Error(debugPrefix + ' jQuery object is a mandatory parameter to the template function when the template is a SVG object'); // process DOM, after the template is instantiated processDom = function () { var componentDef, i, finishedIteration, finishComponent, cjq, params, key, variant, variantsCache, iterationCount, currentIteration, newJq; jq.data('templateName', templateName); // for debugging iterationCount = 1 + components.length; // first one is the sentinel currentIteration = 0; // how many iterations were processed finishedIteration = function () { currentIteration++; if (iterationCount === currentIteration) { jq.makeUnselectable(); if (callback) callback(jq, templateParams); } }; finishComponent = function (cjq, classes, params, reservedParams) { var attrName; if (params['class']) classes.push(params['class']); cjq.attr('class', classes.join(' ')); for (attrName in params) { if (attrName === 'class') continue; // a fixed reserved parameter if (attrName in reservedParams) continue; // reserved parameters as specified by the corresponding handler cjq.attr(attrName, params[attrName]); // set the attribute } finishedIteration(); }; for (i = 0; i < components.length; i++) { componentDef = components[i]; params = componentDef.params; cjq = jq.find('.' + componentDef.key); if (cjq.length == 0) { if (jq.hasClass(componentDef.key)) cjq = jq; } if (cjq.length == 0) { codeq.log.error('codeq.system.createTemplate::fn(): internal error: could not obtain reference to a template component of type ' + componentDef.name + ', key=' + componentDef.key); continue; } if (_internalDoLog_) codeq.log.debug('codeq.system.createTemplate::fn(): ' + debugPrefix + ' searching for a component with class ' + componentDef.key + ', found ' + cjq.length); isSvg = 'ownerSVGElement' in cjq[0]; switch (componentDef.name) { case 'text': key = params.key; if (!key) { codeq.log.error('codeq.system.createTemplate::fn(): ' + debugPrefix + ' A text component is missing the key'); finishedIteration(); } else { if (isSvg) newJq = $(document.createElementNS(codeq.ns.svg, 'tspan')); else newJq = $(''); cjq.replaceWith(newJq); if (jq === cjq) jq = newJq; cjq = newJq; cjq.attr('data-tkey', key); translate(cjq); if (_internalDoLog_) codeq.log.debug('codeq.system.createTemplate::fn(): ' + debugPrefix + ' Instantiated intellitext: key=' + key); finishComponent(cjq, ['intellitext'], params, {'key':true}); } break; case 'image': key = params.key; if (isSvg) { codeq.log.error('codeq.system.createTemplate::fn(): ' + debugPrefix + ' Currently there is no support for inlining images into SVG'); finishedIteration(); } else if (!key) { codeq.log.error('codeq.system.createTemplate::fn(): ' + debugPrefix + ' An image component is missing the key'); finishedIteration(); } else { newJq = $(''); cjq.replaceWith(newJq); if (jq === cjq) jq = newJq; cjq = newJq; variant = params.variant || 'normal'; cjq.attr('data-ikey', key); // HTML5 data cjq.attr('data-ivariant', variant); cjq.data['ikey'] = key; // jQuery data cjq.data['ivariant'] = variant; cjq.data['ivariants'] = variantsCache = {}; setButtonVariant(cjq, variantsCache, key, variant, (function (cjq, params, variant) { return function () { if (_internalDoLog_) codeq.log.debug('codeq.system.createTemplate::fn(): ' + debugPrefix + ' Instantiated image: key=' + params.key + ', variant=' + variant); finishComponent(cjq, ['intellimage'], params, {'key':true, 'variant':true}); }; })(cjq, params, variant)); } break; default: codeq.log.error('codeq.system.createTemplate::fn(): ' + debugPrefix + ' Invalid component name: ' + componentDef.name); finishedIteration(); break; } } finishedIteration(); // the sentinel }; // create DOM try { html = f.apply(templateParams, [components]); if (_internalDoLog_) { codeq.log.debug('createTemplate(): ' + debugPrefix + ' instantiating template:\n' + html); } } catch (e) { codeq.log.error('createTemplate(): ' + debugPrefix + ' Failed to invoke template function: ' + e + '\nfunction(_components_) {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str, e); throw e; } jq = $(html); if (jqContainer) jqContainer.append(jq); processDom(); // TODO: what follows is the code that uses SVG jQuery plugin to instantiate SVG DOM, but the created images are not always correctly sized; needs analysis of what is happening /* if (isSvg) { jqContainer.empty().svg({ 'loadURL': html, 'initPath': '/js/svg/', // where to load blank.svg from if needed 'changeSize': true, // get the size from the SVG markup, don't retain the existing size of the container 'onLoad': function (jqSvg) { // it should hold that jqContainer == jqSvg, but we ignore the parameter anyway jq = $(jqContainer.children()[0]); processDom(); } }); } else { jq = $(html); if (jqContainer) jqContainer.append(jq); processDom(); }*/ }; }, // createTemplate // -------------------------------------------------------------------------------- // Task info parser: converts simplified pythonic syntax to a JavaScript function // -------------------------------------------------------------------------------- // deprecated: use codeq.parseDefinition() instead parseInfo: function (infoText) { var parts = [], n, lines, line, i, j, len, walker, fn, obj; // convert pythonic long-strings to ordinary strings, escaping things as we go walker = new QuotesWalker(infoText, true); while (walker.next()) { if (walker.prefixString.length > 0) parts.push(walker.prefixString); if (walker.quotedString.length > 0) escapePythonicLongString(walker.quotedString, parts); } // split into separate lines, remove comments, add semicolons lines = parts.join('').split('\n'); // split at line feed characters n = lines.length; for (i = 0; i < n; i++) { line = lines[i]; len = line.length; if (len > 0) { // if exists: find the first python's comment character (#) that is not in a string (between two quotes) parts = []; walker = new QuotesWalker(line, false); while (walker.next()) { if (walker.prefixString.length > 0) { j = walker.prefixString.indexOf('#'); if (j >= 0) { parts.push(walker.prefixString.slice(0, j)); break; // commented out till the EOL } parts.push(walker.prefixString); } parts.push(walker.quoteType); parts.push(walker.quotedString); parts.push(walker.quoteType); } line = parts.join('').replace(regexpWhiteSpaceEnd, ''); // trim the white space at the end if ((line.length > 0) && (line[line.length - 1] != ';')) line = line + ';'; // and add a semicolon lines[i] = line; } } // compose the function lines.unshift("var description, hint;"); lines.push("__params__.description = description;", "__params__.hint = hint;"); codeq.log.debug("Creating a new parseInfo function having the body: "); codeq.log.debug(lines); fn = new Function("__params__", lines.join('\n')); obj = {}; fn(obj); return obj; // obj now contains "description" and "hint" }, load: function (request) { $.ajax({ contentType: request.contentType, dataType: request.type, type: request.data ? 'POST' : 'GET', url: request.url + '?_=' + Date.now(), data: request.data, error: function (jqXHR, textStatus, errorThrown) { codeq.log.error('Loading of ' + request.url + ' failed: ' + textStatus, errorThrown); try { if (request.callback) request.callback(null, 'Error: ' + (errorThrown ? '' + errorThrown : textStatus), request.url); } catch (e) { codeq.log.error('Callback with error failed on request ' + request.url + ': ' + e, e); } }, success: function (data, textStatus, jqXHR) { try { if (request.callback) request.callback(data, 'OK', request.url); } catch (e) { codeq.log.error('Callback failed on successful request ' + request.url + ': ' + e, e); } } }); } }; // codeq.system = { /** * Returns the number of Unicode code points in the given string. * * @param s {string} * @returns {number} */ codeq.codePointCount = function (s) { var n = 0, i, code; if (typeof s !== 'string') { code = 'codePointCount(): argument not a string: type = ' + typeof s + ', is null = ' + (s === null); if ((typeof s === 'object') && (s !== null) && s.constructor) code += ', constructor = ' + s.constructor.name; codeq.log.error(code); return 0; } for (i = s.length - 1; i >= 0; i--) { try { code = s.charCodeAt(i); } catch (e) { codeq.log.error('Invocation of charCodeAt() failed at iteration #' + i + ': ' + e, e); return 0; } if ((code >= 0xd800) && (code < 0xe000)) i++; n++; } return n; }; var jqDisabled = $('#disabled'), waitCssEnter = {'cursor': 'wait', 'display': ''}; codeq.wait = function (promise) { jqDisabled.css(waitCssEnter); return promise.fin(function () { jqDisabled.css('display', 'none'); }); }; })();