<commit_before>'use strict'

const WebSocket = require('ws')
const ClientAbstract = require('./ClientAbstract')
const debug = require('debug')
const {inspect} = require('util')
const maybestack = require('maybestack')

class Client extends ClientAbstract {
  constructor (address, options = {}) {
    super()
    this._d = debug('socketeer:Client')
    this._constructOpts = [address, options.protocols, options.ws]
    this._d(`constructing websocket with options: ${inspect(this._constructOpts)}`)

    this.heartbeatTimeout = options.heartbeatTimeout || 15000
    this.reconnectWait = options.reconnectWait || 5000
    this.failless = (options.failless !== false)
    this._d(`heartbeat timeout set to ${this.heartbeatTimeout}`)
    this._d(`reconnect wait set to ${this.reconnectWait}`)
    this._d(`failless set to ${this.failless}`)

    this.closed = false
    this.ready = false
    this.isResume = false

    if (this.failless) {
      this._d('[failless] adding client error handler')
      this.on('error', (err) => {
        this._d(`[failless] handling client error: ${maybestack(err)}`)
      })
    }

    this._createWebsocket()
    this._attachEvents()
  }

  _createWebsocket () {
    let o = this._constructOpts
    this.ws = new WebSocket(o[0], o[1], o[2])
  }

  _attachEvents () {
    this._d('attaching events')
    this.ws.once('open', this._handleOpen.bind(this))
    super._attachEvents()
  }

  _handleOpen () {
    this._d('handling open')
    this._emit('_open', this.isReconnection)
    this._handshakeStage = 1
    this._beginHandshake()
  }

  _beginHandshake () {
    this.ws.send('v' + this._PROTOCOL_VERSION)
  }

  _handleMessage (data, flags) {
    this._d('handling message')
    if (!this.isOpen()) {
      this._d('ignoring message, as socket is not open')
      return
    }

    if (!this.ready) {
      switch (this._handshakeStage) {
        case 1:
          return this._handleStage1(data, flags)
        case 2:
          return this._handleStage2(data, flags)
        default:
          let err = new Error('[internal] unknown handshake stage: ' + this._handshakeStage)
          this._handleError(err)
          return
      }
    } else {
      if (data === 'h') {
        this._d('message is heartbeat')
        this._handleHeartbeat()
        return
      }

      super._handleMessage(data, flags)
    }
  }

  _handleStage1 (data, flags) {
    this._d('handling handshake stage 1')
    if (typeof data !== 'string' || data.indexOf('hi') !== 0) {
      this._d('stage 1 unexpected data, killing connection')
      return this.kill()
    }
    this._d('message is heartbeat interval')
    data = data.replace('hi', '')
    let interval = Math.floor(+data)
    if (Number.isNaN(interval) || interval < 1 || interval > 2147483647) {
      this._handleError(new Error('invalid heartbeat interval from server'))
      return
    }
    this.heartbeatInterval = interval
    this._d(`heartbeat interval set to ${this.heartbeatInterval}`)
    this._resetHeartbeatTimeout()
    this._handshakeSessionResume()
  }

  _resetHeartbeatTimeout () {
    let timeoutPeriod = this.heartbeatInterval + this.heartbeatTimeout
    this._d(`resetting heartbeat timeout: ${timeoutPeriod}`)

    this._stopHeartbeatTimeout()

    this._heartbeatTimer = setTimeout(() => {
      this._d('heartbeat timeout called')
      // This means that the server took too long to send a timeout.
      this._emit('timeout')
      // Terminate the connection because it timed out: there's no
      //  point to doing a formal close, since that is also likely to
      //  time out.
      this.kill()
    }, timeoutPeriod)
  }

  _stopHeartbeatTimeout () {
    this._d('trying to stop heartbeat timeout')
    if (this._heartbeatTimer) {
      this._d('stopping heartbeat timeout')
      clearTimeout(this._heartbeatTimer)
    }
  }

  _handshakeSessionResume () {
    if (this._resumeToken) {
      this._d('sending session resume token: ' + this._resumeToken)
      this.ws.send('r@' + this._resumeToken)
    } else {
      this._d('querying for session resume token')
      this.ws.send('r?')
    }
    this._handshakeStage = 2
  }

  _handleStage2 (data, flags) {
    this._d('handling handshake stage 2')
    if (this.isResume) {
      if (typeof data === 'string' && data.indexOf('r+') === 0) {
        let reg = /^r\+(.*)$/.exec(String(data))
        let token = String(reg[1])
        if (token.length < 5 || token.length > 200) {
          this._d('resume token length invalid, killing connection')
          return this.kill()
        } else {
          this._d('session resume ok with new token: ' + token)
          this._resumeToken = token
          this.isResume = true
        }
      } else if (data === 'r-') {
        this._d('session resume failed')
        this.isResume = false
        this._resumeToken = null
      } else {
        this._d('got unexpected data, killing connection')
        return this.kill()
      }
    } else {
      if (data === 'r') {
        this._d('server does not support session resuming')
      } else if (typeof data === 'string' && data.indexOf('r:') === 0) {
        let reg = /^r\:(.*)$/.exec(String(data))
        let token = String(reg[1])
        if (token.length < 5 || token.length > 200) {
          this._d('resume token length invalid, killing connection')
          return this.kill()
        } else {
          this._d('set resume token: ' + token)
          this._resumeToken = token
          this.isResume = false
        }
      }
    }
    this._markReady()
  }

  _markReady () {
    this._handshakeStage = null
    this.ready = true
    this._emit('open', this.isReconnection)
  }

  _handleHeartbeat () {
    this._d('handling heartbeat')
    this._resetHeartbeatTimeout()
    this.ws.send('h')
  }

  /**
   * Resets the heartbeat timeout by stopping any existing timeout,
   * and starting a new one. If the timeout function runs, the 'timeout' event
   * is emitted from the client.
   * @private
   */

  /**
   * Stops the existing heartbeat timeout, if any.
   * @private
   */

  /**
   * Handles the WebSocket 'close' event.
   *
   * This is where the client's 'ready' property is set to false,
   * and 'closed' property set to true.
   * @private
   * @param  code    Close code.
   * @param  message Close message.
   * @param  error   Error, if closed due to a socket error.
   */
  _handleClose (code, message, error) {
    this._d('handling close')
    if (!this.ready) {
      this._d('connection closed before handshake finished')
      this._emit('error', new Error('connection closed before handshake could complete'))
    }
    this.ready = false
    this.closed = true
    this._stopHeartbeatTimeout()
    super._handleClose(code, message, error)
  }

  /**
   * Emits an event or an action, depending on the
   * existence of the callback argument.
   * @param  {String}   name      Action/event name.
   * @param             data      Action/event data.
   * @param  {Function} callback  Action callback for responses.
   * @throws Will throw an error if the client is not ready.
   */
  emit (name, data, callback) {
    this._d(`emitting ${name}`)
    if (!this.ready) throw new Error('client is not ready')
    super.emit(name, data, callback)
  }

  /**
   * Reconnects to the server.
   * Waits 'reconnectWait' ms before re-establishing connection,
   * unless 'immediate' is true.
   * Will not do anything if a reconnection is already scheduled.
   * @param  {Boolean} immediate  Whether to not wait before
   *                              re-establishing connection.
   * @throws Will throw an error if the connection hasn't closed yet.
   */
  reconnect (immediate) {
    this._d('trying reconnect')
    if (!this.closed) {
      this._d('not closed, not going to reconnect')
      throw new Error('client has not disconnected to reconnect yet')
    }
    if (this.willReconnect) return
    this.willReconnect = true
    // Clear out the resume token because we are not
    // going to resume the session.
    this._resumeToken = null
    let timeout = (immediate ? 0 : this.reconnectWait)
    this._d(`will reconnect in ${timeout} ms`)
    setTimeout(() => {
      this._d('reconnecting')
      this.willReconnect = false
      this.closed = false
      this.isReconnection = true
      this._createWebsocket()
      this._attachEvents()
    }, timeout)
  }
}

module.exports = Client
<commit_msg>add handshake timeout option<commit_after>'use strict'

const WebSocket = require('ws')
const ClientAbstract = require('./ClientAbstract')
const debug = require('debug')
const {inspect} = require('util')
const maybestack = require('maybestack')

class Client extends ClientAbstract {
  constructor (address, options = {}) {
    super()
    this._d = debug('socketeer:Client')
    this._constructOpts = [address, options.protocols, options.ws]
    this._d(`constructing websocket with options: ${inspect(this._constructOpts)}`)

    this.heartbeatTimeout = options.heartbeatTimeout || 15000
    this.reconnectWait = options.reconnectWait || 5000
    this.handshakeTimeout = options.handshakeTimeout || this.heartbeatTimeout
    this.failless = (options.failless !== false)
    this._d(`heartbeat timeout set to ${this.heartbeatTimeout}`)
    this._d(`reconnect wait set to ${this.reconnectWait}`)
    this._d(`failless set to ${this.failless}`)

    this.closed = false
    this.ready = false
    this.isResume = false

    if (this.failless) {
      this._d('[failless] adding client error handler')
      this.on('error', (err) => {
        this._d(`[failless] handling client error: ${maybestack(err)}`)
      })
    }

    this._createWebsocket()
    this._attachEvents()
  }

  _createWebsocket () {
    let o = this._constructOpts
    this.ws = new WebSocket(o[0], o[1], o[2])
  }

  _attachEvents () {
    this._d('attaching events')
    this.ws.once('open', this._handleOpen.bind(this))
    super._attachEvents()
  }

  _handleOpen () {
    this._d('handling open')
    this._emit('_open', this.isReconnection)
    this._handshakeStage = 1
    // TODO: handshake timeout
    this._beginHandshake()
  }

  _beginHandshake () {
    this.ws.send('v' + this._PROTOCOL_VERSION)
  }

  _handleMessage (data, flags) {
    this._d('handling message')
    if (!this.isOpen()) {
      this._d('ignoring message, as socket is not open')
      return
    }

    if (!this.ready) {
      switch (this._handshakeStage) {
        case 1:
          return this._handleStage1(data, flags)
        case 2:
          return this._handleStage2(data, flags)
        default:
          let err = new Error('[internal] unknown handshake stage: ' + this._handshakeStage)
          this._handleError(err)
          return
      }
    } else {
      if (data === 'h') {
        this._d('message is heartbeat')
        this._handleHeartbeat()
        return
      }

      super._handleMessage(data, flags)
    }
  }

  _handleStage1 (data, flags) {
    this._d('handling handshake stage 1')
    if (typeof data !== 'string' || data.indexOf('hi') !== 0) {
      this._d('stage 1 unexpected data, killing connection')
      return this.kill()
    }
    this._d('message is heartbeat interval')
    data = data.replace('hi', '')
    let interval = Math.floor(+data)
    if (Number.isNaN(interval) || interval < 1 || interval > 2147483647) {
      this._handleError(new Error('invalid heartbeat interval from server'))
      return
    }
    this.heartbeatInterval = interval
    this._d(`heartbeat interval set to ${this.heartbeatInterval}`)
    this._resetHeartbeatTimeout()
    this._handshakeSessionResume()
  }

  _resetHeartbeatTimeout () {
    let timeoutPeriod = this.heartbeatInterval + this.heartbeatTimeout
    this._d(`resetting heartbeat timeout: ${timeoutPeriod}`)

    this._stopHeartbeatTimeout()

    this._heartbeatTimer = setTimeout(() => {
      this._d('heartbeat timeout called')
      // This means that the server took too long to send a timeout.
      this._emit('timeout')
      // Terminate the connection because it timed out: there's no
      //  point to doing a formal close, since that is also likely to
      //  time out.
      this.kill()
    }, timeoutPeriod)
  }

