<commit_before>/**
 * LINKURIOUS
 * Copyright Linkurious SAS 2012 - 2017
 *
 * - Created by david on 2017-02-09.
 * http://dashflows.com/wp-content/uploads/2014/12/process_mgmt-dilbert.gif
 */
'use strict';

const Promise = require('bluebird');

const Utils = require('./Utils');
const PattyClient = require('./PattyClient');
const PattyServer = require('./PattyServer');
const PattyError = require('./PattyError');
const System = require('./system/System');
const Menu = require('./Menu');
const ServerLogger = require('./log/ServerLogger');
const ClientLogger = require('./log/ClientLogger');

class Patty {

  /**
   * @returns {Promise<Patty>}
   */
  static load() {
    return Utils.resolveHome().then(home => {
      return Utils
        .loadOptions(home)
        .then(options => new Patty(home, options))
        .then(p => p.init());
    });
  }

  /**
   * @returns {Promise<Patty>}
   */
  static loadConfig(configPath) {
    const home = Utils.parseConfigPath(configPath);
    return Utils
      .loadOptions(home)
      .then(options => new Patty(home, options))
      .then(p => p.init());
  }

  /**
   * @param {string} targetConfigPath
   * @param {PattyOptions} options
   * @returns {Promise<Patty>}
   */
  static createConfigFile(targetConfigPath, options) {
    const home = Utils.parseConfigPath(targetConfigPath);
    return Utils.ensureDir(home.dir).then(() => {
      Utils.checkOptions(home, options);
      return Utils.writeFile(targetConfigPath, JSON.stringify(options, null, '  '));
    });
  }

  /**
   * @param {PattyHome} home
   * @param {PattyOptions} options
   */
  constructor(home, options) {
    /** @type {PattyHome} */
    this.home = home;

    /** @type {PattyOptions} */
    this.options = options;

    this._logger = new ClientLogger(home, options);

    /** @type {PattyClient} */
    this.client = new PattyClient(options, this._logger);

    /** @type {System} */
    this.system = System.makePlatformSpecific(home, options, this._logger);

    /** @type {Menu} */
    this.menu = new Menu(home, options, this);

    /**
     * @type {Map<string, {date: Date, promise: Promise, value: *}>}
     * @private
     */
    this._cache = new Map();
  }

  /**
   * @returns {Promise}
   */
  init() {
    return this.system.init().then(() => {
      // check that the server-logger works
      const logger = new ServerLogger(this.home, this.options);
      return Promise.props({
        s: logger.init(),
        c: this._logger.init()
      }).return(this);
    });
  }

  /**
   * @param {function(MenuItem):boolean|null} [menuFilter=null]
   * @returns {Promise}
   */
  showMenu(menuFilter) {
    return this.menu.show(menuFilter);
  }

  /**
   * @returns {Promise<{
   *   installed: boolean,
   *   started: boolean,
   *   processOwner: string,
   *   services: Array<{name: string, state: ServiceState}>}
   * >}
   */
  getStatus() {
    return Promise.props({
      installed: this.system.isInstalled(),
      started: this.isServerStarted()
    }).then(result => {
      const whenOffline = (result) => {
        result.started = false;
        result.processOwner = this.options.processOwner;
        result.services = this.options.services.map(so => ({
          name: so.name,
          state: {
            started: false,
            disabled: so.disabled,
            restarts: 0
          }
        }));
        return result;
      };

      if (!result.started) {
        return whenOffline(result);
      }

      return this.client.getOptions().then(options => {
        result.processOwner = options.processOwner;
        return this.client.getServicesState();
      }).then(s => {
        result.services = s;
        return result;
      }).catch(e => {
        this._logger.warn(
          `getStatus partially failed (${e.message}), assuming the manager went offline.`);
        return whenOffline(result);
      });
    });
  }

  /**
   * @param {string} processOwner
   * @returns {Promise} will reject if the manager is running with at least one started service.
   */
  setProcessOwner(processOwner) {
    if (this.options.processOwner === processOwner) {
      return Promise.resolve();
    }
    return this.getStatus().then(status => {
      if (!status.started) {
        this.options.processOwner = processOwner;
        return this.saveConfig();
      } else if (status.services.filter(si => si.state.started).length === 0) {
        return this.ensureStopped().then(() =>  {
          this.options.processOwner = processOwner;
          return this.saveConfig();
        }).then(() => {
          return this.ensureStarted();
        });
      } else {
        return PattyError.otherP(
          'Cannot set processOwner while manager is started, please stop manager first.'
        );
      }
    });
  }

  /**
   * @return {number}
   */
  get enabledServiceCount() {
    return this.options.services.filter(so => !so.disabled).length;
  }

  /**
   * @return {string}
   */
  get serviceSystem() {
    return this.system.getServiceSystem();
  }

  /**
   * @param {string} name
   * @return {Promise.<ServiceOptions|PattyError>}
   */
  getServiceOptions(name) {
    return this.isServerStarted().then(started => {
      if (started) {
        return this.client.getService(name).then(s => s.options);
      } else {
        return Utils.clone(this.options.services.find(so => so.name === name));
      }
    });
  }

  /**
   * @param {string} name
   * @param {ServiceOptions} options
   * @returns {Promise}
   */
  setServiceOptions(name, options) {
    options.name = name;
    Utils.check.properties('options', options, Utils.SERVICE_OPTIONS_PROPERTIES(this.home));

    return this.isServerStarted().then(started => {
      if (started) {
        return this.client.updateService(name, options);
      }

      let done = false;
      for (let i = 0, l = this.options.services.length; i < l && !done; ++i) {
        if (this.options.services[i].name === name) {
          this.options.services[i] = options;
          done = true;
        }
      }

      if (!done) {
        return PattyError.businessP(`Service "${name}" was not found`);
      }
      return this.saveConfig();
    });
  }

  /**
   * @returns {Promise}
   */
  saveConfig() {
    const options = Utils.clone(this.options);
    delete options.version;
    return Utils.writeFile(Utils.getConfigPath(this.home), JSON.stringify(options, null, '  '));
  }

  /**
   * @param {number} [retries=1]
   * @param {boolean} [bypassCache=false]
   * @returns {Promise.<boolean|PattyError>}
   */
  isServerStarted(retries, bypassCache) {
    if (retries === undefined) { retries = 1; }
    return this._tryCache('started', bypassCache ? -1 : 2000, () => {
      return this.client.pingServer({noReject: true, retries, timeout: 300, retryDelay: 300});
    });
  }

  /**
   *
   * @param {string} key
   * @param {number} maxAge if < 0, will bypass the cache
   * @param {function(): Promise.<T>} valuePromiseFunction
   * @return {Promise.<T>}
   * @template {T}
   * @private
   */
  _tryCache(key, maxAge, valuePromiseFunction) {
    /** @type {{date: Date, promise: Promise, value: *}} */
    let entry = this._cache.get(key);

    if (maxAge > 0 && entry && (Date.now() - entry.date) <= maxAge) {
      if (entry.promise) {
        return entry.promise;
      } else {
        return Promise.resolve(entry.value);
      }
    } else {
      const p = valuePromiseFunction().then(r => {
        this._cache.set(key, {date: Date.now(), promise: null, value: r});
        return r;
      });
      this._cache.set(key, {date: Date.now(), promise: p});
      return p;
    }
  }

  /**
   * @param {boolean} [fromService=false] Whether this was called from a service script
   * @returns {Promise<boolean>} true if the server was just started
   */
  ensureStarted(fromService) {
    return this.isServerStarted(undefined, true).then(started => {
      // already started, nothing to do
      if (started) { return false; }

      // needs to be started
      return this.system.isInstalled().then(installed => {
        let p;
        if (installed && !fromService) {
          // console.log('(started:no, installed:yes) starting system-service');
          p = this.system.start();
        } else {
          // console.log('(started:no, installed:no) spawning server');
          p = PattyServer.spawn(this.home).catch(err => {
            this._logger.error('Spawn failed', err);
            return Promise.reject(err);
          });
        }

        return p.delay(1500).then(() => {
          return this.client.pingServer({retries: 2, timeout: 1500}).catch(e => {
            return PattyError.otherP(this.options.name + ' could not be started', e);
          });
        }).return(true);
      });
    });
  }

  /**
   * @param {string} m
   * @private
   */
  _debug(m) {
    this._logger.debug(m);
  }

  /**
   * @param {boolean} [fromService=false] Whether this was called from a service script
   * @returns {Promise<boolean>} true if the server was just killed
   */
  ensureStopped(fromService) {
    return this.isServerStarted(2).then(started => {
      // manager is not started, skip
      if (!started) {
        return false;
      }

      // 1) stop all running services
      return this.client.stopServices().then(() => {
        return this.system.isInstalled();
      }).then(installed => {

        // 2.a) if installed as a service, stop system service
        if (installed && !fromService) {
          return this.system.stop();
        }

        // 2.b) if not installed as a service, kill the manager
        return this.client.stopServices().then(() => {
          return this.client.killServer().return(true);
        });
      });
    });
  }

  /**
   * @returns {Promise<boolean>} true is newly installed
   */
  ensureInstalled() {
    return this.system.checkAdmin('install a service').then(() => {
      return this.system.isInstalled();
    }).then(installed => {
      // service already installed (nothing to do)
      if (installed) {
        return Promise.resolve(false);
      }

      // service not installed yet
      return this.isServerStarted().then(started => {
        if (started) {
          // the server is running (stopping it)
          return this.client.killServer();
        }
      }).then(() => {
        return this.system.install().then(() => {
          // start the newly installed service
          return this.system.start().delay(2000);
        });
      }).return(true);
    });
  }

  /**
   * @returns {Promise<boolean>} true if newly uninstalled
   */
  ensureUninstalled() {
    return this.system.checkAdmin('uninstall a service').then(() => {
      return this.system.isInstalled();
    }).then(installed => {
      // not installed (nothing to do)
      if (!installed) {
        return Promise.resolve(false);
      }

      // service is installed (stopping and uninstalling)
      return this.ensureStopped().then(() => {
        return this.system.uninstall();
      }).return(true);
    });
  }

}
module.exports = Patty;
<commit_msg>now logging error details when Patty.getStatus fails<commit_after>/**
 * LINKURIOUS
 * Copyright Linkurious SAS 2012 - 2017
 *
 * - Created by david on 2017-02-09.
 * http://dashflows.com/wp-content/uploads/2014/12/process_mgmt-dilbert.gif
 */
'use strict';

const Promise = require('bluebird');

const Utils = require('./Utils');
const PattyClient = require('./PattyClient');
const PattyServer = require('./PattyServer');
const PattyError = require('./PattyError');
const System = require('./system/System');
const Menu = require('./Menu');
const ServerLogger = require('./log/ServerLogger');
const ClientLogger = require('./log/ClientLogger');

class Patty {

  /**
   * @returns {Promise<Patty>}
   */
  static load() {
    return Utils.resolveHome().then(home => {
      return Utils
        .loadOptions(home)
        .then(options => new Patty(home, options))
        .then(p => p.init());
    });
  }

  /**
   * @returns {Promise<Patty>}
   */
  static loadConfig(configPath) {
    const home = Utils.parseConfigPath(configPath);
    return Utils
      .loadOptions(home)
      .then(options => new Patty(home, options))
      .then(p => p.init());
  }

  /**
   * @param {string} targetConfigPath
   * @param {PattyOptions} options
   * @returns {Promise<Patty>}
   */
  static createConfigFile(targetConfigPath, options) {
    const home = Utils.parseConfigPath(targetConfigPath);
    return Utils.ensureDir(home.dir).then(() => {
      Utils.checkOptions(home, options);
      return Utils.writeFile(targetConfigPath, JSON.stringify(options, null, '  '));
    });
  }

