/**
* A helper for working with the Voyant Notebook app.
* @memberof Spyral
* @hideconstructor
*/
class Util {
/**
* Generates a random ID of the specified length.
* @param {Number} len The length of the ID to generate?
* @returns {String}
* @static
*/
static id(len = 8) {
// based on https://stackoverflow.com/a/13403498
const times = Math.ceil(len / 11);
let id = '';
for (let i = 0; i < times; i++) {
id += Math.random().toString(36).substring(2); // the result of this is 11 characters long
}
const letters = 'abcdefghijklmnopqrstuvwxyz';
id = letters[Math.floor(Math.random()*26)] + id; // ensure the id starts with a letter
return id.substring(0, len);
}
/**
*
* @param {Array|Object|String} contents
* @returns {String}
* @static
*/
static toString(contents) {
if (contents.constructor === Array || contents.constructor===Object) {
contents = JSON.stringify(contents);
if (contents.length>500) {
contents = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'+contents.substring(0,500)+' <a href="">+</a><div style="display: none">'+contents.substring(501)+'</div>';
}
}
return contents.toString();
}
/**
*
* @param {String} before
* @param {String} more
* @param {String} after
* @static
*/
static more(before, more, after) {
return before + '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'+more.substring(0,500)+' <a href="">+</a><div style="display: none">'+more.substring(501)+'</div>' + after;
}
/**
* Take a data URL and convert it to a Blob.
* @param {String} dataUrl
* @returns {Blob}
* @static
*/
static dataUrlToBlob(dataUrl) {
const parts = dataUrl.split(',');
const byteString = atob(parts[1]);
const mimeString = parts[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], {type: mimeString});
}
/**
* Take a Blob and convert it to a data URL.
* @param {Blob} blob
* @returns {Promise<String>} a Promise for a data URL
* @static
*/
static blobToDataUrl(blob) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = function(e) {
resolve(e.target.result);
};
try {
fr.readAsDataURL(blob);
} catch(e) {
reject(e);
}
});
}
/**
* Take a Blob and convert it to a String.
* @param {Blob} blob
* @returns {Promise<String>} a Promise for a String
* @static
*/
static blobToString(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('loadend', function(ev) {
try {
const td = new TextDecoder();
const data = td.decode(ev.target.result);
resolve(data);
} catch (err) {
reject(err);
}
});
reader.readAsArrayBuffer(blob);
});
}
/**
* Takes an XML document and XSL stylesheet and returns the resulting transformation.
* @param {(Document|String)} xmlDoc The XML document to transform
* @param {(Document|String)} xslStylesheet The XSL to use for the transformation
* @param {Boolean} [returnDoc=false] True to return a Document, false to return a DocumentFragment
* @returns {Document}
* @static
*/
static transformXml(xmlDoc, xslStylesheet, returnDoc=false) {
if (this.isString(xmlDoc)) {
const parser = new DOMParser();
xmlDoc = parser.parseFromString(xmlDoc, 'application/xml');
const error = this._getParserError(xmlDoc);
if (error) {
throw error;
}
}
if (this.isString(xslStylesheet)) {
const parser = new DOMParser();
xslStylesheet = parser.parseFromString(xslStylesheet, 'application/xml');
const error = this._getParserError(xslStylesheet);
if (error) {
throw error;
}
}
const xslRoot = xslStylesheet.firstElementChild;
if (xslRoot.hasAttribute('version') === false) {
// Transform fails in Firefox if version is missing, so return a more helpful error message instead of the default.
throw new Error('XSL stylesheet is missing version attribute.');
}
const xsltProcessor = new XSLTProcessor();
try {
xsltProcessor.importStylesheet(xslStylesheet);
} catch (e) {
console.warn(e);
}
let result;
if (returnDoc) {
result = xsltProcessor.transformToDocument(xmlDoc);
} else {
result = xsltProcessor.transformToFragment(xmlDoc, document);
}
return result;
}
/**
* Checks the Document for a parser error and returns an Error if found, or null.
* @ignore
* @param {Document} doc
* @param {Boolean} [includePosition=false] True to include the error position information
* @returns {Error|null}
* @static
*/
static _getParserError(doc, includePosition=false) {
// fairly naive check for parsererror, consider something like https://stackoverflow.com/a/55756548
const parsererror = doc.querySelector('parsererror');
if (parsererror !== null) {
const errorMsg = parsererror.textContent;
const error = new Error(errorMsg);
if (includePosition) {
const lineNumber = parseInt(errorMsg.match(/line[\s\w]+?(\d+)/i)[1]);
const columnNumber = parseInt(errorMsg.match(/column[\s\w]+?(\d+)/i)[1]);
error.lineNumber = lineNumber;
error.columnNumber = columnNumber;
}
return error;
} else {
return null;
}
}
/**
* Returns true if the value is a String.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isString(val) {
return typeof val === 'string';
}
/**
* Returns true if the value is a Number.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isNumber(val) {
return typeof val === 'number';
}
/**
* Returns true if the value is a Boolean.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isBoolean(val) {
return typeof val === 'boolean';
}
/**
* Returns true if the value is Undefined.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isUndefined(val) {
return typeof val === 'undefined';
}
/**
* Returns true if the value is an Array.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isArray(val) {
return Object.prototype.toString.call(val) === '[object Array]';
}
/**
* Returns true if the value is an Object.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isObject(val) {
return Object.prototype.toString.call(val) === '[object Object]';
}
/**
* Returns true if the value is Null.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isNull(val) {
return Object.prototype.toString.call(val) === '[object Null]';
}
/**
* Returns true if the value is a Node.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isNode(val) {
return val instanceof Node;
}
/**
* Returns true if the value is a Function.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isFunction(val) {
const typeString = Object.prototype.toString.call(val);
return typeString === '[object Function]' || typeString === '[object AsyncFunction]';
}
/**
* Returns true if the value is a Promise.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isPromise(val) {
// ES6 promise detection
// return Object.prototype.toString.call(val) === '[object Promise]';
// general promise detection
return !!val && (typeof val === 'object' || typeof val === 'function') && typeof val.then === 'function';
}
/**
* Returns true if the value is a Blob.
* @param {*} val
* @returns {Boolean}
* @static
*/
static isBlob(val) {
return val instanceof Blob;
}
/**
* Takes a MIME type and returns the related file extension.
* Only handles file types supported by Voyant.
* @param {String} mimeType
* @returns {String}
* @static
*/
static getFileExtensionFromMimeType(mimeType) {
mimeType = mimeType.trim().toLowerCase();
switch (mimeType) {
case 'application/atom+xml':
return 'xml';
case 'application/rss+xml':
return 'xml';
case 'application/xml':
return 'xml';
case 'text/xml':
return 'xml';
case 'application/xhtml+xml':
return 'xhtml';
case 'text/html':
return 'html';
case 'text/plain':
return 'txt';
case 'application/pdf':
return 'pdf';
case 'application/json':
return 'json';
case 'application/vnd.apple.pages':
return 'pages';
case 'application/rtf':
return 'rtf';
case 'application/vnd.oasis.opendocument.text':
return 'odt';
case 'application/epub+zip':
return 'epub';
case 'application/msword':
return 'doc';
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return 'docx';
case 'application/vnd.ms-excel':
return 'xls';
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return 'xlsx';
case 'application/zip':
return 'zip';
case 'application/gzip':
return 'gzip';
case 'application/x-bzip2':
return 'bzip2';
default:
if (mimeType.indexOf('text') === 0) {
return 'txt'; // fallback
} else {
return undefined;
}
}
}
/**
* Takes a file extension and returns the corresponding Voyant Document Format name.
* @param {String} fileExtension
* @returns {String}
* @static
*/
static getVoyantDocumentFormatFromFileExtension(fileExtension) {
fileExtension = fileExtension.trim().toLowerCase();
switch(fileExtension) {
case 'txt':
return 'text';
case 'xhtml':
return 'html';
case 'doc':
return 'msword';
case 'docx':
return 'mswordx';
case 'xls':
return 'xlsx';
case 'zip':
return 'archive';
case 'gzip':
case 'bzip2':
return 'compressed';
default:
return fileExtension;
}
}
}
export default Util;