1. 1 : /**
  2. 2 : * A helper for working with the Voyant Notebook app.
  3. 3 : * @memberof Spyral
  4. 4 : * @hideconstructor
  5. 5 : */
  6. 6 : class Util {
  7. 7 :
  8. 8 : /**
  9. 9 : * Generates a random ID of the specified length.
  10. 10 : * @param {Number} len The length of the ID to generate?
  11. 11 : * @returns {String}
  12. 12 : * @static
  13. 13 : */
  14. 14 : static id(len = 8) {
  15. 15 : // based on https://stackoverflow.com/a/13403498
  16. 16 : const times = Math.ceil(len / 11);
  17. 17 : let id = '';
  18. 18 : for (let i = 0; i < times; i++) {
  19. 19 : id += Math.random().toString(36).substring(2); // the result of this is 11 characters long
  20. 20 : }
  21. 21 : const letters = 'abcdefghijklmnopqrstuvwxyz';
  22. 22 : id = letters[Math.floor(Math.random()*26)] + id; // ensure the id starts with a letter
  23. 23 : return id.substring(0, len);
  24. 24 : }
  25. 25 :
  26. 26 : /**
  27. 27 : *
  28. 28 : * @param {Array|Object|String} contents
  29. 29 : * @returns {String}
  30. 30 : * @static
  31. 31 : */
  32. 32 : static toString(contents) {
  33. 33 : if (contents.constructor === Array || contents.constructor===Object) {
  34. 34 : contents = JSON.stringify(contents);
  35. 35 : if (contents.length>500) {
  36. 36 : 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>';
  37. 37 : }
  38. 38 : }
  39. 39 : return contents.toString();
  40. 40 : }
  41. 41 :
  42. 42 : /**
  43. 43 : *
  44. 44 : * @param {String} before
  45. 45 : * @param {String} more
  46. 46 : * @param {String} after
  47. 47 : * @static
  48. 48 : */
  49. 49 : static more(before, more, after) {
  50. 50 : 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;
  51. 51 : }
  52. 52 :
  53. 53 :
  54. 54 : /**
  55. 55 : * Take a data URL and convert it to a Blob.
  56. 56 : * @param {String} dataUrl
  57. 57 : * @returns {Blob}
  58. 58 : * @static
  59. 59 : */
  60. 60 : static dataUrlToBlob(dataUrl) {
  61. 61 : const parts = dataUrl.split(',');
  62. 62 : const byteString = atob(parts[1]);
  63. 63 : const mimeString = parts[0].split(':')[1].split(';')[0];
  64. 64 :
  65. 65 : const ab = new ArrayBuffer(byteString.length);
  66. 66 : const ia = new Uint8Array(ab);
  67. 67 : for (let i = 0; i < byteString.length; i++) {
  68. 68 : ia[i] = byteString.charCodeAt(i);
  69. 69 : }
  70. 70 :
  71. 71 : return new Blob([ab], {type: mimeString});
  72. 72 : }
  73. 73 :
  74. 74 : /**
  75. 75 : * Take a Blob and convert it to a data URL.
  76. 76 : * @param {Blob} blob
  77. 77 : * @returns {Promise<String>} a Promise for a data URL
  78. 78 : * @static
  79. 79 : */
  80. 80 : static blobToDataUrl(blob) {
  81. 81 : return new Promise((resolve, reject) => {
  82. 82 : const fr = new FileReader();
  83. 83 : fr.onload = function(e) {
  84. 84 : resolve(e.target.result);
  85. 85 : };
  86. 86 :
  87. 87 : try {
  88. 88 : fr.readAsDataURL(blob);
  89. 89 : } catch(e) {
  90. 90 : reject(e);
  91. 91 : }
  92. 92 : });
  93. 93 : }
  94. 94 :
  95. 95 : /**
  96. 96 : * Take a Blob and convert it to a String.
  97. 97 : * @param {Blob} blob
  98. 98 : * @returns {Promise<String>} a Promise for a String
  99. 99 : * @static
  100. 100 : */
  101. 101 : static blobToString(blob) {
  102. 102 : return new Promise((resolve, reject) => {
  103. 103 : const reader = new FileReader();
  104. 104 : reader.addEventListener('loadend', function(ev) {
  105. 105 : try {
  106. 106 : const td = new TextDecoder();
  107. 107 : const data = td.decode(ev.target.result);
  108. 108 : resolve(data);
  109. 109 : } catch (err) {
  110. 110 : reject(err);
  111. 111 : }
  112. 112 : });
  113. 113 : reader.readAsArrayBuffer(blob);
  114. 114 : });
  115. 115 : }
  116. 116 :
  117. 117 : /**
  118. 118 : * Takes an XML document and XSL stylesheet and returns the resulting transformation.
  119. 119 : * @param {(Document|String)} xmlDoc The XML document to transform
  120. 120 : * @param {(Document|String)} xslStylesheet The XSL to use for the transformation
  121. 121 : * @param {Boolean} [returnDoc=false] True to return a Document, false to return a DocumentFragment
  122. 122 : * @returns {Document}
  123. 123 : * @static
  124. 124 : */
  125. 125 : static transformXml(xmlDoc, xslStylesheet, returnDoc=false) {
  126. 126 : if (this.isString(xmlDoc)) {
  127. 127 : const parser = new DOMParser();
  128. 128 : xmlDoc = parser.parseFromString(xmlDoc, 'application/xml');
  129. 129 : const error = this._getParserError(xmlDoc);
  130. 130 : if (error) {
  131. 131 : throw error;
  132. 132 : }
  133. 133 : }
  134. 134 : if (this.isString(xslStylesheet)) {
  135. 135 : const parser = new DOMParser();
  136. 136 : xslStylesheet = parser.parseFromString(xslStylesheet, 'application/xml');
  137. 137 : const error = this._getParserError(xslStylesheet);
  138. 138 : if (error) {
  139. 139 : throw error;
  140. 140 : }
  141. 141 : }
  142. 142 : const xslRoot = xslStylesheet.firstElementChild;
  143. 143 : if (xslRoot.hasAttribute('version') === false) {
  144. 144 : // Transform fails in Firefox if version is missing, so return a more helpful error message instead of the default.
  145. 145 : throw new Error('XSL stylesheet is missing version attribute.');
  146. 146 : }
  147. 147 :
  148. 148 : const xsltProcessor = new XSLTProcessor();
  149. 149 : try {
  150. 150 : xsltProcessor.importStylesheet(xslStylesheet);
  151. 151 : } catch (e) {
  152. 152 : console.warn(e);
  153. 153 : }
  154. 154 : let result;
  155. 155 : if (returnDoc) {
  156. 156 : result = xsltProcessor.transformToDocument(xmlDoc);
  157. 157 : } else {
  158. 158 : result = xsltProcessor.transformToFragment(xmlDoc, document);
  159. 159 : }
  160. 160 : return result;
  161. 161 : }
  162. 162 :
  163. 163 : /**
  164. 164 : * Checks the Document for a parser error and returns an Error if found, or null.
  165. 165 : * @ignore
  166. 166 : * @param {Document} doc
  167. 167 : * @param {Boolean} [includePosition=false] True to include the error position information
  168. 168 : * @returns {Error|null}
  169. 169 : * @static
  170. 170 : */
  171. 171 : static _getParserError(doc, includePosition=false) {
  172. 172 : // fairly naive check for parsererror, consider something like https://stackoverflow.com/a/55756548
  173. 173 : const parsererror = doc.querySelector('parsererror');
  174. 174 : if (parsererror !== null) {
  175. 175 : const errorMsg = parsererror.textContent;
  176. 176 : const error = new Error(errorMsg);
  177. 177 : if (includePosition) {
  178. 178 : const lineNumber = parseInt(errorMsg.match(/line[\s\w]+?(\d+)/i)[1]);
  179. 179 : const columnNumber = parseInt(errorMsg.match(/column[\s\w]+?(\d+)/i)[1]);
  180. 180 : error.lineNumber = lineNumber;
  181. 181 : error.columnNumber = columnNumber;
  182. 182 : }
  183. 183 : return error;
  184. 184 : } else {
  185. 185 : return null;
  186. 186 : }
  187. 187 : }
  188. 188 :
  189. 189 : /**
  190. 190 : * Returns true if the value is a String.
  191. 191 : * @param {*} val
  192. 192 : * @returns {Boolean}
  193. 193 : * @static
  194. 194 : */
  195. 195 : static isString(val) {
  196. 196 : return typeof val === 'string';
  197. 197 : }
  198. 198 :
  199. 199 : /**
  200. 200 : * Returns true if the value is a Number.
  201. 201 : * @param {*} val
  202. 202 : * @returns {Boolean}
  203. 203 : * @static
  204. 204 : */
  205. 205 : static isNumber(val) {
  206. 206 : return typeof val === 'number';
  207. 207 : }
  208. 208 :
  209. 209 : /**
  210. 210 : * Returns true if the value is a Boolean.
  211. 211 : * @param {*} val
  212. 212 : * @returns {Boolean}
  213. 213 : * @static
  214. 214 : */
  215. 215 : static isBoolean(val) {
  216. 216 : return typeof val === 'boolean';
  217. 217 : }
  218. 218 :
  219. 219 : /**
  220. 220 : * Returns true if the value is Undefined.
  221. 221 : * @param {*} val
  222. 222 : * @returns {Boolean}
  223. 223 : * @static
  224. 224 : */
  225. 225 : static isUndefined(val) {
  226. 226 : return typeof val === 'undefined';
  227. 227 : }
  228. 228 :
  229. 229 : /**
  230. 230 : * Returns true if the value is an Array.
  231. 231 : * @param {*} val
  232. 232 : * @returns {Boolean}
  233. 233 : * @static
  234. 234 : */
  235. 235 : static isArray(val) {
  236. 236 : return Object.prototype.toString.call(val) === '[object Array]';
  237. 237 : }
  238. 238 :
  239. 239 : /**
  240. 240 : * Returns true if the value is an Object.
  241. 241 : * @param {*} val
  242. 242 : * @returns {Boolean}
  243. 243 : * @static
  244. 244 : */
  245. 245 : static isObject(val) {
  246. 246 : return Object.prototype.toString.call(val) === '[object Object]';
  247. 247 : }
  248. 248 :
  249. 249 : /**
  250. 250 : * Returns true if the value is Null.
  251. 251 : * @param {*} val
  252. 252 : * @returns {Boolean}
  253. 253 : * @static
  254. 254 : */
  255. 255 : static isNull(val) {
  256. 256 : return Object.prototype.toString.call(val) === '[object Null]';
  257. 257 : }
  258. 258 :
  259. 259 : /**
  260. 260 : * Returns true if the value is a Node.
  261. 261 : * @param {*} val
  262. 262 : * @returns {Boolean}
  263. 263 : * @static
  264. 264 : */
  265. 265 : static isNode(val) {
  266. 266 : return val instanceof Node;
  267. 267 : }
  268. 268 :
  269. 269 : /**
  270. 270 : * Returns true if the value is a Function.
  271. 271 : * @param {*} val
  272. 272 : * @returns {Boolean}
  273. 273 : * @static
  274. 274 : */
  275. 275 : static isFunction(val) {
  276. 276 : const typeString = Object.prototype.toString.call(val);
  277. 277 : return typeString === '[object Function]' || typeString === '[object AsyncFunction]';
  278. 278 : }
  279. 279 :
  280. 280 : /**
  281. 281 : * Returns true if the value is a Promise.
  282. 282 : * @param {*} val
  283. 283 : * @returns {Boolean}
  284. 284 : * @static
  285. 285 : */
  286. 286 : static isPromise(val) {
  287. 287 : // ES6 promise detection
  288. 288 : // return Object.prototype.toString.call(val) === '[object Promise]';
  289. 289 :
  290. 290 : // general promise detection
  291. 291 : return !!val && (typeof val === 'object' || typeof val === 'function') && typeof val.then === 'function';
  292. 292 : }
  293. 293 :
  294. 294 : /**
  295. 295 : * Returns true if the value is a Blob.
  296. 296 : * @param {*} val
  297. 297 : * @returns {Boolean}
  298. 298 : * @static
  299. 299 : */
  300. 300 : static isBlob(val) {
  301. 301 : return val instanceof Blob;
  302. 302 : }
  303. 303 :
  304. 304 : /**
  305. 305 : * Takes a MIME type and returns the related file extension.
  306. 306 : * Only handles file types supported by Voyant.
  307. 307 : * @param {String} mimeType
  308. 308 : * @returns {String}
  309. 309 : * @static
  310. 310 : */
  311. 311 : static getFileExtensionFromMimeType(mimeType) {
  312. 312 : mimeType = mimeType.trim().toLowerCase();
  313. 313 : switch (mimeType) {
  314. 314 : case 'application/atom+xml':
  315. 315 : return 'xml';
  316. 316 : case 'application/rss+xml':
  317. 317 : return 'xml';
  318. 318 : case 'application/xml':
  319. 319 : return 'xml';
  320. 320 : case 'text/xml':
  321. 321 : return 'xml';
  322. 322 : case 'application/xhtml+xml':
  323. 323 : return 'xhtml';
  324. 324 : case 'text/html':
  325. 325 : return 'html';
  326. 326 : case 'text/plain':
  327. 327 : return 'txt';
  328. 328 : case 'application/pdf':
  329. 329 : return 'pdf';
  330. 330 : case 'application/json':
  331. 331 : return 'json';
  332. 332 : case 'application/vnd.apple.pages':
  333. 333 : return 'pages';
  334. 334 : case 'application/rtf':
  335. 335 : return 'rtf';
  336. 336 : case 'application/vnd.oasis.opendocument.text':
  337. 337 : return 'odt';
  338. 338 : case 'application/epub+zip':
  339. 339 : return 'epub';
  340. 340 : case 'application/msword':
  341. 341 : return 'doc';
  342. 342 : case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
  343. 343 : return 'docx';
  344. 344 : case 'application/vnd.ms-excel':
  345. 345 : return 'xls';
  346. 346 : case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
  347. 347 : return 'xlsx';
  348. 348 : case 'application/zip':
  349. 349 : return 'zip';
  350. 350 : case 'application/gzip':
  351. 351 : return 'gzip';
  352. 352 : case 'application/x-bzip2':
  353. 353 : return 'bzip2';
  354. 354 : default:
  355. 355 : if (mimeType.indexOf('text') === 0) {
  356. 356 : return 'txt'; // fallback
  357. 357 : } else {
  358. 358 : return undefined;
  359. 359 : }
  360. 360 : }
  361. 361 : }
  362. 362 :
  363. 363 : /**
  364. 364 : * Takes a file extension and returns the corresponding Voyant Document Format name.
  365. 365 : * @param {String} fileExtension
  366. 366 : * @returns {String}
  367. 367 : * @static
  368. 368 : */
  369. 369 : static getVoyantDocumentFormatFromFileExtension(fileExtension) {
  370. 370 : fileExtension = fileExtension.trim().toLowerCase();
  371. 371 : switch(fileExtension) {
  372. 372 : case 'txt':
  373. 373 : return 'text';
  374. 374 : case 'xhtml':
  375. 375 : return 'html';
  376. 376 : case 'doc':
  377. 377 : return 'msword';
  378. 378 : case 'docx':
  379. 379 : return 'mswordx';
  380. 380 : case 'xls':
  381. 381 : return 'xlsx';
  382. 382 : case 'zip':
  383. 383 : return 'archive';
  384. 384 : case 'gzip':
  385. 385 : case 'bzip2':
  386. 386 : return 'compressed';
  387. 387 : default:
  388. 388 : return fileExtension;
  389. 389 : }
  390. 390 : }
  391. 391 : }
  392. 392 :
  393. 393 : export default Util;