  /**
   * @param {PattyHome} home
   * @param {PattyOptions} options
   */
  constructor(home, options) {
    /** @type {PattyHome} */
    this.home = home;

    /** @type {PattyOptions} */
    this.options = options;

    this._logger = new ClientLogger(home, options);

    /** @type {PattyClient} */
    this.client = new PattyClient(options, this._logger);

    /** @type {System} */
    this.system = System.makePlatformSpecific(home, options, this._logger);

    /** @type {Menu} */
    this.menu = new Menu(home, options, this);

    /**
     * @type {Map<string, {date: Date, promise: Promise, value: *}>}
     * @private
     */
    this._cache = new Map();
  }

  /**
   * @returns {Promise}
   */
  init() {
    return this.system.init().then(() => {
      // check that the server-logger works
      const logger = new ServerLogger(this.home, this.options);
      return Promise.props({
        s: logger.init(),
        c: this._logger.init()
      }).return(this);
    });
  }

  /**
   * @param {function(MenuItem):boolean|null} [menuFilter=null]
   * @returns {Promise}
   */
  showMenu(menuFilter) {
    return this.menu.show(menuFilter);
  }

  /**
   * @returns {Promise<{
   *   installed: boolean,
   *   started: boolean,
   *   processOwner: string,
   *   services: Array<{name: string, state: ServiceState}>}
   * >}
   */
  getStatus() {
    return Promise.props({
      installed: this.system.isInstalled(),
      started: this.isServerStarted()
    }).then(result => {
      const whenOffline = (result) => {
        result.started = false;
        result.processOwner = this.options.processOwner;
        result.services = this.options.services.map(so => ({
          name: so.name,
          state: {
            started: false,
            disabled: so.disabled,
            restarts: 0
          }
        }));
        return result;
      };

      if (!result.started) {
        return whenOffline(result);
      }

      return this.client.getOptions().then(options => {
        result.processOwner = options.processOwner;
        return this.client.getServicesState();
      }).then(s => {
        result.services = s;
        return result;
      }).catch(e => {
        this._logger.error(`getStatus partially failed, assuming the manager went offline.`, e);
        return whenOffline(result);
      });
    });
  }

  /**
   * @param {string} processOwner
   * @returns {Promise} will reject if the manager is running with at least one started service.
   */
  setProcessOwner(processOwner) {
    if (this.options.processOwner === processOwner) {
      return Promise.resolve();
    }
    return this.getStatus().then(status => {
      if (!status.started) {
        this.options.processOwner = processOwner;
        return this.saveConfig();
      } else if (status.services.filter(si => si.state.started).length === 0) {
        return this.ensureStopped().then(() =>  {
          this.options.processOwner = processOwner;
          return this.saveConfig();
        }).then(() => {
          return this.ensureStarted();
        });
      } else {
        return PattyError.otherP(
          'Cannot set processOwner while manager is started, please stop manager first.'
        );
      }
    });
  }

  /**
   * @return {number}
   */
  get enabledServiceCount() {
    return this.options.services.filter(so => !so.disabled).length;
  }

  /**
   * @return {string}
   */
  get serviceSystem() {
    return this.system.getServiceSystem();
  }

  /**
   * @param {string} name
   * @return {Promise.<ServiceOptions|PattyError>}
   */
  getServiceOptions(name) {
    return this.isServerStarted().then(started => {
      if (started) {
        return this.client.getService(name).then(s => s.options);
      } else {
        return Utils.clone(this.options.services.find(so => so.name === name));
      }
    });
  }

  /**
   * @param {string} name
   * @param {ServiceOptions} options
   * @returns {Promise}
   */
  setServiceOptions(name, options) {
    options.name = name;
    Utils.check.properties('options', options, Utils.SERVICE_OPTIONS_PROPERTIES(this.home));

    return this.isServerStarted().then(started => {
      if (started) {
        return this.client.updateService(name, options);
      }

      let done = false;
      for (let i = 0, l = this.options.services.length; i < l && !done; ++i) {
        if (this.options.services[i].name === name) {
          this.options.services[i] = options;
          done = true;
        }
      }

      if (!done) {
        return PattyError.businessP(`Service "${name}" was not found`);
      }
      return this.saveConfig();
    });
  }

  /**
   * @returns {Promise}
   */
  saveConfig() {
    const options = Utils.clone(this.options);
    delete options.version;
    return Utils.writeFile(Utils.getConfigPath(this.home), JSON.stringify(options, null, '  '));
  }

  /**
   * @param {number} [retries=1]
   * @param {boolean} [bypassCache=false]
   * @returns {Promise.<boolean|PattyError>}
   */
  isServerStarted(retries, bypassCache) {
    if (retries === undefined) { retries = 1; }
    return this._tryCache('started', bypassCache ? -1 : 2000, () => {
      return this.client.pingServer({noReject: true, retries, timeout: 300, retryDelay: 300});
    });
  }

  /**
   *
   * @param {string} key
   * @param {number} maxAge if < 0, will bypass the cache
   * @param {function(): Promise.<T>} valuePromiseFunction
   * @return {Promise.<T>}
   * @template {T}
   * @private
   */
  _tryCache(key, maxAge, valuePromiseFunction) {
    /** @type {{date: Date, promise: Promise, value: *}} */
    let entry = this._cache.get(key);

    if (maxAge > 0 && entry && (Date.now() - entry.date) <= maxAge) {
      if (entry.promise) {
        return entry.promise;
      } else {
        return Promise.resolve(entry.value);
      }
    } else {
      const p = valuePromiseFunction().then(r => {
        this._cache.set(key, {date: Date.now(), promise: null, value: r});
        return r;
      });
      this._cache.set(key, {date: Date.now(), promise: p});
      return p;
    }
  }

  /**
   * @param {boolean} [fromService=false] Whether this was called from a service script
   * @returns {Promise<boolean>} true if the server was just started
   */
  ensureStarted(fromService) {
    return this.isServerStarted(undefined, true).then(started => {
      // already started, nothing to do
      if (started) { return false; }

      // needs to be started
      return this.system.isInstalled().then(installed => {
        let p;
        if (installed && !fromService) {
          // console.log('(started:no, installed:yes) starting system-service');
          p = this.system.start();
        } else {
          // console.log('(started:no, installed:no) spawning server');
          p = PattyServer.spawn(this.home).catch(err => {
            this._logger.error('Spawn failed', err);
            return Promise.reject(err);
          });
        }

        return p.delay(1500).then(() => {
          return this.client.pingServer({retries: 2, timeout: 1500}).catch(e => {
            return PattyError.otherP(this.options.name + ' could not be started', e);
          });
        }).return(true);
      });
    });
  }

  /**
   * @param {string} m
   * @private
   */
  _debug(m) {
    this._logger.debug(m);
  }

  /**
   * @param {boolean} [fromService=false] Whether this was called from a service script
   * @returns {Promise<boolean>} true if the server was just killed
   */
  ensureStopped(fromService) {
    return this.isServerStarted(2).then(started => {
      // manager is not started, skip
      if (!started) {
        return false;
      }

      // 1) stop all running services
      return this.client.stopServices().then(() => {
        return this.system.isInstalled();
      }).then(installed => {

        // 2.a) if installed as a service, stop system service
        if (installed && !fromService) {
          return this.system.stop();
        }

        // 2.b) if not installed as a service, kill the manager
        return this.client.stopServices().then(() => {
          return this.client.killServer().return(true);
        });
      });
    });
  }

  /**
   * @returns {Promise<boolean>} true is newly installed
   */
  ensureInstalled() {
    return this.system.checkAdmin('install a service').then(() => {
      return this.system.isInstalled();
    }).then(installed => {
      // service already installed (nothing to do)
      if (installed) {
        return Promise.resolve(false);
      }

      // service not installed yet
      return this.isServerStarted().then(started => {
        if (started) {
          // the server is running (stopping it)
          return this.client.killServer();
        }
      }).then(() => {
        return this.system.install().then(() => {
          // start the newly installed service
          return this.system.start().delay(2000);
        });
      }).return(true);
    });
  }

  /**
   * @returns {Promise<boolean>} true if newly uninstalled
   */
  ensureUninstalled() {
    return this.system.checkAdmin('uninstall a service').then(() => {
      return this.system.isInstalled();
    }).then(installed => {
      // not installed (nothing to do)
      if (!installed) {
        return Promise.resolve(false);
      }

      // service is installed (stopping and uninstalling)
      return this.ensureStopped().then(() => {
        return this.system.uninstall();
      }).return(true);
    });
  }

}
module.exports = Patty;
<|endoftext|>


<commit_before>// Copyright (c) 2011-2012 UX Productivity Pty Ltd. All rights reserved.

