netron / browser.js
shethjenil's picture
Upload 30 files
d0d9416 verified
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;