  _stopHeartbeatTimeout () {
    this._d('trying to stop heartbeat timeout')
    if (this._heartbeatTimer) {
      this._d('stopping heartbeat timeout')
      clearTimeout(this._heartbeatTimer)
    }
  }

  _handshakeSessionResume () {
    if (this._resumeToken) {
      this._d('sending session resume token: ' + this._resumeToken)
      this.ws.send('r@' + this._resumeToken)
    } else {
      this._d('querying for session resume token')
      this.ws.send('r?')
    }
    this._handshakeStage = 2
  }

  _handleStage2 (data, flags) {
    this._d('handling handshake stage 2')
    if (this.isResume) {
      if (typeof data === 'string' && data.indexOf('r+') === 0) {
        let reg = /^r\+(.*)$/.exec(String(data))
        let token = String(reg[1])
        if (token.length < 5 || token.length > 200) {
          this._d('resume token length invalid, killing connection')
          return this.kill()
        } else {
          this._d('session resume ok with new token: ' + token)
          this._resumeToken = token
          this.isResume = true
        }
      } else if (data === 'r-') {
        this._d('session resume failed')
        this.isResume = false
        this._resumeToken = null
      } else {
        this._d('got unexpected data, killing connection')
        return this.kill()
      }
    } else {
      if (data === 'r') {
        this._d('server does not support session resuming')
      } else if (typeof data === 'string' && data.indexOf('r:') === 0) {
        let reg = /^r\:(.*)$/.exec(String(data))
        let token = String(reg[1])
        if (token.length < 5 || token.length > 200) {
          this._d('resume token length invalid, killing connection')
          return this.kill()
        } else {
          this._d('set resume token: ' + token)
          this._resumeToken = token
          this.isResume = false
        }
      }
    }
    this._markReady()
  }

  _markReady () {
    this._handshakeStage = null
    this.ready = true
    this._emit('open', this.isReconnection)
  }

  _handleHeartbeat () {
    this._d('handling heartbeat')
    this._resetHeartbeatTimeout()
    this.ws.send('h')
  }

  /**
   * Resets the heartbeat timeout by stopping any existing timeout,
   * and starting a new one. If the timeout function runs, the 'timeout' event
   * is emitted from the client.
   * @private
   */

  /**
   * Stops the existing heartbeat timeout, if any.
   * @private
   */

  /**
   * Handles the WebSocket 'close' event.
   *
   * This is where the client's 'ready' property is set to false,
   * and 'closed' property set to true.
   * @private
   * @param  code    Close code.
   * @param  message Close message.
   * @param  error   Error, if closed due to a socket error.
   */
  _handleClose (code, message, error) {
    this._d('handling close')
    if (!this.ready) {
      this._d('connection closed before handshake finished')
      this._emit('error', new Error('connection closed before handshake could complete'))
    }
    this.ready = false
    this.closed = true
    this._stopHeartbeatTimeout()
    super._handleClose(code, message, error)
  }

  /**
   * Emits an event or an action, depending on the
   * existence of the callback argument.
   * @param  {String}   name      Action/event name.
   * @param             data      Action/event data.
   * @param  {Function} callback  Action callback for responses.
   * @throws Will throw an error if the client is not ready.
   */
  emit (name, data, callback) {
    this._d(`emitting ${name}`)
    if (!this.ready) throw new Error('client is not ready')
    super.emit(name, data, callback)
  }

  /**
   * Reconnects to the server.
   * Waits 'reconnectWait' ms before re-establishing connection,
   * unless 'immediate' is true.
   * Will not do anything if a reconnection is already scheduled.
   * @param  {Boolean} immediate  Whether to not wait before
   *                              re-establishing connection.
   * @throws Will throw an error if the connection hasn't closed yet.
   */
  reconnect (immediate) {
    this._d('trying reconnect')
    if (!this.closed) {
      this._d('not closed, not going to reconnect')
      throw new Error('client has not disconnected to reconnect yet')
    }
    if (this.willReconnect) return
    this.willReconnect = true
    // Clear out the resume token because we are not
    // going to resume the session.
    this._resumeToken = null
    let timeout = (immediate ? 0 : this.reconnectWait)
    this._d(`will reconnect in ${timeout} ms`)
    setTimeout(() => {
      this._d('reconnecting')
      this.willReconnect = false
      this.closed = false
      this.isReconnection = true
      this._createWebsocket()
      this._attachEvents()
    }, timeout)
  }
}

module.exports = Client
<|endoftext|>


<commit_before>/* eslint-disable */

describe('Utils Library', () => {
  describe('objectSize Method', () => {
    const {objectSize} = require('../utils');

    test('should return true when correct object size calculated', () => {
      let obj = {
        1: '1',
        2: '2',
        3: '3',
        4: '4',
        5: '5',
        6: '6'
      }

      let result = objectSize(obj);

      expect(typeof result).toEqual('number');
      expect(result).toEqual(6);
    });
  });

  describe('Data Type Checking', () => {
    const {isObject, isFunction, isString, isArray, isUndefined} = require('../utils');
    let testObject;
    let testString;
    let testNumber;
    let testFunc;
    let testArray;
    let testUndefined;

    beforeAll(() => {
      testObject = {value: 'One'};
      testString = 'Two';
      testNumber = 3;
      testFunc = function() {};
      testArray = [4, 5];
      testUndefined = undefined;
    });

    // isObject Function
    describe('isObject Method', () => {
      test('should return true if object type is passed in', () => {
        expect(isObject(testObject)).toEqual(true);
      });

      test('should return false if object type is not passed in', () => {
        expect(isObject(testString)).toEqual(false);
        expect(isObject(testNumber)).toEqual(false);
        expect(isObject(testFunc)).toEqual(false);
        expect(isObject(testArray)).toEqual(false);
        expect(isObject(testUndefined)).toEqual(false);
      });
    });

    // isFunction Function
    describe('isFunction Method', () => {
      test('should return true if function type is passed in', () => {
        expect(isFunction(testFunc)).toEqual(true);
      });

      test('should return false if function type is not passed in', () => {
        expect(isFunction(testString)).toEqual(false);
        expect(isFunction(testNumber)).toEqual(false);
        expect(isFunction(testObject)).toEqual(false);
        expect(isFunction(testArray)).toEqual(false);
        expect(isFunction(testUndefined)).toEqual(false);
      });
    });

    // isString Function
    describe('isString Method', () => {
      test('should return true if string type is passed in', () => {
        expect(isString(testString)).toEqual(true);
      });

      test('should return false if string type is not passed in', () => {
        expect(isString(testFunc)).toEqual(false);
        expect(isString(testNumber)).toEqual(false);
        expect(isString(testObject)).toEqual(false);
        expect(isString(testArray)).toEqual(false);
        expect(isString(testUndefined)).toEqual(false);
      });
    });

    // isArray Function
    describe('isArray Method', () => {
      test('should return true if array type is passed in', () => {
        expect(isArray(testArray)).toEqual(true);
      });

      test('should return false if array type is not passed in', () => {
        expect(isArray(testFunc)).toEqual(false);
        expect(isArray(testNumber)).toEqual(false);
        expect(isArray(testObject)).toEqual(false);
        expect(isArray(testString)).toEqual(false);
        expect(isArray(testUndefined)).toEqual(false);
      });
    });

    // isUndefined Function
    describe('isUndefined Method', () => {
      test('should return true if udefined type is passed in', () => {
        expect(isUndefined(testUndefined)).toEqual(true);
      });

      test('should return false if undefined type is not passed in', () => {
        expect(isUndefined(testFunc)).toEqual(false);
        expect(isUndefined(testNumber)).toEqual(false);
        expect(isUndefined(testObject)).toEqual(false);
        expect(isUndefined(testString)).toEqual(false);
        expect(isUndefined(testArray)).toEqual(false);
      });
    });
  });

  describe('bind & bindAll Methods', () => {
    const {bind, bindAll} = require('../utils');

    test('should return true if function bind to itself', () => {
      function testFunc() { }
      let testObj = { testFunc };

      expect(testObj.testFunc.prototype).toBeDefined();
      bind(testObj, 'testFunc');
      // If bind correctly, prototype should be undefined
      expect(testObj.testFunc.prototype).not.toBeDefined();
    });

    test('should return true if function bindAll to itself', () => {
      function testFunc1() { }
      function testFunc2() { }
      function testFunc3() { }
      let testObj = { testFunc1, testFunc2, testFunc3 };

      expect(testObj.testFunc1.prototype).toBeDefined();
      expect(testObj.testFunc2.prototype).toBeDefined();
      expect(testObj.testFunc3.prototype).toBeDefined();

      bindAll(testObj);

      // If bind correctly, prototype should be undefined
      expect(testObj.testFunc1.prototype).not.toBeDefined();
      expect(testObj.testFunc2.prototype).not.toBeDefined();
      expect(testObj.testFunc3.prototype).not.toBeDefined();
    });
  });

  describe('mergeDeep Method', () => {
    const {mergeDeep} = require('../utils');

    test('should return true if merge 2 objects properlly', () => {
      const testObj1 = {
        name1: 'Test',
        value1: 'Testing',
        age1: 30,
        testFunc1: function() {}
      };

      const testObj2 = {
        name2: 'Test 2',
        value2: 'Testing 2',
        age2: 35,
        testFunc2: function() {}
      };

      expect(testObj1).not.toEqual(testObj2);

      let result = mergeDeep(testObj1, testObj2);

      expect(result.name1).toBeDefined();
      expect(result.name2).toBeDefined();
      expect(result.age1).toBeDefined();
      expect(result.age2).toBeDefined();
      expect(result.testFunc1).toBeDefined();
      expect(result.testFunc2).toBeDefined();
    });
  });

  describe('deepCopy', function(){
    const {deepCopy} = require('../utils');
    test('should export a function', function() {
      expect(typeof deepCopy).toBe('function');
    });

    test('should return array type if input is an array', function() {
      let obj1 = [
        {
          name: 'test1',
          test1: false
        },
        {
          name: 'test2',
          test2: true
        }
      ];

      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
    });

    test('should return object type if input is an object', function() {
      let obj1 = {
          name: 'test1',
          test1: false
        };
      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
    });

    test('should return object type if input is a date', function() {
      let obj1 = {
        d: new Date()
      }
      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
      expect(result.d).toEqual(obj1.d);
    });

    test('should deeply copy an object', function() {
      let obj = {
        name: 'test',
        test: false,
        config: {
          host: 'test.com',
          port: 3000,
          deepConfig: {
            db: 0,
            ssl: false
          },
          cache: {
            type: 'test'
          }
        }
      };

      let result = deepCopy(obj);

      expect(result.config.port).toBe(obj.config.port);
      expect(result.config.deepConfig.ssl).toBe(obj.config.deepConfig.ssl);
    });

    test('should deeply copy an array of objects', function() {
      let arr = [
        {
          name: 'test',
          test: false,
          config: {
            host: 'test.com',
            port: 3000,
            deepConfig: {
              db: 0,
              ssl: false
            },
            cache: {
              type: 'test'
            }
          }
        },
        {
          name: 'test2',
          test: false,
          config: {
            host: 'test2.com',
            port: 4000,
            deepConfig: {
              db: 1,
              ssl: true
            },
            cache: {
              type: 'test2'
            }
          }
        }
      ];

      let result = deepCopy(arr);

      expect(result.length).toEqual(arr.length);
      expect(result[0].config.port).toBe(arr[0].config.port);
      expect(result[1].config.deepConfig.ssl).toBe(arr[1].config.deepConfig.ssl);
    });

    test('should copy the object correctly, not reference copy', function() {
      // Making sure it's actually copy
      // and not reference copy
      let obj = {
        first: 'test 1',
        second: true
      };

      let result = deepCopy(obj);

      // There are not a same object in memory
      expect(result).not.toBe(obj);
      expect(result.first).toEqual(obj.first);

      // Now changing the source object,
      // but result object should not be changed
      obj.second = false;

      expect(result.second).not.toEqual(obj.second);
    });

    test('should copy the object correctly, not mutate', function() {
      // Mutating a source object should have
      // no effect on the result object
      let a = {
        test: true
      };

      let result = deepCopy(a);
      result.changed = true;

      expect(result.changed).not.toBe(a.changed);
    });

    test('should copy the object correctly, nested object structure', function() {

      function Nested(y) {
        this.y = y;
      }

      let a = {
        x: 'a',
        nested: new Nested('a')
      };

      let b = deepCopy(a);

      expect(a.x).toEqual(b.x);
      expect(a.nested.y).toEqual(b.nested.y);

      b.x = 'b';
      b.nested.y = 'b';

      expect(a.x).not.toEqual(b.x);
      expect(a.nested.y).not.toEqual(b.nested.y);
    });
  });

  describe('isIEBrowser Method', () => {
    const {isIEBrowser} = require('../utils');

    test('should return true if IE10', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'MSIE 10' // customized appVersion
      });

      expect(isIEBrowser('ie10')).toEqual(true);
      expect(isIEBrowser('ie11')).toEqual(false);
      expect(isIEBrowser('edge')).toEqual(false);
    });

    test('should return true if IE11', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'Trident' // customized appVersion
      });

      expect(isIEBrowser('ie11')).toEqual(true);
      expect(isIEBrowser('ie10')).toEqual(false);
      expect(isIEBrowser('edge')).toEqual(false);
    });

    test('should return true if edge', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'Edge' // customized appVersion
      });

      expect(isIEBrowser('edge')).toEqual(true);
      expect(isIEBrowser('ie10')).toEqual(false);
      expect(isIEBrowser('ie11')).toEqual(false);
    });
  });

  describe('detectIEVersion Method', () => {
    const {detectIEVersion} = require('../utils');

    test('should return true if IE10+ detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(10);
    });

    test('should return true if IE11 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(11);
    });

    test('should return true if Edge 12 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(12);
    });

    test('should return true if Edge 13 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(13);
    });
  });
});
<commit_msg>Removes the duplicated mutation test on deepCopy function<commit_after>/* eslint-disable */