(function() {
    function Range(startNode,startOffset,endNode,endOffset)
    {
        this.start = new Position(startNode,startOffset);
        this.end = new Position(endNode,endOffset);
    }

    function copy()
    {
        return new Range(this.start.node,this.start.offset,
                         this.end.node,this.end.offset);
    }

    function isEmpty()
    {
        return ((this.start.node == this.end.node) &&
                (this.start.offset == this.end.offset));
    }

    Range.prototype.toString = function()
    {
        return this.start.toString() + " - " + this.end.toString();
    }

    function trackWhileExecuting(fun)
    {
        Position.trackWhileExecuting([this.start,this.end],fun);
    }

    function selectWholeWords()
    {
        if ((this.start.node.nodeType == Node.TEXT_NODE) &&
            (this.end.node.nodeType == Node.TEXT_NODE)) {
            if (this.isForwards()) {
                // this.start comes before this.end
                this.start.moveToStartOfWord();
                this.end.moveToEndOfWord();
            }
            else {
                // this.end comes before this.end
                this.start.moveToEndOfWord();
                this.end.moveToStartOfWord();
            }
        }
    }

    function expand()
    {
        var doc = this.start.node.ownerDocument;
        while ((this.start.offset == 0) && (this.start.node != doc.body)) {
            var offset = DOM.nodeOffset(this.start.node);
            this.start.node = this.start.node.parentNode;
            this.start.offset = offset;
        }

        while ((this.end.offset == DOM.maxChildOffset(this.end.node)) &&
               (this.end.node != doc.body)) {
            var offset = DOM.nodeOffset(this.end.node);
            this.end.node = this.end.node.parentNode;
            this.end.offset = offset+1;
        }
    }

    function omitEmptyTextSelection()
    {
        if (!this.start.moveBackwardIfAtStart())
            this.start.moveForwardIfAtEnd()
        if (!this.end.moveBackwardIfAtStart())
            this.end.moveForwardIfAtEnd()
    }

    function isForwards()
    {
        var doc = this.start.node.ownerDocument;
        if ((this.start.node.parentNode == null) && (this.start.node != doc.documentElement))
            throw new Error("Range.isForwards "+this+": start node has been removed from document");
        if ((this.end.node.parentNode == null) && (this.end.node != doc.documentElement))
            throw new Error("Range.isForwards "+this+": end node has been removed from document");

        var start = this.start;
        var end = this.end;

        if ((start.node == end.node) && (start.node.nodeType == Node.TEXT_NODE))
            return (end.offset >= start.offset);

        var startParent = null;
        var startChild = null;
        var endParent = null;
        var endChild = null;

        if (end.node.nodeType == Node.ELEMENT_NODE) {
            endParent = end.node;
            endChild = end.node.childNodes[end.offset];
        }
        else {
            endParent = end.node.parentNode;
            endChild = end.node;
        }

        if (start.node.nodeType == Node.ELEMENT_NODE) {
            startParent = start.node;
            startChild = start.node.childNodes[start.offset];
        }
        else {
            startParent = start.node.parentNode;
            startChild = start.node;
            if (startChild == endChild)
                return false;
        }

        var startC = startChild;
        var startP = startParent;
        while (startP != null) {

            var endC = endChild;
            var endP = endParent;
            while (endP != null) {

                if (startP == endC)
                    return false;

                if (startP == endP) {
                    if (endC == null) // endC is last child, so startC must be endC or come before it
                        return true;
                    for (var n = startC; n != null; n = n.nextSibling) {
                        if (n == endC)
                            return true;
                    }
                    return false;
                }

                endC = endP;
                endP = endP.parentNode;
            }

            startC = startP;
            startP = startP.parentNode;
        }
        throw new Error("Could not find common ancestor");
    }

    function getInlineNodes(atLeastOne)
    {
        var all = this.getAllNodes(atLeastOne);
        var result = new Array();
        for (var i = 0; i < all.length; i++) {
            if (isInlineNode(all[i]))
                result.push(all[i]);
        }
        return result;
    }

    function getAllNodes(atLeastOne)
    {
        var result = new Array();
        var outermost = this.getOutermostNodes(atLeastOne);
        for (var i = 0; i < outermost.length; i++)
            addRecursive(outermost[i]);
        return result;

        function addRecursive(node)
        {
            result.push(node);
            for (var child = node.firstChild; child != null; child = child.nextSibling)
                addRecursive(child);
        }
    }

    function singleNode()
    {
        return this.start.closestActualNode();
    }

    function ensureRangeValidHierarchy()
    {
        var range = this;
        this.trackWhileExecuting(function() {
            var nodes = range.getAllNodes(true);

            var depths = new Array();
            for (var i = 0; i < nodes.length; i++) {
                var depth = getNodeDepth(nodes[i]);
                if (depths[depth] == null) {
                    depths[depth] = new Array();
                }
                depths[depth].push(nodes[i]);
            }

            for (var depth = 0; depth < depths.length; depth++) {
                var firstDepth = true;
                if (depths[depth] != null) {
                    for (var i = 0; i < depths[depth].length; i++) {
                        var node = depths[depth][i];
                        Hierarchy.ensureValidHierarchy(node,firstDepth);
                    }
                    firstDepth = false;
                }
            }
        });
    };

    function forwards()
    {
        if (this.isForwards()) {
            return this;
        }
        else {
            var reverse = new Range(this.end.node,this.end.offset,this.start.node,this.start.offset);
            if (!reverse.isForwards())
                throw new Error("Both range "+this+" and its reverse are not forwards");
            return reverse;
        }
    }

    function detail()
    {
        if (!this.isForwards()) {
            var reverse = new Range(this.end.node,this.end.offset,this.start.node,this.start.offset);
            if (!reverse.isForwards())
                throw new Error("Both range "+this+" and its reverse are not forwards");
            return reverse.detail();
        }

        var detail = new Object();
        var start = this.start;
        var end = this.end;

        // Start location
        if (start.node.nodeType == Node.ELEMENT_NODE) {
            detail.startParent = start.node;
            detail.startChild = start.node.childNodes[start.offset];
        }
        else if ((start.node.nodeValue.length > 0) && (start.offset == start.node.nodeValue.length)) {
            detail.startParent = start.node.parentNode;
            detail.startChild = start.node.nextSibling;
        }
        else {
            detail.startParent = start.node.parentNode;
            detail.startChild = start.node;
        }

        // End location
        if (end.node.nodeType == Node.ELEMENT_NODE) {
            detail.endParent = end.node;
            detail.endChild = end.node.childNodes[end.offset];
        }
        else if (end.offset == 0) {
            detail.endParent = end.node.parentNode;
            detail.endChild = end.node;
        }
        else {
            detail.endParent = end.node.parentNode;
            detail.endChild = end.node.nextSibling;
        }

        // Common ancestor
        var startP = detail.startParent;
        var startC = detail.startChild;
        while (startP != null) {
            var endP = detail.endParent;
            var endC = detail.endChild
            while (endP != null) {
                if (startP == endP) {
                    detail.commonAncestor = startP;
                    detail.startAncestor = startC;
                    detail.endAncestor = endC;
                    // Found it
                    return detail;
                }
                endC = endP;
                endP = endP.parentNode;
            }
            startC = startP;
            startP = startP.parentNode;
        }
        throw new Error("Start and end of range have no common ancestor");
    }

    function getOutermostNodes(atLeastOne,info)
    {
        var beforeNodes = new Array();
        var middleNodes = new Array();
        var afterNodes = new Array();

        if (info != null) {
            info.beginning = beforeNodes;
            info.middle = middleNodes;
            info.end = afterNodes;
        }

        if (this.isEmpty())
            return atLeastOne ? [this.singleNode()] : [];

        // Note: start and end are *points* - they are always *in between* nodes or characters, never
        // *at* a node or character.
        // Everything after the end point is excluded from the selection
        // Everything after the start point, but before the end point, is included in the selection

        // We use (parent,child) pairs so that we have a way to represent a point that comes after all
        // the child nodes in a container - in which case the child is null. The parent, however, is
        // always non-null;

        var detail = this.detail();
        if (detail.commonAncestor == null)
            return atLeastOne ? [this.singleNode()] : [];
        var startParent = detail.startParent;
        var startChild = detail.startChild;
        var endParent = detail.endParent;
        var endChild = detail.endChild;
        var commonParent = detail.commonAncestor;
        var startAncestor = detail.startAncestor;
        var endAncestor = detail.endAncestor;

        // Add start nodes
        var topParent = startParent;
        var topChild = startChild;
        while (topParent != commonParent) {
            if (topChild != null)
                beforeNodes.push(topChild);

            while (((topChild == null) || (topChild.nextSibling == null)) &&
                   (topParent != commonParent)) {
                topChild = topParent;
                topParent = topParent.parentNode;
            }
            if (topParent != commonParent)
                topChild = topChild.nextSibling;
        }

        // Add middle nodes
        if (startAncestor != endAncestor) {
            var c = startAncestor;
            if ((c != null) && (c != startChild))
                c = c.nextSibling;
            for (; c != endAncestor; c = c.nextSibling)
                middleNodes.push(c);
        }

        // Add end nodes
        var bottomParent = endParent;
        var bottomChild = endChild;
        while (true) {

            while ((getPreviousSibling(bottomParent,bottomChild) == null) &&
                   (bottomParent != commonParent)) {
                bottomChild = bottomParent;
                bottomParent = bottomParent.parentNode;
            }
            if (bottomParent != commonParent)
                bottomChild = getPreviousSibling(bottomParent,bottomChild);

            if (bottomParent == commonParent)
                break;

            afterNodes.push(bottomChild);
        }
        afterNodes = afterNodes.reverse();

        var result = new Array();

        Array.prototype.push.apply(result,beforeNodes);
        Array.prototype.push.apply(result,middleNodes);
        Array.prototype.push.apply(result,afterNodes);

        if (result.length == 0)
            return atLeastOne ? [this.singleNode()] : [];
        else
            return result;

        function getPreviousSibling(parent,child)
        {
            if (child != null)
                return child.previousSibling;
            else if (parent.lastChild != null)
                return parent.lastChild;
            else
                return null;
        }

        function isAncestorLocation(ancestorParent,ancestorChild,
                                    descendantParent,descendantChild)
        {
            while ((descendantParent != null) &&
                   ((descendantParent != ancestorParent) || (descendantChild != ancestorChild))) {
                descendantChild = descendantParent;
                descendantParent = descendantParent.parentNode;
            }

            return ((descendantParent == ancestorParent) &&
                    (descendantChild == ancestorChild));
        }
    }

    function getClientRects()
    {
        var nodes = this.getOutermostNodes(true);

        // WebKit in iOS 5.0 has a bug where if the selection spans multiple paragraphs, the complete
        // rect for paragraphs other than the first is returned, instead of just the portions of it
        // that are actually in the range. To get around this problem, we go through each text node
        // individually and collect all the rects.
        var result = new Array();
        var doc = this.start.node.ownerDocument;
        var domRange = doc.createRange();
        for (var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
            var node = nodes[nodeIndex];
            if (node.nodeType == Node.TEXT_NODE) {
                var startOffset = (node == this.start.node) ? this.start.offset : 0;
                var endOffset = (node == this.end.node) ? this.end.offset : node.nodeValue.length;
                domRange.setStart(node,startOffset);
                domRange.setEnd(node,endOffset);
                var rects = domRange.getClientRects();
                for (var rectIndex = 0; rectIndex < rects.length; rectIndex++)
                    result.push(rects[rectIndex]);
            }
            else if (node.nodeType == Node.ELEMENT_NODE) {
                result.push(node.getBoundingClientRect());
            }
        }
        return result;
    }

    function cloneContents()
    {
        var cloneMap = new NodeMap();
        var detail = this.detail();
        var cloneRoot = DOM.cloneNode(detail.commonAncestor,false);
        cloneMap.put(detail.commonAncestor,cloneRoot);
        var outermost = this.getOutermostNodes();
        for (var i = 0; i < outermost.length; i++) {

            var node = outermost[i];
            if (node.nodeType == Node.TEXT_NODE) {
                // FIXME: do this without modifying nodeValue
                var saved = node.nodeValue;
                if ((node == this.start.node) && (node == this.end.node)) {
                    node.nodeValue = node.nodeValue.slice(this.start.offset,this.end.offset);
                }
                else if (node == this.start.node) {
                    node.nodeValue = node.nodeValue.slice(this.start.offset);
                }
                else if (node == this.end.node) {
                    node.nodeValue = node.nodeValue.slice(0,this.end.offset);
                }
                add(outermost[i],true);
                node.nodeValue = saved;
            }
            else {
                add(outermost[i],true);
            }
        }

        var result = new Array();
        for (var child = cloneRoot.firstChild; child != null; child = child.nextSibling)
            result.push(child);

        return result;

        function add(node,deep)
        {
            if (cloneMap.containsKey(node))
                return cloneMap.get(node);

            var clone = DOM.cloneNode(node,deep);
            cloneMap.put(node,clone);
            if (node.parentNode == detail.commonAncestor) {
                DOM.appendChild(cloneRoot,clone);
            }
            else {
                var parentClone = add(node.parentNode,false);
                DOM.appendChild(parentClone,clone);
            }
            return clone;
        }
    }

    window.Range = Range;
    Range.prototype.copy = trace(copy);
    Range.prototype.isEmpty = trace(isEmpty);
    Range.prototype.trackWhileExecuting = trace(trackWhileExecuting);
    Range.prototype.selectWholeWords = trace(selectWholeWords);
    Range.prototype.expand = trace(expand);
    Range.prototype.omitEmptyTextSelection = trace(omitEmptyTextSelection);
    Range.prototype.isForwards = trace(isForwards);
    Range.prototype.getInlineNodes = trace(getInlineNodes);
    Range.prototype.getAllNodes = trace(getAllNodes);
    Range.prototype.singleNode = trace(singleNode);
    Range.prototype.ensureRangeValidHierarchy = trace(ensureRangeValidHierarchy);
    Range.prototype.forwards = trace(forwards);
    Range.prototype.detail = trace(detail);
    Range.prototype.getOutermostNodes = trace(getOutermostNodes);
    Range.prototype.getClientRects = trace(getClientRects);
    Range.prototype.cloneContents = trace(cloneContents);

})();
<commit_msg>In ensureRangeValidHierarchy(), check that each node is still present in the document before calling ensureValidHierarchy() with it.<commit_after>// Copyright (c) 2011-2012 UX Productivity Pty Ltd. All rights reserved.

