text: const events = require('events');
  const {EventEmitter} = events;
  const path = require('path');
  const fs = require('fs');
  const url = require('url');
  const http = require('http');
  const https = require('https');
  const ws = require('ws');
  const os = require('os');
  const util = require('util');
  const {URL} = url;
  const {TextEncoder, TextDecoder} = util;
  const {performance} = require('perf_hooks');
  
  const mkdirp = require('mkdirp');
  
  const {_makeWindowWithDocument} = require('./Window.js');
  const {FileReader} = require('./File.js');
  
  const {XMLHttpRequest: XMLHttpRequestBase, FormData} = require('window-xhr');
  
  const fetch = require('window-fetch');
  const {Request, Response, Headers, Blob} = fetch;
  
  const WebSocket = require('ws/lib/websocket');
  
  //const nativeWorker = require('worker-native');
  
  const {LocalStorage} = require('node-localstorage');
  const indexedDB = require('fake-indexeddb');
  const parseXml = require('@rgrove/parse-xml');
  const THREE = require('../lib/three-min.js');
  const {
    MRDisplay,
    VRDisplay,
    FakeVRDisplay,
    VRFrameData,
    VRPose,
    VRStageParameters,
    Gamepad,
    GamepadButton,
    getGamepads,
  } = require('./VR.js');
  
  const {defaultCanvasSize} = require('./constants');
  const GlobalContext = require('./GlobalContext');
  const symbols = require('./symbols');
  const {urls} = require('./urls');
  
  GlobalContext.commands = [];
  
  const contexts = [];
  GlobalContext.contexts = contexts;
  const _windowHandleEquals = (a, b) => a[0] === b[0] && a[1] === b[1];
  
  const windows = [];
  GlobalContext.windows = windows;
  
  
  // Class imports.
  const {_parseDocument, _parseDocumentAst, Document, DocumentFragment, DocumentType, DOMImplementation, initDocument} = require('./Document');
  const {
    Element,
    HTMLElement,
    HTMLBodyElement,
    HTMLAnchorElement,
    HTMLStyleElement,
    HTMLScriptElement,
    HTMLLinkElement,
    HTMLImageElement,
    HTMLAudioElement,
    HTMLVideoElement,
    HTMLSourceElement,
    HTMLIFrameElement,
    SVGElement,
    HTMLCanvasElement,
    HTMLTemplateElement,
    createImageBitmap,
    DOMRect,
    DOMPoint,
    Node,
    NodeList,
    Text,
    Comment,
    HTMLCollection,
  } = require('./DOM');
  const {CustomEvent, DragEvent, ErrorEvent, Event, EventTarget, KeyboardEvent, MessageEvent, MouseEvent, WheelEvent, PromiseRejectionEvent} = require('./Event');
  const {History} = require('./History');
  const {Location} = require('./Location');
  const {XMLHttpRequest} = require('./Network');
  const XR = require('./XR');
  const DevTools = require('./DevTools');
  const utils = require('./utils');
  const {_elementGetter, _elementSetter, _download} = utils;
  
  const btoa = s => Buffer.from(s, 'binary').toString('base64');
  const atob = s => Buffer.from(s, 'base64').toString('binary');
  
  const maxParallelResources = 8;
  class Resource {
    constructor(getCb = (onprogress, cb) => cb(), value = 0.5, total = 1) {
      this.getCb = getCb;
      this.value = value;
      this.total = total;
      
      this.onupdate = null;
    }
  
    setProgress(value) {
      this.value = value;
  
      this.onupdate && this.onupdate();
    }
    
    get() {
      return new Promise((accept, reject) => {
        this.getCb(progress => {
          this.setValue(progress);
        }, err => {
          if (!err) {
            accept();
          } else {
            reject(err);
          }
        });
      });
    }
    
    destroy() {
      this.setProgress(1);
    }
  }
  class Resources extends EventTarget {
    constructor() {
      super();
      
      this.resources = [];
      this.queue = [];
      this.numRunning = 0;
    }
  
    getValue() {
      let value = 0;
      for (let i = 0; i < this.resources.length; i++) {
        value += this.resources[i].value;
      }
      return value;
    }
    getTotal() {
      let total = 0;
      for (let i = 0; i < this.resources.length; i++) {
        total += this.resources[i].total;
      }
      return total;
    }
    getProgress() {
      let value = 0;
      let total = 0;
      for (let i = 0; i < this.resources.length; i++) {
        const resource = this.resources[i];
        value += resource.value;
        total += resource.total;
      }
      return total > 0 ? (value / total) : 1;
    }
  
    addResource(getCb) {
      return new Promise((accept, reject) => {
        const resource = new Resource(getCb);
        resource.onupdate = () => {
          if (resource.value >= resource.total) {
            this.resources.splice(this.resources.indexOf(resource), 1);
            
            resource.onupdate = null;
            
            accept();
          }
  
          const e = new Event('update');
          e.value = this.getValue();
          e.total = this.getTotal();
          e.progress = this.getProgress();
          this.dispatchEvent(e);
        };
        this.resources.push(resource);
        this.queue.push(resource);
        
        this.drain();
      });
    }
    
    drain() {
      if (this.queue.length > 0 && this.numRunning < maxParallelResources) {
        const resource = this.queue.shift();
        resource.get()
          .catch(err => {
            console.warn(err.stack);
          })
          .finally(() => {
            resource.destroy();
            
            this.numRunning--;
            
            this.drain();
          });
        
        this.numRunning++;
      } else {
        const _isDone = () => this.numRunning === 0 && this.queue.length === 0;
        if (_isDone()) {
          process.nextTick(() => { // wait one tick for more resources before emitting drain
            if (_isDone()) {
              this.emit('drain');
            }
          });
        }
      }
    }
  }
  GlobalContext.Resources = Resources;
  
  const _fromAST = (node, window, parentNode, ownerDocument, uppercase) => {
    if (node.nodeName === '#text') {
      const text = new Text(node.value);
      text.parentNode = parentNode;
      text.ownerDocument = ownerDocument;
      return text;
    } else if (node.nodeName === '#comment') {
      const comment = new Comment(node.data);
      comment.parentNode = parentNode;
      comment.ownerDocument = ownerDocument;
      return comment;
    } else {
      let {tagName} = node;
      if (tagName && uppercase) {
        tagName = tagName.toUpperCase();
      }
      let {attrs, value, content, childNodes, sourceCodeLocation} = node;
      const HTMLElementTemplate = window[symbols.htmlTagsSymbol][tagName];
      const location = sourceCodeLocation  ? {
        line: sourceCodeLocation.startLine,
        col: sourceCodeLocation.startCol,
      } : null;
      const element = HTMLElementTemplate ?
        new HTMLElementTemplate(
          attrs,
          value,
          location,
        )
      :
        new HTMLElement(
          tagName,
          attrs,
          value,
          location,
        );
      element.parentNode = parentNode;
      if (!ownerDocument) { // if there is no owner document, it's us
        ownerDocument = element;
        ownerDocument.defaultView = window;
      }
      element.ownerDocument = ownerDocument;
      if (content) {
        element.childNodes = new NodeList(
          content.childNodes.map(childNode =>
            _fromAST(childNode, window, element, ownerDocument, uppercase)
          )
        );
      } else if (childNodes) {
        element.childNodes = new NodeList(
          childNodes.map(childNode =>
            _fromAST(childNode, window, element, ownerDocument, uppercase)
          )
        );
      }
      return element;
    }
  };
  GlobalContext._fromAST = _fromAST;
  
  // To "run" the HTML means to walk it and execute behavior on the elements such as <script src="...">.
  // Each candidate element exposes a method on runSymbol which returns whether to await the element load or not.
  const _runHtml = (element, window) => {
    if (element instanceof HTMLElement) {
      return new Promise((accept, reject) => {
        const {document} = window;
  
        element.traverse(el => {
          const {id} = el;
          if (id) {
            el._emit('attribute', 'id', id);
          }
  
          if (el[symbols.runSymbol]) {
            document[symbols.addRunSymbol](el[symbols.runSymbol].bind(el));
          }
  
          if (/\-/.test(el.tagName)) {
            const constructor = window.customElements.get(el.tagName);
            if (constructor) {
              window.customElements.upgrade(el, constructor);
            }
          }
        });
        if (document[symbols.runningSymbol]) {
          document.once('flush', () => {
            accept();
          });
        } else {
          accept();
        }
      });
    } else {
      return Promise.resolve();
    }
  };
  GlobalContext._runHtml = _runHtml;
  
  const SweetieKitDOM = (s = '', options = {}) => {
    options.url = options.url || 'http://127.0.0.1/';
    options.baseUrl = options.baseUrl || options.url;
    options.dataPath = options.dataPath || __dirname;
    options.args = options.args || {};
    options.replacements = options.replacements || {};
    return _makeWindowWithDocument(s, options);
  };
  
  SweetieKitDOM.load = (src, options = {}) => {
    if (!url.parse(src).protocol) {
      src = 'http://' + src;
    }
    options.args = options.args || {};
    options.replacements = options.replacements || {};
  
    let redirectCount = 0;
    const _fetchTextFollow = src => fetch(src, {
      redirect: 'manual',
    })
      .then(res => {
        if (res.status >= 200 && res.status < 300) {
          return res.text()
            .then(t => {
              if (options.args.download) {
                return _download('GET', src, t, t => Buffer.from(t, 'utf8'), options.args.download);
              } else {
                return Promise.resolve(t);
              }
            })
            .then(htmlString => ({
              src,
              htmlString,
            }));
        } else if (res.status >= 300 && res.status < 400) {
          const l = res.headers.get('Location');
          if (l) {
            if (redirectCount < 10) {
              redirectCount++;
              return _fetchTextFollow(l);
            } else {
              return Promise.reject(new Error('fetch got too many redirects: ' + res.status + ' : ' + src));
            }
          } else {
            return Promise.reject(new Error('fetch got redirect with no location header: ' + res.status + ' : ' + src));
          }
        } else {
          return Promise.reject(new Error('fetch got invalid status code: ' + res.status + ' : ' + src));
        }
      });
    return _fetchTextFollow(src)
      .then(({src, htmlString}) => {
        let baseUrl;
        if (options.baseUrl) {
          baseUrl = options.baseUrl;
        } else {
          baseUrl = utils._getBaseUrl(src);
        }
  
        return SweetieKitDOM(htmlString, Object.assign({
          url: options.url || src,
          baseUrl,
          dataPath: options.dataPath,
          args: options.args,
          replacements: options.replacements,
        }, options));
      });
  };
  SweetieKitDOM.download = (src, dst) => SweetieKitDOM.load(src, {
    args: {
      download: dst,
      headless: true,
    },
  })
    .then(window => new Promise((accept, reject) => {
      window.document.resources.addEventListener('drain', () => {
        accept();
      });
    }));
  // SweetieKitDOM.THREE = THREE;
  SweetieKitDOM.setArgs = newArgs => {
    GlobalContext.args = newArgs;
  };
  SweetieKitDOM.setVersion = newVersion => {
    GlobalContext.version = newVersion;
  };
  module.exports = SweetieKitDOM;
  