Spaces:
Running
Running
| import * as base from './base.js'; | |
| const browser = {}; | |
| browser.Host = class { | |
| constructor() { | |
| this._window = window; | |
| this._navigator = window.navigator; | |
| this._document = window.document; | |
| this._telemetry = new base.Telemetry(this._window); | |
| this._window.eval = () => { | |
| throw new Error('window.eval() not supported.'); | |
| }; | |
| this._meta = {}; | |
| for (const element of Array.from(this._document.getElementsByTagName('meta'))) { | |
| if (element.name !== undefined && element.name !== '' && element.content !== undefined) { | |
| this._meta[element.name] = this._meta[element.name] || []; | |
| this._meta[element.name].push(element.content); | |
| } | |
| } | |
| this._environment = { | |
| name: this._document.title, | |
| type: this._meta.type ? this._meta.type[0] : 'Browser', | |
| version: this._meta.version ? this._meta.version[0] : null, | |
| date: Array.isArray(this._meta.date) && this._meta.date.length > 0 && this._meta.date[0] ? new Date(`${this._meta.date[0].split(' ').join('T')}Z`) : new Date(), | |
| packaged: this._meta.version && this._meta.version[0] !== '0.0.0', | |
| platform: /(Mac|iPhone|iPod|iPad)/i.test(this._navigator.platform) ? 'darwin' : undefined, | |
| agent: this._navigator.userAgent.toLowerCase().indexOf('safari') !== -1 && this._navigator.userAgent.toLowerCase().indexOf('chrome') === -1 ? 'safari' : '', | |
| repository: this._element('logo-github').getAttribute('href'), | |
| menu: true | |
| }; | |
| if (this.version && !/^\d+\.\d+\.\d+$/.test(this.version)) { | |
| throw new Error('Invalid version.'); | |
| } | |
| } | |
| get window() { | |
| return this._window; | |
| } | |
| get document() { | |
| return this._document; | |
| } | |
| get version() { | |
| return this._environment.version; | |
| } | |
| get type() { | |
| return this._environment.type; | |
| } | |
| async view(view) { | |
| const window = this.window; | |
| const document = this.document; | |
| this._view = view; | |
| const age = async () => { | |
| const days = (new Date() - new Date(this._environment.date)) / (24 * 60 * 60 * 1000); | |
| if (days > 180) { | |
| const link = this._element('logo-github').href; | |
| document.body.classList.remove('spinner'); | |
| for (;;) { | |
| /* eslint-disable no-await-in-loop */ | |
| await this.message('Please update to the newest version.', null, 'Update'); | |
| /* eslint-enable no-await-in-loop */ | |
| this.openURL(link); | |
| } | |
| } | |
| return Promise.resolve(); | |
| }; | |
| const consent = async () => { | |
| if (this._getCookie('consent') || this._getCookie('_ga')) { | |
| return; | |
| } | |
| let consent = true; | |
| try { | |
| const text = await this._request('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 'utf-8', null, 2000); | |
| const json = JSON.parse(text); | |
| const countries = ['AT', 'BE', 'BG', 'HR', 'CZ', 'CY', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'SK', 'ES', 'SE', 'GB', 'UK', 'GR', 'EU', 'RO']; | |
| if (json && json.country && countries.indexOf(json.country) === -1) { | |
| consent = false; | |
| } | |
| } catch { | |
| // continue regardless of error | |
| } | |
| if (consent) { | |
| document.body.classList.remove('spinner'); | |
| await this.message('This app uses cookies to report errors and anonymous usage information.', null, 'Accept'); | |
| } | |
| this._setCookie('consent', Date.now().toString(), 30); | |
| }; | |
| const telemetry = async () => { | |
| if (this._environment.packaged) { | |
| window.addEventListener('error', (event) => { | |
| if (event instanceof window.ErrorEvent && event.error && event.error instanceof Error) { | |
| this.exception(event.error, true); | |
| } else { | |
| const message = event && event.message ? event.message : JSON.stringify(event); | |
| const error = new Error(message); | |
| this.exception(error, true); | |
| } | |
| }); | |
| const measurement_id = '848W2NVWVH'; | |
| const user = this._getCookie('_ga').replace(/^(GA1\.\d\.)*/, ''); | |
| const session = this._getCookie(`_ga${measurement_id}`); | |
| await this._telemetry.start(`G-${measurement_id}`, user, session); | |
| this._telemetry.set('page_location', document.location && document.location.href ? document.location.href : null); | |
| this._telemetry.set('page_title', document.title ? document.title : null); | |
| this._telemetry.set('page_referrer', document.referrer ? document.referrer : null); | |
| this._telemetry.send('page_view', { | |
| app_name: this.type, | |
| app_version: this.version, | |
| }); | |
| this._telemetry.send('scroll', { | |
| percent_scrolled: 90, | |
| app_name: this.type, | |
| app_version: this.version | |
| }); | |
| this._setCookie('_ga', `GA1.2.${this._telemetry.get('client_id')}`, 1200); | |
| this._setCookie(`_ga${measurement_id}`, `GS1.1.${this._telemetry.session}`, 1200); | |
| } | |
| }; | |
| const capabilities = async () => { | |
| const filter = (list) => { | |
| return list.filter((capability) => { | |
| const path = capability.split('.').reverse(); | |
| let obj = window[path.pop()]; | |
| while (obj && path.length > 0) { | |
| obj = obj[path.pop()]; | |
| } | |
| return obj; | |
| }); | |
| }; | |
| const capabilities = filter(['fetch', 'DataView.prototype.getBigInt64', 'Worker', 'Array.prototype.flat']); | |
| this.event('browser_open', { | |
| browser_capabilities: capabilities.map((capability) => capability.split('.').pop()).join(',') | |
| }); | |
| return Promise.resolve(); | |
| }; | |
| await age(); | |
| await consent(); | |
| await telemetry(); | |
| await capabilities(); | |
| } | |
| async start() { | |
| if (this._meta.file) { | |
| const [url] = this._meta.file; | |
| if (this._view.accept(url)) { | |
| const identifier = Array.isArray(this._meta.identifier) && this._meta.identifier.length === 1 ? this._meta.identifier[0] : null; | |
| const name = this._meta.name || null; | |
| const status = await this._openModel(this._url(url), identifier || null, name); | |
| if (status === '') { | |
| return; | |
| } | |
| } | |
| } | |
| const window = this.window; | |
| const document = this.document; | |
| const search = window.location.search; | |
| const params = new Map(search ? new window.URLSearchParams(window.location.search) : []); | |
| const hash = window.location.hash ? window.location.hash.replace(/^#/, '') : ''; | |
| const url = hash ? hash : params.get('url'); | |
| if (url) { | |
| const identifier = params.get('identifier') || null; | |
| const location = url | |
| .replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/blob\/([\w/\-_.]*)(\?raw=true)?$/, 'https://raw.githubusercontent.com/$1/$2') | |
| .replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/raw\/([\w/\-_.]*)$/, 'https://raw.githubusercontent.com/$1/$2') | |
| .replace(/^https:\/\/huggingface.co\/(.*)\/blob\/(.*)$/, 'https://huggingface.co/$1/resolve/$2'); | |
| if (this._view.accept(identifier || location) && location.indexOf('*') === -1) { | |
| const status = await this._openModel(location, identifier); | |
| if (status === '') { | |
| return; | |
| } | |
| } | |
| } | |
| const gist = params.get('gist'); | |
| if (gist) { | |
| this._openGist(gist); | |
| return; | |
| } | |
| const openFileButton = this._element('open-file-button'); | |
| const openFileDialog = this._element('open-file-dialog'); | |
| if (openFileButton && openFileDialog) { | |
| openFileButton.addEventListener('click', () => { | |
| this.execute('open'); | |
| }); | |
| const mobileSafari = this.environment('platform') === 'darwin' && window.navigator.maxTouchPoints && window.navigator.maxTouchPoints > 1; | |
| if (!mobileSafari) { | |
| const extensions = new base.Metadata().extensions.map((extension) => `.${extension}`); | |
| openFileDialog.setAttribute('accept', extensions.join(', ')); | |
| } | |
| openFileDialog.addEventListener('change', (e) => { | |
| if (e.target && e.target.files && e.target.files.length > 0) { | |
| const files = Array.from(e.target.files); | |
| const file = files.find((file) => this._view.accept(file.name, file.size)); | |
| if (file) { | |
| this._open(file, files); | |
| } | |
| } | |
| }); | |
| } | |
| document.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| }); | |
| document.body.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) { | |
| const files = Array.from(e.dataTransfer.files); | |
| const file = files.find((file) => this._view.accept(file.name, file.size)); | |
| if (file) { | |
| this._open(file, files); | |
| } | |
| } | |
| }); | |
| this._view.show('welcome'); | |
| } | |
| environment(name) { | |
| return this._environment[name]; | |
| } | |
| async require(id) { | |
| return import(`${id}.js`); | |
| } | |
| worker(id) { | |
| const window = this.window; | |
| return new window.Worker(`${id}.js`, { type: 'module' }); | |
| } | |
| async save(name, extension, defaultPath) { | |
| return `${defaultPath}.${extension}`; | |
| } | |
| async export(file, blob) { | |
| const window = this.window; | |
| const document = this.document; | |
| const element = document.createElement('a'); | |
| element.download = file; | |
| const url = window.URL.createObjectURL(blob); | |
| element.href = url; | |
| document.body.appendChild(element); | |
| element.click(); | |
| document.body.removeChild(element); | |
| window.URL.revokeObjectURL(url); | |
| } | |
| async execute(name /*, value */) { | |
| switch (name) { | |
| case 'open': { | |
| const openFileDialog = this._element('open-file-dialog'); | |
| if (openFileDialog) { | |
| openFileDialog.value = ''; | |
| openFileDialog.click(); | |
| } | |
| break; | |
| } | |
| case 'report-issue': { | |
| this.openURL(`${this.environment('repository')}/issues/new`); | |
| break; | |
| } | |
| case 'about': { | |
| this._view.about(); | |
| break; | |
| } | |
| default: { | |
| break; | |
| } | |
| } | |
| } | |
| async request(file, encoding, base) { | |
| const url = base ? `${base}/${file}` : this._url(file); | |
| if (base === null) { | |
| this._requests = this._requests || new Map(); | |
| const key = `${url}:${encoding}`; | |
| if (!this._requests.has(key)) { | |
| const promise = this._request(url, null, encoding); | |
| this._requests.set(key, promise); | |
| } | |
| return this._requests.get(key); | |
| } | |
| return this._request(url, null, encoding); | |
| } | |
| openURL(url) { | |
| const window = this.window; | |
| window.location = url; | |
| } | |
| exception(error, fatal) { | |
| if (this._telemetry && error) { | |
| const name = error.name ? `${error.name}: ` : ''; | |
| const message = error.message ? error.message : JSON.stringify(error); | |
| let context = ''; | |
| let stack = ''; | |
| if (error.stack) { | |
| const format = (file, line, column) => { | |
| return `${file.split('\\').join('/').split('/').pop()}:${line}:${column}`; | |
| }; | |
| const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/); | |
| if (match) { | |
| stack = `${match[1]} (${format(match[2], match[3], match[4])})`; | |
| } else { | |
| const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/); | |
| if (match) { | |
| stack = `(${format(match[1], match[2], match[3])})`; | |
| } else { | |
| const match = error.stack.match(/\n {4}at (.*)\((.*)\)/); | |
| if (match) { | |
| stack = `(${format(match[1], match[2], match[3])})`; | |
| } else { | |
| const match = error.stack.match(/\s*@\s*(.*):(.*):(.*)/); | |
| if (match) { | |
| stack = `(${format(match[1], match[2], match[3])})`; | |
| } else { | |
| const match = error.stack.match(/.*\n\s*(.*)\s*/); | |
| if (match) { | |
| [, stack] = match; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (error.context) { | |
| context = typeof error.context === 'string' ? error.context : JSON.stringify(error.context); | |
| } | |
| this._telemetry.send('exception', { | |
| app_name: this.type, | |
| app_version: this.version, | |
| error_name: name, | |
| error_message: message, | |
| error_context: context, | |
| error_stack: stack, | |
| error_fatal: fatal ? true : false | |
| }); | |
| } | |
| } | |
| event(name, params) { | |
| if (name && params) { | |
| params.app_name = this.type; | |
| params.app_version = this.version; | |
| this._telemetry.send(name, params); | |
| } | |
| } | |
| async _request(url, headers, encoding, callback, timeout) { | |
| const window = this.window; | |
| if (!url.startsWith('data:')) { | |
| const date = new Date().getTime(); | |
| const separator = (/\?/).test(url) ? '&' : '?'; | |
| url = `${url}${separator}cb=${date}`; | |
| } | |
| return new Promise((resolve, reject) => { | |
| const request = new window.XMLHttpRequest(); | |
| if (!encoding) { | |
| request.responseType = 'arraybuffer'; | |
| } | |
| if (timeout) { | |
| request.timeout = timeout; | |
| } | |
| const progress = (value) => { | |
| if (callback) { | |
| callback(value); | |
| } | |
| }; | |
| request.onload = () => { | |
| progress(0); | |
| if (request.status === 200) { | |
| let value = null; | |
| if (request.responseType === 'arraybuffer') { | |
| const buffer = new Uint8Array(request.response); | |
| value = new base.BinaryStream(buffer); | |
| } else { | |
| value = request.responseText; | |
| } | |
| resolve(value); | |
| } else { | |
| const error = new Error(`The web request failed with status code '${request.status}'.`); | |
| error.context = url; | |
| reject(error); | |
| } | |
| }; | |
| request.onerror = () => { | |
| progress(0); | |
| const error = new Error(`The web request failed.`); | |
| error.context = url; | |
| reject(error); | |
| }; | |
| request.ontimeout = () => { | |
| progress(0); | |
| request.abort(); | |
| const error = new Error('The web request timed out.', 'timeout', url); | |
| error.context = url; | |
| reject(error); | |
| }; | |
| request.onprogress = (e) => { | |
| if (e && e.lengthComputable) { | |
| progress(e.loaded / e.total * 100); | |
| } | |
| }; | |
| request.open('GET', url, true); | |
| if (headers) { | |
| for (const [name, value] of Object.entries(headers)) { | |
| request.setRequestHeader(name, value); | |
| } | |
| } | |
| request.send(); | |
| }); | |
| } | |
| _url(file) { | |
| if (file.startsWith('./')) { | |
| file = file.substring(2); | |
| } else if (file.startsWith('/')) { | |
| file = file.substring(1); | |
| } | |
| const window = this.window; | |
| const location = window.location; | |
| const pathname = location.pathname.endsWith('/') ? location.pathname : `${location.pathname.split('/').slice(0, -1).join('/')}/`; | |
| return `${location.protocol}//${location.host}${pathname}${file}`; | |
| } | |
| async _openModel(url, identifier, name) { | |
| this._view.show('welcome spinner'); | |
| let context = null; | |
| try { | |
| const progress = (value) => { | |
| this._view.progress(value); | |
| }; | |
| let stream = await this._request(url, null, null, progress); | |
| if (url.startsWith('https://raw.githubusercontent.com/') && stream.length < 150) { | |
| const buffer = stream.peek(); | |
| const content = Array.from(buffer).map((c) => String.fromCodePoint(c)).join(''); | |
| if (content.split('\n')[0] === 'version https://git-lfs.github.com/spec/v1') { | |
| url = url.replace('https://raw.githubusercontent.com/', 'https://media.githubusercontent.com/media/'); | |
| stream = await this._request(url, null, null, progress); | |
| } | |
| } | |
| context = new browser.Context(this, url, identifier, name, stream); | |
| this._telemetry.set('session_engaged', 1); | |
| } catch (error) { | |
| await this._view.error(error, 'Model load request failed.'); | |
| this._view.show('welcome'); | |
| return null; | |
| } | |
| return await this._openContext(context); | |
| } | |
| async _open(file, files) { | |
| this._view.show('welcome spinner'); | |
| const context = new browser.BrowserFileContext(this, file, files); | |
| try { | |
| await context.open(); | |
| await this._openContext(context); | |
| } catch (error) { | |
| await this._view.error(error); | |
| } | |
| } | |
| async _openGist(gist) { | |
| this._view.show('welcome spinner'); | |
| const url = `https://api.github.com/gists/${gist}`; | |
| try { | |
| const text = await this._request(url, { 'Content-Type': 'application/json' }, 'utf-8'); | |
| const json = JSON.parse(text); | |
| let message = json.message; | |
| let file = null; | |
| if (!message) { | |
| file = Object.values(json.files).find((file) => this._view.accept(file.filename)); | |
| if (!file) { | |
| message = 'Gist does not contain a model file.'; | |
| } | |
| } | |
| if (message) { | |
| const error = new Error(message); | |
| error.name = 'Error while loading Gist.'; | |
| throw error; | |
| } | |
| const identifier = file.filename; | |
| const encoder = new TextEncoder(); | |
| const buffer = encoder.encode(file.content); | |
| const stream = new base.BinaryStream(buffer); | |
| const context = new browser.Context(this, '', identifier, null, stream); | |
| await this._openContext(context); | |
| } catch (error) { | |
| await this._view.error(error, 'Error while loading Gist.'); | |
| this._view.show('welcome'); | |
| } | |
| } | |
| async _openContext(context) { | |
| const document = this.document; | |
| this._telemetry.set('session_engaged', 1); | |
| try { | |
| const attachment = await this._view.attach(context); | |
| if (attachment) { | |
| this._view.show(null); | |
| return 'context-open-attachment'; | |
| } | |
| const model = await this._view.open(context); | |
| if (model) { | |
| this._view.show(null); | |
| document.title = context.name || context.identifier; | |
| return ''; | |
| } | |
| document.title = ''; | |
| return 'context-open-failed'; | |
| } catch (error) { | |
| await this._view.error(error, error.name); | |
| return 'context-open-error'; | |
| } | |
| } | |
| _setCookie(name, value, days) { | |
| const window = this.window; | |
| const document = this.document; | |
| document.cookie = `${name}=; Max-Age=0`; | |
| const location = window.location; | |
| const domain = location && location.hostname && location.hostname.indexOf('.') !== -1 ? `;domain=.${location.hostname.split('.').slice(-2).join('.')}` : ''; | |
| const date = new Date(); | |
| date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); | |
| document.cookie = `${name}=${value}${domain};path=/;expires=${date.toUTCString()}`; | |
| } | |
| _getCookie(name) { | |
| const document = this.document; | |
| for (const cookie of document.cookie.split(';')) { | |
| const entry = cookie.split('='); | |
| if (entry[0].trim() === name) { | |
| return entry[1].trim(); | |
| } | |
| } | |
| return ''; | |
| } | |
| get(name) { | |
| const window = this.window; | |
| try { | |
| if (typeof window.localStorage !== 'undefined') { | |
| const content = window.localStorage.getItem(name); | |
| return JSON.parse(content); | |
| } | |
| } catch { | |
| // continue regardless of error | |
| } | |
| return undefined; | |
| } | |
| set(name, value) { | |
| const window = this.window; | |
| try { | |
| if (typeof window.localStorage !== 'undefined') { | |
| window.localStorage.setItem(name, JSON.stringify(value)); | |
| } | |
| } catch { | |
| // continue regardless of error | |
| } | |
| } | |
| delete(name) { | |
| const window = this.window; | |
| try { | |
| if (typeof window.localStorage !== 'undefined') { | |
| window.localStorage.removeItem(name); | |
| } | |
| } catch { | |
| // continue regardless of error | |
| } | |
| } | |
| _element(id) { | |
| const document = this.document; | |
| return document.getElementById(id); | |
| } | |
| update() { | |
| } | |
| async message(message, alert, action) { | |
| return new Promise((resolve) => { | |
| const document = this.document; | |
| const type = document.body.getAttribute('class'); | |
| this._element('message-text').innerText = message || ''; | |
| const button = this._element('message-button'); | |
| if (action) { | |
| button.style.removeProperty('display'); | |
| button.innerText = action; | |
| button.onclick = () => { | |
| button.onclick = null; | |
| document.body.setAttribute('class', type); | |
| resolve(0); | |
| }; | |
| } else { | |
| button.style.display = 'none'; | |
| button.onclick = null; | |
| } | |
| if (alert) { | |
| document.body.setAttribute('class', 'alert'); | |
| } else { | |
| document.body.classList.add('notification'); | |
| document.body.classList.remove('default'); | |
| } | |
| if (action) { | |
| button.focus(); | |
| } | |
| }); | |
| } | |
| }; | |
| browser.BrowserFileContext = class { | |
| constructor(host, file, blobs) { | |
| this._host = host; | |
| this._file = file; | |
| this._blobs = {}; | |
| for (const blob of blobs) { | |
| this._blobs[blob.name] = blob; | |
| } | |
| } | |
| get identifier() { | |
| return this._file.name; | |
| } | |
| get stream() { | |
| return this._stream; | |
| } | |
| async request(file, encoding, basename) { | |
| if (basename !== undefined) { | |
| return this._host.request(file, encoding, basename); | |
| } | |
| const blob = this._blobs[file]; | |
| if (!blob) { | |
| throw new Error(`File not found '${file}'.`); | |
| } | |
| return new Promise((resolve, reject) => { | |
| const window = this._host.window; | |
| const reader = new window.FileReader(); | |
| const size = 0x10000000; | |
| let position = 0; | |
| const chunks = []; | |
| reader.onload = (e) => { | |
| if (encoding) { | |
| resolve(e.target.result); | |
| } else { | |
| const buffer = new Uint8Array(e.target.result); | |
| if (position === 0 && buffer.length === blob.size) { | |
| const stream = new base.BinaryStream(buffer); | |
| resolve(stream); | |
| } else { | |
| chunks.push(buffer); | |
| position += buffer.length; | |
| if (position < blob.size) { | |
| const slice = blob.slice(position, Math.min(position + size, blob.size)); | |
| reader.readAsArrayBuffer(slice); | |
| } else { | |
| const stream = new browser.FileStream(chunks, size, 0, position); | |
| resolve(stream); | |
| } | |
| } | |
| } | |
| }; | |
| reader.onerror = (event) => { | |
| event = event || this._host.window.event; | |
| let message = ''; | |
| const error = event.target.error; | |
| switch (error.code) { | |
| case error.NOT_FOUND_ERR: | |
| message = `File not found '${file}'.`; | |
| break; | |
| case error.NOT_READABLE_ERR: | |
| message = `File not readable '${file}'.`; | |
| break; | |
| case error.SECURITY_ERR: | |
| message = `File access denied '${file}'.`; | |
| break; | |
| default: | |
| message = error.message ? error.message : `File read '${error.code}' error '${file}'.`; | |
| break; | |
| } | |
| reject(new Error(message)); | |
| }; | |
| if (encoding === 'utf-8') { | |
| reader.readAsText(blob, encoding); | |
| } else { | |
| const slice = blob.slice(position, Math.min(position + size, blob.size)); | |
| reader.readAsArrayBuffer(slice); | |
| } | |
| }); | |
| } | |
| async require(id) { | |
| return this._host.require(id); | |
| } | |
| error(error, fatal) { | |
| this._host.exception(error, fatal); | |
| } | |
| async open() { | |
| this._stream = await this.request(this._file.name, null); | |
| } | |
| }; | |
| browser.FileStream = class { | |
| constructor(chunks, size, start, length) { | |
| this._chunks = chunks; | |
| this._size = size; | |
| this._start = start; | |
| this._length = length; | |
| this._position = 0; | |
| } | |
| get position() { | |
| return this._position; | |
| } | |
| get length() { | |
| return this._length; | |
| } | |
| stream(length) { | |
| const file = new browser.FileStream(this._chunks, this._size, this._start + this._position, length); | |
| this.skip(length); | |
| return file; | |
| } | |
| seek(position) { | |
| this._position = position >= 0 ? position : this._length + position; | |
| } | |
| skip(offset) { | |
| this._position += offset; | |
| if (this._position > this._length) { | |
| throw new Error(`Expected ${this._position - this._length} more bytes. The file might be corrupted. Unexpected end of file.`); | |
| } | |
| } | |
| peek(length) { | |
| length = length === undefined ? this._length - this._position : length; | |
| if (length < 0x10000000) { | |
| const position = this._fill(length); | |
| this._position -= length; | |
| return this._buffer.subarray(position, position + length); | |
| } | |
| const position = this._start + this._position; | |
| if (position % this._size === 0) { | |
| const index = Math.floor(position / this._size); | |
| const chunk = this._chunks[index]; | |
| if (chunk && chunk.length === length) { | |
| return chunk; | |
| } | |
| } | |
| const buffer = new Uint8Array(length); | |
| this._read(buffer, position); | |
| return buffer; | |
| } | |
| read(length) { | |
| length = length === undefined ? this._length - this._position : length; | |
| if (length < 0x10000000) { | |
| const position = this._fill(length); | |
| return this._buffer.slice(position, position + length); | |
| } | |
| const position = this._start + this._position; | |
| this.skip(length); | |
| if (position % this._size === 0) { | |
| const index = Math.floor(position / this._size); | |
| const chunk = this._chunks[index]; | |
| if (chunk && chunk.length === length) { | |
| return chunk; | |
| } | |
| } | |
| const buffer = new Uint8Array(length); | |
| this._read(buffer, position); | |
| return buffer; | |
| } | |
| _fill(length) { | |
| if (this._position + length > this._length) { | |
| throw new Error(`Expected ${this._position + length - this._length} more bytes. The file might be corrupted. Unexpected end of file.`); | |
| } | |
| if (!this._buffer || this._position < this._offset || this._position + length > this._offset + this._buffer.length) { | |
| this._offset = this._start + this._position; | |
| const length = Math.min(0x10000000, this._start + this._length - this._offset); | |
| if (!this._buffer || length !== this._buffer.length) { | |
| this._buffer = new Uint8Array(length); | |
| } | |
| this._read(this._buffer, this._offset); | |
| } | |
| const position = this._start + this._position - this._offset; | |
| this._position += length; | |
| return position; | |
| } | |
| _read(buffer, offset) { | |
| let index = Math.floor(offset / this._size); | |
| offset -= index * this._size; | |
| const chunk = this._chunks[index++]; | |
| let destination = Math.min(chunk.length - offset, buffer.length); | |
| buffer.set(chunk.subarray(offset, offset + destination), 0); | |
| while (destination < buffer.length) { | |
| const chunk = this._chunks[index++]; | |
| const size = Math.min(this._size, buffer.length - destination); | |
| buffer.set(chunk.subarray(0, size), destination); | |
| destination += size; | |
| } | |
| } | |
| }; | |
| browser.Context = class { | |
| constructor(host, url, identifier, name, stream) { | |
| this._host = host; | |
| this._name = name; | |
| this._stream = stream; | |
| const parts = url.split('?')[0].split('/'); | |
| this._identifier = parts.pop(); | |
| this._base = parts.join('/'); | |
| if (identifier) { | |
| this._identifier = identifier; | |
| } | |
| } | |
| get identifier() { | |
| return this._identifier; | |
| } | |
| get name() { | |
| return this._name; | |
| } | |
| get stream() { | |
| return this._stream; | |
| } | |
| async request(file, encoding, base) { | |
| base = base === undefined ? this._base : base; | |
| return this._host.request(file, encoding, base); | |
| } | |
| async require(id) { | |
| return this._host.require(id); | |
| } | |
| error(error, fatal) { | |
| this._host.exception(error, fatal); | |
| } | |
| }; | |
| if (!('scrollBehavior' in window.document.documentElement.style)) { | |
| const __scrollTo__ = window.Element.prototype.scrollTo; | |
| window.Element.prototype.scrollTo = function(...args) { | |
| const [options] = args; | |
| if (options !== undefined) { | |
| if (options === null || typeof options !== 'object' || options.behavior === undefined || options.behavior === 'auto' || options.behavior === 'instant') { | |
| if (__scrollTo__) { | |
| __scrollTo__.apply(this, args); | |
| } | |
| } else { | |
| const now = () => window.performance && window.performance.now ? window.performance.now() : Date.now(); | |
| const ease = (k) => 0.5 * (1 - Math.cos(Math.PI * k)); | |
| const step = (context) => { | |
| const value = ease(Math.min((now() - context.startTime) / 468, 1)); | |
| const x = context.startX + (context.x - context.startX) * value; | |
| const y = context.startY + (context.y - context.startY) * value; | |
| context.element.scrollLeft = x; | |
| context.element.scrollTop = y; | |
| if (x !== context.x || y !== context.y) { | |
| window.requestAnimationFrame(step.bind(window, context)); | |
| } | |
| }; | |
| const context = { | |
| element: this, | |
| x: typeof options.left === 'undefined' ? this.scrollLeft : ~~options.left, | |
| y: typeof options.top === 'undefined' ? this.scrollTop : ~~options.top, | |
| startX: this.scrollLeft, | |
| startY: this.scrollTop, | |
| startTime: now() | |
| }; | |
| step(context); | |
| } | |
| } | |
| }; | |
| } | |
| if (typeof window !== 'undefined' && window.exports) { | |
| window.exports.browser = browser; | |
| } | |
| export const Host = browser.Host; | |