(function() {
    function Range(startNode,startOffset,endNode,endOffset)
    {
        this.start = new Position(startNode,startOffset);
        this.end = new Position(endNode,endOffset);
    }

    function copy()
    {
        return new Range(this.start.node,this.start.offset,
                         this.end.node,this.end.offset);
    }

    function isEmpty()
    {
        return ((this.start.node == this.end.node) &&
                (this.start.offset == this.end.offset));
    }

    Range.prototype.toString = function()
    {
        return this.start.toString() + " - " + this.end.toString();
    }

    function trackWhileExecuting(fun)
    {
        Position.trackWhileExecuting([this.start,this.end],fun);
    }

    function selectWholeWords()
    {
        if ((this.start.node.nodeType == Node.TEXT_NODE) &&
            (this.end.node.nodeType == Node.TEXT_NODE)) {
            if (this.isForwards()) {
                // this.start comes before this.end
                this.start.moveToStartOfWord();
                this.end.moveToEndOfWord();
            }
            else {
                // this.end comes before this.end
                this.start.moveToEndOfWord();
                this.end.moveToStartOfWord();
            }
        }
    }

    function expand()
    {
        var doc = this.start.node.ownerDocument;
        while ((this.start.offset == 0) && (this.start.node != doc.body)) {
            var offset = DOM.nodeOffset(this.start.node);
            this.start.node = this.start.node.parentNode;
            this.start.offset = offset;
        }

        while ((this.end.offset == DOM.maxChildOffset(this.end.node)) &&
               (this.end.node != doc.body)) {
            var offset = DOM.nodeOffset(this.end.node);
            this.end.node = this.end.node.parentNode;
            this.end.offset = offset+1;
        }
    }

    function omitEmptyTextSelection()
    {
        if (!this.start.moveBackwardIfAtStart())
            this.start.moveForwardIfAtEnd()
        if (!this.end.moveBackwardIfAtStart())
            this.end.moveForwardIfAtEnd()
    }

    function isForwards()
    {
        var doc = this.start.node.ownerDocument;
        if ((this.start.node.parentNode == null) && (this.start.node != doc.documentElement))
            throw new Error("Range.isForwards "+this+": start node has been removed from document");
        if ((this.end.node.parentNode == null) && (this.end.node != doc.documentElement))
            throw new Error("Range.isForwards "+this+": end node has been removed from document");

        var start = this.start;
        var end = this.end;

        if ((start.node == end.node) && (start.node.nodeType == Node.TEXT_NODE))
            return (end.offset >= start.offset);

        var startParent = null;
        var startChild = null;
        var endParent = null;
        var endChild = null;

        if (end.node.nodeType == Node.ELEMENT_NODE) {
            endParent = end.node;
            endChild = end.node.childNodes[end.offset];
        }
        else {
            endParent = end.node.parentNode;
            endChild = end.node;
        }

        if (start.node.nodeType == Node.ELEMENT_NODE) {
            startParent = start.node;
            startChild = start.node.childNodes[start.offset];
        }
        else {
            startParent = start.node.parentNode;
            startChild = start.node;
            if (startChild == endChild)
                return false;
        }

        var startC = startChild;
        var startP = startParent;
        while (startP != null) {

            var endC = endChild;
            var endP = endParent;
            while (endP != null) {

                if (startP == endC)
                    return false;

                if (startP == endP) {
                    if (endC == null) // endC is last child, so startC must be endC or come before it
                        return true;
                    for (var n = startC; n != null; n = n.nextSibling) {
                        if (n == endC)
                            return true;
                    }
                    return false;
                }

                endC = endP;
                endP = endP.parentNode;
            }

            startC = startP;
            startP = startP.parentNode;
        }
        throw new Error("Could not find common ancestor");
    }

    function getInlineNodes(atLeastOne)
    {
        var all = this.getAllNodes(atLeastOne);
        var result = new Array();
        for (var i = 0; i < all.length; i++) {
            if (isInlineNode(all[i]))
                result.push(all[i]);
        }
        return result;
    }

    function getAllNodes(atLeastOne)
    {
        var result = new Array();
        var outermost = this.getOutermostNodes(atLeastOne);
        for (var i = 0; i < outermost.length; i++)
            addRecursive(outermost[i]);
        return result;

        function addRecursive(node)
        {
            result.push(node);
            for (var child = node.firstChild; child != null; child = child.nextSibling)
                addRecursive(child);
        }
    }

    function singleNode()
    {
        return this.start.closestActualNode();
    }

    function ensureRangeValidHierarchy()
    {
        var range = this;
        this.trackWhileExecuting(function() {
            var nodes = range.getAllNodes(true);

            var depths = new Array();
            for (var i = 0; i < nodes.length; i++) {
                var depth = getNodeDepth(nodes[i]);
                if (depths[depth] == null) {
                    depths[depth] = new Array();
                }
                depths[depth].push(nodes[i]);
            }

            for (var depth = 0; depth < depths.length; depth++) {
                var firstDepth = true;
                if (depths[depth] != null) {
                    for (var i = 0; i < depths[depth].length; i++) {
                        var node = depths[depth][i];
                        if (node.parentNode != null)
                            Hierarchy.ensureValidHierarchy(node,firstDepth);
                    }
                    firstDepth = false;
                }
            }
        });
    };

    function forwards()
    {
        if (this.isForwards()) {
            return this;
        }
        else {
            var reverse = new Range(this.end.node,this.end.offset,this.start.node,this.start.offset);
            if (!reverse.isForwards())
                throw new Error("Both range "+this+" and its reverse are not forwards");
            return reverse;
        }
    }

    function detail()
    {
        if (!this.isForwards()) {
            var reverse = new Range(this.end.node,this.end.offset,this.start.node,this.start.offset);
            if (!reverse.isForwards())
                throw new Error("Both range "+this+" and its reverse are not forwards");
            return reverse.detail();
        }

        var detail = new Object();
        var start = this.start;
        var end = this.end;

        // Start location
        if (start.node.nodeType == Node.ELEMENT_NODE) {
            detail.startParent = start.node;
            detail.startChild = start.node.childNodes[start.offset];
        }
        else if ((start.node.nodeValue.length > 0) && (start.offset == start.node.nodeValue.length)) {
            detail.startParent = start.node.parentNode;
            detail.startChild = start.node.nextSibling;
        }
        else {
            detail.startParent = start.node.parentNode;
            detail.startChild = start.node;
        }

        // End location
        if (end.node.nodeType == Node.ELEMENT_NODE) {
            detail.endParent = end.node;
            detail.endChild = end.node.childNodes[end.offset];
        }
        else if (end.offset == 0) {
            detail.endParent = end.node.parentNode;
            detail.endChild = end.node;
        }
        else {
            detail.endParent = end.node.parentNode;
            detail.endChild = end.node.nextSibling;
        }

        // Common ancestor
        var startP = detail.startParent;
        var startC = detail.startChild;
        while (startP != null) {
            var endP = detail.endParent;
            var endC = detail.endChild
            while (endP != null) {
                if (startP == endP) {
                    detail.commonAncestor = startP;
                    detail.startAncestor = startC;
                    detail.endAncestor = endC;
                    // Found it
                    return detail;
                }
                endC = endP;
                endP = endP.parentNode;
            }
            startC = startP;
            startP = startP.parentNode;
        }
        throw new Error("Start and end of range have no common ancestor");
    }

    function getOutermostNodes(atLeastOne,info)
    {
        var beforeNodes = new Array();
        var middleNodes = new Array();
        var afterNodes = new Array();

        if (info != null) {
            info.beginning = beforeNodes;
            info.middle = middleNodes;
            info.end = afterNodes;
        }

        if (this.isEmpty())
            return atLeastOne ? [this.singleNode()] : [];

        // Note: start and end are *points* - they are always *in between* nodes or characters, never
        // *at* a node or character.
        // Everything after the end point is excluded from the selection
        // Everything after the start point, but before the end point, is included in the selection

        // We use (parent,child) pairs so that we have a way to represent a point that comes after all
        // the child nodes in a container - in which case the child is null. The parent, however, is
        // always non-null;

        var detail = this.detail();
        if (detail.commonAncestor == null)
            return atLeastOne ? [this.singleNode()] : [];
        var startParent = detail.startParent;
        var startChild = detail.startChild;
        var endParent = detail.endParent;
        var endChild = detail.endChild;
        var commonParent = detail.commonAncestor;
        var startAncestor = detail.startAncestor;
        var endAncestor = detail.endAncestor;

        // Add start nodes
        var topParent = startParent;
        var topChild = startChild;
        while (topParent != commonParent) {
            if (topChild != null)
                beforeNodes.push(topChild);

            while (((topChild == null) || (topChild.nextSibling == null)) &&
                   (topParent != commonParent)) {
                topChild = topParent;
                topParent = topParent.parentNode;
            }
            if (topParent != commonParent)
                topChild = topChild.nextSibling;
        }

        // Add middle nodes
        if (startAncestor != endAncestor) {
            var c = startAncestor;
            if ((c != null) && (c != startChild))
                c = c.nextSibling;
            for (; c != endAncestor; c = c.nextSibling)
                middleNodes.push(c);
        }

        // Add end nodes
        var bottomParent = endParent;
        var bottomChild = endChild;
        while (true) {

            while ((getPreviousSibling(bottomParent,bottomChild) == null) &&
                   (bottomParent != commonParent)) {
                bottomChild = bottomParent;
                bottomParent = bottomParent.parentNode;
            }
            if (bottomParent != commonParent)
                bottomChild = getPreviousSibling(bottomParent,bottomChild);

            if (bottomParent == commonParent)
                break;

            afterNodes.push(bottomChild);
        }
        afterNodes = afterNodes.reverse();

        var result = new Array();

        Array.prototype.push.apply(result,beforeNodes);
        Array.prototype.push.apply(result,middleNodes);
        Array.prototype.push.apply(result,afterNodes);

        if (result.length == 0)
            return atLeastOne ? [this.singleNode()] : [];
        else
            return result;

        function getPreviousSibling(parent,child)
        {
            if (child != null)
                return child.previousSibling;
            else if (parent.lastChild != null)
                return parent.lastChild;
            else
                return null;
        }

        function isAncestorLocation(ancestorParent,ancestorChild,
                                    descendantParent,descendantChild)
        {
            while ((descendantParent != null) &&
                   ((descendantParent != ancestorParent) || (descendantChild != ancestorChild))) {
                descendantChild = descendantParent;
                descendantParent = descendantParent.parentNode;
            }

            return ((descendantParent == ancestorParent) &&
                    (descendantChild == ancestorChild));
        }
    }

    function getClientRects()
    {
        var nodes = this.getOutermostNodes(true);

        // WebKit in iOS 5.0 has a bug where if the selection spans multiple paragraphs, the complete
        // rect for paragraphs other than the first is returned, instead of just the portions of it
        // that are actually in the range. To get around this problem, we go through each text node
        // individually and collect all the rects.
        var result = new Array();
        var doc = this.start.node.ownerDocument;
        var domRange = doc.createRange();
        for (var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
            var node = nodes[nodeIndex];
            if (node.nodeType == Node.TEXT_NODE) {
                var startOffset = (node == this.start.node) ? this.start.offset : 0;
                var endOffset = (node == this.end.node) ? this.end.offset : node.nodeValue.length;
                domRange.setStart(node,startOffset);
                domRange.setEnd(node,endOffset);
                var rects = domRange.getClientRects();
                for (var rectIndex = 0; rectIndex < rects.length; rectIndex++)
                    result.push(rects[rectIndex]);
            }
            else if (node.nodeType == Node.ELEMENT_NODE) {
                result.push(node.getBoundingClientRect());
            }
        }
        return result;
    }

    function cloneContents()
    {
        var cloneMap = new NodeMap();
        var detail = this.detail();
        var cloneRoot = DOM.cloneNode(detail.commonAncestor,false);
        cloneMap.put(detail.commonAncestor,cloneRoot);
        var outermost = this.getOutermostNodes();
        for (var i = 0; i < outermost.length; i++) {

            var node = outermost[i];
            if (node.nodeType == Node.TEXT_NODE) {
                // FIXME: do this without modifying nodeValue
                var saved = node.nodeValue;
                if ((node == this.start.node) && (node == this.end.node)) {
                    node.nodeValue = node.nodeValue.slice(this.start.offset,this.end.offset);
                }
                else if (node == this.start.node) {
                    node.nodeValue = node.nodeValue.slice(this.start.offset);
                }
                else if (node == this.end.node) {
                    node.nodeValue = node.nodeValue.slice(0,this.end.offset);
                }
                add(outermost[i],true);
                node.nodeValue = saved;
            }
            else {
                add(outermost[i],true);
            }
        }

        var result = new Array();
        for (var child = cloneRoot.firstChild; child != null; child = child.nextSibling)
            result.push(child);

        return result;

        function add(node,deep)
        {
            if (cloneMap.containsKey(node))
                return cloneMap.get(node);

            var clone = DOM.cloneNode(node,deep);
            cloneMap.put(node,clone);
            if (node.parentNode == detail.commonAncestor) {
                DOM.appendChild(cloneRoot,clone);
            }
            else {
                var parentClone = add(node.parentNode,false);
                DOM.appendChild(parentClone,clone);
            }
            return clone;
        }
    }

    window.Range = Range;
    Range.prototype.copy = trace(copy);
    Range.prototype.isEmpty = trace(isEmpty);
    Range.prototype.trackWhileExecuting = trace(trackWhileExecuting);
    Range.prototype.selectWholeWords = trace(selectWholeWords);
    Range.prototype.expand = trace(expand);
    Range.prototype.omitEmptyTextSelection = trace(omitEmptyTextSelection);
    Range.prototype.isForwards = trace(isForwards);
    Range.prototype.getInlineNodes = trace(getInlineNodes);
    Range.prototype.getAllNodes = trace(getAllNodes);
    Range.prototype.singleNode = trace(singleNode);
    Range.prototype.ensureRangeValidHierarchy = trace(ensureRangeValidHierarchy);
    Range.prototype.forwards = trace(forwards);
    Range.prototype.detail = trace(detail);
    Range.prototype.getOutermostNodes = trace(getOutermostNodes);
    Range.prototype.getClientRects = trace(getClientRects);
    Range.prototype.cloneContents = trace(cloneContents);

})();
<|endoftext|>


