browser.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902
  1. import * as base from './base.js';
  2. const browser = {};
  3. browser.Host = class {
  4. constructor() {
  5. this._window = window;
  6. this._navigator = window.navigator;
  7. this._document = window.document;
  8. this._telemetry = new base.Telemetry(this._window);
  9. this._window.eval = () => {
  10. throw new Error('window.eval() not supported.');
  11. };
  12. this._meta = {};
  13. for (const element of Array.from(this._document.getElementsByTagName('meta'))) {
  14. if (element.name !== undefined && element.name !== '' && element.content !== undefined) {
  15. this._meta[element.name] = this._meta[element.name] || [];
  16. this._meta[element.name].push(element.content);
  17. }
  18. }
  19. this._environment = {
  20. name: this._document.title,
  21. type: this._meta.type ? this._meta.type[0] : 'Browser',
  22. version: this._meta.version ? this._meta.version[0] : null,
  23. 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(),
  24. packaged: this._meta.version && this._meta.version[0] !== '0.0.0',
  25. platform: /(Mac|iPhone|iPod|iPad)/i.test(this._navigator.platform) ? 'darwin' : undefined,
  26. agent: this._navigator.userAgent.toLowerCase().indexOf('safari') !== -1 && this._navigator.userAgent.toLowerCase().indexOf('chrome') === -1 ? 'safari' : '',
  27. repository: this._element('logo-github').getAttribute('href'),
  28. menu: true
  29. };
  30. if (this.version && !/^\d+\.\d+\.\d+$/.test(this.version)) {
  31. throw new Error('Invalid version.');
  32. }
  33. }
  34. get window() {
  35. return this._window;
  36. }
  37. get document() {
  38. return this._document;
  39. }
  40. get version() {
  41. return this._environment.version;
  42. }
  43. get type() {
  44. return this._environment.type;
  45. }
  46. async view(view) {
  47. const window = this.window;
  48. const document = this.document;
  49. this._view = view;
  50. const age = async () => {
  51. const days = (new Date() - new Date(this._environment.date)) / (24 * 60 * 60 * 1000);
  52. if (days > 180) {
  53. const link = this._element('logo-github').href;
  54. document.body.classList.remove('spinner');
  55. for (;;) {
  56. /* eslint-disable no-await-in-loop */
  57. await this.message('Please update to the newest version.', null, 'Update');
  58. /* eslint-enable no-await-in-loop */
  59. this.openURL(link);
  60. }
  61. }
  62. return Promise.resolve();
  63. };
  64. const consent = async () => {
  65. if (this._getCookie('consent') || this._getCookie('_ga')) {
  66. return;
  67. }
  68. let consent = true;
  69. try {
  70. const text = await this._request('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 'utf-8', null, 2000);
  71. const json = JSON.parse(text);
  72. 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'];
  73. if (json && json.country && countries.indexOf(json.country) === -1) {
  74. consent = false;
  75. }
  76. } catch {
  77. // continue regardless of error
  78. }
  79. if (consent) {
  80. document.body.classList.remove('spinner');
  81. await this.message('This app uses cookies to report errors and anonymous usage information.', null, 'Accept');
  82. }
  83. this._setCookie('consent', Date.now().toString(), 30);
  84. };
  85. const telemetry = async () => {
  86. if (this._environment.packaged) {
  87. window.addEventListener('error', (event) => {
  88. if (event instanceof window.ErrorEvent && event.error && event.error instanceof Error) {
  89. this.exception(event.error, true);
  90. } else {
  91. const message = event && event.message ? event.message : JSON.stringify(event);
  92. const error = new Error(message);
  93. this.exception(error, true);
  94. }
  95. });
  96. const measurement_id = '848W2NVWVH';
  97. const user = this._getCookie('_ga').replace(/^(GA1\.\d\.)*/, '');
  98. const session = this._getCookie(`_ga${measurement_id}`);
  99. await this._telemetry.start(`G-${measurement_id}`, user, session);
  100. this._telemetry.set('page_location', document.location && document.location.href ? document.location.href : null);
  101. this._telemetry.set('page_title', document.title ? document.title : null);
  102. this._telemetry.set('page_referrer', document.referrer ? document.referrer : null);
  103. this._telemetry.send('page_view', {
  104. app_name: this.type,
  105. app_version: this.version,
  106. });
  107. this._telemetry.send('scroll', {
  108. percent_scrolled: 90,
  109. app_name: this.type,
  110. app_version: this.version
  111. });
  112. this._setCookie('_ga', `GA1.2.${this._telemetry.get('client_id')}`, 1200);
  113. this._setCookie(`_ga${measurement_id}`, `GS1.1.${this._telemetry.session}`, 1200);
  114. }
  115. };
  116. const capabilities = async () => {
  117. const filter = (list) => {
  118. return list.filter((capability) => {
  119. const path = capability.split('.').reverse();
  120. let obj = window[path.pop()];
  121. while (obj && path.length > 0) {
  122. obj = obj[path.pop()];
  123. }
  124. return obj;
  125. });
  126. };
  127. const capabilities = filter(['fetch', 'DataView.prototype.getBigInt64', 'Worker', 'Array.prototype.flat']);
  128. this.event('browser_open', {
  129. browser_capabilities: capabilities.map((capability) => capability.split('.').pop()).join(',')
  130. });
  131. return Promise.resolve();
  132. };
  133. await age();
  134. await consent();
  135. await telemetry();
  136. await capabilities();
  137. }
  138. async start() {
  139. if (this._meta.file) {
  140. const [url] = this._meta.file;
  141. if (this._view.accept(url)) {
  142. const identifier = Array.isArray(this._meta.identifier) && this._meta.identifier.length === 1 ? this._meta.identifier[0] : null;
  143. const name = this._meta.name || null;
  144. const status = await this._openModel(this._url(url), identifier || null, name);
  145. if (status === '') {
  146. return;
  147. }
  148. }
  149. }
  150. const window = this.window;
  151. const document = this.document;
  152. const search = window.location.search;
  153. const params = new Map(search ? new window.URLSearchParams(window.location.search) : []);
  154. const hash = window.location.hash ? window.location.hash.replace(/^#/, '') : '';
  155. const url = hash ? hash : params.get('url');
  156. if (url) {
  157. const identifier = params.get('identifier') || null;
  158. const location = url
  159. .replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/blob\/([\w/\-_.]*)(\?raw=true)?$/, 'https://raw.githubusercontent.com/$1/$2')
  160. .replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/raw\/([\w/\-_.]*)$/, 'https://raw.githubusercontent.com/$1/$2')
  161. .replace(/^https:\/\/huggingface.co\/(.*)\/blob\/(.*)$/, 'https://huggingface.co/$1/resolve/$2');
  162. if (this._view.accept(identifier || location) && location.indexOf('*') === -1) {
  163. const status = await this._openModel(location, identifier);
  164. if (status === '') {
  165. return;
  166. }
  167. }
  168. }
  169. const gist = params.get('gist');
  170. if (gist) {
  171. this._openGist(gist);
  172. return;
  173. }
  174. const openFileButton = this._element('open-file-button');
  175. const openFileDialog = this._element('open-file-dialog');
  176. if (openFileButton && openFileDialog) {
  177. openFileButton.addEventListener('click', () => {
  178. this.execute('open');
  179. });
  180. const mobileSafari = this.environment('platform') === 'darwin' && window.navigator.maxTouchPoints && window.navigator.maxTouchPoints > 1;
  181. if (!mobileSafari) {
  182. const extensions = new base.Metadata().extensions.map((extension) => `.${extension}`);
  183. openFileDialog.setAttribute('accept', extensions.join(', '));
  184. }
  185. openFileDialog.addEventListener('change', (e) => {
  186. if (e.target && e.target.files && e.target.files.length > 0) {
  187. const files = Array.from(e.target.files);
  188. const file = files.find((file) => this._view.accept(file.name, file.size));
  189. if (file) {
  190. this._open(file, files);
  191. }
  192. }
  193. });
  194. }
  195. document.addEventListener('dragover', (e) => {
  196. e.preventDefault();
  197. });
  198. document.addEventListener('drop', (e) => {
  199. e.preventDefault();
  200. });
  201. document.body.addEventListener('drop', (e) => {
  202. e.preventDefault();
  203. if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  204. const files = Array.from(e.dataTransfer.files);
  205. const file = files.find((file) => this._view.accept(file.name, file.size));
  206. if (file) {
  207. this._open(file, files);
  208. }
  209. }
  210. });
  211. this._view.show('welcome');
  212. }
  213. environment(name) {
  214. return this._environment[name];
  215. }
  216. async require(id) {
  217. return import(`${id}.js`);
  218. }
  219. worker(id) {
  220. const window = this.window;
  221. return new window.Worker(`${id}.js`, { type: 'module' });
  222. }
  223. async save(name, extension, defaultPath) {
  224. return `${defaultPath}.${extension}`;
  225. }
  226. async export(file, blob) {
  227. const window = this.window;
  228. const document = this.document;
  229. const element = document.createElement('a');
  230. element.download = file;
  231. const url = window.URL.createObjectURL(blob);
  232. element.href = url;
  233. document.body.appendChild(element);
  234. element.click();
  235. document.body.removeChild(element);
  236. window.URL.revokeObjectURL(url);
  237. }
  238. async execute(name /*, value */) {
  239. switch (name) {
  240. case 'open': {
  241. const openFileDialog = this._element('open-file-dialog');
  242. if (openFileDialog) {
  243. openFileDialog.value = '';
  244. openFileDialog.click();
  245. }
  246. break;
  247. }
  248. case 'report-issue': {
  249. this.openURL(`${this.environment('repository')}/issues/new`);
  250. break;
  251. }
  252. case 'about': {
  253. this._view.about();
  254. break;
  255. }
  256. default: {
  257. break;
  258. }
  259. }
  260. }
  261. async request(file, encoding, base) {
  262. const url = base ? `${base}/${file}` : this._url(file);
  263. if (base === null) {
  264. this._requests = this._requests || new Map();
  265. const key = `${url}:${encoding}`;
  266. if (!this._requests.has(key)) {
  267. const promise = this._request(url, null, encoding);
  268. this._requests.set(key, promise);
  269. }
  270. return this._requests.get(key);
  271. }
  272. return this._request(url, null, encoding);
  273. }
  274. openURL(url) {
  275. const window = this.window;
  276. window.location = url;
  277. }
  278. exception(error, fatal) {
  279. if (this._telemetry && error) {
  280. const name = error.name ? `${error.name}: ` : '';
  281. const message = error.message ? error.message : JSON.stringify(error);
  282. let context = '';
  283. let stack = '';
  284. if (error.stack) {
  285. const format = (file, line, column) => {
  286. return `${file.split('\\').join('/').split('/').pop()}:${line}:${column}`;
  287. };
  288. const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/);
  289. if (match) {
  290. stack = `${match[1]} (${format(match[2], match[3], match[4])})`;
  291. } else {
  292. const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/);
  293. if (match) {
  294. stack = `(${format(match[1], match[2], match[3])})`;
  295. } else {
  296. const match = error.stack.match(/\n {4}at (.*)\((.*)\)/);
  297. if (match) {
  298. stack = `(${format(match[1], match[2], match[3])})`;
  299. } else {
  300. const match = error.stack.match(/\s*@\s*(.*):(.*):(.*)/);
  301. if (match) {
  302. stack = `(${format(match[1], match[2], match[3])})`;
  303. } else {
  304. const match = error.stack.match(/.*\n\s*(.*)\s*/);
  305. if (match) {
  306. [, stack] = match;
  307. }
  308. }
  309. }
  310. }
  311. }
  312. }
  313. if (error.context) {
  314. context = typeof error.context === 'string' ? error.context : JSON.stringify(error.context);
  315. }
  316. this._telemetry.send('exception', {
  317. app_name: this.type,
  318. app_version: this.version,
  319. error_name: name,
  320. error_message: message,
  321. error_context: context,
  322. error_stack: stack,
  323. error_fatal: fatal ? true : false
  324. });
  325. }
  326. }
  327. event(name, params) {
  328. if (name && params) {
  329. params.app_name = this.type;
  330. params.app_version = this.version;
  331. this._telemetry.send(name, params);
  332. }
  333. }
  334. async _request(url, headers, encoding, callback, timeout) {
  335. const window = this.window;
  336. if (!url.startsWith('data:')) {
  337. const date = new Date().getTime();
  338. const separator = (/\?/).test(url) ? '&' : '?';
  339. url = `${url}${separator}cb=${date}`;
  340. }
  341. return new Promise((resolve, reject) => {
  342. const request = new window.XMLHttpRequest();
  343. if (!encoding) {
  344. request.responseType = 'arraybuffer';
  345. }
  346. if (timeout) {
  347. request.timeout = timeout;
  348. }
  349. const progress = (value) => {
  350. if (callback) {
  351. callback(value);
  352. }
  353. };
  354. request.onload = () => {
  355. progress(0);
  356. if (request.status === 200) {
  357. let value = null;
  358. if (request.responseType === 'arraybuffer') {
  359. const buffer = new Uint8Array(request.response);
  360. value = new base.BinaryStream(buffer);
  361. } else {
  362. value = request.responseText;
  363. }
  364. resolve(value);
  365. } else {
  366. const error = new Error(`The web request failed with status code '${request.status}'.`);
  367. error.context = url;
  368. reject(error);
  369. }
  370. };
  371. request.onerror = () => {
  372. progress(0);
  373. const error = new Error(`The web request failed.`);
  374. error.context = url;
  375. reject(error);
  376. };
  377. request.ontimeout = () => {
  378. progress(0);
  379. request.abort();
  380. const error = new Error('The web request timed out.', 'timeout', url);
  381. error.context = url;
  382. reject(error);
  383. };
  384. request.onprogress = (e) => {
  385. if (e && e.lengthComputable) {
  386. progress(e.loaded / e.total * 100);
  387. }
  388. };
  389. request.open('GET', url, true);
  390. if (headers) {
  391. for (const [name, value] of Object.entries(headers)) {
  392. request.setRequestHeader(name, value);
  393. }
  394. }
  395. request.send();
  396. });
  397. }
  398. _url(file) {
  399. if (file.startsWith('./')) {
  400. file = file.substring(2);
  401. } else if (file.startsWith('/')) {
  402. file = file.substring(1);
  403. }
  404. const window = this.window;
  405. const location = window.location;
  406. const pathname = location.pathname.endsWith('/') ? location.pathname : `${location.pathname.split('/').slice(0, -1).join('/')}/`;
  407. return `${location.protocol}//${location.host}${pathname}${file}`;
  408. }
  409. async _openModel(url, identifier, name) {
  410. this._view.show('welcome spinner');
  411. let context = null;
  412. try {
  413. const progress = (value) => {
  414. this._view.progress(value);
  415. };
  416. let stream = await this._request(url, null, null, progress);
  417. if (url.startsWith('https://raw.githubusercontent.com/') && stream.length < 150) {
  418. const buffer = stream.peek();
  419. const content = Array.from(buffer).map((c) => String.fromCodePoint(c)).join('');
  420. if (content.split('\n')[0] === 'version https://git-lfs.github.com/spec/v1') {
  421. url = url.replace('https://raw.githubusercontent.com/', 'https://media.githubusercontent.com/media/');
  422. stream = await this._request(url, null, null, progress);
  423. }
  424. }
  425. context = new browser.Context(this, url, identifier, name, stream);
  426. this._telemetry.set('session_engaged', 1);
  427. } catch (error) {
  428. await this._view.error(error, 'Model load request failed.');
  429. this._view.show('welcome');
  430. return null;
  431. }
  432. return await this._openContext(context);
  433. }
  434. async _open(file, files) {
  435. this._view.show('welcome spinner');
  436. const context = new browser.BrowserFileContext(this, file, files);
  437. try {
  438. await context.open();
  439. await this._openContext(context);
  440. } catch (error) {
  441. await this._view.error(error);
  442. }
  443. }
  444. async _openGist(gist) {
  445. this._view.show('welcome spinner');
  446. const url = `https://api.github.com/gists/${gist}`;
  447. try {
  448. const text = await this._request(url, { 'Content-Type': 'application/json' }, 'utf-8');
  449. const json = JSON.parse(text);
  450. let message = json.message;
  451. let file = null;
  452. if (!message) {
  453. file = Object.values(json.files).find((file) => this._view.accept(file.filename));
  454. if (!file) {
  455. message = 'Gist does not contain a model file.';
  456. }
  457. }
  458. if (message) {
  459. const error = new Error(message);
  460. error.name = 'Error while loading Gist.';
  461. throw error;
  462. }
  463. const identifier = file.filename;
  464. const encoder = new TextEncoder();
  465. const buffer = encoder.encode(file.content);
  466. const stream = new base.BinaryStream(buffer);
  467. const context = new browser.Context(this, '', identifier, null, stream);
  468. await this._openContext(context);
  469. } catch (error) {
  470. await this._view.error(error, 'Error while loading Gist.');
  471. this._view.show('welcome');
  472. }
  473. }
  474. async _openContext(context) {
  475. const document = this.document;
  476. this._telemetry.set('session_engaged', 1);
  477. try {
  478. const attachment = await this._view.attach(context);
  479. if (attachment) {
  480. this._view.show(null);
  481. return 'context-open-attachment';
  482. }
  483. const model = await this._view.open(context);
  484. if (model) {
  485. this._view.show(null);
  486. document.title = context.name || context.identifier;
  487. return '';
  488. }
  489. document.title = '';
  490. return 'context-open-failed';
  491. } catch (error) {
  492. await this._view.error(error, error.name);
  493. return 'context-open-error';
  494. }
  495. }
  496. _setCookie(name, value, days) {
  497. const window = this.window;
  498. const document = this.document;
  499. document.cookie = `${name}=; Max-Age=0`;
  500. const location = window.location;
  501. const domain = location && location.hostname && location.hostname.indexOf('.') !== -1 ? `;domain=.${location.hostname.split('.').slice(-2).join('.')}` : '';
  502. const date = new Date();
  503. date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
  504. document.cookie = `${name}=${value}${domain};path=/;expires=${date.toUTCString()}`;
  505. }
  506. _getCookie(name) {
  507. const document = this.document;
  508. for (const cookie of document.cookie.split(';')) {
  509. const entry = cookie.split('=');
  510. if (entry[0].trim() === name) {
  511. return entry[1].trim();
  512. }
  513. }
  514. return '';
  515. }
  516. get(name) {
  517. const window = this.window;
  518. try {
  519. if (typeof window.localStorage !== 'undefined') {
  520. const content = window.localStorage.getItem(name);
  521. return JSON.parse(content);
  522. }
  523. } catch {
  524. // continue regardless of error
  525. }
  526. return undefined;
  527. }
  528. set(name, value) {
  529. const window = this.window;
  530. try {
  531. if (typeof window.localStorage !== 'undefined') {
  532. window.localStorage.setItem(name, JSON.stringify(value));
  533. }
  534. } catch {
  535. // continue regardless of error
  536. }
  537. }
  538. delete(name) {
  539. const window = this.window;
  540. try {
  541. if (typeof window.localStorage !== 'undefined') {
  542. window.localStorage.removeItem(name);
  543. }
  544. } catch {
  545. // continue regardless of error
  546. }
  547. }
  548. _element(id) {
  549. const document = this.document;
  550. return document.getElementById(id);
  551. }
  552. update() {
  553. }
  554. async message(message, alert, action) {
  555. return new Promise((resolve) => {
  556. const document = this.document;
  557. const type = document.body.getAttribute('class');
  558. this._element('message-text').innerText = message || '';
  559. const button = this._element('message-button');
  560. if (action) {
  561. button.style.removeProperty('display');
  562. button.innerText = action;
  563. button.onclick = () => {
  564. button.onclick = null;
  565. document.body.setAttribute('class', type);
  566. resolve(0);
  567. };
  568. } else {
  569. button.style.display = 'none';
  570. button.onclick = null;
  571. }
  572. if (alert) {
  573. document.body.setAttribute('class', 'alert');
  574. } else {
  575. document.body.classList.add('notification');
  576. document.body.classList.remove('default');
  577. }
  578. if (action) {
  579. button.focus();
  580. }
  581. });
  582. }
  583. };
  584. browser.BrowserFileContext = class {
  585. constructor(host, file, blobs) {
  586. this._host = host;
  587. this._file = file;
  588. this._blobs = {};
  589. for (const blob of blobs) {
  590. this._blobs[blob.name] = blob;
  591. }
  592. }
  593. get identifier() {
  594. return this._file.name;
  595. }
  596. get stream() {
  597. return this._stream;
  598. }
  599. async request(file, encoding, basename) {
  600. if (basename !== undefined) {
  601. return this._host.request(file, encoding, basename);
  602. }
  603. const blob = this._blobs[file];
  604. if (!blob) {
  605. throw new Error(`File not found '${file}'.`);
  606. }
  607. return new Promise((resolve, reject) => {
  608. const window = this._host.window;
  609. const reader = new window.FileReader();
  610. const size = 0x10000000;
  611. let position = 0;
  612. const chunks = [];
  613. reader.onload = (e) => {
  614. if (encoding) {
  615. resolve(e.target.result);
  616. } else {
  617. const buffer = new Uint8Array(e.target.result);
  618. if (position === 0 && buffer.length === blob.size) {
  619. const stream = new base.BinaryStream(buffer);
  620. resolve(stream);
  621. } else {
  622. chunks.push(buffer);
  623. position += buffer.length;
  624. if (position < blob.size) {
  625. const slice = blob.slice(position, Math.min(position + size, blob.size));
  626. reader.readAsArrayBuffer(slice);
  627. } else {
  628. const stream = new browser.FileStream(chunks, size, 0, position);
  629. resolve(stream);
  630. }
  631. }
  632. }
  633. };
  634. reader.onerror = (event) => {
  635. event = event || this._host.window.event;
  636. let message = '';
  637. const error = event.target.error;
  638. switch (error.code) {
  639. case error.NOT_FOUND_ERR:
  640. message = `File not found '${file}'.`;
  641. break;
  642. case error.NOT_READABLE_ERR:
  643. message = `File not readable '${file}'.`;
  644. break;
  645. case error.SECURITY_ERR:
  646. message = `File access denied '${file}'.`;
  647. break;
  648. default:
  649. message = error.message ? error.message : `File read '${error.code}' error '${file}'.`;
  650. break;
  651. }
  652. reject(new Error(message));
  653. };
  654. if (encoding === 'utf-8') {
  655. reader.readAsText(blob, encoding);
  656. } else {
  657. const slice = blob.slice(position, Math.min(position + size, blob.size));
  658. reader.readAsArrayBuffer(slice);
  659. }
  660. });
  661. }
  662. async require(id) {
  663. return this._host.require(id);
  664. }
  665. error(error, fatal) {
  666. this._host.exception(error, fatal);
  667. }
  668. async open() {
  669. this._stream = await this.request(this._file.name, null);
  670. }
  671. };
  672. browser.FileStream = class {
  673. constructor(chunks, size, start, length) {
  674. this._chunks = chunks;
  675. this._size = size;
  676. this._start = start;
  677. this._length = length;
  678. this._position = 0;
  679. }
  680. get position() {
  681. return this._position;
  682. }
  683. get length() {
  684. return this._length;
  685. }
  686. stream(length) {
  687. const file = new browser.FileStream(this._chunks, this._size, this._start + this._position, length);
  688. this.skip(length);
  689. return file;
  690. }
  691. seek(position) {
  692. this._position = position >= 0 ? position : this._length + position;
  693. }
  694. skip(offset) {
  695. this._position += offset;
  696. if (this._position > this._length) {
  697. throw new Error(`Expected ${this._position - this._length} more bytes. The file might be corrupted. Unexpected end of file.`);
  698. }
  699. }
  700. peek(length) {
  701. length = length === undefined ? this._length - this._position : length;
  702. if (length < 0x10000000) {
  703. const position = this._fill(length);
  704. this._position -= length;
  705. return this._buffer.subarray(position, position + length);
  706. }
  707. const position = this._start + this._position;
  708. if (position % this._size === 0) {
  709. const index = Math.floor(position / this._size);
  710. const chunk = this._chunks[index];
  711. if (chunk && chunk.length === length) {
  712. return chunk;
  713. }
  714. }
  715. const buffer = new Uint8Array(length);
  716. this._read(buffer, position);
  717. return buffer;
  718. }
  719. read(length) {
  720. length = length === undefined ? this._length - this._position : length;
  721. if (length < 0x10000000) {
  722. const position = this._fill(length);
  723. return this._buffer.slice(position, position + length);
  724. }
  725. const position = this._start + this._position;
  726. this.skip(length);
  727. if (position % this._size === 0) {
  728. const index = Math.floor(position / this._size);
  729. const chunk = this._chunks[index];
  730. if (chunk && chunk.length === length) {
  731. return chunk;
  732. }
  733. }
  734. const buffer = new Uint8Array(length);
  735. this._read(buffer, position);
  736. return buffer;
  737. }
  738. _fill(length) {
  739. if (this._position + length > this._length) {
  740. throw new Error(`Expected ${this._position + length - this._length} more bytes. The file might be corrupted. Unexpected end of file.`);
  741. }
  742. if (!this._buffer || this._position < this._offset || this._position + length > this._offset + this._buffer.length) {
  743. this._offset = this._start + this._position;
  744. const length = Math.min(0x10000000, this._start + this._length - this._offset);
  745. if (!this._buffer || length !== this._buffer.length) {
  746. this._buffer = new Uint8Array(length);
  747. }
  748. this._read(this._buffer, this._offset);
  749. }
  750. const position = this._start + this._position - this._offset;
  751. this._position += length;
  752. return position;
  753. }
  754. _read(buffer, offset) {
  755. let index = Math.floor(offset / this._size);
  756. offset -= index * this._size;
  757. const chunk = this._chunks[index++];
  758. let destination = Math.min(chunk.length - offset, buffer.length);
  759. buffer.set(chunk.subarray(offset, offset + destination), 0);
  760. while (destination < buffer.length) {
  761. const chunk = this._chunks[index++];
  762. const size = Math.min(this._size, buffer.length - destination);
  763. buffer.set(chunk.subarray(0, size), destination);
  764. destination += size;
  765. }
  766. }
  767. };
  768. browser.Context = class {
  769. constructor(host, url, identifier, name, stream) {
  770. this._host = host;
  771. this._name = name;
  772. this._stream = stream;
  773. const parts = url.split('?')[0].split('/');
  774. this._identifier = parts.pop();
  775. this._base = parts.join('/');
  776. if (identifier) {
  777. this._identifier = identifier;
  778. }
  779. }
  780. get identifier() {
  781. return this._identifier;
  782. }
  783. get name() {
  784. return this._name;
  785. }
  786. get stream() {
  787. return this._stream;
  788. }
  789. async request(file, encoding, base) {
  790. base = base === undefined ? this._base : base;
  791. return this._host.request(file, encoding, base);
  792. }
  793. async require(id) {
  794. return this._host.require(id);
  795. }
  796. error(error, fatal) {
  797. this._host.exception(error, fatal);
  798. }
  799. };
  800. if (!('scrollBehavior' in window.document.documentElement.style)) {
  801. const __scrollTo__ = window.Element.prototype.scrollTo;
  802. window.Element.prototype.scrollTo = function(...args) {
  803. const [options] = args;
  804. if (options !== undefined) {
  805. if (options === null || typeof options !== 'object' || options.behavior === undefined || options.behavior === 'auto' || options.behavior === 'instant') {
  806. if (__scrollTo__) {
  807. __scrollTo__.apply(this, args);
  808. }
  809. } else {
  810. const now = () => window.performance && window.performance.now ? window.performance.now() : Date.now();
  811. const ease = (k) => 0.5 * (1 - Math.cos(Math.PI * k));
  812. const step = (context) => {
  813. const value = ease(Math.min((now() - context.startTime) / 468, 1));
  814. const x = context.startX + (context.x - context.startX) * value;
  815. const y = context.startY + (context.y - context.startY) * value;
  816. context.element.scrollLeft = x;
  817. context.element.scrollTop = y;
  818. if (x !== context.x || y !== context.y) {
  819. window.requestAnimationFrame(step.bind(window, context));
  820. }
  821. };
  822. const context = {
  823. element: this,
  824. x: typeof options.left === 'undefined' ? this.scrollLeft : ~~options.left,
  825. y: typeof options.top === 'undefined' ? this.scrollTop : ~~options.top,
  826. startX: this.scrollLeft,
  827. startY: this.scrollTop,
  828. startTime: now()
  829. };
  830. step(context);
  831. }
  832. }
  833. };
  834. }
  835. if (typeof window !== 'undefined' && window.exports) {
  836. window.exports.browser = browser;
  837. }
  838. export const Host = browser.Host;