@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
JavaScript
/* 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("&").split("<").join("<").split(">").join(">").split('"').join(""");
}
/**
* 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}`;
}
}