<commit_before>'use strict';

const Scope = require('./Scope');
const Value = require('./Value');
const CompletionRecord = require('./CompletionRecord');
const ObjectValue = require('./values/ObjectValue');
const PrimitiveValue = require('./values/PrimitiveValue.js');
const StringValue = require('./values/StringValue');
const LinkValue = require('./values/LinkValue');
const SmartLinkValue = require('./values/SmartLinkValue');
const BridgeValue = require('./values/BridgeValue');
const ASTPreprocessor = require('./ASTPreprocessor');
const EasyNativeFunction = require('./values/EasyNativeFunction');
const PropertyDescriptor = require('./values/PropertyDescriptor');
const Evaluator = require('./Evaluator');
const EvaluatorInstruction = require('./EvaluatorInstruction');

const ObjectPrototype = require('./stdlib/ObjectPrototype');
const FunctionPrototype = require('./stdlib/FunctionPrototype');
const ObjectClass = require('./stdlib/Object');
const FunctionClass = require('./stdlib/Function');
const NumberPrototype = require('./stdlib/NumberPrototype');

const StringPrototype = require('./stdlib/StringPrototype');

const ArrayPrototype = require('./stdlib/ArrayPrototype');
const ArrayClass = require('./stdlib/Array');
const StringClass = require('./stdlib/String');
const NumberClass = require('./stdlib/Number');

const BooleanPrototype = require('./stdlib/BooleanPrototype');
const BooleanClass = require('./stdlib/Boolean');
const RegExpPrototype = require('./stdlib/RegExpPrototype');
const RegExpClass = require('./stdlib/RegExp');
const EsperClass = require('./stdlib/Esper');
const ErrorPrototype = require('./stdlib/ErrorPrototype');
const ErrorClass = require('./stdlib/Error');

const AssertClass = require('./stdlib/Assert');
const MathClass = require('./stdlib/Math.js');
const ConsoleClass = require('./stdlib/Console');
const JSONClass = require('./stdlib/JSON');
const ProxyClass = require('./stdlib/Proxy')
const esper = require('./index.js');

class EvalFunction extends ObjectValue {

	constructor(realm) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
	}

	*call(thiz, args, scope) {
		let cv = Value.undef;
		if ( args.length > 0 ) cv = args[0];
		if ( !(cv instanceof StringValue) ) return cv;
		let code = yield * cv.toStringNative();
		let ast;
		try {
			let oast = scope.realm.parser(code, {loc: true});
			ast = ASTPreprocessor.process(oast);
		} catch ( e ) {
			var eo;
			let desc = e.description || e.message;
			if ( e.name == 'ReferenceError' || /Invalid left-hand side in/.test(desc) ) eo = new ReferenceError(e.description, e.fileName, e.lineNumber);
			else eo = new SyntaxError(e.description, e.fileName, e.lineNumber);

			if ( e.stack ) eo.stack = e.stack;
			return new CompletionRecord(CompletionRecord.THROW, Value.fromNative(eo, scope.realm));
		}

		//TODO: Dont run in the parent scope if we are called indirectly
		let bak = yield EvaluatorInstruction.branch('eval', ast, scope);
		//console.log("EVALED: ", bak);
		return bak;
	}
}

let timeoutIds = 0;;
class SetTimeoutFunction extends ObjectValue {

	constructor(realm, runtime, isSetInterval) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
		this.runtime = runtime;
		this.isSetInterval = isSetInterval;
	}

	*call(thiz, args, scope) {
		let engine = scope.realm.engine;
		let ev = args[0];
		let evaluator = new Evaluator(scope.realm, null, scope.global);
		let time = args.length > 1 ? 0 + args[1].toNative() : 0;
		let id = timeoutIds++;
		let isSetInterval = this.isSetInterval;

		if ( !ev || ev.jsTypeName !== "function" && ev.jsTypeName !== "string" ) {
			return new CompletionRecord(CompletionRecord.THROW, Value.fromNative(new TypeError('"callback" argument must be a function'), scope.realm));
		}

		evaluator.pushFrame({generator: (function*() {
			while ( true ) {
				yield esper.FutureValue.make(yield * engine.runtime.wait(time));
				if ( ev.jsTypeName == "function" ) {
					let tv = scope.strict ? esper.undef : scope.global.thiz;
					yield * ev.call(tv, args.slice(2), scope.global);
				} else if ( ev.jsTypeName == "string" ) {
					let oast = scope.realm.parser(yield * ev.toStringNative(), {loc: true});
					let ast = ASTPreprocessor.process(oast);
					yield EvaluatorInstruction.branch('eval', ast, scope.global);
				}
				if ( !isSetInterval ) break;
			}
			return Value.undef;
		})(), type: 'invoke'});

		let o = evaluator.generator();
		evaluator.id = id;
		engine.threads.push(o);
		return Value.fromNative(id);
	}
}

class ClearTimeoutFunction extends ObjectValue {

	constructor(realm, runtime) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
		this.runtime = runtime;
	}

	*call(thiz, args, scope) {
		if ( args.length < 1 ) return Value.undef;
		let target = yield * args[0].toIntNative();
		let engine = scope.realm.engine;

		for ( let i = 0; i < engine.threads.length; ++i ) {
			if ( engine.threads[i].evaluator.id == target ) {
				let thr = engine.threads.splice(i, 1);
				thr[0].evaluator.dispose.map((x) => x());
				return Value.fromNative(true);
			}
		}
		return Value.fromNative(false);
	}
}

/**
 * Represents a javascript execution environment including
 * it's scopes and standard libraries.
 */
class Realm {
	print() {
		console.log.apply(console, arguments);
	}

	write() {
		this.print.apply(this, arguments);
	}

	parser(code, options) {
		if ( !esper.languages[this.language] ) {
			throw new Error(`Unknown language ${this.language}. Load the lang-${this.language} plugin?`);
		}
		return esper.languages[this.language].parser(code, options);
	}

	makeLiteralValue(v, n) {
		let lang = esper.languages[this.language];
		if ( lang && lang.makeLiteralValue ) {
			let langv = lang.makeLiteralValue(v, this, n);
			if ( langv ) return langv;
		}
		return this.fromNative(v, n);
	}