describe('Utils Library', () => {
  describe('objectSize Method', () => {
    const {objectSize} = require('../utils');

    test('should return true when correct object size calculated', () => {
      let obj = {
        1: '1',
        2: '2',
        3: '3',
        4: '4',
        5: '5',
        6: '6'
      }

      let result = objectSize(obj);

      expect(typeof result).toEqual('number');
      expect(result).toEqual(6);
    });
  });

  describe('Data Type Checking', () => {
    const {isObject, isFunction, isString, isArray, isUndefined} = require('../utils');
    let testObject;
    let testString;
    let testNumber;
    let testFunc;
    let testArray;
    let testUndefined;

    beforeAll(() => {
      testObject = {value: 'One'};
      testString = 'Two';
      testNumber = 3;
      testFunc = function() {};
      testArray = [4, 5];
      testUndefined = undefined;
    });

    // isObject Function
    describe('isObject Method', () => {
      test('should return true if object type is passed in', () => {
        expect(isObject(testObject)).toEqual(true);
      });

      test('should return false if object type is not passed in', () => {
        expect(isObject(testString)).toEqual(false);
        expect(isObject(testNumber)).toEqual(false);
        expect(isObject(testFunc)).toEqual(false);
        expect(isObject(testArray)).toEqual(false);
        expect(isObject(testUndefined)).toEqual(false);
      });
    });

    // isFunction Function
    describe('isFunction Method', () => {
      test('should return true if function type is passed in', () => {
        expect(isFunction(testFunc)).toEqual(true);
      });

      test('should return false if function type is not passed in', () => {
        expect(isFunction(testString)).toEqual(false);
        expect(isFunction(testNumber)).toEqual(false);
        expect(isFunction(testObject)).toEqual(false);
        expect(isFunction(testArray)).toEqual(false);
        expect(isFunction(testUndefined)).toEqual(false);
      });
    });

    // isString Function
    describe('isString Method', () => {
      test('should return true if string type is passed in', () => {
        expect(isString(testString)).toEqual(true);
      });

      test('should return false if string type is not passed in', () => {
        expect(isString(testFunc)).toEqual(false);
        expect(isString(testNumber)).toEqual(false);
        expect(isString(testObject)).toEqual(false);
        expect(isString(testArray)).toEqual(false);
        expect(isString(testUndefined)).toEqual(false);
      });
    });

    // isArray Function
    describe('isArray Method', () => {
      test('should return true if array type is passed in', () => {
        expect(isArray(testArray)).toEqual(true);
      });

      test('should return false if array type is not passed in', () => {
        expect(isArray(testFunc)).toEqual(false);
        expect(isArray(testNumber)).toEqual(false);
        expect(isArray(testObject)).toEqual(false);
        expect(isArray(testString)).toEqual(false);
        expect(isArray(testUndefined)).toEqual(false);
      });
    });

    // isUndefined Function
    describe('isUndefined Method', () => {
      test('should return true if udefined type is passed in', () => {
        expect(isUndefined(testUndefined)).toEqual(true);
      });

      test('should return false if undefined type is not passed in', () => {
        expect(isUndefined(testFunc)).toEqual(false);
        expect(isUndefined(testNumber)).toEqual(false);
        expect(isUndefined(testObject)).toEqual(false);
        expect(isUndefined(testString)).toEqual(false);
        expect(isUndefined(testArray)).toEqual(false);
      });
    });
  });

  describe('bind & bindAll Methods', () => {
    const {bind, bindAll} = require('../utils');

    test('should return true if function bind to itself', () => {
      function testFunc() { }
      let testObj = { testFunc };

      expect(testObj.testFunc.prototype).toBeDefined();
      bind(testObj, 'testFunc');
      // If bind correctly, prototype should be undefined
      expect(testObj.testFunc.prototype).not.toBeDefined();
    });

    test('should return true if function bindAll to itself', () => {
      function testFunc1() { }
      function testFunc2() { }
      function testFunc3() { }
      let testObj = { testFunc1, testFunc2, testFunc3 };

      expect(testObj.testFunc1.prototype).toBeDefined();
      expect(testObj.testFunc2.prototype).toBeDefined();
      expect(testObj.testFunc3.prototype).toBeDefined();

      bindAll(testObj);

      // If bind correctly, prototype should be undefined
      expect(testObj.testFunc1.prototype).not.toBeDefined();
      expect(testObj.testFunc2.prototype).not.toBeDefined();
      expect(testObj.testFunc3.prototype).not.toBeDefined();
    });
  });

  describe('mergeDeep Method', () => {
    const {mergeDeep} = require('../utils');

    test('should return true if merge 2 objects properlly', () => {
      const testObj1 = {
        name1: 'Test',
        value1: 'Testing',
        age1: 30,
        testFunc1: function() {}
      };

      const testObj2 = {
        name2: 'Test 2',
        value2: 'Testing 2',
        age2: 35,
        testFunc2: function() {}
      };

      expect(testObj1).not.toEqual(testObj2);

      let result = mergeDeep(testObj1, testObj2);

      expect(result.name1).toBeDefined();
      expect(result.name2).toBeDefined();
      expect(result.age1).toBeDefined();
      expect(result.age2).toBeDefined();
      expect(result.testFunc1).toBeDefined();
      expect(result.testFunc2).toBeDefined();
    });
  });

  describe('deepCopy', function(){
    const {deepCopy} = require('../utils');
    test('should export a function', function() {
      expect(typeof deepCopy).toBe('function');
    });

    test('should return array type if input is an array', function() {
      let obj1 = [
        {
          name: 'test1',
          test1: false
        },
        {
          name: 'test2',
          test2: true
        }
      ];

      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
    });

    test('should return object type if input is an object', function() {
      let obj1 = {
          name: 'test1',
          test1: false
        };
      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
    });

    test('should return object type if input is a date', function() {
      let obj1 = {
        d: new Date()
      }
      let result = deepCopy(obj1);

      expect(typeof result).toBe(typeof obj1);
      expect(result.d).toEqual(obj1.d);
    });

    test('should deeply copy an object', function() {
      let obj = {
        name: 'test',
        test: false,
        config: {
          host: 'test.com',
          port: 3000,
          deepConfig: {
            db: 0,
            ssl: false
          },
          cache: {
            type: 'test'
          }
        }
      };

      let result = deepCopy(obj);

      expect(result.config.port).toBe(obj.config.port);
      expect(result.config.deepConfig.ssl).toBe(obj.config.deepConfig.ssl);
    });

    test('should deeply copy an array of objects', function() {
      let arr = [
        {
          name: 'test',
          test: false,
          config: {
            host: 'test.com',
            port: 3000,
            deepConfig: {
              db: 0,
              ssl: false
            },
            cache: {
              type: 'test'
            }
          }
        },
        {
          name: 'test2',
          test: false,
          config: {
            host: 'test2.com',
            port: 4000,
            deepConfig: {
              db: 1,
              ssl: true
            },
            cache: {
              type: 'test2'
            }
          }
        }
      ];

      let result = deepCopy(arr);

      expect(result.length).toEqual(arr.length);
      expect(result[0].config.port).toBe(arr[0].config.port);
      expect(result[1].config.deepConfig.ssl).toBe(arr[1].config.deepConfig.ssl);
    });

    test('should copy the object correctly, not mutate', function() {
      // Mutating a source object should have
      // no effect on the result object
      let a = {
        test: true
      };

      let result = deepCopy(a);
      result.changed = true;

      expect(result.changed).not.toBe(a.changed);
    });

    test('should copy the object correctly, nested object structure', function() {

      function Nested(y) {
        this.y = y;
      }

      let a = {
        x: 'a',
        nested: new Nested('a')
      };

      let b = deepCopy(a);

      expect(a.x).toEqual(b.x);
      expect(a.nested.y).toEqual(b.nested.y);

      b.x = 'b';
      b.nested.y = 'b';

      expect(a.x).not.toEqual(b.x);
      expect(a.nested.y).not.toEqual(b.nested.y);
    });
  });

  describe('isIEBrowser Method', () => {
    const {isIEBrowser} = require('../utils');

    test('should return true if IE10', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'MSIE 10' // customized appVersion
      });

      expect(isIEBrowser('ie10')).toEqual(true);
      expect(isIEBrowser('ie11')).toEqual(false);
      expect(isIEBrowser('edge')).toEqual(false);
    });

    test('should return true if IE11', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'Trident' // customized appVersion
      });

      expect(isIEBrowser('ie11')).toEqual(true);
      expect(isIEBrowser('ie10')).toEqual(false);
      expect(isIEBrowser('edge')).toEqual(false);
    });

    test('should return true if edge', () => {
      navigator.__defineGetter__('appVersion', function(){
        return 'Edge' // customized appVersion
      });

      expect(isIEBrowser('edge')).toEqual(true);
      expect(isIEBrowser('ie10')).toEqual(false);
      expect(isIEBrowser('ie11')).toEqual(false);
    });
  });

  describe('detectIEVersion Method', () => {
    const {detectIEVersion} = require('../utils');

    test('should return true if IE10+ detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(10);
    });

    test('should return true if IE11 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(11);
    });

    test('should return true if Edge 12 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(12);
    });

    test('should return true if Edge 13 detected', () => {
      navigator.__defineGetter__('userAgent', function(){
        return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586' // customized appVersion
      });

      let result = detectIEVersion();
      expect(result).toBe(13);
    });
  });
});
<|endoftext|>


