UNPKG

@wiris/mathtype-html-integration-devkit

Version:

Allows to integrate MathType Web into any JavaScript HTML WYSIWYG rich text editor.

1,142 lines (1,008 loc) 36.4 kB
/* eslint-disable no-bitwise */ import DOMPurify from "dompurify"; import MathML from "./mathml"; import Configuration from "./configuration"; import Latex from "./latex"; import StringManager from "./stringmanager"; /** * This class represents an utility class. */ export default class Util { /** * Fires an event in a target. * @param {EventTarget} eventTarget - target where event should be fired. * @param {string} eventName event to fire. * @static */ static fireEvent(eventTarget, eventName) { if (document.createEvent) { const eventObject = document.createEvent("HTMLEvents"); eventObject.initEvent(eventName, true, true); return !eventTarget.dispatchEvent(eventObject); } const eventObject = document.createEventObject(); return eventTarget.fireEvent(`on${eventName}`, eventObject); } /** * Cross-browser addEventListener/attachEvent function. * @param {EventTarget} eventTarget - target to add the event. * @param {string} eventName - specifies the type of event. * @param {Function} callBackFunction - callback function. * @static */ static addEvent(eventTarget, eventName, callBackFunction) { if (eventTarget.addEventListener) { eventTarget.addEventListener(eventName, callBackFunction, true); } else if (eventTarget.attachEvent) { // Backwards compatibility. eventTarget.attachEvent(`on${eventName}`, callBackFunction); } } /** * Cross-browser removeEventListener/detachEvent function. * @param {EventTarget} eventTarget - target to add the event. * @param {string} eventName - specifies the type of event. * @param {Function} callBackFunction - function to remove from the event target. * @static */ static removeEvent(eventTarget, eventName, callBackFunction) { if (eventTarget.removeEventListener) { eventTarget.removeEventListener(eventName, callBackFunction, true); } else if (eventTarget.detachEvent) { eventTarget.detachEvent(`on${eventName}`, callBackFunction); } } /** * Adds the a callback function, for a certain event target, to the following event types: * - dblclick * - mousedown * - mouseup * @param {EventTarget} eventTarget - event target. * @param {Function} doubleClickHandler - function to run when on dblclick event. * @param {Function} mousedownHandler - function to run when on mousedown event. * @param {Function} mouseupHandler - function to run when on mouseup event. * @static */ static addElementEvents(eventTarget, doubleClickHandler, mousedownHandler, mouseupHandler) { if (doubleClickHandler) { this.callbackDblclick = (event) => { const realEvent = event || window.event; const element = realEvent.srcElement ? realEvent.srcElement : realEvent.target; doubleClickHandler(element, realEvent); }; Util.addEvent(eventTarget, "dblclick", this.callbackDblclick); } if (mousedownHandler) { this.callbackMousedown = (event) => { const realEvent = event || window.event; const element = realEvent.srcElement ? realEvent.srcElement : realEvent.target; mousedownHandler(element, realEvent); }; Util.addEvent(eventTarget, "mousedown", this.callbackMousedown); } if (mouseupHandler) { this.callbackMouseup = (event) => { const realEvent = event || window.event; const element = realEvent.srcElement ? realEvent.srcElement : realEvent.target; mouseupHandler(element, realEvent); }; // Chrome doesn't trigger this event for eventTarget if we release the mouse button // while the mouse is outside the editor text field. // This is a workaround: we trigger the event independently of where the mouse // is when we release its button. Util.addEvent(document, "mouseup", this.callbackMouseup); Util.addEvent(eventTarget, "mouseup", this.callbackMouseup); } } /** * Remove all callback function, for a certain event target, to the following event types: * - dblclick * - mousedown * - mouseup * @param {EventTarget} eventTarget - event target. * @static */ static removeElementEvents(eventTarget) { Util.removeEvent(eventTarget, "dblclick", this.callbackDblclick); Util.removeEvent(eventTarget, "mousedown", this.callbackMousedown); Util.removeEvent(document, "mouseup", this.callbackMouseup); Util.removeEvent(eventTarget, "mouseup", this.callbackMouseup); } /** * Adds a class name to a HTMLElement. * @param {HTMLElement} element - the HTML element. * @param {string} className - the class name. * @static */ static addClass(element, className) { if (!Util.containsClass(element, className)) { element.className += ` ${className}`; } } /** * Checks if a HTMLElement contains a certain class. * @param {HTMLElement} element - the HTML element. * @param {string} className - the className. * @returns {boolean} true if the HTMLElement contains the class name. false otherwise. * @static */ static containsClass(element, className) { if (element == null || !("className" in element)) { return false; } const currentClasses = element.className.split(" "); for (let i = currentClasses.length - 1; i >= 0; i -= 1) { if (currentClasses[i] === className) { return true; } } return false; } /** * Remove a certain class for a HTMLElement. * @param {HTMLElement} element - the HTML element. * @param {string} className - the class name. * @static */ static removeClass(element, className) { let newClassName = ""; const classes = element.className.split(" "); for (let i = 0; i < classes.length; i += 1) { if (classes[i] !== className) { newClassName += `${classes[i]} `; } } element.className = newClassName.trim(); } /** * Converts old xml initial text attribute (with «») to the correct one(with §lt;§gt;). It's * used to parse old applets. * @param {string} text - string containing safeXml characters * @returns {string} a string with safeXml characters parsed. * @static */ static convertOldXmlinitialtextAttribute(text) { // Used to fix a bug with Cas imported from Moodle 1.9 to Moodle 2.x. // This could be removed in future. const val = "value="; const xitpos = text.indexOf("xmlinitialtext"); const valpos = text.indexOf(val, xitpos); const quote = text.charAt(valpos + val.length); const startquote = valpos + val.length + 1; const endquote = text.indexOf(quote, startquote); const value = text.substring(startquote, endquote); let newvalue = value.split("«").join("§lt;"); newvalue = newvalue.split("»").join("§gt;"); newvalue = newvalue.split("&").join("§"); newvalue = newvalue.split("¨").join("§quot;"); text = text.split(value).join(newvalue); return text; } /** * Convert a string representation of key-value pairs to an object. * @param {string} keyValueString - String with key-value pairs in the format key1='value1', key2='value2' * @returns {Object} - Object containing the key-value pairs */ static convertStringToObject(keyValueString) { if (!keyValueString || typeof keyValueString !== "string") { return {}; } return keyValueString .split(",") .map((pair) => pair.trim().split("=")) .reduce((resultObject, [key, value]) => { if (key && value) { resultObject[key] = value; } return resultObject; }, {}); } /** * Cross-browser solution for creating new elements. * @param {string} tagName - tag name of the wished element. * @param {Object} attributes - an object where each key is a wished * attribute name and each value is its value. * @param {Object} [creator] - if supplied, this function will use * the "createElement" method from this param. Otherwise * document will be used as creator. * @returns {Element} The DOM element with the specified attributes assigned. * @static */ static createElement(tagName, attributes, creator) { if (attributes === undefined) { attributes = {}; } if (creator === undefined) { creator = document; } let element; /* * Internet Explorer fix: * If you create a new object dynamically, you can't set a non-standard attribute. * For example, you can't set the "src" attribute on an "applet" object. * Other browsers will throw an exception and will run the standard code. */ try { let html = `<${tagName}`; Object.keys(attributes).forEach((attributeName) => { html += ` ${attributeName}="${Util.htmlEntities(attributes[attributeName])}"`; }); html += ">"; element = creator.createElement(html); } catch (e) { element = creator.createElement(tagName); Object.keys(attributes).forEach((attributeName) => { element.setAttribute(attributeName, attributes[attributeName]); }); } return element; } /** * Creates new HTML from it's HTML code as string. * @param {string} objectCode - html code * @returns {Element} the HTML element. * @static */ static createObject(objectCode, creator) { if (creator === undefined) { creator = document; } // Internet Explorer can't include "param" tag when is setting an innerHTML property. objectCode = objectCode .split("<applet ") .join('<span wirisObject="WirisApplet" ') .split("<APPLET ") .join('<span wirisObject="WirisApplet" '); // It is a 'span' because 'span' objects can contain 'br' nodes. objectCode = objectCode.split("</applet>").join("</span>").split("</APPLET>").join("</span>"); objectCode = objectCode .split("<param ") .join('<br wirisObject="WirisParam" ') .split("<PARAM ") .join('<br wirisObject="WirisParam" '); // It is a 'br' because 'br' can't contain nodes. objectCode = objectCode.split("</param>").join("</br>").split("</PARAM>").join("</br>"); const container = Util.createElement("div", {}, creator); container.innerHTML = objectCode; function recursiveParamsFix(object) { if (object.getAttribute && object.getAttribute("wirisObject") === "WirisParam") { const attributesParsed = {}; for (let i = 0; i < object.attributes.length; i += 1) { if (object.attributes[i].nodeValue !== null) { attributesParsed[object.attributes[i].nodeName] = object.attributes[i].nodeValue; } } const param = Util.createElement("param", attributesParsed, creator); // IE fix. if (param.NAME) { param.name = param.NAME; param.value = param.VALUE; } param.removeAttribute("wirisObject"); object.parentNode.replaceChild(param, object); } else if (object.getAttribute && object.getAttribute("wirisObject") === "WirisApplet") { const attributesParsed = {}; for (let i = 0; i < object.attributes.length; i += 1) { if (object.attributes[i].nodeValue !== null) { attributesParsed[object.attributes[i].nodeName] = object.attributes[i].nodeValue; } } const applet = Util.createElement("applet", attributesParsed, creator); applet.removeAttribute("wirisObject"); for (let i = 0; i < object.childNodes.length; i += 1) { recursiveParamsFix(object.childNodes[i]); if (object.childNodes[i].nodeName.toLowerCase() === "param") { applet.appendChild(object.childNodes[i]); i -= 1; // When we insert the object child into the applet, object loses one child. } } object.parentNode.replaceChild(applet, object); } else { for (let i = 0; i < object.childNodes.length; i += 1) { recursiveParamsFix(object.childNodes[i]); } } } recursiveParamsFix(container); return container.firstChild; } /** * Converts an Element to its HTML code. * @param {Element} element - entry element. * @return {string} the HTML code of the input element. * @static */ static createObjectCode(element) { // In case that the image was not created, the object can be null or undefined. if (typeof element === "undefined" || element === null) { return null; } if (element.nodeType === 1) { // ELEMENT_NODE. let output = `<${element.tagName}`; for (let i = 0; i < element.attributes.length; i += 1) { if (element.attributes[i].specified) { output += ` ${element.attributes[i].name}="${Util.htmlEntities(element.attributes[i].value)}"`; } } if (element.childNodes.length > 0) { output += ">"; for (let i = 0; i < element.childNodes.length; i += 1) { output += Util.createObject(element.childNodes[i]); } output += `</${element.tagName}>`; } else if (element.nodeName === "DIV" || element.nodeName === "SCRIPT") { output += `></${element.tagName}>`; } else { output += "/>"; } return output; } if (element.nodeType === 3) { // TEXT_NODE. return Util.htmlEntities(element.nodeValue); } return ""; } /** * Concatenates two URL paths. * @param {string} path1 - first URL path * @param {string} path2 - second URL path * @returns {string} new URL. */ static concatenateUrl(path1, path2) { let separator = ""; if (path1.indexOf("/") !== path1.length && path2.indexOf("/") !== 0) { separator = "/"; } return (path1 + separator + path2).replace(/([^:]\/)\/+/g, "$1"); } /** * Parses a text and replaces all HTML special characters by their correspondent entities. * @param {string} input - text to be parsed. * @returns {string} the input text with all their special characters replaced by their entities. * @static */ static htmlEntities(input) { return input.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;"); } /** * Sanitize HTML to prevent XSS injections. * @param {string} html - html to be sanitize. * @returns {string} html sanitized. * @static */ static htmlSanitize(html) { const annotationRegex = /\<annotation.+\<\/annotation\>/; // Get all the annotation content including the tags. const annotation = html.match(annotationRegex); // Sanitize html code without removing our supported MathML tags and attributes. html = DOMPurify.sanitize(html, { ADD_TAGS: ["semantics", "annotation", "mstack", "msline", "msrow", "none"], ADD_ATTR: ["linebreak", "charalign", "stackalign"], }); // Readd old annotation content. return html.replace(annotationRegex, annotation); } /** * Parses a text and replaces all the HTML entities by their characters. * @param {string} input - text to be parsed * @returns {string} the input text with all their entities replaced by characters. * @static */ static htmlEntitiesDecode(input) { // Textarea element decodes when inner html is set. const textarea = document.createElement("textarea"); textarea.innerHTML = input; return textarea.value; } /** * Returns a cross-browser http request. * @return {object} httpRequest request object. * @returns {XMLHttpRequest|ActiveXObject} the proper request object. */ static createHttpRequest() { const currentPath = window.location.toString().substr(0, window.location.toString().lastIndexOf("/") + 1); if (currentPath.substr(0, 7) === "file://") { throw StringManager.get("exception_cross_site"); } if (typeof XMLHttpRequest !== "undefined") { return new XMLHttpRequest(); } try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (oc) { return null; } } } /** * Converts a hash to a HTTP query. * @param {Object[]} properties - a key/value object. * @returns {string} a HTTP query containing all the key value pairs with * all the special characters replaced by their entities. * @static */ static httpBuildQuery(properties) { let result = ""; Object.keys(properties).forEach((i) => { if (properties[i] != null) { result += `${Util.urlEncode(i)}=${Util.urlEncode(properties[i])}&`; } }); // Deleting last '&' empty character. if (result.substring(result.length - 1) === "&") { result = result.substring(0, result.length - 1); } return result; } /** * Convert a hash to string sorting keys to get a deterministic output * @param {Object[]} hash - a key/value object. * @returns {string} a string with the form key1=value1...keyn=valuen * @static */ static propertiesToString(hash) { // 1. Sort keys. We sort the keys because we want a deterministic output. const keys = []; Object.keys(hash).forEach((key) => { if (Object.prototype.hasOwnProperty.call(hash, key)) { keys.push(key); } }); const n = keys.length; for (let i = 0; i < n; i += 1) { for (let j = i + 1; j < n; j += 1) { const s1 = keys[i]; const s2 = keys[j]; if (Util.compareStrings(s1, s2) > 0) { // Swap. keys[i] = s2; keys[j] = s1; } } } // 2. Generate output. let output = ""; for (let i = 0; i < n; i += 1) { const key = keys[i]; output += key; output += "="; let value = hash[key]; value = value.replace("\\", "\\\\"); value = value.replace("\n", "\\n"); value = value.replace("\r", "\\r"); value = value.replace("\t", "\\t"); output += value; output += "\n"; } return output; } /** * Compare two strings using charCodeAt method * @param {string} a - first string to compare. * @param {string} b - second string to compare. * @returns {number} the difference between a and b * @static */ static compareStrings(a, b) { let i; const an = a.length; const bn = b.length; const n = an > bn ? bn : an; for (i = 0; i < n; i += 1) { const c = Util.fixedCharCodeAt(a, i) - Util.fixedCharCodeAt(b, i); if (c !== 0) { return c; } } return a.length - b.length; } /** * Fix charCodeAt() JavaScript function to handle non-Basic-Multilingual-Plane characters. * @param {string} string - input string * @param {number} idx - an integer greater than or equal to 0 * and less than the length of the string * @returns {number} an integer representing the UTF-16 code of the string at the given index. * @static */ static fixedCharCodeAt(string, idx) { idx = idx || 0; const code = string.charCodeAt(idx); let hi; let low; /* High surrogate (could change last hex to 0xDB7F to treat high private surrogates as single characters) */ if (code >= 0xd800 && code <= 0xdbff) { hi = code; low = string.charCodeAt(idx + 1); if (Number.isNaN(low)) { throw StringManager.get("exception_high_surrogate"); } return (hi - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000; } if (code >= 0xdc00 && code <= 0xdfff) { // Low surrogate. /* We return false to allow loops to skip this iteration since should have already handled high surrogate above in the previous iteration. */ return false; } return code; } /** * Returns an URL with it's query params converted into array. * @param {string} url - input URL. * @returns {Object[]} an array containing all URL query params. * @static */ static urlToAssArray(url) { let i; i = url.indexOf("?"); if (i > 0) { const query = url.substring(i + 1); const ss = query.split("&"); const h = {}; for (i = 0; i < ss.length; i += 1) { const s = ss[i]; const kv = s.split("="); if (kv.length > 1) { h[kv[0]] = decodeURIComponent(kv[1].replace(/\+/g, " ")); } } return h; } return {}; } /** * Returns an encoded URL by replacing each instance of certain characters by * one, two, three or four escape sequences using encodeURIComponent method. * !'()* . will not be encoded. * * @param {string} clearString - URL string to be encoded * @returns {string} URL with it's special characters replaced. * @static */ static urlEncode(clearString) { let output = ""; // Method encodeURIComponent doesn't encode !'()*~ . output = encodeURIComponent(clearString); return output; } // TODO: To parser? /** * Converts the HTML of a image into the output code that WIRIS must return. * By default returns the MathML stored on data-mahml attribute (if imgCode is a formula) * or the Wiriscas attribute of a WIRIS applet. * @param {string} imgCode - the html code from a formula or a CAS image. * @param {boolean} convertToXml - true if the image should be converted to XML. * @param {boolean} convertToSafeXml - true if the image should be converted to safeXML. * @returns {string} the XML or safeXML of a WIRIS image. * @static */ static getWIRISImageOutput(imgCode, convertToXml, convertToSafeXml) { const imgObject = Util.createObject(imgCode); if (imgObject) { if ( imgObject.className === Configuration.get("imageClassName") || imgObject.getAttribute(Configuration.get("imageMathmlAttribute")) ) { if (!convertToXml) { return imgCode; } const dataMathML = imgObject.getAttribute(Configuration.get("imageMathmlAttribute")); // To handle annotations, first we need the MathML in XML. let mathML = MathML.safeXmlDecode(dataMathML); if (!Configuration.get("saveHandTraces")) { mathML = MathML.removeAnnotation(mathML, "application/json"); } if (mathML == null) { mathML = imgObject.getAttribute("alt"); } if (convertToSafeXml) { const safeMathML = MathML.safeXmlEncode(mathML); return safeMathML; } return mathML; } } return imgCode; } /** * Gets the node length in characters. * @param {Node} node - HTML node. * @returns {number} node length. * @static */ static getNodeLength(node) { const staticNodeLengths = { IMG: 1, BR: 1, }; if (node.nodeType === 3) { // TEXT_NODE. return node.nodeValue.length; } if (node.nodeType === 1) { // ELEMENT_NODE. let length = staticNodeLengths[node.nodeName.toUpperCase()]; if (length === undefined) { length = 0; } for (let i = 0; i < node.childNodes.length; i += 1) { length += Util.getNodeLength(node.childNodes[i]); } return length; } return 0; } /** * Gets a selected node or text from an editable HTMLElement. * If the caret is on a text node, concatenates it with all the previous and next text nodes. * @param {HTMLElement} target - the editable HTMLElement. * @param {boolean} isIframe - specifies if the target is an iframe or not * @param {boolean} forceGetSelection - if true, ignores IE system to get * the current selection and uses window.getSelection() * @returns {object} an object with the 'node' key set if the item is an * element or the keys 'node' and 'caretPosition' if the element is text. * @static */ static getSelectedItem(target, isIframe, forceGetSelection) { let windowTarget; if (isIframe) { windowTarget = target.contentWindow; windowTarget.focus(); } else { windowTarget = window; target.focus(); } if (document.selection && !forceGetSelection) { const range = windowTarget.document.selection.createRange(); if (range.parentElement) { if (range.htmlText.length > 0) { if (range.text.length === 0) { return Util.getSelectedItem(target, isIframe, true); } return null; } windowTarget.document.execCommand("InsertImage", false, "#"); let temporalObject = range.parentElement(); if (temporalObject.nodeName.toUpperCase() !== "IMG") { // IE9 fix: parentElement() does not return the IMG node, // returns the parent DIV node. In IE < 9, pasteHTML does not work well. range.pasteHTML('<span id="wrs_openEditorWindow_temporalObject"></span>'); temporalObject = windowTarget.document.getElementById("wrs_openEditorWindow_temporalObject"); } let node; let caretPosition; if (temporalObject.nextSibling && temporalObject.nextSibling.nodeType === 3) { // TEXT_NODE. node = temporalObject.nextSibling; caretPosition = 0; } else if (temporalObject.previousSibling && temporalObject.previousSibling.nodeType === 3) { node = temporalObject.previousSibling; caretPosition = node.nodeValue.length; } else { node = windowTarget.document.createTextNode(""); temporalObject.parentNode.insertBefore(node, temporalObject); caretPosition = 0; } temporalObject.parentNode.removeChild(temporalObject); return { node, caretPosition, }; } if (range.length > 1) { return null; } return { node: range.item(0), }; } if (windowTarget.getSelection) { let range; const selection = windowTarget.getSelection(); try { range = selection.getRangeAt(0); } catch (e) { range = windowTarget.document.createRange(); } const node = range.startContainer; if (node.nodeType === 3) { // TEXT_NODE. return { node, caretPosition: range.startOffset, }; } if (node !== range.endContainer) { return null; } if (node.nodeType === 1) { // ELEMENT_NODE. const position = range.startOffset; if (node.childNodes[position]) { // In case that a formula is detected but not selected, // we create an empty span where we could insert the new formula. if (range.startOffset === range.endOffset) { if ( position !== 0 && node.childNodes[position - 1].localName === "span" && node.childNodes[position].classList?.contains("Wirisformula") ) { node.childNodes[position - 1].remove(); return Util.getSelectedItem(target, isIframe, forceGetSelection); } if (node.childNodes[position].classList?.contains("Wirisformula")) { if ( (position > 0 && node.childNodes[position - 1].classList?.contains("Wirisformula")) || position === 0 ) { const emptySpan = document.createElement("span"); node.insertBefore(emptySpan, node.childNodes[position]); return { node: node.childNodes[position], }; } } } return { node: node.childNodes[position], }; } } } return null; } /** * Returns null if there isn't any item or if it is malformed. * Otherwise returns an object containing the node with the MathML image * and the cursor position inside the textarea. * @param {HTMLTextAreaElement} textarea - textarea element. * @returns {Object} An object containing the node, the index of the * beginning of the selected text, caret position and the start and end position of the * text node. * @static */ static getSelectedItemOnTextarea(textarea) { const textNode = document.createTextNode(textarea.value); const textNodeValues = Latex.getLatexFromTextNode(textNode, textarea.selectionStart); if (textNodeValues === null) { return null; } return { node: textNode, caretPosition: textarea.selectionStart, startPosition: textNodeValues.startPosition, endPosition: textNodeValues.endPosition, }; } /** * Looks for elements that match the given name in a HTML code string. * Important: this function is very concrete for WIRIS code. * It takes as preconditions lots of behaviors that are not the general case. * @param {string} code - HTML code. * @param {string} name - element name. * @param {boolean} autoClosed - true if the elements are autoClosed. * @return {Object[]} an object containing all HTML elements of code matching the name argument. * @static */ static getElementsByNameFromString(code, name, autoClosed) { const elements = []; code = code.toLowerCase(); name = name.toLowerCase(); let start = code.indexOf(`<${name} `); while (start !== -1) { // Look for nodes. let endString; if (autoClosed) { endString = ">"; } else { endString = `</${name}>`; } let end = code.indexOf(endString, start); if (end !== -1) { end += endString.length; elements.push({ start, end, }); } else { end = start + 1; } start = code.indexOf(`<${name} `, end); } return elements; } /** * Returns the numeric value of a base64 character. * @param {string} character - base64 character. * @returns {number} base64 character numeric value. * @static */ static decode64(character) { const PLUS = "+".charCodeAt(0); const SLASH = "/".charCodeAt(0); const NUMBER = "0".charCodeAt(0); const LOWER = "a".charCodeAt(0); const UPPER = "A".charCodeAt(0); const PLUS_URL_SAFE = "-".charCodeAt(0); const SLASH_URL_SAFE = "_".charCodeAt(0); const code = character.charCodeAt(0); if (code === PLUS || code === PLUS_URL_SAFE) { return 62; // Char '+'. } if (code === SLASH || code === SLASH_URL_SAFE) { return 63; // Char '/'. } if (code < NUMBER) { return -1; // No match. } if (code < NUMBER + 10) { return code - NUMBER + 26 + 26; } if (code < UPPER + 26) { return code - UPPER; } if (code < LOWER + 26) { return code - LOWER + 26; } return null; } /** * Converts a base64 string to a array of bytes. * @param {string} b64String - base64 string. * @param {number} length - dimension of byte array (by default whole string). * @return {Object[]} the resultant byte array. * @static */ static b64ToByteArray(b64String, length) { let tmp; if (b64String.length % 4 > 0) { throw new Error("Invalid string. Length must be a multiple of 4"); // Tipped base64. Length is fixed. } const arr = []; let l; let placeHolders; if (!length) { // All b64String string. if (b64String.charAt(b64String.length - 2) === "=") { placeHolders = 2; } else if (b64String.charAt(b64String.length - 1) === "=") { placeHolders = 1; } else { placeHolders = 0; } l = placeHolders > 0 ? b64String.length - 4 : b64String.length; } else { l = length; } let i; for (i = 0; i < l; i += 4) { // Ignoring code checker standards (bitewise operators). // See https://tracker.moodle.org/browse/CONTRIB-5862 for further information. // @codingStandardsIgnoreStart // eslint-disable-next-line max-len tmp = (Util.decode64(b64String.charAt(i)) << 18) | (Util.decode64(b64String.charAt(i + 1)) << 12) | (Util.decode64(b64String.charAt(i + 2)) << 6) | Util.decode64(b64String.charAt(i + 3)); arr.push((tmp >> 16) & 0xff); arr.push((tmp >> 8) & 0xff); arr.push(tmp & 0xff); // @codingStandardsIgnoreEnd } if (placeHolders) { if (placeHolders === 2) { // Ignoring code checker standards (bitewise operators). // @codingStandardsIgnoreStart // eslint-disable-next-line max-len tmp = (Util.decode64(b64String.charAt(i)) << 2) | (Util.decode64(b64String.charAt(i + 1)) >> 4); arr.push(tmp & 0xff); } else if (placeHolders === 1) { // eslint-disable-next-line max-len tmp = (Util.decode64(b64String.charAt(i)) << 10) | (Util.decode64(b64String.charAt(i + 1)) << 4) | (Util.decode64(b64String.charAt(i + 2)) >> 2); arr.push((tmp >> 8) & 0xff); arr.push(tmp & 0xff); // @codingStandardsIgnoreEnd } } return arr; } /** * Returns the first 32-bit signed integer from a byte array. * @param {Object[]} bytes - array of bytes. * @returns {number} the 32-bit signed integer. * @static */ static readInt32(bytes) { if (bytes.length < 4) { return false; } const int32 = bytes.splice(0, 4); // @codingStandardsIgnoreStart¡ return (int32[0] << 24) | (int32[1] << 16) | (int32[2] << 8) | (int32[3] << 0); // @codingStandardsIgnoreEnd } /** * Read the first byte from a byte array. * @param {Object} bytes - input byte array. * @returns {number} first byte of the byte array. * @static */ static readByte(bytes) { // @codingStandardsIgnoreStart return bytes.shift() << 0; // @codingStandardsIgnoreEnd } /** * Read an arbitrary number of bytes, from a fixed position on a byte array. * @param {Object[]} bytes - byte array. * @param {number} pos - start position. * @param {number} len - number of bytes to read. * @returns {Object[]} the byte array. * @static */ static readBytes(bytes, pos, len) { return bytes.splice(pos, len); } /** * Inserts or modifies formulas or CAS on a textarea. * @param {HTMLTextAreaElement} textarea - textarea target. * @param {string} text - text to add in the textarea. For example, to add the link to the image, * call this function as (textarea, Parser.createImageSrc(mathml)); * @static */ static updateTextArea(textarea, text) { if (textarea && text) { textarea.focus(); if (textarea.selectionStart != null) { const { selectionEnd } = textarea; const selectionStart = textarea.value.substring(0, textarea.selectionStart); const selectionEndSub = textarea.value.substring(selectionEnd, textarea.value.length); textarea.value = selectionStart + text + selectionEndSub; textarea.selectionEnd = selectionEnd + text.length; } else { const selection = document.selection.createRange(); selection.text = text; } } } /** * Modifies existing formula on a textarea. * @param {HTMLTextAreaElement} textarea - text area target. * @param {string} text - text to add in the textarea. For example, if you want to add the link * to the image,you can call this function as * Util.updateTextarea(textarea, Parser.createImageSrc(mathml)); * @param {number} start - beginning index from textarea where it needs to be replaced by text. * @param {number} end - ending index from textarea where it needs to be replaced by text * @static */ static updateExistingTextOnTextarea(textarea, text, start, end) { textarea.focus(); const textareaStart = textarea.value.substring(0, start); textarea.value = textareaStart + text + textarea.value.substring(end, textarea.value.length); textarea.selectionEnd = start + text.length; } /** * Add a parameter with it's correspondent value to an URL (GET). * @param {string} path - URL path * @param {string} parameter - parameter * @param {string} value - value * @static */ static addArgument(path, parameter, value) { let sep; if (path.indexOf("?") > 0) { sep = "&"; } else { sep = "?"; } return `${path + sep + parameter}=${value}`; } }