	constructor(options, engine) {
		this.engine = engine;
		this.options = options || {};
		this.language = options.language || 'javascript';
		/** @type {Value} */
		this.ObjectPrototype =  new ObjectPrototype(this);
		this.FunctionPrototype = new FunctionPrototype(this);
		this.Object = new ObjectClass(this);
		this.ObjectPrototype._init(this);
		this.FunctionPrototype._init(this);
		this.Object.setPrototype(this.ObjectPrototype);
		this.FunctionPrototype.setPrototype(this.ObjectPrototype);

		//TODO: Do this when we can make the property non enumerable.
		this.ObjectPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Object, false));

		this.Function = new FunctionClass(this);
		this.FunctionPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Function, false));

		/** @type {Math} */
		this.Math = new MathClass(this);

		/** @type {NumberPrototype} */
		this.NumberPrototype = new NumberPrototype(this);

		/** @type {StringPrototype} */
		this.StringPrototype = new StringPrototype(this);

		this.ArrayPrototype = new ArrayPrototype(this);
		this.Array = new ArrayClass(this);
		this.String = new StringClass(this);
		this.Number = new NumberClass(this);

		this.BooleanPrototype = new BooleanPrototype(this);
		this.Boolean = new BooleanClass(this);

		this.RegExpPrototype = new RegExpPrototype(this);
		this.RegExp = new RegExpClass(this);
		this.Proxy = new ProxyClass(this);

		this.Esper = new EsperClass(this);
		this.ErrorPrototype = new ErrorPrototype(this);
		this.Error = new ErrorClass(this);
		this.ErrorPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Error, false));

		/** @type {Value} */
		this.console = new ConsoleClass(this);

		let scope = new Scope(this);
		scope.object.clazz = 'global';
		scope.strict = options.strict || false;
		let that = this;
		var printer = EasyNativeFunction.makeForNative(this, function() {
			that.print.apply(that, arguments);
		});
		scope.set('print', printer);
		scope.set('log', printer);

		scope.addConst('NaN', this.fromNative(NaN));
		scope.addConst('Infinity', this.fromNative(Infinity));

		scope.set('console', this.console);
		scope.set('JSON', new JSONClass(this));

		if ( options.exposeEsperGlobal ) {
			scope.set('Esper', this.Esper);
		}

		scope.set('Math', this.Math);

		scope.set('Number', this.Number);
		scope.set('Boolean', this.Boolean);
		scope.set('Object', this.Object);
		scope.set('Function', this.Function);
		scope.set('Array', this.Array);
		scope.set('String', this.String);
		scope.set('RegExp', this.RegExp);
		scope.set('Proxy', this.Proxy);

		scope.set('Error', this.Error);
		scope.set('TypeError', this.TypeError = this.Error.makeErrorType(TypeError));
		scope.set('SyntaxError', this.SyntaxError = this.Error.makeErrorType(SyntaxError));
		scope.set('ReferenceError', this.ReferenceError = this.Error.makeErrorType(ReferenceError));
		scope.set('RangeError', this.RangeError = this.Error.makeErrorType(RangeError));
		scope.set('EvalError', this.EvalError = this.Error.makeErrorType(EvalError));
		scope.set('URIError', this.URIError = this.Error.makeErrorType(URIError));

		scope.set('parseInt', EasyNativeFunction.makeForNative(this, parseInt));
		scope.set('parseFloat', EasyNativeFunction.makeForNative(this, parseFloat));
		scope.set('isNaN', EasyNativeFunction.makeForNative(this, isNaN));
		scope.set('isFinite', EasyNativeFunction.makeForNative(this, isFinite));

		//scope.set('Date', this.fromNative(Date));
		scope.set('eval', new EvalFunction(this));
		scope.set('assert', new AssertClass(this));

		if ( options.runtime ) {
			scope.set("setTimeout", new SetTimeoutFunction(this, options.runtime, false));
			scope.set("setInterval", new SetTimeoutFunction(this, options.runtime, true));
			scope.set("clearTimeout", new ClearTimeoutFunction(this, options.runtime));
			scope.set("clearInterval", new ClearTimeoutFunction(this, options.runtime));
		}

		scope.thiz = scope.object;
		this.importCache = new WeakMap();
		/** @type {Scope} */
		this.globalScope = scope;

		let lang = esper.languages[this.language];
		if ( lang && lang.setupRealm ) lang.setupRealm(this);
	}

	lookupWellKnown(v) {
		if ( v === Object ) return this.Object;
		if ( v === Object.prototype ) return this.ObjectPrototype;
		if ( v === Function ) return this.Function;
		if ( v === Function.prototype ) return this.FunctionPrototype;
		if ( v === Math ) return this.Math;
		if ( v === Number ) return this.Number;
		if ( v === Number.prototype ) return this.NumberPrototype;
		if ( v === String ) return this.String;
		if ( v === String.prototype ) return this.StringPrototype;
		if ( v === Array ) return this.Array;
		if ( v === Array.prototype ) return this.ArrayPrototype;
		if ( v === RegExp ) return this.RegExp;
		if ( v === RegExp.prototype ) return this.RegExpPrototype;
		if ( typeof console !== 'undefined' && v === console ) return this.console;

	}

	lookupWellKnownByName(v) {
		switch ( v ) {
			case '%Object%':  return this.Object;
			case '%ObjectPrototype%':  return this.ObjectPrototype;
			case '%Function%':  return this.Function;
			case '%FunctionPrototype%':  return this.FunctionPrototype;
			case '%Math%':  return this.Math;
			case '%Number%':  return this.Number;
			case '%NumberPrototype%':  return this.NumberPrototype;
			case '%Array%':  return this.Array;
			case '%ArrayPrototype%':  return this.ArrayPrototype;
			case '%RegExp%':  return this.RegExp;
			case '%RegExpPrototype%':  return this.RegExpPrototype;
		}
	}

	fromNative(native, x) {
		return Value.fromNative(native, this);
	}

	import(native, modeHint) {
		if ( native instanceof Value ) return native;
		if ( native === undefined ) return Value.undef;

		let prim = Value.fromPrimativeNative(native);
		if ( prim ) return prim;

		//if ( this.importCache.has(native) ) {
		//	return this.importCache.get(native);
		//}

		if ( Value.hasBookmark(native) ) {
			return Value.getBookmark(native);
		}

		let result;
		switch ( modeHint || this.options.foreignObjectMode ) {
			case 'bridge':
				result = BridgeValue.make(native, this);
				break;
			case 'smart':
				result = SmartLinkValue.make(native, this);
				break;
			case 'link':
			default:
				result = LinkValue.make(native, this);
				break;
		}

		//this.importCache.set(native, result);
		return result;
	}
}

Realm.prototype.makeForForeignObject = Realm.prototype.import;

module.exports = Realm;
<commit_msg>Track intrinsics.<commit_after>'use strict';

const Scope = require('./Scope');
const Value = require('./Value');
const CompletionRecord = require('./CompletionRecord');
const ObjectValue = require('./values/ObjectValue');
const PrimitiveValue = require('./values/PrimitiveValue.js');
const StringValue = require('./values/StringValue');
const LinkValue = require('./values/LinkValue');
const SmartLinkValue = require('./values/SmartLinkValue');
const BridgeValue = require('./values/BridgeValue');
const ASTPreprocessor = require('./ASTPreprocessor');
const EasyNativeFunction = require('./values/EasyNativeFunction');
const PropertyDescriptor = require('./values/PropertyDescriptor');
const Evaluator = require('./Evaluator');
const EvaluatorInstruction = require('./EvaluatorInstruction');

const ObjectPrototype = require('./stdlib/ObjectPrototype');
const FunctionPrototype = require('./stdlib/FunctionPrototype');
const ObjectClass = require('./stdlib/Object');
const FunctionClass = require('./stdlib/Function');
const NumberPrototype = require('./stdlib/NumberPrototype');

const StringPrototype = require('./stdlib/StringPrototype');

const ArrayPrototype = require('./stdlib/ArrayPrototype');
const ArrayClass = require('./stdlib/Array');
const StringClass = require('./stdlib/String');
const NumberClass = require('./stdlib/Number');

const BooleanPrototype = require('./stdlib/BooleanPrototype');
const BooleanClass = require('./stdlib/Boolean');
const RegExpPrototype = require('./stdlib/RegExpPrototype');
const RegExpClass = require('./stdlib/RegExp');
const EsperClass = require('./stdlib/Esper');
const ErrorPrototype = require('./stdlib/ErrorPrototype');
const ErrorClass = require('./stdlib/Error');

const AssertClass = require('./stdlib/Assert');
const MathClass = require('./stdlib/Math.js');
const ConsoleClass = require('./stdlib/Console');
const JSONClass = require('./stdlib/JSON');
const ProxyClass = require('./stdlib/Proxy')
const esper = require('./index.js');

class EvalFunction extends ObjectValue {

	constructor(realm) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
	}

	*call(thiz, args, scope) {
		let cv = Value.undef;
		if ( args.length > 0 ) cv = args[0];
		if ( !(cv instanceof StringValue) ) return cv;
		let code = yield * cv.toStringNative();
		let ast;
		try {
			let oast = scope.realm.parser(code, {loc: true});
			ast = ASTPreprocessor.process(oast);
		} catch ( e ) {
			var eo;
			let desc = e.description || e.message;
			if ( e.name == 'ReferenceError' || /Invalid left-hand side in/.test(desc) ) eo = new ReferenceError(e.description, e.fileName, e.lineNumber);
			else eo = new SyntaxError(e.description, e.fileName, e.lineNumber);

			if ( e.stack ) eo.stack = e.stack;
			return new CompletionRecord(CompletionRecord.THROW, Value.fromNative(eo, scope.realm));
		}

		//TODO: Dont run in the parent scope if we are called indirectly
		let bak = yield EvaluatorInstruction.branch('eval', ast, scope);
		//console.log("EVALED: ", bak);
		return bak;
	}
}

let timeoutIds = 0;;
class SetTimeoutFunction extends ObjectValue {

	constructor(realm, runtime, isSetInterval) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
		this.runtime = runtime;
		this.isSetInterval = isSetInterval;
	}

	*call(thiz, args, scope) {
		let engine = scope.realm.engine;
		let ev = args[0];
		let evaluator = new Evaluator(scope.realm, null, scope.global);
		let time = args.length > 1 ? 0 + args[1].toNative() : 0;
		let id = timeoutIds++;
		let isSetInterval = this.isSetInterval;

		if ( !ev || ev.jsTypeName !== "function" && ev.jsTypeName !== "string" ) {
			return new CompletionRecord(CompletionRecord.THROW, Value.fromNative(new TypeError('"callback" argument must be a function'), scope.realm));
		}

		evaluator.pushFrame({generator: (function*() {
			while ( true ) {
				yield esper.FutureValue.make(yield * engine.runtime.wait(time));
				if ( ev.jsTypeName == "function" ) {
					let tv = scope.strict ? esper.undef : scope.global.thiz;
					yield * ev.call(tv, args.slice(2), scope.global);
				} else if ( ev.jsTypeName == "string" ) {
					let oast = scope.realm.parser(yield * ev.toStringNative(), {loc: true});
					let ast = ASTPreprocessor.process(oast);
					yield EvaluatorInstruction.branch('eval', ast, scope.global);
				}
				if ( !isSetInterval ) break;
			}
			return Value.undef;
		})(), type: 'invoke'});

		let o = evaluator.generator();
		evaluator.id = id;
		engine.threads.push(o);
		return Value.fromNative(id);
	}
}

class ClearTimeoutFunction extends ObjectValue {

	constructor(realm, runtime) {
		super(realm);
		this.setPrototype(realm.FunctionPrototype);
		this.runtime = runtime;
	}

	*call(thiz, args, scope) {
		if ( args.length < 1 ) return Value.undef;
		let target = yield * args[0].toIntNative();
		let engine = scope.realm.engine;

		for ( let i = 0; i < engine.threads.length; ++i ) {
			if ( engine.threads[i].evaluator.id == target ) {
				let thr = engine.threads.splice(i, 1);
				thr[0].evaluator.dispose.map((x) => x());
				return Value.fromNative(true);
			}
		}
		return Value.fromNative(false);
	}
}

/**
 * Represents a javascript execution environment including
 * it's scopes and standard libraries.
 */
class Realm {
	print() {
		console.log.apply(console, arguments);
	}

	write() {
		this.print.apply(this, arguments);
	}

	parser(code, options) {
		if ( !esper.languages[this.language] ) {
			throw new Error(`Unknown language ${this.language}. Load the lang-${this.language} plugin?`);
		}
		return esper.languages[this.language].parser(code, options);
	}

	makeLiteralValue(v, n) {
		let lang = esper.languages[this.language];
		if ( lang && lang.makeLiteralValue ) {
			let langv = lang.makeLiteralValue(v, this, n);
			if ( langv ) return langv;
		}
		return this.fromNative(v, n);
	}