<commit_before>// author: Blair Vanderhoof
// https://github.com/blairvanderhoof/gist-embed
// version 1.9
(function($) {

  function getLineNumbers(lineRangeString) {
    var lineNumbers = [], range, lineNumberSections;

    if (typeof lineRangeString === 'number') {
      lineNumbers.push(lineRangeString);
    } else {
      lineNumberSections = lineRangeString.split(',');

      for (var i = 0; i < lineNumberSections.length; i++) {
        range = lineNumberSections[i].split('-');
        if (range.length === 2) {
          for (var j = parseInt(range[0], 10); j <= range[1]; j++) {
            lineNumbers.push(j);
          }
        } else if (range.length === 1) {
          lineNumbers.push(parseInt(range[0], 10));
        }
      }
    }
    return lineNumbers;
  }

  $.fn.gist = function() {
    return this.each(function() {
      var $elem = $(this),
        id,
        url,
        file,
        lines,
        highlightLines,
        hideFooterOption,
        hideLineNumbersOption,
        data = {};

      // make block level so loading text shows properly
      $elem.css('display', 'block');

      id = $elem.data('gist-id') || '';
      file = $elem.data('gist-file');
      hideFooterOption = $elem.data('gist-hide-footer') === true;
      hideLineNumbersOption = $elem.data('gist-hide-line-numbers') === true;
      lines = $elem.data('gist-line');
      highlightLines = $elem.data('gist-highlight-line');

      if (file) {
        data.file = file;
      }

      // if the id doesn't exist, then ignore the code block
      if (!id) return false;

      url = 'https://gist.github.com/' + id + '.json';

      // loading
      $elem.html('Loading gist ' + url + (data.file ? ', file: ' + data.file : '') + '...');

      // request the json version of this gist
      $.ajax({
        url: url,
        data: data,
        dataType: 'jsonp',
        timeout: 10000,
        success: function(response) {
          var linkTag,
            head,
            lineNumbers,
            highlightLineNumbers,
            $responseDiv;

          // the html payload is in the div property
          if (response && response.div) {
            // github returns /assets/embed-id.css now, but let's be sure about that
            if (response.stylesheet && response.stylesheet.indexOf('http') !== 0) {
              // add leading slash if missing
              if (response.stylesheet.indexOf('/') !== 0) {
                response.stylesheet = '/' + response.stylesheet;
              }
              response.stylesheet = 'https://gist.github.com' + response.stylesheet;
            }

            // add the stylesheet if it does not exist
            if (response.stylesheet && $('link[href="' + response.stylesheet + '"]').length === 0) {
              linkTag = document.createElement('link');
              head = document.getElementsByTagName('head')[0];

              linkTag.type = 'text/css';
              linkTag.rel = 'stylesheet';
              linkTag.href = response.stylesheet;
              head.insertBefore(linkTag, head.firstChild);
            }

            // refernce to div
            $responseDiv = $(response.div);

            // remove id for uniqueness constraints
            $responseDiv.removeAttr('id');

            $elem.html('').append($responseDiv);

            // option to highlight lines
            if (highlightLines) {
              highlightLineNumbers = getLineNumbers(highlightLines);

              // we need to set the line-data td to 100% so the highlight expands the whole line
              $responseDiv.find('td.line-data').css({
                'width': '100%'
              });

              // find all .line divs (acutal code lines) that match the highlightLines and add the highlight class
              $responseDiv.find('.line').each(function(index) {
                if ($.inArray(index + 1, highlightLineNumbers) !== -1) {
                  $(this).css({
                    'background-color': 'rgb(255, 255, 204)'
                  });
                }
              });
            }

            // if user provided a line param, get the line numbers baesed on the criteria
            if (lines) {
              lineNumbers = getLineNumbers(lines);

              // find all .line divs (acutal code lines) and remove them if they don't exist in the line param
              $responseDiv.find('.line').each(function(index) {
                if (($.inArray(index + 1, lineNumbers)) === -1) {
                  $(this).remove();
                }
              });

              // find all .line-number divs (numbers on the gutter) and remove them if they don't exist in the line param
              $responseDiv.find('.line-number').each(function(index) {
                if (($.inArray(index + 1, lineNumbers)) === -1) {
                  $(this).remove();
                }
              });
            }

            // option to remove footer
            if (hideFooterOption) {
              $responseDiv.find('.gist-meta').remove();

              // present a uniformed border when footer is hidden
              $responseDiv.find('.gist-data').css("border-bottom", "0px");
              $responseDiv.find('.gist-file').css("border-bottom", "1px solid #ddd");
            }

            // option to remove
            if (hideLineNumbersOption) {
              $responseDiv.find('.line-numbers').remove();
            }

          } else {
            $elem.html('Failed loading gist ' + url);
          }
        },
        error: function(jqXHR, textStatus) {
          $elem.html('Failed loading gist ' + url + ': ' + textStatus);
        }
      });

    });
  };

  $(function() {
    // find all elements containing "data-gist-id" attribute.
    $('[data-gist-id]').gist();
  });

})(jQuery);
<commit_msg>Version bump<commit_after>// author: Blair Vanderhoof
// https://github.com/blairvanderhoof/gist-embed
// version 2.0
(function($) {

  function getLineNumbers(lineRangeString) {
    var lineNumbers = [], range, lineNumberSections;

    if (typeof lineRangeString === 'number') {
      lineNumbers.push(lineRangeString);
    } else {
      lineNumberSections = lineRangeString.split(',');

      for (var i = 0; i < lineNumberSections.length; i++) {
        range = lineNumberSections[i].split('-');
        if (range.length === 2) {
          for (var j = parseInt(range[0], 10); j <= range[1]; j++) {
            lineNumbers.push(j);
          }
        } else if (range.length === 1) {
          lineNumbers.push(parseInt(range[0], 10));
        }
      }
    }
    return lineNumbers;
  }

  $.fn.gist = function() {
    return this.each(function() {
      var $elem = $(this),
        id,
        url,
        file,
        lines,
        highlightLines,
        hideFooterOption,
        hideLineNumbersOption,
        data = {};

      // make block level so loading text shows properly
      $elem.css('display', 'block');

      id = $elem.data('gist-id') || '';
      file = $elem.data('gist-file');
      hideFooterOption = $elem.data('gist-hide-footer') === true;
      hideLineNumbersOption = $elem.data('gist-hide-line-numbers') === true;
      lines = $elem.data('gist-line');
      highlightLines = $elem.data('gist-highlight-line');

      if (file) {
        data.file = file;
      }

      // if the id doesn't exist, then ignore the code block
      if (!id) return false;

      url = 'https://gist.github.com/' + id + '.json';

      // loading
      $elem.html('Loading gist ' + url + (data.file ? ', file: ' + data.file : '') + '...');

      // request the json version of this gist
      $.ajax({
        url: url,
        data: data,
        dataType: 'jsonp',
        timeout: 10000,
        success: function(response) {
          var linkTag,
            head,
            lineNumbers,
            highlightLineNumbers,
            $responseDiv;

          // the html payload is in the div property
          if (response && response.div) {
            // github returns /assets/embed-id.css now, but let's be sure about that
            if (response.stylesheet && response.stylesheet.indexOf('http') !== 0) {
              // add leading slash if missing
              if (response.stylesheet.indexOf('/') !== 0) {
                response.stylesheet = '/' + response.stylesheet;
              }
              response.stylesheet = 'https://gist.github.com' + response.stylesheet;
            }

            // add the stylesheet if it does not exist
            if (response.stylesheet && $('link[href="' + response.stylesheet + '"]').length === 0) {
              linkTag = document.createElement('link');
              head = document.getElementsByTagName('head')[0];

              linkTag.type = 'text/css';
              linkTag.rel = 'stylesheet';
              linkTag.href = response.stylesheet;
              head.insertBefore(linkTag, head.firstChild);
            }

            // refernce to div
            $responseDiv = $(response.div);

            // remove id for uniqueness constraints
            $responseDiv.removeAttr('id');

            $elem.html('').append($responseDiv);

            // option to highlight lines
            if (highlightLines) {
              highlightLineNumbers = getLineNumbers(highlightLines);

              // we need to set the line-data td to 100% so the highlight expands the whole line
              $responseDiv.find('td.line-data').css({
                'width': '100%'
              });

              // find all .line divs (acutal code lines) that match the highlightLines and add the highlight class
              $responseDiv.find('.line').each(function(index) {
                if ($.inArray(index + 1, highlightLineNumbers) !== -1) {
                  $(this).css({
                    'background-color': 'rgb(255, 255, 204)'
                  });
                }
              });
            }

            // if user provided a line param, get the line numbers baesed on the criteria
            if (lines) {
              lineNumbers = getLineNumbers(lines);

              // find all .line divs (acutal code lines) and remove them if they don't exist in the line param
              $responseDiv.find('.line').each(function(index) {
                if (($.inArray(index + 1, lineNumbers)) === -1) {
                  $(this).remove();
                }
              });

              // find all .line-number divs (numbers on the gutter) and remove them if they don't exist in the line param
              $responseDiv.find('.line-number').each(function(index) {
                if (($.inArray(index + 1, lineNumbers)) === -1) {
                  $(this).remove();
                }
              });
            }

            // option to remove footer
            if (hideFooterOption) {
              $responseDiv.find('.gist-meta').remove();

              // present a uniformed border when footer is hidden
              $responseDiv.find('.gist-data').css("border-bottom", "0px");
              $responseDiv.find('.gist-file').css("border-bottom", "1px solid #ddd");
            }

            // option to remove
            if (hideLineNumbersOption) {
              $responseDiv.find('.line-numbers').remove();
            }

          } else {
            $elem.html('Failed loading gist ' + url);
          }
        },
        error: function(jqXHR, textStatus) {
          $elem.html('Failed loading gist ' + url + ': ' + textStatus);
        }
      });

    });
  };

  $(function() {
    // find all elements containing "data-gist-id" attribute.
    $('[data-gist-id]').gist();
  });

})(jQuery);
<|endoftext|>


<commit_before>var ElementType = require("./ElementType.js");

function Parser(cbs, options) {
    this._options = options || defaultOpts;
    this._cbs = cbs || defaultCbs;
    this._buffer = "";
    this._tagSep = "";
    this._stack = [];
    this._wroteSpecial = false;
    this._contentFlags = 0;
    this._done = false;
    this._running = true; //false if paused
}

//Regular expressions used for cleaning up and parsing (stateless)

