/** * Copyright 2020 Baidu Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const view = require('./view'); const host = {}; host.BrowserHost = class { constructor() { window.eval = () => { throw new Error('window.eval() not supported.'); }; this._document = window.document; this._meta = {}; for (const element of Array.from(this._document.getElementsByTagName('meta'))) { if (element.content) { this._meta[element.name] = this._meta[element.name] || []; this._meta[element.name].push(element.content); } } this._type = this._meta.type ? this._meta.type[0] : 'Browser'; this._version = this._meta.version ? this._meta.version[0] : null; this._ready = false; } get document() { return this._document; } get version() { return this._version; } get type() { return this._type; } initialize(view) { this._view = view; return Promise.resolve(); } start() { window.addEventListener( 'message', event => { const originalData = event.data; if (originalData) { const type = originalData.type; const data = originalData.data; switch (type) { case 'change-files': return this._changeFiles(data); case 'zoom-in': return this._view.zoomIn(); case 'zoom-out': return this._view.zoomOut(); case 'zoom-reset': return this._view.resetZoom(); case 'toggle-attributes': return this._view.toggleAttributes(data); case 'toggle-initializers': return this._view.toggleInitializers(data); case 'toggle-names': return this._view.toggleNames(data); case 'toggle-direction': return this._view.toggleDirection(data); case 'toggle-theme': return this._view.toggleTheme(data); case 'export': return this._view.export(`${document.title}.${data}`); case 'change-graph': return this._view.changeGraph(data); case 'search': return this._view.find(data); case 'select': return this._view.select(data); case 'show-model-properties': return this._view.showModelProperties(); case 'show-node-documentation': return this._view.showNodeDocumentation(data); case 'ready': if (this._ready) { return this.status('ready'); } return; } } }, false ); this._ready = true; this.status('ready'); } message(type, data) { if (window.parent) { window.parent.postMessage({type: type, data: data}, '*'); } } status(status) { this.message('status', status); } error(message, detail) { this.message('error', (message === 'Error' ? '' : message + ' ') + detail); } confirm(message, detail) { const result = confirm(message + ' ' + detail); if (!result) { this.message('cancel'); } return result; } require(id) { const url = this._url(id + '.js'); window.__modules__ = window.__modules__ || {}; if (window.__modules__[url]) { return Promise.resolve(window.__exports__[url]); } return new Promise((resolve, reject) => { window.module = {exports: {}}; const script = document.createElement('script'); script.setAttribute('id', id); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', url); script.onload = () => { const exports = window.module.exports; delete window.module; window.__modules__[id] = exports; resolve(exports); }; script.onerror = e => { delete window.module; reject(new Error("The script '" + e.target.src + "' failed to load.")); }; this.document.head.appendChild(script); }); } save(name, extension, defaultPath, callback) { callback(defaultPath + '.' + extension); } export(file, blob) { const element = this.document.createElement('a'); element.download = file; element.href = URL.createObjectURL(blob); this.document.body.appendChild(element); element.click(); this.document.body.removeChild(element); } request(base, file, encoding) { const url = base ? base + '/' + file : this._url(file); return this._request(url, null, encoding); } _changeFiles(files) { if (files && files.length) { files = Array.from(files); const file = files.find(file => this._view.accept(file.name)); if (!file) { this.error('Error opening file.', 'Cannot open file ' + files[0].name); return; } this._open( files.find(file => this._view.accept(file.name)), files ); } } _request(url, headers, encoding, timeout) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); if (!encoding) { request.responseType = 'arraybuffer'; } if (timeout) { request.timeout = timeout; } const error = status => { const err = new Error('The web request failed with status code ' + status + " at '" + url + "'."); err.type = 'error'; err.url = url; return err; }; request.onload = () => { if (request.status == 200) { if (request.responseType == 'arraybuffer') { resolve(new Uint8Array(request.response)); } else { resolve(request.responseText); } } else { reject(error(request.status)); } }; request.onerror = e => { const err = error(request.status); err.type = e.type; reject(err); }; request.ontimeout = () => { request.abort(); const err = new Error("The web request timed out in '" + url + "'."); err.type = 'timeout'; err.url = url; reject(err); }; request.open('GET', url, true); if (headers) { for (const name of Object.keys(headers)) { request.setRequestHeader(name, headers[name]); } } request.send(); }); } _url(file) { let url = file; if (window && window.location && window.location.href) { let location = window.location.href.split('?').shift(); if (location.endsWith('.html')) { location = location.split('/').slice(0, -1).join('/'); } if (location.endsWith('/')) { location = location.slice(0, -1); } url = location + '/' + file; } return url; } _open(file, files) { this.status('loading'); const context = new BrowserFileContext(file, files); context .open() .then(() => { return this._view.open(context).then(model => { if (this._view.actived) { this.status('rendered'); } this.document.title = files[0].name; return model; }); }) .catch(error => { this.error(error.name, error.message); }); } }; if (typeof TextDecoder === 'undefined') { TextDecoder = function TextDecoder(encoding) { this._encoding = encoding; }; TextDecoder.prototype.decode = function decode(buffer) { let result = ''; const length = buffer.length; let i = 0; switch (this._encoding) { case 'utf-8': while (i < length) { const c = buffer[i++]; switch (c >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: { result += String.fromCharCode(c); break; } case 12: case 13: { const c2 = buffer[i++]; result += String.fromCharCode(((c & 0x1f) << 6) | (c2 & 0x3f)); break; } case 14: { const c2 = buffer[i++]; const c3 = buffer[i++]; result += String.fromCharCode(((c & 0x0f) << 12) | ((c2 & 0x3f) << 6) | ((c3 & 0x3f) << 0)); break; } } } break; case 'ascii': while (i < length) { result += String.fromCharCode(buffer[i++]); } break; } return result; }; } if (typeof TextEncoder === 'undefined') { // eslint-disable-next-line @typescript-eslint/no-empty-function TextEncoder = function TextEncoder() {}; TextEncoder.prototype.encode = function encode(str) { 'use strict'; const length = str.length; let resPos = -1; const resArr = typeof Uint8Array === 'undefined' ? new Array(length * 2) : new Uint8Array(length * 3); for (let point = 0, nextcode = 0, i = 0; i !== length; ) { point = str.charCodeAt(i); i += 1; if (point >= 0xd800 && point <= 0xdbff) { if (i === length) { resArr[(resPos += 1)] = 0xef; resArr[(resPos += 1)] = 0xbf; resArr[(resPos += 1)] = 0xbd; break; } nextcode = str.charCodeAt(i); if (nextcode >= 0xdc00 && nextcode <= 0xdfff) { point = (point - 0xd800) * 0x400 + nextcode - 0xdc00 + 0x10000; i += 1; if (point > 0xffff) { resArr[(resPos += 1)] = (0x1e << 3) | (point >>> 18); resArr[(resPos += 1)] = (0x2 << 6) | ((point >>> 12) & 0x3f); resArr[(resPos += 1)] = (0x2 << 6) | ((point >>> 6) & 0x3f); resArr[(resPos += 1)] = (0x2 << 6) | (point & 0x3f); continue; } } else { resArr[(resPos += 1)] = 0xef; resArr[(resPos += 1)] = 0xbf; resArr[(resPos += 1)] = 0xbd; continue; } } if (point <= 0x007f) { resArr[(resPos += 1)] = (0x0 << 7) | point; } else if (point <= 0x07ff) { resArr[(resPos += 1)] = (0x6 << 5) | (point >>> 6); resArr[(resPos += 1)] = (0x2 << 6) | (point & 0x3f); } else { resArr[(resPos += 1)] = (0xe << 4) | (point >>> 12); resArr[(resPos += 1)] = (0x2 << 6) | ((point >>> 6) & 0x3f); resArr[(resPos += 1)] = (0x2 << 6) | (point & 0x3f); } } if (typeof Uint8Array !== 'undefined') { return new Uint8Array(resArr.buffer.slice(0, resPos + 1)); } else { return resArr.length === resPos + 1 ? resArr : resArr.slice(0, resPos + 1); } }; TextEncoder.prototype.toString = function () { return '[object TextEncoder]'; }; try { Object.defineProperty(TextEncoder.prototype, 'encoding', { get: function () { if (Object.prototype.isPrototypeOf.call(TextEncoder.prototype, this)) { return 'utf-8'; } else { throw TypeError('Illegal invocation'); } } }); } catch (e) { TextEncoder.prototype.encoding = 'utf-8'; } if (typeof Symbol !== 'undefined') { TextEncoder.prototype[Symbol.toStringTag] = 'TextEncoder'; } } if (typeof URLSearchParams === 'undefined') { URLSearchParams = function URLSearchParams(search) { const decode = str => { return str.replace(/[ +]/g, '%20').replace(/(%[a-f0-9]{2})+/gi, match => { return decodeURIComponent(match); }); }; this._dict = {}; if (typeof search === 'string') { search = search.indexOf('?') === 0 ? search.substring(1) : search; const properties = search.split('&'); for (const property of properties) { const index = property.indexOf('='); const name = index > -1 ? decode(property.substring(0, index)) : decode(property); const value = index > -1 ? decode(property.substring(index + 1)) : ''; if (!Object.prototype.hasOwnProperty.call(this._dict, name)) { this._dict[name] = []; } this._dict[name].push(value); } } }; URLSearchParams.prototype.get = function (name) { return Object.prototype.hasOwnProperty.call(this._dict, name) ? this._dict[name][0] : null; }; } if (!HTMLCanvasElement.prototype.toBlob) { HTMLCanvasElement.prototype.toBlob = function (callback, type, quality) { setTimeout(() => { const data = atob(this.toDataURL(type, quality).split(',')[1]); const length = data.length; const buffer = new Uint8Array(length); for (let i = 0; i < length; i++) { buffer[i] = data.charCodeAt(i); } callback(new Blob([buffer], {type: type || 'image/png'})); }, 0); }; } class BrowserFileContext { constructor(file, blobs) { this._file = file; this._blobs = {}; for (const blob of blobs) { this._blobs[blob.name] = blob; } } get identifier() { return this._file.name; } get buffer() { return this._buffer; } open() { return this.request(this._file.name, null).then(data => { this._buffer = data; }); } request(file, encoding) { const blob = this._blobs[file]; if (!blob) { return Promise.reject(new Error("File not found '" + file + "'.")); } return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => { resolve(encoding ? e.target.result : new Uint8Array(e.target.result)); }; reader.onerror = e => { e = e || window.event; let message = ''; switch (e.target.error.code) { case e.target.error.NOT_FOUND_ERR: message = "File not found '" + file + "'."; break; case e.target.error.NOT_READABLE_ERR: message = "File not readable '" + file + "'."; break; case e.target.error.SECURITY_ERR: message = "File access denied '" + file + "'."; break; default: message = "File read '" + e.target.error.code.toString() + "' error '" + file + "'."; break; } reject(new Error(message)); }; if (encoding === 'utf-8') { reader.readAsText(blob, encoding); } else { reader.readAsArrayBuffer(blob); } }); } } window.__view__ = new view.View(new host.BrowserHost());