	constructor(options, engine) {
		this.engine = engine;
		this.options = options || {};
		this.language = options.language || 'javascript';
		/** @type {Value} */
		this.ObjectPrototype =  new ObjectPrototype(this);
		this.FunctionPrototype = new FunctionPrototype(this);
		this.Object = new ObjectClass(this);
		this.ObjectPrototype._init(this);
		this.FunctionPrototype._init(this);
		this.Object.setPrototype(this.ObjectPrototype);
		this.FunctionPrototype.setPrototype(this.ObjectPrototype);

		//TODO: Do this when we can make the property non enumerable.
		this.ObjectPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Object, false));

		this.Function = new FunctionClass(this);
		this.FunctionPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Function, false));

		/** @type {Math} */
		this.Math = new MathClass(this);

		/** @type {NumberPrototype} */
		this.NumberPrototype = new NumberPrototype(this);

		/** @type {StringPrototype} */
		this.StringPrototype = new StringPrototype(this);

		this.ArrayPrototype = new ArrayPrototype(this);
		this.Array = new ArrayClass(this);
		this.String = new StringClass(this);
		this.Number = new NumberClass(this);

		this.BooleanPrototype = new BooleanPrototype(this);
		this.Boolean = new BooleanClass(this);

		this.RegExpPrototype = new RegExpPrototype(this);
		this.RegExp = new RegExpClass(this);
		this.Proxy = new ProxyClass(this);

		this.Esper = new EsperClass(this);
		this.ErrorPrototype = new ErrorPrototype(this);
		this.Error = new ErrorClass(this);
		this.ErrorPrototype.rawSetProperty('constructor', new PropertyDescriptor(this.Error, false));

		/** @type {Value} */
		this.console = new ConsoleClass(this);

		this.intrinsics = {
			ObjectPrototype: this.ObjectPrototype,
			ArrayPrototype: this.ArrayPrototype,
			BooleanPrototype: this.BooleanPrototype,
			RegExpPrototype: this.RegExpPrototype,
			FunctionPrototype: this.FunctionPrototype
		}

		let scope = new Scope(this);
		scope.object.clazz = 'global';
		scope.strict = options.strict || false;
		this.intrinsicScope = scope;
		let that = this;
		var printer = EasyNativeFunction.makeForNative(this, function() {
			that.print.apply(that, arguments);
		});
		scope.set('print', printer);
		scope.set('log', printer);

		scope.addConst('NaN', this.fromNative(NaN));
		scope.addConst('Infinity', this.fromNative(Infinity));

		this.addIntrinsic('console', this.console);
		this.addIntrinsic('JSON', new JSONClass(this));

		if ( options.exposeEsperGlobal ) {
			scope.set('Esper', this.Esper);
		}

		this.addIntrinsic('Math', this.Math);

		this.addIntrinsic('Number', this.Number);
		this.addIntrinsic('Boolean', this.Boolean);
		this.addIntrinsic('Object', this.Object);
		this.addIntrinsic('Function', this.Function);
		this.addIntrinsic('Array', this.Array);
		this.addIntrinsic('String', this.String);
		this.addIntrinsic('RegExp', this.RegExp);
		this.addIntrinsic('Proxy', this.Proxy);

		this.addIntrinsic('Error', this.Error);
		this.addIntrinsic('TypeError', this.TypeError = this.Error.makeErrorType(TypeError));
		this.addIntrinsic('SyntaxError', this.SyntaxError = this.Error.makeErrorType(SyntaxError));
		this.addIntrinsic('ReferenceError', this.ReferenceError = this.Error.makeErrorType(ReferenceError));
		this.addIntrinsic('RangeError', this.RangeError = this.Error.makeErrorType(RangeError));
		this.addIntrinsic('EvalError', this.EvalError = this.Error.makeErrorType(EvalError));
		this.addIntrinsic('URIError', this.URIError = this.Error.makeErrorType(URIError));

		this.addIntrinsic('parseInt', EasyNativeFunction.makeForNative(this, parseInt));
		this.addIntrinsic('parseFloat', EasyNativeFunction.makeForNative(this, parseFloat));
		this.addIntrinsic('isNaN', EasyNativeFunction.makeForNative(this, isNaN));
		this.addIntrinsic('isFinite', EasyNativeFunction.makeForNative(this, isFinite));

		//this.addIntrinsic('Date', this.fromNative(Date));
		this.eval = new EvalFunction(this);
		this.addIntrinsic('eval', this.eval);
		this.addIntrinsic('assert', new AssertClass(this));

		if ( options.runtime ) {
			this.addIntrinsic("setTimeout", new SetTimeoutFunction(this, options.runtime, false));
			this.addIntrinsic("setInterval", new SetTimeoutFunction(this, options.runtime, true));
			this.addIntrinsic("clearTimeout", new ClearTimeoutFunction(this, options.runtime));
			this.addIntrinsic("clearInterval", new ClearTimeoutFunction(this, options.runtime));
		}

		if ( options.esposeESHostGlobal ) {
			this.addIntrinsic("$", new (require('./stdlib/ESHostDollarsign'))(this));
		}
		scope.thiz = scope.object;
		this.importCache = new WeakMap();
		/** @type {Scope} */
		this.globalScope = scope;

		let lang = esper.languages[this.language];
		if ( lang && lang.setupRealm ) lang.setupRealm(this);
	}

	addIntrinsic(name, instance) {
		this.intrinsics[name] = instance;
		this.intrinsicScope.set(name, instance);
	}

	lookupWellKnown(v) {
		if ( v === Object ) return this.Object;
		if ( v === Object.prototype ) return this.ObjectPrototype;
		if ( v === Function ) return this.Function;
		if ( v === Function.prototype ) return this.FunctionPrototype;
		if ( v === Math ) return this.Math;
		if ( v === Number ) return this.Number;
		if ( v === Number.prototype ) return this.NumberPrototype;
		if ( v === String ) return this.String;
		if ( v === String.prototype ) return this.StringPrototype;
		if ( v === Array ) return this.Array;
		if ( v === Array.prototype ) return this.ArrayPrototype;
		if ( v === RegExp ) return this.RegExp;
		if ( v === RegExp.prototype ) return this.RegExpPrototype;
		if ( typeof console !== 'undefined' && v === console ) return this.console;

	}

	lookupWellKnownByName(v) {
		switch ( v ) {
			case '%Object%':  return this.Object;
			case '%ObjectPrototype%':  return this.ObjectPrototype;
			case '%Function%':  return this.Function;
			case '%FunctionPrototype%':  return this.FunctionPrototype;
			case '%Math%':  return this.Math;
			case '%Number%':  return this.Number;
			case '%NumberPrototype%':  return this.NumberPrototype;
			case '%Array%':  return this.Array;
			case '%ArrayPrototype%':  return this.ArrayPrototype;
			case '%RegExp%':  return this.RegExp;
			case '%RegExpPrototype%':  return this.RegExpPrototype;
		}
	}

	fromNative(native, x) {
		return Value.fromNative(native, this);
	}

	import(native, modeHint) {
		if ( native instanceof Value ) return native;
		if ( native === undefined ) return Value.undef;

		let prim = Value.fromPrimativeNative(native);
		if ( prim ) return prim;

		//if ( this.importCache.has(native) ) {
		//	return this.importCache.get(native);
		//}

		if ( Value.hasBookmark(native) ) {
			return Value.getBookmark(native);
		}

		let result;
		switch ( modeHint || this.options.foreignObjectMode ) {
			case 'bridge':
				result = BridgeValue.make(native, this);
				break;
			case 'smart':
				result = SmartLinkValue.make(native, this);
				break;
			case 'link':
			default:
				result = LinkValue.make(native, this);
				break;
		}

		//this.importCache.set(native, result);
		return result;
	}
}

Realm.prototype.makeForForeignObject = Realm.prototype.import;

module.exports = Realm;
<|endoftext|>


<commit_before>'use strict';
const fs = require('fs');
module.exports = function (gameServer, args) {
var walkSync = function(dir, filelist) {

  if( dir[dir.length-1] != '/') dir=dir.concat('/')

  var fs = fs || require('fs'),
      files = fs.readdirSync(dir);
  filelist = filelist || [];
  files.forEach(function(file) {
    if (fs.statSync(dir + file).isDirectory()) {
      filelist = walkSync(dir + file + '/', filelist);
    }
    else {
      filelist.push(dir+file);
    }
  });
  return filelist;
};
var result = [];
var files = walkSync("./");

for (var i in files) {
if (files[i].indexOf("/node_modules/") != -1 || files[i].indexOf("/plugins/") != -1) continue;
  var file = fs.readFileSync(files[i],"utf8");
var startindex = 0;
for (;1==1;) {
 var index = file.indexOf("console.log(\"", startindex) + 13;
var ind = file.indexOf("\");", index);
if (index == 12 || ind == -1) break;
startindex = ind;
var tx = file.substr(index, ind - index);
var split = tx.split("\n");
if (split[1]) continue;

for (;1==1;) {
 var n = tx.indexOf("\"") + 1;
 var c = tx.indexOf("\"",n);
 if (n == 0 || c == -1) break;
  tx = tx.slice(n,c);
}
result.push(tx);
}
}
var final = "";
for (var i in result) {
  final = final + result[i] + "\n";
}
fs.writeFileSync("console.txt",final);
}
<commit_msg>Update consolelogfinder.js<commit_after>'use strict';
const fs = require('fs');
module.exports = function (gameServer, args) {
var walkSync = function(dir, filelist) {

  if( dir[dir.length-1] != '/') dir=dir.concat('/')

  var fs = fs || require('fs'),
      files = fs.readdirSync(dir);
  filelist = filelist || [];
  files.forEach(function(file) {
    if (fs.statSync(dir + file).isDirectory()) {
      filelist = walkSync(dir + file + '/', filelist);
    }
    else {
      filelist.push(dir+file);
    }
  });
  return filelist;
};
var result = [];
var files = walkSync("./");

for (var i in files) {
if (files[i].indexOf("/node_modules/") != -1 || files[i].indexOf("/plugins/") != -1) continue;
  var file = fs.readFileSync(files[i],"utf8");
var startindex = 0;
for (;1==1;) {
 var index = file.indexOf("console.log(\"", startindex) + 13;
var ind = file.indexOf("\");", index);
if (index == 12 || ind == -1) break;
startindex = ind;
var tx = file.substr(index, ind - index);
var split = tx.split("\n");
if (split[1]) continue;

for (;1==1;) {
 var n = tx.indexOf("\"") + 1;
 var c = tx.indexOf("\"",n);
 if (n == 0 || c == -1) break;
 tx = tx.replace(tx.slice(n,c),"");
}
result.push(tx);
}
}
var final = "";
for (var i in result) {
  final = final + result[i] + "\n";
}
fs.writeFileSync("console.txt",final);
}
<|endoftext|>


<commit_before><commit_msg>Fix minor formatting<commit_after><|endoftext|>


<commit_before>angular.module('PlaylistDetailCtrl', [])