/* http://dev.w3.org/html5/html-author/#attributes
 * - Whitespace is permitted after the tag name, but it is not permitted before the tag name.
 * - Attribute names must consist of one or more characters other than the space characters,
 *   control characters, NULL, one of the characters: double quote ("), single quote ('),
 *   greater-than sign (>), solidus (/), equals sign (=), nor any characters that are not defined by Unicode.
 * - An empty attribute is one where the value has been omitted. (<input disabled>...</input>
 * - An unquoted attribute value must not contain any literal space characters, any of the characters:
 *   double quote ("), apostrophe ('), equals sign (=), less-than sign (<), greater-than sign (>),
 *   or grave accent (`), and the value must not be the empty string.
 * - There may be space characters between the attribute name and the equals sign (=),
 *   and between that and the attribute value.
 * - Double-quoted attributes must not contain any double-quote characters or ambiguous ampersands.
 * - Single-quoted attributes must not contain any single-quote characters or ambiguous ampersands.
 */
// element name:	(<[^<& ]+)
// attribute name:	( [^"'=>\/]+)
// attribute value:	(\s*=\s*(?:
//						"([^"]*)"|
//						'([^']*)'|
//						[^\s"'=<>`]+)
// tag end: (?=\s|\/|$)

var _reAttrib = /\s+([^"'=>\/\s]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))|(?=\s)|\/|$)/g,
    _reTail = /\s|\/|$/;

var defaultOpts = {
    xmlMode: false, //Special behavior for script/style tags by default
    lowerCaseAttributeNames: false, //call .toLowerCase for each attribute name
    lowerCaseTags: false //call .toLowerCase for each tag name
};

var defaultCbs = {
    /*
		This is just a plain object
		so that the parser doesn't
		throw if no arguments were
		provided.
	*/
    /*
		oncdataend,
		oncdatastart,
		onclosetag,
		oncomment,
		oncommentend,
		onerror,
		onopentag,
		onopentagend,
		onprocessinginstruction,
		onreset,
		ontext
	*/
};

var formTags = {
    input: 1,
    option: 1,
    optgroup: 1,
    select: 1,
    button: 1,
    datalist: 1,
    textarea: 1
};
var openImpliesClose = {
    tr: { tr: 1, th: 1, td: 1 },
    th: { th: 1 },
    td: { thead: 1, td: 1 },
    body: { head: 1, link: 1, script: 1 },
    li: { li: 1 },
    p: { p: 1 },
    select: formTags,
    input: formTags,
    output: formTags,
    button: formTags,
    datalist: formTags,
    textarea: formTags,
    option: { option: 1 },
    optgroup: { optgroup: 1 }
};

//Parses a complete HTML and pushes it to the handler
Parser.prototype.parseComplete = function(data) {
    this.reset();
    this.end(data);
};

//Parses a piece of an HTML document
Parser.prototype.parseChunk = Parser.prototype.write = function(data) {
    if (this._done)
        this._handleError(
            "Attempted to parse chunk after parsing already done"
        );
    this._buffer += data; //FIXME: this can be a bottleneck
    if (this._running) this._parseTags();
};

//Tells the parser that the HTML being parsed is complete
Parser.prototype.done = Parser.prototype.end = function(chunk) {
    if (this._done) return;

    if (chunk) this.write(chunk);
    this._done = true;

    if (this._running) this._finishParsing();
};

Parser.prototype._finishParsing = function() {
    //Parse the buffer to its end
    if (this._buffer) this._parseTags(true);

    if (this._cbs.onclosetag) {
        while (this._stack.length) this._cbs.onclosetag(this._stack.pop());
    }

    if (this._cbs.onend) this._cbs.onend();
};

Parser.prototype.pause = function() {
    if (!this._done) this._running = false;
};

Parser.prototype.resume = function() {
    if (this._running) return;
    this._running = true;
    this._parseTags();
    if (this._done) this._finishParsing();
};

//Resets the parser to a blank state, ready to parse a new HTML document
Parser.prototype.reset = function() {
    Parser.call(this, this._cbs, this._options);
    if (this._cbs.onreset) this._cbs.onreset();
};

//Extracts the base tag name from the data value of an element
Parser.prototype._parseTagName = function(data) {
    var match = data.substr(0, data.search(_reTail));
    if (!this._options.lowerCaseTags) return match;
    return match.toLowerCase();
};

//Special tags that are treated differently
var SpecialTags = {};
//SpecialTags[ElementType.Tag]   = 0x0;
SpecialTags[ElementType.Style] = 0x1; //2^0
SpecialTags[ElementType.Script] = 0x2; //2^1
SpecialTags[ElementType.Comment] = 0x4; //2^2
SpecialTags[ElementType.CDATA] = 0x8; //2^3

var TagValues = {
    style: 1,
    script: 2
};

//Parses through HTML text and returns an array of found elements
Parser.prototype._parseTags = function(force) {
    var current = 0,
        opening = this._buffer.indexOf("<"),
        closing = this._buffer.indexOf(">"),
        next,
        rawData,
        elementData,
        lastTagSep;

    //if force is true, parse everything
    if (force) opening = Infinity;

    //opening !== closing is just false if both are -1
    while (opening !== closing && this._running) {
        lastTagSep = this._tagSep;

        if ((opening !== -1 && opening < closing) || closing === -1) {
            next = opening;
            this._tagSep = "<";
            opening = this._buffer.indexOf("<", next + 1);
        } else {
            next = closing;
            this._tagSep = ">";
            closing = this._buffer.indexOf(">", next + 1);
        }
        rawData = this._buffer.substring(current, next); //The next chunk of data to parse

        //set elements for next run
        current = next + 1;

        if (this._contentFlags >= SpecialTags[ElementType.CDATA]) {
            // We're inside a CDATA section
            this._writeCDATA(rawData);
        } else if (this._contentFlags >= SpecialTags[ElementType.Comment]) {
            //We're in a comment tag
            this._writeComment(rawData);
        } else if (lastTagSep === "<") {
            elementData = rawData.trimLeft();
            if (elementData.charAt(0) === "/") {
                //elementData = elementData.substr(1).trim();
                elementData = this._parseTagName(elementData.substr(1));
                if (this._contentFlags !== 0) {
                    //if it's a closing tag, remove the flag
                    if (this._contentFlags & TagValues[elementData]) {
                        //remove the flag
                        this._contentFlags ^= TagValues[elementData];
                    } else {
                        this._writeSpecial(rawData, lastTagSep);
                        continue;
                    }
                }
                this._processCloseTag(elementData);
            } else if (elementData.charAt(0) === "!") {
                if (elementData.substr(1, 7) === "[CDATA[") {
                    this._contentFlags |= SpecialTags[ElementType.CDATA];
                    if (this._cbs.oncdatastart) this._cbs.oncdatastart();
                    this._writeCDATA(elementData.substr(8));
                } else if (this._contentFlags !== 0)
                    this._writeSpecial(rawData, lastTagSep);
                else if (elementData.substr(1, 2) === "--") {
                    //This tag is a comment
                    this._contentFlags |= SpecialTags[ElementType.Comment];
                    this._writeComment(rawData.substr(3));
                }
                //TODO: This isn't a processing instruction, needs a new name
                else if (this._cbs.onprocessinginstruction) {
                    this._cbs.onprocessinginstruction(
                        "!" + this._parseTagName(elementData.substr(1)),
                        elementData
                    );
                }
            } else if (this._contentFlags !== 0)
                this._writeSpecial(rawData, lastTagSep);
            else if (elementData.charAt(0) === "?") {
                if (this._cbs.onprocessinginstruction) {
                    this._cbs.onprocessinginstruction(
                        "?" + this._parseTagName(elementData.substr(1)),
                        elementData
                    );
                }
            } else this._processOpenTag(elementData);
        } else {
            if (this._contentFlags !== 0) {
                this._writeSpecial(rawData, ">");
            } else if (rawData !== "" && this._cbs.ontext) {
                if (this._tagSep === ">") rawData += ">"; //it's the second > in a row
                this._cbs.ontext(rawData);
            }
        }
    }

    this._buffer = this._buffer.substr(current);
};

Parser.prototype._writeCDATA = function(data) {
    if (this._tagSep === ">" && data.substr(-2) === "]]") {
        // CDATA ends
        if (data.length !== 2 && this._cbs.ontext) {
            this._cbs.ontext(data.slice(0, -2));
        }
        this._contentFlags ^= SpecialTags[ElementType.CDATA];
        if (this._cbs.oncdataend) this._cbs.oncdataend();
    } else if (this._cbs.ontext) this._cbs.ontext(data + this._tagSep);
};

Parser.prototype._writeComment = function(rawData) {
    if (this._tagSep === ">" && rawData.substr(-2) === "--") {
        //comment ends
        //remove the written flag (also removes the comment flag)
        this._contentFlags ^= SpecialTags[ElementType.Comment];
        this._wroteSpecial = false;
        if (this._cbs.oncomment) this._cbs.oncomment(rawData.slice(0, -2));
        if (this._cbs.oncommentend) this._cbs.oncommentend();
    } else if (this._cbs.oncomment) this._cbs.oncomment(rawData + this._tagSep);
};

Parser.prototype._writeSpecial = function(rawData, lastTagSep) {
    //if the previous element is text, append the last tag sep to element
    if (this._wroteSpecial) {
        if (this._cbs.ontext) this._cbs.ontext(lastTagSep + rawData);
    } else {
        //The previous element was not text
        this._wroteSpecial = true;
        if (rawData !== "" && this._cbs.ontext) this._cbs.ontext(rawData);
    }
};

var emptyTags = {
    __proto__: null,
    area: true,
    base: true,
    basefont: true,
    br: true,
    col: true,
    frame: true,
    hr: true,
    img: true,
    input: true,
    isindex: true,
    link: true,
    meta: true,
    param: true,
    embed: true
};

Parser.prototype._processCloseTag = function(name) {
    if (this._stack && (!(name in emptyTags) || this._options.xmlMode)) {
        var pos = this._stack.lastIndexOf(name);
        if (pos !== -1)
            if (this._cbs.onclosetag) {
                pos = this._stack.length - pos;
                while (pos--) this._cbs.onclosetag(this._stack.pop());
            } else this._stack.splice(pos);
    }
    //many browsers (eg. Safari, Chrome) convert </br> to <br>
    else if (name === "br" && !this._options.xmlMode) {
        this._processOpenTag(name + "/");
    }
};

Parser.prototype._parseAttributes = function(data, lcNames) {
    for (var match; (match = _reAttrib.exec(data)); ) {
        this._cbs.onattribute(
            lcNames ? match[1].toLowerCase() : match[1],
            match[2] || match[3] || match[4] || ""
        );
    }
};

//parses the attribute string
var parseAttributes = function(data, lcNames) {
    var attrs = {};
    for (var match; (match = _reAttrib.exec(data)); ) {
        attrs[lcNames ? match[1].toLowerCase() : match[1]] =
            match[2] || match[3] || match[4] || "";
    }
    return attrs;
};

Parser.prototype._processOpenTag = function(data) {
    var name = this._parseTagName(data),
        attributes = parseAttributes(
            data,
            this._options.lowerCaseAttributeNames
        ),
        type = ElementType.Tag;

    if (this._options.xmlMode) {
        /*do nothing*/
    } else if (name === "script") type = ElementType.Script;
    else if (name === "style") type = ElementType.Style;

    if (name in openImpliesClose) {
        var el;
        while (
            (el = this._stack[this._stack.length]) in openImpliesClose[name]
        ) {
            this._stack.pop();
            this._processCloseTag(el);
        }
    }

    if (this._cbs.onopentagname) this._cbs.onopentagname(name);
    if (this._cbs.onopentag) this._cbs.onopentag(name, attributes);
    if (this._cbs.onattribute) {
        this._parseAttributes(data, this._options.lowerCaseAttributeNames);
    }

    //If tag self-terminates, add an explicit, separate closing tag
    /* http://dev.w3.org/html5/html-author/#tags
	 * In XHTML, self-closing tags are valid but attribute values must be quoted.
	 * In HTML, self-closing tags must be either void elements or foreign elements.
	 * Invalid HTML self-closing tag syntax is ignored (treated as an opening tag).
	 * Foreign elements use XML rules
	 */
    if (
        (!Object.keys(attributes).length && data.substr(-1) === "/") ||
        (name in emptyTags && !this._options.xmlMode)
    ) {
        if (this._cbs.onclosetag) this._cbs.onclosetag(name);
    } else {
        if (type !== ElementType.Tag) {
            this._contentFlags |= SpecialTags[type];
            this._wroteSpecial = false;
        }
        this._stack.push(name);
    }
};

Parser.prototype._handleError = function(error) {
    error = new Error(error);
    if (this._cbs.onerror) this._cbs.onerror(error);
    else throw error;
};

module.exports = Parser;
<commit_msg>Derp.<commit_after>var ElementType = require("./ElementType.js");

function Parser(cbs, options) {
    this._options = options || defaultOpts;
    this._cbs = cbs || defaultCbs;
    this._buffer = "";
    this._tagSep = "";
    this._stack = [];
    this._wroteSpecial = false;
    this._contentFlags = 0;
    this._done = false;
    this._running = true; //false if paused
}

//Regular expressions used for cleaning up and parsing (stateless)

/* http://dev.w3.org/html5/html-author/#attributes
 * - Whitespace is permitted after the tag name, but it is not permitted before the tag name.
 * - Attribute names must consist of one or more characters other than the space characters,
 *   control characters, NULL, one of the characters: double quote ("), single quote ('),
 *   greater-than sign (>), solidus (/), equals sign (=), nor any characters that are not defined by Unicode.
 * - An empty attribute is one where the value has been omitted. (<input disabled>...</input>
 * - An unquoted attribute value must not contain any literal space characters, any of the characters:
 *   double quote ("), apostrophe ('), equals sign (=), less-than sign (<), greater-than sign (>),
 *   or grave accent (`), and the value must not be the empty string.
 * - There may be space characters between the attribute name and the equals sign (=),
 *   and between that and the attribute value.
 * - Double-quoted attributes must not contain any double-quote characters or ambiguous ampersands.
 * - Single-quoted attributes must not contain any single-quote characters or ambiguous ampersands.
 */
// element name:	(<[^<& ]+)
// attribute name:	( [^"'=>\/]+)
// attribute value:	(\s*=\s*(?:
//						"([^"]*)"|
//						'([^']*)'|
//						[^\s"'=<>`]+)
// tag end: (?=\s|\/|$)

var _reAttrib = /\s+([^"'=>\/\s]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))|(?=\s)|\/|$)/g,
    _reTail = /\s|\/|$/;

var defaultOpts = {
    xmlMode: false, //Special behavior for script/style tags by default
    lowerCaseAttributeNames: false, //call .toLowerCase for each attribute name
    lowerCaseTags: false //call .toLowerCase for each tag name
};

var defaultCbs = {
    /*
		This is just a plain object
		so that the parser doesn't
		throw if no arguments were
		provided.
	*/
    /*
		oncdataend,
		oncdatastart,
		onclosetag,
		oncomment,
		oncommentend,
		onerror,
		onopentag,
		onopentagend,
		onprocessinginstruction,
		onreset,
		ontext
	*/
};

var formTags = {
    input: 1,
    option: 1,
    optgroup: 1,
    select: 1,
    button: 1,
    datalist: 1,
    textarea: 1
};
var openImpliesClose = {
    tr: { tr: 1, th: 1, td: 1 },
    th: { th: 1 },
    td: { thead: 1, td: 1 },
    body: { head: 1, link: 1, script: 1 },
    li: { li: 1 },
    p: { p: 1 },
    select: formTags,
    input: formTags,
    output: formTags,
    button: formTags,
    datalist: formTags,
    textarea: formTags,
    option: { option: 1 },
    optgroup: { optgroup: 1 }
};

//Parses a complete HTML and pushes it to the handler
Parser.prototype.parseComplete = function(data) {
    this.reset();
    this.end(data);
};

//Parses a piece of an HTML document
Parser.prototype.parseChunk = Parser.prototype.write = function(data) {
    if (this._done)
        this._handleError(
            "Attempted to parse chunk after parsing already done"
        );
    this._buffer += data; //FIXME: this can be a bottleneck
    if (this._running) this._parseTags();
};

//Tells the parser that the HTML being parsed is complete
Parser.prototype.done = Parser.prototype.end = function(chunk) {
    if (this._done) return;

    if (chunk) this.write(chunk);
    this._done = true;

    if (this._running) this._finishParsing();
};

Parser.prototype._finishParsing = function() {
    //Parse the buffer to its end
    if (this._buffer) this._parseTags(true);

    if (this._cbs.onclosetag) {
        while (this._stack.length) this._cbs.onclosetag(this._stack.pop());
    }

    if (this._cbs.onend) this._cbs.onend();
};

Parser.prototype.pause = function() {
    if (!this._done) this._running = false;
};

Parser.prototype.resume = function() {
    if (this._running) return;
    this._running = true;
    this._parseTags();
    if (this._done) this._finishParsing();
};

//Resets the parser to a blank state, ready to parse a new HTML document
Parser.prototype.reset = function() {
    Parser.call(this, this._cbs, this._options);
    if (this._cbs.onreset) this._cbs.onreset();
};

//Extracts the base tag name from the data value of an element
Parser.prototype._parseTagName = function(data) {
    var match = data.substr(0, data.search(_reTail));
    if (!this._options.lowerCaseTags) return match;
    return match.toLowerCase();
};

//Special tags that are treated differently
var SpecialTags = {};
//SpecialTags[ElementType.Tag]   = 0x0;
SpecialTags[ElementType.Style] = 0x1; //2^0
SpecialTags[ElementType.Script] = 0x2; //2^1
SpecialTags[ElementType.Comment] = 0x4; //2^2
SpecialTags[ElementType.CDATA] = 0x8; //2^3

var TagValues = {
    style: 1,
    script: 2
};

//Parses through HTML text and returns an array of found elements
Parser.prototype._parseTags = function(force) {
    var current = 0,
        opening = this._buffer.indexOf("<"),
        closing = this._buffer.indexOf(">"),
        next,
        rawData,
        elementData,
        lastTagSep;

    //if force is true, parse everything
    if (force) opening = Infinity;

    //opening !== closing is just false if both are -1
    while (opening !== closing && this._running) {
        lastTagSep = this._tagSep;

        if ((opening !== -1 && opening < closing) || closing === -1) {
            next = opening;
            this._tagSep = "<";
            opening = this._buffer.indexOf("<", next + 1);
        } else {
            next = closing;
            this._tagSep = ">";
            closing = this._buffer.indexOf(">", next + 1);
        }
        rawData = this._buffer.substring(current, next); //The next chunk of data to parse

        //set elements for next run
        current = next + 1;

        if (this._contentFlags >= SpecialTags[ElementType.CDATA]) {
            // We're inside a CDATA section
            this._writeCDATA(rawData);
        } else if (this._contentFlags >= SpecialTags[ElementType.Comment]) {
            //We're in a comment tag
            this._writeComment(rawData);
        } else if (lastTagSep === "<") {
            elementData = rawData.trimLeft();
            if (elementData.charAt(0) === "/") {
                //elementData = elementData.substr(1).trim();
                elementData = this._parseTagName(elementData.substr(1));
                if (this._contentFlags !== 0) {
                    //if it's a closing tag, remove the flag
                    if (this._contentFlags & TagValues[elementData]) {
                        //remove the flag
                        this._contentFlags ^= TagValues[elementData];
                    } else {
                        this._writeSpecial(rawData, lastTagSep);
                        continue;
                    }
                }
                this._processCloseTag(elementData);
            } else if (elementData.charAt(0) === "!") {
                if (elementData.substr(1, 7) === "[CDATA[") {
                    this._contentFlags |= SpecialTags[ElementType.CDATA];
                    if (this._cbs.oncdatastart) this._cbs.oncdatastart();
                    this._writeCDATA(elementData.substr(8));
                } else if (this._contentFlags !== 0)
                    this._writeSpecial(rawData, lastTagSep);
                else if (elementData.substr(1, 2) === "--") {
                    //This tag is a comment
                    this._contentFlags |= SpecialTags[ElementType.Comment];
                    this._writeComment(rawData.substr(3));
                }
                //TODO: This isn't a processing instruction, needs a new name
                else if (this._cbs.onprocessinginstruction) {
                    this._cbs.onprocessinginstruction(
                        "!" + this._parseTagName(elementData.substr(1)),
                        elementData
                    );
                }
            } else if (this._contentFlags !== 0)
                this._writeSpecial(rawData, lastTagSep);
            else if (elementData.charAt(0) === "?") {
                if (this._cbs.onprocessinginstruction) {
                    this._cbs.onprocessinginstruction(
                        "?" + this._parseTagName(elementData.substr(1)),
                        elementData
                    );
                }
            } else this._processOpenTag(elementData);
        } else {
            if (this._contentFlags !== 0) {
                this._writeSpecial(rawData, ">");
            } else if (rawData !== "" && this._cbs.ontext) {
                if (this._tagSep === ">") rawData += ">"; //it's the second > in a row
                this._cbs.ontext(rawData);
            }
        }
    }

    this._buffer = this._buffer.substr(current);
};

Parser.prototype._writeCDATA = function(data) {
    if (this._tagSep === ">" && data.substr(-2) === "]]") {
        // CDATA ends
        if (data.length !== 2 && this._cbs.ontext) {
            this._cbs.ontext(data.slice(0, -2));
        }
        this._contentFlags ^= SpecialTags[ElementType.CDATA];
        if (this._cbs.oncdataend) this._cbs.oncdataend();
    } else if (this._cbs.ontext) this._cbs.ontext(data + this._tagSep);
};

Parser.prototype._writeComment = function(rawData) {
    if (this._tagSep === ">" && rawData.substr(-2) === "--") {
        //comment ends
        //remove the written flag (also removes the comment flag)
        this._contentFlags ^= SpecialTags[ElementType.Comment];
        this._wroteSpecial = false;
        if (this._cbs.oncomment) this._cbs.oncomment(rawData.slice(0, -2));
        if (this._cbs.oncommentend) this._cbs.oncommentend();
    } else if (this._cbs.oncomment) this._cbs.oncomment(rawData + this._tagSep);
};

Parser.prototype._writeSpecial = function(rawData, lastTagSep) {
    //if the previous element is text, append the last tag sep to element
    if (this._wroteSpecial) {
        if (this._cbs.ontext) this._cbs.ontext(lastTagSep + rawData);
    } else {
        //The previous element was not text
        this._wroteSpecial = true;
        if (rawData !== "" && this._cbs.ontext) this._cbs.ontext(rawData);
    }
};

var emptyTags = {
    __proto__: null,
    area: true,
    base: true,
    basefont: true,
    br: true,
    col: true,
    frame: true,
    hr: true,
    img: true,
    input: true,
    isindex: true,
    link: true,
    meta: true,
    param: true,
    embed: true
};

Parser.prototype._processCloseTag = function(name) {
    if (this._stack && (!(name in emptyTags) || this._options.xmlMode)) {
        var pos = this._stack.lastIndexOf(name);
        if (pos !== -1)
            if (this._cbs.onclosetag) {
                pos = this._stack.length - pos;
                while (pos--) this._cbs.onclosetag(this._stack.pop());
            } else this._stack.splice(pos);
    }
    //many browsers (eg. Safari, Chrome) convert </br> to <br>
    else if (name === "br" && !this._options.xmlMode) {
        this._processOpenTag(name + "/");
    }
};

Parser.prototype._parseAttributes = function(data, lcNames) {
    for (var match; (match = _reAttrib.exec(data)); ) {
        this._cbs.onattribute(
            lcNames ? match[1].toLowerCase() : match[1],
            match[2] || match[3] || match[4] || ""
        );
    }
};

//parses the attribute string
var parseAttributes = function(data, lcNames) {
    var attrs = {};
    for (var match; (match = _reAttrib.exec(data)); ) {
        attrs[lcNames ? match[1].toLowerCase() : match[1]] =
            match[2] || match[3] || match[4] || "";
    }
    return attrs;
};

Parser.prototype._processOpenTag = function(data) {
    var name = this._parseTagName(data),
        attributes = parseAttributes(
            data,
            this._options.lowerCaseAttributeNames
        ),
        type = ElementType.Tag;

    if (this._options.xmlMode) {
        /*do nothing*/
    } else if (name === "script") type = ElementType.Script;
    else if (name === "style") type = ElementType.Style;
    if (name in openImpliesClose) {
        var el;
        while (
            (el = this._stack[this._stack.length - 1]) in openImpliesClose[name]
        ) {
            this._processCloseTag(el);
        }
    }
    if (this._cbs.onopentagname) this._cbs.onopentagname(name);
    if (this._cbs.onopentag) this._cbs.onopentag(name, attributes);
    if (this._cbs.onattribute) {
        this._parseAttributes(data, this._options.lowerCaseAttributeNames);
    }

    //If tag self-terminates, add an explicit, separate closing tag
    /* http://dev.w3.org/html5/html-author/#tags
	 * In XHTML, self-closing tags are valid but attribute values must be quoted.
	 * In HTML, self-closing tags must be either void elements or foreign elements.
	 * Invalid HTML self-closing tag syntax is ignored (treated as an opening tag).
	 * Foreign elements use XML rules
	 */
    if (
        (!Object.keys(attributes).length && data.substr(-1) === "/") ||
        (name in emptyTags && !this._options.xmlMode)
    ) {
        if (this._cbs.onclosetag) this._cbs.onclosetag(name);
    } else {
        if (type !== ElementType.Tag) {
            this._contentFlags |= SpecialTags[type];
            this._wroteSpecial = false;
        }
        this._stack.push(name);
    }
};

Parser.prototype._handleError = function(error) {
    error = new Error(error);
    if (this._cbs.onerror) this._cbs.onerror(error);
    else throw error;
};

module.exports = Parser;
<|endoftext|>


<commit_before>goog.provide('olcs.AbstractSynchronizer');

goog.require('goog.events');
goog.require('ol.layer.Group');
goog.require('ol.layer.Layer');

/**
 * @param {!ol.Map} map
 * @param {!Cesium.Scene} scene
 * @constructor
 * @template T
 */
olcs.AbstractSynchronizer = function(map, scene) {
  /**
   * @type {!ol.Map}
   * @protected
   */
  this.map = map;

  /**
   * @type {ol.View}
   * @protected
   */
  this.view = null;

  /**
   * @type {!Cesium.Scene}
   * @protected
   */
  this.scene = scene;

  /**
   * @type {ol.Collection.<ol.layer.Base>}
   * @protected
   */
  this.olLayers = null;

  /**
   * @type {!Array}
   * @private
   */
  this.olLayersListenKeys_ = [];

  /**
   * Map of ol3 layer ids (from goog.getUid) to the Cesium ImageryLayers.
   * null value means, that we are unable to create equivalent layer.
   * @type {Object.<number, ?T>}
   * @protected
   */
  this.layerMap = {};

  /**
   * Map of listen keys for ol3 layer groups ids (from goog.getUid).
   * @type {!Object.<number, !Array>}
   * @private
   */
  this.olGroupListenKeys_ = {};

  /**
   * @type {Object.<number, !Array>}
   * @private
   */
  this.unusedGroups_ = null;

  /**
   * @type {Object.<?T, number>}
   * @private
   */
  this.unusedCesiumObjects_ = null;

  this.map.on('change:view', function(e) {
    this.setView_(this.map.getView());
  }, this);
  this.setView_(this.map.getView());

  this.map.on('change:layergroup', function(e) {
    this.setLayers_(this.map.getLayers());
  }, this);
  this.setLayers_(this.map.getLayers());
};

/**
 * @param {ol.View} view New view to use.
 * @private
 */
olcs.AbstractSynchronizer.prototype.setView_ = function(view) {
  this.view = view;

  // destroy all, the change of view can affect which layers are synced
  this.destroyAll();
  this.synchronize();
};

/**
 * @param {ol.Collection.<ol.layer.Base>} layers New layers to use.
 * @private
 */
olcs.AbstractSynchronizer.prototype.setLayers_ = function(layers) {
  if (!goog.isNull(this.olLayers)) {
    goog.array.forEach(this.olLayersListenKeys_, this.olLayers.unByKey);
  }

  this.olLayers = layers;
  if (!goog.isNull(layers)) {
    var handleCollectionEvent_ = goog.bind(function(e) {
      this.synchronize();
    }, this);

    this.olLayersListenKeys_ = [
      layers.on('add', handleCollectionEvent_),
      layers.on('remove', handleCollectionEvent_)
    ];
  } else {
    this.olLayersListenKeys_ = [];
  }

  this.destroyAll();
  this.synchronize();
};

/**
 * Performs complete synchronization of the layers.
 */
olcs.AbstractSynchronizer.prototype.synchronize = function() {
  if (goog.isNull(this.view) || goog.isNull(this.olLayers)) {
    return;
  }
  this.unusedGroups_ = goog.object.clone(this.olGroupListenKeys_);
  this.unusedCesiumObjects_ = goog.object.transpose(this.layerMap);
  this.removeAllCesiumObjects(false); // only remove, don't destroy

  this.olLayers.forEach(function(el, i, arr) {
    this.synchronizeSingle(el);
  }, this);

  // destroy unused Cesium Objects
  goog.array.forEach(goog.object.getValues(this.unusedCesiumObjects_),
      function(el, i, arr) {
        var layerId = el;
        var object = this.layerMap[layerId];
        if (goog.isDef(object)) {
          delete this.layerMap[layerId];
          if (!goog.isNull(object)) {
            this.destroyCesiumObject(object);
          }
        }
      }, this);
  this.unusedCesiumObjects_ = null;

  // unlisten unused ol layer groups
  goog.object.forEach(this.unusedGroups_, function(keys, groupId, obj) {
    goog.array.forEach(keys, this.map.unByKey);
    delete this.olGroupListenKeys_[groupId];
  }, this);
  this.unusedGroups_ = null;
};

/**
 * Synchronizes single layer.
 * @param {ol.layer.Base} olLayer
 * @protected
 */
olcs.AbstractSynchronizer.prototype.synchronizeSingle = function(olLayer) {
  if (goog.isNull(olLayer)) {
    return;
  }
  var olLayerId = goog.getUid(olLayer);

  // handle layer groups
  if (olLayer instanceof ol.layer.Group) {
    var sublayers = olLayer.getLayers();
    if (goog.isDef(sublayers)) {
      sublayers.forEach(function(el, i, arr) {
        this.synchronizeSingle(el);
      }, this);
    }

    if (!goog.isDef(this.olGroupListenKeys_[olLayerId])) {
      var listenKeyArray = [];
      this.olGroupListenKeys_[olLayerId] = listenKeyArray;

      // only the keys that need to be relistened when collection changes
      var collection, contentKeys = [];
      var listenAddRemove = goog.bind(function() {
        collection = olLayer.getLayers();
        if (goog.isDef(collection)) {
          var handleContentChange_ = goog.bind(function(e) {
            this.synchronize();
          }, this);
          contentKeys = [
            collection.on('add', handleContentChange_),
            collection.on('remove', handleContentChange_)
          ];
          listenKeyArray.push.apply(listenKeyArray, contentKeys);
        }
      }, this);
      listenAddRemove();

      listenKeyArray.push(olLayer.on('change:layers', function(e) {
        goog.array.forEach(contentKeys, function(el, i, arr) {
          goog.array.remove(listenKeyArray, el);
          collection.unByKey(el);
        });
        listenAddRemove();
      }));
    }

    delete this.unusedGroups_[olLayerId];

    return;
  } else if (!(olLayer instanceof ol.layer.Layer)) {
    return;
  }

  var cesiumObject = this.layerMap[olLayerId];

  // no mapping -> create new layer and set up synchronization
  if (!goog.isDef(cesiumObject)) {
    cesiumObject = this.createSingleCounterpart(olLayer);
    this.layerMap[olLayerId] = cesiumObject;
  }

  // add Cesium layers
  if (goog.isDefAndNotNull(cesiumObject)) {
    this.addCesiumObject(cesiumObject);
    delete this.unusedCesiumObjects_[cesiumObject];
  }
};

/**
 * Destroys all the created Cesium objects.
 * @protected
 */
olcs.AbstractSynchronizer.prototype.destroyAll = function() {
  this.removeAllCesiumObjects(true); // destroy
  this.layerMap = {};
};

/**
 * Adds a single Cesium object to the collection.
 * @param {!T} object
 * @protected
 */
olcs.AbstractSynchronizer.prototype.addCesiumObject = goog.abstractMethod;

/**
 * @param {!T} object
 * @protected
 */
olcs.AbstractSynchronizer.prototype.destroyCesiumObject = goog.abstractMethod;

/**
 * Remove all Cesium objects from the collection.
 * @param {boolean} destroy
 * @protected
 */
olcs.AbstractSynchronizer.prototype.removeAllCesiumObjects =
    goog.abstractMethod;

/**
 * @param {!ol.layer.Layer} olLayer
 * @return {T}
 * @protected
 */
olcs.AbstractSynchronizer.prototype.createSingleCounterpart =
    goog.abstractMethod;
<commit_msg>Remind the compiler of the type of olLayer<commit_after>goog.provide('olcs.AbstractSynchronizer');

goog.require('goog.events');
goog.require('ol.layer.Group');
goog.require('ol.layer.Layer');

/**
 * @param {!ol.Map} map
 * @param {!Cesium.Scene} scene
 * @constructor
 * @template T
 */
olcs.AbstractSynchronizer = function(map, scene) {
  /**
   * @type {!ol.Map}
   * @protected
   */
  this.map = map;

  /**
   * @type {ol.View}
   * @protected
   */
  this.view = null;

  /**
   * @type {!Cesium.Scene}
   * @protected
   */
  this.scene = scene;

  /**
   * @type {ol.Collection.<ol.layer.Base>}
   * @protected
   */
  this.olLayers = null;

  /**
   * @type {!Array}
   * @private
   */
  this.olLayersListenKeys_ = [];

  /**
   * Map of ol3 layer ids (from goog.getUid) to the Cesium ImageryLayers.
   * null value means, that we are unable to create equivalent layer.
   * @type {Object.<number, ?T>}
   * @protected
   */
  this.layerMap = {};

  /**
   * Map of listen keys for ol3 layer groups ids (from goog.getUid).
   * @type {!Object.<number, !Array>}
   * @private
   */
  this.olGroupListenKeys_ = {};

  /**
   * @type {Object.<number, !Array>}
   * @private
   */
  this.unusedGroups_ = null;

  /**
   * @type {Object.<?T, number>}
   * @private
   */
  this.unusedCesiumObjects_ = null;

  this.map.on('change:view', function(e) {
    this.setView_(this.map.getView());
  }, this);
  this.setView_(this.map.getView());

  this.map.on('change:layergroup', function(e) {
    this.setLayers_(this.map.getLayers());
  }, this);
  this.setLayers_(this.map.getLayers());
};

/**
 * @param {ol.View} view New view to use.
 * @private
 */
olcs.AbstractSynchronizer.prototype.setView_ = function(view) {
  this.view = view;

  // destroy all, the change of view can affect which layers are synced
  this.destroyAll();
  this.synchronize();
};

/**
 * @param {ol.Collection.<ol.layer.Base>} layers New layers to use.
 * @private
 */
olcs.AbstractSynchronizer.prototype.setLayers_ = function(layers) {
  if (!goog.isNull(this.olLayers)) {
    goog.array.forEach(this.olLayersListenKeys_, this.olLayers.unByKey);
  }

  this.olLayers = layers;
  if (!goog.isNull(layers)) {
    var handleCollectionEvent_ = goog.bind(function(e) {
      this.synchronize();
    }, this);

    this.olLayersListenKeys_ = [
      layers.on('add', handleCollectionEvent_),
      layers.on('remove', handleCollectionEvent_)
    ];
  } else {
    this.olLayersListenKeys_ = [];
  }

  this.destroyAll();
  this.synchronize();
};

/**
 * Performs complete synchronization of the layers.
 */
olcs.AbstractSynchronizer.prototype.synchronize = function() {
  if (goog.isNull(this.view) || goog.isNull(this.olLayers)) {
    return;
  }
  this.unusedGroups_ = goog.object.clone(this.olGroupListenKeys_);
  this.unusedCesiumObjects_ = goog.object.transpose(this.layerMap);
  this.removeAllCesiumObjects(false); // only remove, don't destroy

  this.olLayers.forEach(function(el, i, arr) {
    this.synchronizeSingle(el);
  }, this);

  // destroy unused Cesium Objects
  goog.array.forEach(goog.object.getValues(this.unusedCesiumObjects_),
      function(el, i, arr) {
        var layerId = el;
        var object = this.layerMap[layerId];
        if (goog.isDef(object)) {
          delete this.layerMap[layerId];
          if (!goog.isNull(object)) {
            this.destroyCesiumObject(object);
          }
        }
      }, this);
  this.unusedCesiumObjects_ = null;

  // unlisten unused ol layer groups
  goog.object.forEach(this.unusedGroups_, function(keys, groupId, obj) {
    goog.array.forEach(keys, this.map.unByKey);
    delete this.olGroupListenKeys_[groupId];
  }, this);
  this.unusedGroups_ = null;
};

/**
 * Synchronizes single layer.
 * @param {ol.layer.Base} olLayer
 * @protected
 */
olcs.AbstractSynchronizer.prototype.synchronizeSingle = function(olLayer) {
  if (goog.isNull(olLayer)) {
    return;
  }
  var olLayerId = goog.getUid(olLayer);

  // handle layer groups
  if (olLayer instanceof ol.layer.Group) {
    var sublayers = olLayer.getLayers();
    if (goog.isDef(sublayers)) {
      sublayers.forEach(function(el, i, arr) {
        this.synchronizeSingle(el);
      }, this);
    }

    if (!goog.isDef(this.olGroupListenKeys_[olLayerId])) {
      var listenKeyArray = [];
      this.olGroupListenKeys_[olLayerId] = listenKeyArray;

      // only the keys that need to be relistened when collection changes
      var collection, contentKeys = [];
      var listenAddRemove = goog.bind(function() {
        collection = /** @type {ol.layer.Group} */ (olLayer).getLayers();
        if (goog.isDef(collection)) {
          var handleContentChange_ = goog.bind(function(e) {
            this.synchronize();
          }, this);
          contentKeys = [
            collection.on('add', handleContentChange_),
            collection.on('remove', handleContentChange_)
          ];
          listenKeyArray.push.apply(listenKeyArray, contentKeys);
        }
      }, this);
      listenAddRemove();

      listenKeyArray.push(olLayer.on('change:layers', function(e) {
        goog.array.forEach(contentKeys, function(el, i, arr) {
          goog.array.remove(listenKeyArray, el);
          collection.unByKey(el);
        });
        listenAddRemove();
      }));
    }

    delete this.unusedGroups_[olLayerId];

    return;
  } else if (!(olLayer instanceof ol.layer.Layer)) {
    return;
  }

  var cesiumObject = this.layerMap[olLayerId];

  // no mapping -> create new layer and set up synchronization
  if (!goog.isDef(cesiumObject)) {
    cesiumObject = this.createSingleCounterpart(olLayer);
    this.layerMap[olLayerId] = cesiumObject;
  }

  // add Cesium layers
  if (goog.isDefAndNotNull(cesiumObject)) {
    this.addCesiumObject(cesiumObject);
    delete this.unusedCesiumObjects_[cesiumObject];
  }
};

/**
 * Destroys all the created Cesium objects.
 * @protected
 */
olcs.AbstractSynchronizer.prototype.destroyAll = function() {
  this.removeAllCesiumObjects(true); // destroy
  this.layerMap = {};
};

/**
 * Adds a single Cesium object to the collection.
 * @param {!T} object
 * @protected
 */
olcs.AbstractSynchronizer.prototype.addCesiumObject = goog.abstractMethod;

/**
 * @param {!T} object
 * @protected
 */
olcs.AbstractSynchronizer.prototype.destroyCesiumObject = goog.abstractMethod;

/**
 * Remove all Cesium objects from the collection.
 * @param {boolean} destroy
 * @protected
 */
olcs.AbstractSynchronizer.prototype.removeAllCesiumObjects =
    goog.abstractMethod;

/**
 * @param {!ol.layer.Layer} olLayer
 * @return {T}
 * @protected
 */
olcs.AbstractSynchronizer.prototype.createSingleCounterpart =
    goog.abstractMethod;
<|endoftext|>


<commit_before>
export function setSpecsURLsForDocumentWithID(documentID, specsURLs) {

}
<commit_msg>Add declarative flambeau-style Specs actions<commit_after>// TODO: change to ../config ?
import { getActionURL } from '../stores/ConfigurationStore';

export function setSpecsURLsForDocument({ documentID, specsURLs }) {
  return arguments[0];
}

export function invalidateSpec({ specURL }) {
  return arguments[0];
}

export function beginLoadingSpec({ specURL }) {
  return arguments[0];
}

export function didLoadSpec({ specURL, specJSON }) {
  return arguments[0];
}

export function didFailLoadingSpec({ specURL, error }) {
  return arguments[0];
}

function loadSpec({ specURL }, { dispatch }) {
  let loadURL;

  const actionURL = getActionURL('specs/');
  if (actionURL) {
    loadURL = actionURL + specURL + '/';
  }
  else {
    loadURL = specURL;
  }

  dispatch({
    actionID: 'beginLoadingSpec',
    payload: { specURL }
  });

  qwest.get(loadURL, null, {
    cache: true,
    dataType: 'json',
    responseType: 'json'
  })
  .then((specJSON) => {
    dispatch({
      actionID: 'didLoadSpec',
      payload: didLoadSpec({ specURL, specJSON })
    });
  })
  .catch((message) => {
    console.error(`Failed to load specs with URL ${specURL} ${message}`);
    dispatch({
      actionID: 'didFailLoadingSpec',
      payload: didFailLoadingSpec({ specURL, error: message })
    });
  });
}

export function loadSpecIfNeeded({ specURL }, { dispatch, getConsensus }) {
  if (getConsensus({
    introspectionID: 'needsToLoadSpec',
    payload: { specURL },
    booleanOr: true
  })) {
    loadSpec({ specURL }, { dispatch });
  }
}

export const introspection = {
  needsToLoadSpec({ specURL }) {
    return arguments[0];
  }
}
<|endoftext|>


<commit_before>/* eslint-env browser */

import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import RouterContext from './RouterContext';
import { once } from './utils';
import { getRoutesByName } from './route';
import { changeStateAction } from './redux-reducer';
import { router as routerPropTypes } from './propTypes';

class Router extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    routerState: PropTypes.shape({
      name: PropTypes.string.isRequired,
      params: PropTypes.object.isRequired
    }).isRequired,
    history: PropTypes.shape({ // instance of history.js
      location: PropTypes.shape({
        pathname: PropTypes.string,
        search: PropTypes.string,
        hash: PropTypes.string
      }).isRequired,
      push: PropTypes.func.isRequired,
      listen: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
      createHref: PropTypes.func.isRequired
    }).isRequired,
    changeStateAction: PropTypes.func // from connect
  };

  static childContextTypes = {
    router: routerPropTypes
  };

  getChildContext () {
    return { router: this._router };
  }

  constructor (...args) {
    super(...args);

    this._router = new RouterContext();
  }

  componentWillMount () {
    const { changeStateAction, history, routerState } = this.props;

    this._router.setRoutes(getRoutesByName());
    this._router.setHistory(history);
    this._router.setState(routerState);

    changeStateAction(history.location, history.action);

    this._historyCleanupListener = once(history.listen((location, action) => {
      changeStateAction(location, action);
    }));
  }

  componentWillUpdate ({ routerState }) {
    this._router.setState(routerState);
  }

  componentWillUnmount () {
    this._historyCleanupListener();
    this._router.destroy();
    this._router = null;
  }

  render () {
    const { routerState, children } = this.props;

    return routerState.name ? children : <noscript />;
  }
}

export default connect(
  ({ routerState }) => ({
    routerState
  }),
  {
    changeStateAction
  }
)(Router);
<commit_msg>fix router<commit_after>/* eslint-env browser */

import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import RouterContext from './RouterContext';
import { once } from './utils';
import { getRoutesByName } from './route';
import { changeStateAction } from './redux-reducer';
import { router as routerPropTypes } from './propTypes';

class Router extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    routerState: PropTypes.shape({
      name: PropTypes.string.isRequired,
      params: PropTypes.object.isRequired
    }).isRequired,
    history: PropTypes.shape({ // instance of history.js
      location: PropTypes.shape({
        pathname: PropTypes.string,
        search: PropTypes.string,
        hash: PropTypes.string
      }).isRequired,
      push: PropTypes.func.isRequired,
      listen: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
      createHref: PropTypes.func.isRequired
    }).isRequired,
    changeStateAction: PropTypes.func // from connect
  };

  static childContextTypes = {
    router: routerPropTypes
  };

  getChildContext () {
    return { router: this._router };
  }

  constructor (...args) {
    super(...args);

    this._router = new RouterContext();
  }

  componentWillMount () {
    const { changeStateAction, history, routerState } = this.props;

    this._router.setRoutes(getRoutesByName());
    this._router.setHistory(history);
    this._router.setState(routerState);

    changeStateAction(history.location, history.action);

    this._historyCleanupListener = once(history.listen((location, action) => {
      changeStateAction(location, action);
    }));
  }

  componentWillUpdate ({ routerState }) {
    this._router.setState(routerState);
  }

  componentWillUnmount () {
    this._historyCleanupListener();
    this._router.destroy();
    this._router = null;
  }

  render () {
    const { children } = this.props;

    return this._router.getState().name ? children : <noscript />;
  }
}

export default connect(
  ({ routerState }) => ({
    routerState
  }),
  {
    changeStateAction
  }
)(Router);
<|endoftext|>