.controller('PlaylistDetailCtrl', function($scope, $state, $stateParams, Spotify, $ionicNavBarDelegate, $ionicPopup, $ionicPlatform, $ionicHistory, $cordovaToast) {
  console.log('playlist detail control instantiated')

  // Grab variables from route paramaters
  // This is how I pass the information from PlaylistCtrl
  let listid = $stateParams.listid
  let userid = $stateParams.userid
  $scope.playlistTitle = $stateParams.listTitle

  $scope.editMode = false // toggled on and off
  $scope.changesMade = false // turned to true when first edit made
  $scope.orderCriteria = "none"
  $scope.descending = false
  let editLog = []

  $scope.tracks = []
  getTracks() // load tracks immediately

  /**
   * Get tracks for playlist using playlist id and user id
   */
  function getTracks() {
    Spotify.getPlaylist(userid, listid)
      .then(data => {
        $scope.tracks = data.tracks.items
      }).catch(error => {
        console.dir(error)
      })
  }

  /**
   * Utility functions for entering / exiting edit mode
   */
  function resetSortOptions() {
    $scope.orderCriteria = 'none'
    $scope.descending = false
  }

  function enterEditMode() {
    resetSortOptions()
    $scope.editMode = true
  }

  function exitEditMode() {
    resetSortOptions()
    $scope.editMode = false
  }

  /**
   * Display toast at bottom saying 'playlist saved'
   * Function called whenever playlist is saved
   */
  function showPlaylistSavedToast() {
    $cordovaToast.showWithOptions({
      message: "Playlist saved",
      duration: "short",
      position: "bottom",
      addPixelsY: -175  // move up above tabs
    }).catch(error => {
      console.log(error)
    })
  }

  /**
   * code to execute when user taps edit button
   * Calls saveChanges if attmpting to exiting edit mode
   * Toggles edit mode ON if not in edit mode
   */
  $scope.onEditButtonTap = function() {
    console.log('edit button tap')
    // If leaving edit mode...
    if($scope.editMode) {
      $scope.saveChanges()
    } else {
      enterEditMode()
    }
  }

  /**
   * Called when item is reordered on DOM
   * @param  {obj} item - object containing track info and metadata
   * @param  {integer} fromIndex - index where the object was in $scope.tracks
   * @param  {integer} toIndex - index where the object was moved to in $scope.tracks
   */
  $scope.onItemMove = function(item, fromIndex, toIndex) {
    $scope.tracks.splice(fromIndex, 1)
    $scope.tracks.splice(toIndex, 0, item)
    $scope.$apply()

    editLog.push({
      type: 'move',
      uri: item.track.uri,
      fromIndex,
      toIndex
    })

    $scope.changesMade = true
  }

  /**
   * Called when item deleted in DOM
   * @param  {obj} item - object containing track info and metadata
   */
  $scope.onItemDelete = function(item) {
    $scope.tracks.splice($scope.tracks.indexOf(item), 1)

    editLog.push({
      type: 'delete',
      uri: item.track.uri
    })

    $scope.changesMade = true
  }

   /**
   * Sorts $scope.tracks on different criteria
   * @param  {string} orderCriteria - value of select option
   */
  $scope.onSelectChange = function(orderCriteria) {
    $scope.orderCriteria = orderCriteria
    $scope.changesMade = true
    orderSongs()
  }

  /**
   * Run when user selects toggles the "descending" option
   * @param  {boolean} descending - state of the "descending" toggle
   */
  $scope.onDescendToggle = function(descending) {
    $scope.descending = descending
    $scope.changesMade = true
    orderSongs()
  }

  /**
   * attempts post of new playlist state to Spotify if changes were made
   */
  $scope.saveChanges = function() {
    if($scope.changesMade === false) {
      console.log('no changes made')
      exitEditMode()
    } else {
      console.log('save changes')
      commitChange()
      // let uris = []
      // $scope.tracks.forEach(item => uris.push(item.track.uri))
      // Spotify
      //   .replacePlaylistTracks(userid, listid, uris)
      //   .then(data => {
      //     $scope.changesMade = false // reset after save
      //     exitEditMode()
      //     showPlaylistSavedToast()
      //   })
      //   .catch(error => {
      //     alert('There was an error saving changes.  Please try again.')
      //     console.dir(error)
      //   })
    }
  }

  let editCounter = 0
  function commitChange() {
    if(editCounter >= editLog.length) {
      editCounter = 0
      editLog = []
      return
    }
    if(editLog[editCounter].type === "delete") {
      Spotify
        .removePlaylistTracks(userid, listid, editLog[editCounter].uri)
        .then(() => {
          console.log(`${editCounter}: delete sent`)
          editCounter++
          commitChange()
        })
    } else if(editLog[editCounter].type === "move") {
      Spotify.reorderPlaylistTracks(userid, listid, {
        range_start: editLog[editCounter].fromIndex,
        insert_before: editLog[editCounter].toIndex + 1
      }).then(() => {
        console.log(`${editCounter}: move sent`)
        editCounter++
        commitChange()
      })
    }
  }

  /**
   * Code run when user selects "discard changes"
   * Gets current tracklist from Spotify and updates DOM
   */
  $scope.cancelChanges = function() {
    console.log('cancel changes')

    getTracks()
    exitEditMode()
  }

  /**
   * Called whenever "order by" is changed or "descnding" is changed
   * Sorts based on order criteria and whether user wants them to descend or not
   */
  function orderSongs() {
    console.log("$scope.orderCriteria", $scope.orderCriteria)
    console.log("$scope.descending", $scope.descending)

    let returnVal
    if($scope.descending === false) returnVal = -1
    else returnVal = 1

    switch($scope.orderCriteria) {
      case 'song':
        $scope.tracks.sort((a, b) => {
          if (a.track.name < b.track.name) return returnVal;
          if (a.track.name > b.track.name) return -returnVal;
          return 0;
        })
        break
      case 'artist':
        $scope.tracks.sort((a, b) => {
          if (a.track.artists[0].name < b.track.artists[0].name) return returnVal;
          if (a.track.artists[0].name > b.track.artists[0].name) return -returnVal;
          return 0;
        })
        break
      case 'album':
        $scope.tracks.sort((a, b) => {
          if (a.track.album.name < b.track.album.name) return returnVal;
          if (a.track.album.name > b.track.album.name) return -returnVal;
          return 0;
        })
        break
      case 'length':
        $scope.tracks.sort((a, b) => {
          if (a.track.duration_ms < b.track.duration_ms) return returnVal;
          if (a.track.duration_ms > b.track.duration_ms) return -returnVal;
          return 0;
        })
        break
    }
  } // end orderSongs()

  // Event Listeners
  // Save changes on exit app or view change
  $scope.$on("$ionicView.leave", $scope.saveChanges);
  $ionicPlatform.on('pause', $scope.saveChanges);
})

<commit_msg>descending reorder buggy<commit_after>angular.module('PlaylistDetailCtrl', [])

.controller('PlaylistDetailCtrl', function($scope, $state, $stateParams, Spotify, $ionicNavBarDelegate, $ionicPopup, $ionicPlatform, $ionicHistory, $cordovaToast) {
  console.log('playlist detail control instantiated')

  // Grab variables from route paramaters
  // This is how I pass the information from PlaylistCtrl
  let listid = $stateParams.listid
  let userid = $stateParams.userid
  $scope.playlistTitle = $stateParams.listTitle

  $scope.editMode = false // toggled on and off
  $scope.changesMade = false // turned to true when first edit made
  $scope.orderCriteria = "none"
  $scope.descending = false
  let editLog = []

  $scope.tracks = []
  getTracks() // load tracks immediately

  /**
   * Get tracks for playlist using playlist id and user id
   */
  function getTracks() {
    Spotify.getPlaylist(userid, listid)
      .then(data => {
        $scope.tracks = data.tracks.items
      }).catch(error => {
        console.dir(error)
      })
  }

  /**
   * Utility functions for entering / exiting edit mode
   */
  function resetSortOptions() {
    $scope.orderCriteria = 'none'
    $scope.descending = false
  }

  function enterEditMode() {
    resetSortOptions()
    $scope.editMode = true
  }

  function exitEditMode() {
    resetSortOptions()
    $scope.editMode = false
  }

  /**
   * Display toast at bottom saying 'playlist saved'
   * Function called whenever playlist is saved
   */
  function showPlaylistSavedToast() {
    $cordovaToast.showWithOptions({
      message: "Playlist saved",
      duration: "short",
      position: "bottom",
      addPixelsY: -175  // move up above tabs
    }).catch(error => {
      console.log(error)
    })
  }

  /**
   * code to execute when user taps edit button
   * Calls saveChanges if attmpting to exiting edit mode
   * Toggles edit mode ON if not in edit mode
   */
  $scope.onEditButtonTap = function() {
    console.log('edit button tap')
    // If leaving edit mode...
    if($scope.editMode) {
      $scope.saveChanges()
    } else {
      enterEditMode()
    }
  }

  /**
   * Called when item is reordered on DOM
   * @param  {obj} item - object containing track info and metadata
   * @param  {integer} fromIndex - index where the object was in $scope.tracks
   * @param  {integer} toIndex - index where the object was moved to in $scope.tracks
   */
  $scope.onItemMove = function(item, fromIndex, toIndex) {
    $scope.tracks.splice(fromIndex, 1)
    $scope.tracks.splice(toIndex, 0, item)
    $scope.$apply()

    editLog.push({
      type: 'move',
      uri: item.track.uri,
      fromIndex,
      toIndex
    })

    $scope.changesMade = true
  }

  /**
   * Called when item deleted in DOM
   * @param  {obj} item - object containing track info and metadata
   */
  $scope.onItemDelete = function(item) {
    $scope.tracks.splice($scope.tracks.indexOf(item), 1)

    editLog.push({
      type: 'delete',
      uri: item.track.uri
    })

    $scope.changesMade = true
  }

   /**
   * Sorts $scope.tracks on different criteria
   * @param  {string} orderCriteria - value of select option
   */
  $scope.onSelectChange = function(orderCriteria) {
    $scope.orderCriteria = orderCriteria
    $scope.changesMade = true
    orderSongs()
  }

  /**
   * Run when user selects toggles the "descending" option
   * @param  {boolean} descending - state of the "descending" toggle
   */
  $scope.onDescendToggle = function(descending) {
    $scope.descending = descending
    $scope.changesMade = true
    orderSongs()
  }

  /**
   * attempts post of new playlist state to Spotify if changes were made
   */
  $scope.saveChanges = function() {
    if($scope.changesMade === false) {
      console.log('no changes made')
      exitEditMode()
    } else {
      console.log('save changes')
      commitChange()
      // let uris = []
      // $scope.tracks.forEach(item => uris.push(item.track.uri))
      // Spotify
      //   .replacePlaylistTracks(userid, listid, uris)
      //   .then(data => {
      //     $scope.changesMade = false // reset after save
      //     exitEditMode()
      //     showPlaylistSavedToast()
      //   })
      //   .catch(error => {
      //     alert('There was an error saving changes.  Please try again.')
      //     console.dir(error)
      //   })
    }
  }

  let editCounter = 0
  function commitChange() {
    if(editCounter >= editLog.length) {
      editCounter = 0
      editLog = []
      return
    }
    if(editLog[editCounter].type === "delete") {
      Spotify
        .removePlaylistTracks(userid, listid, editLog[editCounter].uri)
        .then(() => {
          console.log(`${editCounter}: delete sent`)
          editCounter++
          commitChange()
        })
    } else if(editLog[editCounter].type === "move") {
      Spotify.reorderPlaylistTracks(userid, listid, {
        range_start: editLog[editCounter].fromIndex,
        insert_before: editLog[editCounter].toIndex + 1
      }).then(() => {
        console.log(`${editCounter}: move sent`)
        console.log("editLog[editCounter].fromIndex", editLog[editCounter].fromIndex)
        console.log("editLog[editCounter].toIndex", editLog[editCounter].toIndex)
        editCounter++
        commitChange()
      })
    }
  }

  /**
   * Code run when user selects "discard changes"
   * Gets current tracklist from Spotify and updates DOM
   */
  $scope.cancelChanges = function() {
    console.log('cancel changes')

    getTracks()
    exitEditMode()
  }

  /**
   * Called whenever "order by" is changed or "descnding" is changed
   * Sorts based on order criteria and whether user wants them to descend or not
   */
  function orderSongs() {
    console.log("$scope.orderCriteria", $scope.orderCriteria)
    console.log("$scope.descending", $scope.descending)

    let returnVal
    if($scope.descending === false) returnVal = -1
    else returnVal = 1

    switch($scope.orderCriteria) {
      case 'song':
        $scope.tracks.sort((a, b) => {
          if (a.track.name < b.track.name) return returnVal;
          if (a.track.name > b.track.name) return -returnVal;
          return 0;
        })
        break
      case 'artist':
        $scope.tracks.sort((a, b) => {
          if (a.track.artists[0].name < b.track.artists[0].name) return returnVal;
          if (a.track.artists[0].name > b.track.artists[0].name) return -returnVal;
          return 0;
        })
        break
      case 'album':
        $scope.tracks.sort((a, b) => {
          if (a.track.album.name < b.track.album.name) return returnVal;
          if (a.track.album.name > b.track.album.name) return -returnVal;
          return 0;
        })
        break
      case 'length':
        $scope.tracks.sort((a, b) => {
          if (a.track.duration_ms < b.track.duration_ms) return returnVal;
          if (a.track.duration_ms > b.track.duration_ms) return -returnVal;
          return 0;
        })
        break
    }
  } // end orderSongs()

  // Event Listeners
  // Save changes on exit app or view change
  $scope.$on("$ionicView.leave", $scope.saveChanges);
  $ionicPlatform.on('pause', $scope.saveChanges);
})

<|endoftext|>