<commit_before>;(function () {
  'use strict'

  /* imports */
  var funAssert = require('fun-assert')
  var Task = require('data.task')
  var R = require('ramda')
  var arrow = require('fun-arrow')
  var funTry = require('fun-try')
  var taskTimeout = require('task-timeout')
  var defaultStageOptions = require('./default-stage')
  var guarded = require('guarded')

  var INPUT_TYPE = 'Array<Array<{ ' +
    'input: Maybe function, ' +
    'assertion: Maybe function, ' +
    'timeout: Maybe Number, ' +
    'action: Maybe function, ' +
    'update: Maybe function, ' +
    'shouldThrow: Maybe Boolean ' +
    ' }>>'

  /* exports */
  module.exports = guarded({
    inputs: [funAssert.type(INPUT_TYPE)],
    f: funTest,
    output: funAssert.type('Function')
  })

  module.exports.makeStage = makeStage

  function funTest (tests) {
    return function testSuite (subject, reporter) {
      return tests
        .map(makeTest)
        .map(R.apply(R.__, [subject, reporter]))
        .reduce(function (t1, t2) {
          return t1.chain(R.always(t2))
        }, Task.of())
    }
  }

  /**
   *
   * @param {[Object]} stages - that define this test
   * @return {Function} subject -> Task stages
   */
  function makeTest (stages) {
    return function test (subject, reporter) {
      return stages
        .map(makeStage)
        .map(R.apply(R.__, [subject, reporter]))
        .reduce(function (s1, s2) {
          return s1.chain(s2)
        }, Task.of())
    }
  }

  /**
   *
   * @param {Object} options - all function parameters
   * @param {Function} options.input - state -> input
   * @param {Function} options.action - (subject, input) -> Task output
   * @param {Function} options.update - (output, state) -> state
   * @param {Function} options.assertion - state -> state
   * @param {Number} options.timeout - milliseconds
   * @param {Boolean} options.shouldThrow - true if this stage should throw
   *
   * @return {Function} (subject, reporter) -> state -> Task state
   */
  function makeStage (options) {
    options = R.merge(defaultStageOptions, options)

    return function stage (subject, reporter) {
      return function transition (state) {
        var task = Task.of(state)
          .map(debug('state:'))
          .map(options.input)
          .map(debug('input:'))
          .chain(R.curry(options.action)(subject))
          .map(R.repeat(R.__, 2))
          .map(debug('split:'))
          .map(arrow.second(R.always(state)))
          .map(debug('second:'))
          .map(arrow.merge(options.update))
          .chain(funTry(options.assertion))

        return taskTimeout(task, options.timeout)
          .map(debug('state:'))
          .map(R.objOf('state'))
          .map(debug('objOf(state):'))
          .map(R.merge({ options: options }))
          .map(debug('stateAndOptions:'))
          .map(reporter)
          .map(R.prop('state'))
          .map(debug('final state:'))
          .chain(Task.of)
      }
    }
  }

  function debug (message) {
    return function (subject) {
      //console.log(message, subject)

      return subject
    }
  }
})()

<commit_msg>[PATCH][REMOVE] debug statements<commit_after>;(function () {
  'use strict'

  /* imports */
  var funAssert = require('fun-assert')
  var Task = require('data.task')
  var R = require('ramda')
  var arrow = require('fun-arrow')
  var funTry = require('fun-try')
  var taskTimeout = require('task-timeout')
  var defaultStageOptions = require('./default-stage')
  var guarded = require('guarded')

  var INPUT_TYPE = 'Array<Array<{ ' +
    'input: Maybe function, ' +
    'assertion: Maybe function, ' +
    'timeout: Maybe Number, ' +
    'action: Maybe function, ' +
    'update: Maybe function, ' +
    'shouldThrow: Maybe Boolean ' +
    ' }>>'

  /* exports */
  module.exports = guarded({
    inputs: [funAssert.type(INPUT_TYPE)],
    f: funTest,
    output: funAssert.type('Function')
  })

  module.exports.makeStage = makeStage

  function funTest (tests) {
    return function testSuite (subject, reporter) {
      return tests
        .map(makeTest)
        .map(R.apply(R.__, [subject, reporter]))
        .reduce(function (t1, t2) {
          return t1.chain(R.always(t2))
        }, Task.of())
    }
  }

  /**
   *
   * @param {[Object]} stages - that define this test
   * @return {Function} subject -> Task stages
   */
  function makeTest (stages) {
    return function test (subject, reporter) {
      return stages
        .map(makeStage)
        .map(R.apply(R.__, [subject, reporter]))
        .reduce(function (s1, s2) {
          return s1.chain(s2)
        }, Task.of())
    }
  }

  /**
   *
   * @param {Object} options - all function parameters
   * @param {Function} options.input - state -> input
   * @param {Function} options.action - (subject, input) -> Task output
   * @param {Function} options.update - (output, state) -> state
   * @param {Function} options.assertion - state -> state
   * @param {Number} options.timeout - milliseconds
   * @param {Boolean} options.shouldThrow - true if this stage should throw
   *
   * @return {Function} (subject, reporter) -> state -> Task state
   */
  function makeStage (options) {
    options = R.merge(defaultStageOptions, options)

    return function stage (subject, reporter) {
      return function transition (state) {
        var task = Task.of(state)
          .map(options.input)
          .chain(R.curry(options.action)(subject))
          .map(R.repeat(R.__, 2))
          .map(arrow.second(R.always(state)))
          .map(arrow.merge(options.update))
          .chain(funTry(options.assertion))

        return taskTimeout(task, options.timeout)
          .map(R.objOf('state'))
          .map(R.merge({ options: options }))
          .map(reporter)
          .map(R.prop('state'))
          .chain(Task.of)
      }
    }
  }
})()

<|endoftext|>


<commit_before>// @flow

import findMatchingVNodes from './lib/find-matching-vnodes';
import {
  findVueComponents,
  vmCtorMatchesName,
} from './lib/vm';
import {
  isValidSelector,
} from './lib/validators';
import VueWrapper from './VueWrapper';
import {
  error,
  warn,
} from './lib/logger';

export default class Wrapper implements WrapperInterface {
  vNode: VNode;
  vm: Component;
  element: HTMLElement;
  update: Function;
  mountedToDom: boolean;
  isVueComponent: boolean;

  constructor(vNode: VNode, update: Function, mountedToDom: boolean) {
    this.vNode = vNode;
    this.element = vNode.elm;
    this.update = update;
    this.mountedToDom = mountedToDom;
  }

  /**
   * Returns instances computed object
   *
   * @returns {Object}
   */
  computed() {
    if (!this.isVueComponent) {
      error('wrapper.computed() can only be called on a Vue instance');
    }

    warn('functions returned by computed() will not have this bound to the vue instance. Calling a computed function that uses this will result in an error. You can access computed functions by using the vue instance. e.g. to call a computed function named compFunc, call wrapper.vm.compFunc. See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$options.computed;
  }

  /**
   * Checks if wrapper contains provided selector.
   *
   * @param {String} selector
   * @returns {Boolean}
   */
  contains(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.contains() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      const vm = this.vm || this.vNode.context.$root;
      return findVueComponents(vm, selector.name).length > 0;
    }

    return this.element.querySelectorAll(selector).length > 0;
  }

  /**
   * Returns instances data object
   *
   * @returns {Object}
   */
  data() {
    if (!this.isVueComponent) {
      error('wrapper.data() can only be called on a Vue instance');
    }

    return this.vm._data;
  }

  /**
   * Calls destroy on vm
   */
  destroy() {
    if (!this.isVueComponent) {
      error('wrapper.destroy() can only be called on a Vue instance');
    }

    if (this.vm.$el.parentNode) {
      this.vm.$el.parentNode.removeChild(this.vm.$el);
    }

    this.vm.$destroy();
  }

  /**
   * Dispatches a DOM event on wrapper
   *
   * @param {String} type - type of event
   * @returns {Boolean}
   */
  dispatch(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.dispatch() must be passed a string');
    }

    warn('wrapper.dispatch() is deprecated and will be removed from future versions. Use wrapper.trigger() instead - https://eddyerburgh.gitbooks.io/avoriaz/content/api/mount/trigger.html');

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    const eventObject = new window.Event(event[0]);

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    if (this.isVueComponent) {
      this.vm.$emit(type);
    }

    this.element.dispatchEvent(eventObject);

    this.update();
  }

  /**
   * Finds every node in the mount tree of the current wrapper that matches the provided selector.
   *
   * @param {String|Object} selector
   * @returns {VueWrapper||VueWrapper[]}
   */
  find(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.find() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      if (!selector.name) {
        error('.find() requires component to have a name property');
      }
      const vm = this.vm || this.vNode.context.$root;
      const components = findVueComponents(vm, selector.name);
      return components.map(component => new VueWrapper(component, this.mountedToDom));
    }

    const nodes = findMatchingVNodes(this.vNode, selector);

    return nodes.map(node => new Wrapper(node, this.update, this.mountedToDom));
  }

  /**
   * Returns the first node that matches the provided selector.
   *
   * @param {String|Object} selector
   * @returns {VueWrapper}
   */
  first(selector: Selector) {
    const nodes = this.find(selector);

    if (!nodes.length) {
      error('wrapper.first() has no matches with the given selector');
    }

    return nodes[0];
  }

  /**
   * Checks if wrapper has an attribute with matching value
   *
   * @param {String} attribute - attribute to assert
   * @param {String} value - value attribute should contain
   * @returns {Boolean}
   */
  hasAttribute(attribute: string, value: string) {
    if (typeof attribute !== 'string') {
      error('wrapper.hasAttribute() must be passed attribute as a string');
    }

    if (typeof value !== 'string') {
      error('wrapper.hasAttribute() must be passed value as a string');
    }

    return this.element.getAttribute(attribute) === value;
  }

  /**
   * Asserts wrapper has a class name
   *
   * @param {String} className - class name to assert
   * @returns {Boolean}
   */
  hasClass(className: string) {
    if (typeof className !== 'string') {
      error('wrapper.hasClass() must be passed a string');
    }

    return this.element.className.split(' ').indexOf(className) !== -1;
  }

  /**
   * Checks if wrapper has a style with value
   *
   * @param {String} style - style to assert
   * @param {String} value - value to assert style contains
   * @returns {Boolean}
   */
  hasStyle(style: string, value: string) {
    if (typeof style !== 'string') {
      error('wrapper.hasStyle() must be passed style as a string');
    }

    if (typeof value !== 'string') {
      error('wrapper.hasClass() must be passed value as string');
    }

    /* istanbul ignore next */
    if (navigator.userAgent.includes && navigator.userAgent.includes('node.js')) {
      warn('wrapper.hasStyle is not fully supported when running jsdom - only inline styles are supported');
    }
    const body = document.querySelector('body');
    const mockElement = document.createElement('div');
    // $FlowIgnore
    const mockNode = body.insertBefore(mockElement, null);
    // $FlowIgnore
    mockElement.style[style] = value;

    if (!this.mountedToDom) {
      const vm = this.vm || this.vNode.context.$root;
      // $FlowIgnore
      body.insertBefore(vm.$root._vnode.elm, null);
    }

    const elStyle = window.getComputedStyle(this.element)[style];
    const mockNodeStyle = window.getComputedStyle(mockNode)[style];
    return elStyle === mockNodeStyle;
  }

  /**
   * Returns HTML of element as a string
   *
   * @returns {String} HTML of wrapper element
   */
  html() {
    const tmp = document.createElement('div');
    tmp.appendChild(this.element);
    return tmp.innerHTML;
  }

  /**
   * Returns Vue instance
   *
   * @returns {String} HTML of wrapper element
   */
  instance() {
    if (!this.isVueComponent) {
      error('wrapper.instance() can only be called on a Vue instance');
    }

    return this.vm;
  }

  /**
   * Checks if node matches selector
   *
   * @param {String} selector - selector to check node is
   * @returns {Boolean}
   */
  is(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.is() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      if (!this.isVueComponent) {
        return false;
      }

      return vmCtorMatchesName(this.vm, selector.name);
    }

    if (selector[0] === '.') {
      return this.element.className.split(' ').indexOf(selector.substr(1)) !== -1;
    }

    if (selector[0] === '#') {
      return this.element.getAttribute('id') === selector.substr(1);
    }

    return this.element.tagName === selector.toUpperCase();
  }

  /**
   * Checks if node is empty
   *
   * @returns {Boolean}
   */
  isEmpty() {
    return this.vNode.children === undefined;
  }

  /**
   * Returns instances methods object
   *
   * @returns {Object}
   */
  methods() {
    if (!this.isVueComponent) {
      error('wrapper.methods() can only be called on a Vue instance');
    }

    warn('functions returned by methods() will not have this bound to the vue instance. Calling a method that uses this will result in an error. You can access methods by using the vue instance. e.g. to call a method function named aMethod, call wrapper.vm.aMethod(). See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$options.methods;
  }

  /**
   * Returns name of component, or tag name if node is not a Vue component
   *
   * @returns {String}
   */
  name() {
    if (this.isVueComponent) {
      return this.vm.$options.name;
    }

    return this.vNode.tag;
  }

  /**
   * Returns instances propsData object
   *
   * @returns {String}
   */
  propsData() {
    if (!this.isVueComponent) {
      error('wrapper.propsData() can only be called on a Vue instance');
    }

    warn('functions returned by propsData() will not have this bound to the vue instance. Calling a propsData function that uses this will result in an error. You can access propsData functions by using the vue instance. e.g. to call a method function named propsDataFunc, call wrapper.vm.$props.propsDataFunc(). See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$props;
  }

  /**
   * Sets vm data
   *
   * @param {Object} data - data to set
   */
  setData(data: Object) {
    if (!this.isVueComponent) {
      error('wrapper.setData() can only be called on a Vue instance');
    }

    Object.keys(data).forEach((key) => {
      this.vm.$set(this.vm, [key], data[key]);
    });
    this.update();
    this.vNode = this.vm._vnode;
  }

  /**
   * Sets vm props
   *
   * @param {Object} props - props to set
   */
  setProps(props: Object) {
    if (!this.isVueComponent) {
      error('wrapper.setProps() can only be called on a Vue instance');
    }
    const vm = this.vm || this.vNode.context.$root;

    Object.keys(props).forEach((key) => {
      this.vm._props[key] = props[key];
    });
    Object.keys(props).forEach((key) => {
      vm._watchers.forEach((watcher) => {
        if (watcher.expression === key) { watcher.run(); }
      });
    });
    this.update();

    this.vNode = this.vm._vnode;
  }

  /**
   * Simulates a DOM event on wrapper
   *
   * @param {String} type - type of event
   * @returns {Boolean}
   */
  simulate(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.simulate() must be passed a string');
    }

    warn('wrapper.simulate() is deprecated and will be removed from future versions. Use wrapper.trigger() instead - https://eddyerburgh.gitbooks.io/avoriaz/content/api/mount/trigger.html');

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    const eventObject = new window.Event(event[0]);

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    this.element.dispatchEvent(eventObject);
    this.update();
  }

  /**
   * Returns element style object
   *
   * @returns {Object}
   */
  style() {
    warn('wrapper.style() is deprecated and will be removed from future versions. Use wrapper.hasStyle() instead');
    // $FlowIgnore
    const node = document.querySelector('body').insertBefore(this.element, null);
    return window.getComputedStyle(node);
  }

  /**
   * Return text of wrapper element
   *
   * @returns {Boolean}
   */
  text() {
    return this.element.textContent;
  }

  /**
     * Triggers a DOM event on wrapper
     *
     * @param {String} type - type of event
     * @returns {Boolean}
     */
  trigger(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.trigger() must be passed a string');
    }

    this.update();

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    const eventObject = new window.Event(event[0]);

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    this.element.dispatchEvent(eventObject);

    this.update();
  }
}
<commit_msg>[Fix] Add workaround for IE in trigger<commit_after>// @flow

import findMatchingVNodes from './lib/find-matching-vnodes';
import {
  findVueComponents,
  vmCtorMatchesName,
} from './lib/vm';
import {
  isValidSelector,
} from './lib/validators';
import VueWrapper from './VueWrapper';
import {
  error,
  warn,
} from './lib/logger';

export default class Wrapper implements WrapperInterface {
  vNode: VNode;
  vm: Component;
  element: HTMLElement;
  update: Function;
  mountedToDom: boolean;
  isVueComponent: boolean;

  constructor(vNode: VNode, update: Function, mountedToDom: boolean) {
    this.vNode = vNode;
    this.element = vNode.elm;
    this.update = update;
    this.mountedToDom = mountedToDom;
  }

  /**
   * Returns instances computed object
   *
   * @returns {Object}
   */
  computed() {
    if (!this.isVueComponent) {
      error('wrapper.computed() can only be called on a Vue instance');
    }

    warn('functions returned by computed() will not have this bound to the vue instance. Calling a computed function that uses this will result in an error. You can access computed functions by using the vue instance. e.g. to call a computed function named compFunc, call wrapper.vm.compFunc. See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$options.computed;
  }

  /**
   * Checks if wrapper contains provided selector.
   *
   * @param {String} selector
   * @returns {Boolean}
   */
  contains(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.contains() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      const vm = this.vm || this.vNode.context.$root;
      return findVueComponents(vm, selector.name).length > 0;
    }

    return this.element.querySelectorAll(selector).length > 0;
  }

  /**
   * Returns instances data object
   *
   * @returns {Object}
   */
  data() {
    if (!this.isVueComponent) {
      error('wrapper.data() can only be called on a Vue instance');
    }

    return this.vm._data;
  }

  /**
   * Calls destroy on vm
   */
  destroy() {
    if (!this.isVueComponent) {
      error('wrapper.destroy() can only be called on a Vue instance');
    }

    if (this.vm.$el.parentNode) {
      this.vm.$el.parentNode.removeChild(this.vm.$el);
    }

    this.vm.$destroy();
  }

  /**
   * Dispatches a DOM event on wrapper
   *
   * @param {String} type - type of event
   * @returns {Boolean}
   */
  dispatch(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.dispatch() must be passed a string');
    }

    warn('wrapper.dispatch() is deprecated and will be removed from future versions. Use wrapper.trigger() instead - https://eddyerburgh.gitbooks.io/avoriaz/content/api/mount/trigger.html');

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    const eventObject = new window.Event(event[0]);

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    if (this.isVueComponent) {
      this.vm.$emit(type);
    }

    this.element.dispatchEvent(eventObject);

    this.update();
  }

  /**
   * Finds every node in the mount tree of the current wrapper that matches the provided selector.
   *
   * @param {String|Object} selector
   * @returns {VueWrapper||VueWrapper[]}
   */
  find(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.find() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      if (!selector.name) {
        error('.find() requires component to have a name property');
      }
      const vm = this.vm || this.vNode.context.$root;
      const components = findVueComponents(vm, selector.name);
      return components.map(component => new VueWrapper(component, this.mountedToDom));
    }

    const nodes = findMatchingVNodes(this.vNode, selector);

    return nodes.map(node => new Wrapper(node, this.update, this.mountedToDom));
  }

  /**
   * Returns the first node that matches the provided selector.
   *
   * @param {String|Object} selector
   * @returns {VueWrapper}
   */
  first(selector: Selector) {
    const nodes = this.find(selector);

    if (!nodes.length) {
      error('wrapper.first() has no matches with the given selector');
    }

    return nodes[0];
  }

  /**
   * Checks if wrapper has an attribute with matching value
   *
   * @param {String} attribute - attribute to assert
   * @param {String} value - value attribute should contain
   * @returns {Boolean}
   */
  hasAttribute(attribute: string, value: string) {
    if (typeof attribute !== 'string') {
      error('wrapper.hasAttribute() must be passed attribute as a string');
    }

    if (typeof value !== 'string') {
      error('wrapper.hasAttribute() must be passed value as a string');
    }

    return this.element.getAttribute(attribute) === value;
  }

  /**
   * Asserts wrapper has a class name
   *
   * @param {String} className - class name to assert
   * @returns {Boolean}
   */
  hasClass(className: string) {
    if (typeof className !== 'string') {
      error('wrapper.hasClass() must be passed a string');
    }

    return this.element.className.split(' ').indexOf(className) !== -1;
  }

  /**
   * Checks if wrapper has a style with value
   *
   * @param {String} style - style to assert
   * @param {String} value - value to assert style contains
   * @returns {Boolean}
   */
  hasStyle(style: string, value: string) {
    if (typeof style !== 'string') {
      error('wrapper.hasStyle() must be passed style as a string');
    }

    if (typeof value !== 'string') {
      error('wrapper.hasClass() must be passed value as string');
    }

    /* istanbul ignore next */
    if (navigator.userAgent.includes && navigator.userAgent.includes('node.js')) {
      warn('wrapper.hasStyle is not fully supported when running jsdom - only inline styles are supported');
    }
    const body = document.querySelector('body');
    const mockElement = document.createElement('div');
    // $FlowIgnore
    const mockNode = body.insertBefore(mockElement, null);
    // $FlowIgnore
    mockElement.style[style] = value;

    if (!this.mountedToDom) {
      const vm = this.vm || this.vNode.context.$root;
      // $FlowIgnore
      body.insertBefore(vm.$root._vnode.elm, null);
    }

    const elStyle = window.getComputedStyle(this.element)[style];
    const mockNodeStyle = window.getComputedStyle(mockNode)[style];
    return elStyle === mockNodeStyle;
  }

  /**
   * Returns HTML of element as a string
   *
   * @returns {String} HTML of wrapper element
   */
  html() {
    const tmp = document.createElement('div');
    tmp.appendChild(this.element);
    return tmp.innerHTML;
  }

  /**
   * Returns Vue instance
   *
   * @returns {String} HTML of wrapper element
   */
  instance() {
    if (!this.isVueComponent) {
      error('wrapper.instance() can only be called on a Vue instance');
    }

    return this.vm;
  }

  /**
   * Checks if node matches selector
   *
   * @param {String} selector - selector to check node is
   * @returns {Boolean}
   */
  is(selector: Selector) {
    if (!isValidSelector(selector)) {
      error('wrapper.is() must be passed a valid CSS selector or a Vue constructor');
    }

    if (typeof selector === 'object') {
      if (!this.isVueComponent) {
        return false;
      }

      return vmCtorMatchesName(this.vm, selector.name);
    }

    if (selector[0] === '.') {
      return this.element.className.split(' ').indexOf(selector.substr(1)) !== -1;
    }

    if (selector[0] === '#') {
      return this.element.getAttribute('id') === selector.substr(1);
    }

    return this.element.tagName === selector.toUpperCase();
  }

  /**
   * Checks if node is empty
   *
   * @returns {Boolean}
   */
  isEmpty() {
    return this.vNode.children === undefined;
  }

  /**
   * Returns instances methods object
   *
   * @returns {Object}
   */
  methods() {
    if (!this.isVueComponent) {
      error('wrapper.methods() can only be called on a Vue instance');
    }

    warn('functions returned by methods() will not have this bound to the vue instance. Calling a method that uses this will result in an error. You can access methods by using the vue instance. e.g. to call a method function named aMethod, call wrapper.vm.aMethod(). See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$options.methods;
  }

  /**
   * Returns name of component, or tag name if node is not a Vue component
   *
   * @returns {String}
   */
  name() {
    if (this.isVueComponent) {
      return this.vm.$options.name;
    }

    return this.vNode.tag;
  }

  /**
   * Returns instances propsData object
   *
   * @returns {String}
   */
  propsData() {
    if (!this.isVueComponent) {
      error('wrapper.propsData() can only be called on a Vue instance');
    }

    warn('functions returned by propsData() will not have this bound to the vue instance. Calling a propsData function that uses this will result in an error. You can access propsData functions by using the vue instance. e.g. to call a method function named propsDataFunc, call wrapper.vm.$props.propsDataFunc(). See https://github.com/eddyerburgh/avoriaz/issues/15');

    return this.vm.$props;
  }

  /**
   * Sets vm data
   *
   * @param {Object} data - data to set
   */
  setData(data: Object) {
    if (!this.isVueComponent) {
      error('wrapper.setData() can only be called on a Vue instance');
    }

    Object.keys(data).forEach((key) => {
      this.vm.$set(this.vm, [key], data[key]);
    });
    this.update();
    this.vNode = this.vm._vnode;
  }

  /**
   * Sets vm props
   *
   * @param {Object} props - props to set
   */
  setProps(props: Object) {
    if (!this.isVueComponent) {
      error('wrapper.setProps() can only be called on a Vue instance');
    }
    const vm = this.vm || this.vNode.context.$root;

    Object.keys(props).forEach((key) => {
      this.vm._props[key] = props[key];
    });
    Object.keys(props).forEach((key) => {
      vm._watchers.forEach((watcher) => {
        if (watcher.expression === key) { watcher.run(); }
      });
    });
    this.update();

    this.vNode = this.vm._vnode;
  }

  /**
   * Simulates a DOM event on wrapper
   *
   * @param {String} type - type of event
   * @returns {Boolean}
   */
  simulate(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.simulate() must be passed a string');
    }

    warn('wrapper.simulate() is deprecated and will be removed from future versions. Use wrapper.trigger() instead - https://eddyerburgh.gitbooks.io/avoriaz/content/api/mount/trigger.html');

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    const eventObject = new window.Event(event[0]);

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    this.element.dispatchEvent(eventObject);
    this.update();
  }

  /**
   * Returns element style object
   *
   * @returns {Object}
   */
  style() {
    warn('wrapper.style() is deprecated and will be removed from future versions. Use wrapper.hasStyle() instead');
    // $FlowIgnore
    const node = document.querySelector('body').insertBefore(this.element, null);
    return window.getComputedStyle(node);
  }

  /**
   * Return text of wrapper element
   *
   * @returns {Boolean}
   */
  text() {
    return this.element.textContent;
  }

  /**
     * Triggers a DOM event on wrapper
     *
     * @param {String} type - type of event
     * @returns {Boolean}
     */
  trigger(type: string) {
    if (typeof type !== 'string') {
      error('wrapper.trigger() must be passed a string');
    }

    this.update();

    const modifiers = {
      enter: 13,
      tab: 9,
      delete: 46,
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39,
    };

    const event = type.split('.');

    let eventObject = new window.Event(event[0]);

    // Fallback for IE10,11 - https://stackoverflow.com/questions/26596123
    if (typeof (Event) === 'function') {
      eventObject = new Event(event[0]);
    } else {
      eventObject = document.createEvent('Event');
      eventObject.initEvent(event[0], true, true);
    }

    if (event.length === 2) {
      eventObject.keyCode = modifiers[event[1]];
    }

    this.element.dispatchEvent(eventObject);

    this.update();
  }
}
<|endoftext|>


<commit_before>'use strict'

import * as hazy from '../node_modules/hazy/lib/hazy.js'
import protagonist from 'protagonist'
import hercule from 'hercule'

import _ from 'lodash'
import fs from 'fs'
import glob from 'glob'

/**
 * Parses, transcludes and compiles API blueprint files
 * and their associated fixtures. Allows compiled
 * API blueprints to be exported to the filesystem.
 */
export class Blueprint {

  /**
   * @param {String} markdown Valid API blueprint markdown
   */
  constructor(markdown: String) {
    this.markdown = markdown

    const statics = ['compile', 'fixtures', 'parse', 'transclude']

    statics.forEach(method => {
      this[method] = () => Blueprint[method](this.markdown)
    })
  }

  /**
   * Compiles API blueprint markdown data by translcuding it and then parsing it for fixtures
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static compile(markdown: String): Promise {
    return Blueprint
      .transclude(markdown)
      .then(embedMd => Blueprint.interpolate(embedMd))
      .then(finalMd => Blueprint.fixtures(finalMd).then(fixtures => {
        return Object.assign(this, {compiled: {fixtures, markdown: finalMd}})
      }))
  }

  /**
   * Extracts JSON fixtures from API blueprint markdown
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static fixtures(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      let fixtures = []
      const jsonStrMatches = markdown.match(plainJson)

      jsonStrMatches.forEach(jsonStr => {
        try {
          const fixture = JSON.parse(jsonStr)

          fixtures.push(fixture)
        } catch (e) {
          if (e instanceof SyntaxError) {
            reject(`Found invalid JSON in API blueprint ${blueprint.filepath}: ${jsonStr}\n\n${e}`)
          } else {
            reject(`Failed to parse JSON fixtures in API blueprint: ${e}`)
          }
        }
      })

      resolve(fixtures)
    })
  }

  /**
   * Parses markdown into a usable object via protagonist
   *
   * @param {String} markdown Valid API blueprint markdown
   */
  static parse(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      if (markdown) {
        protagonist.parse(markdown, (err, blueprint) => {
          if (!err) {
            resolve(blueprint)
          } else {
            reject(`Failed to parse file as valid API blueprint: ${err}`)
          }
        })
      } else {
        reject(`Markdown data required`)
      }
    })
  }

  /**
   * Scans API blueprint markdown for Hercule references and transcludes them
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static transclude(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      if (markdown) {
        const transcludedMarkdown = hercule.transcludeString(markdown, _ => _)

        if (transcludedMarkdown) {
          resolve(transcludedMarkdown)
        } else {
          reject('Failed to parse markdown for Hercule transclusions!')
        }
      } else {
        reject('Valid markdown required for transclusion')
      }
    })
  }

  /**
   * Interpolates markdown with relevant replacements. Uses hazy's random
   * fixture data interpolator by default
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static interpolate(markdown: String): Promise {
    return new Promise((resolve, reject) => resolve(interpolator.lang.process(markdown)))
  }

  /**
   * Parses and compiles an API blueprint from the file system
   *
   * @param {String} filepath
   * @returns {Promise}
   */
  static src(filepath: String): Promise {
    return new Promise((resolve, reject) => {
      if (filepath) {
        fs.readFile(__dirname + '/' + filepath, 'utf-8', (err, data) => {
          if (!err) {
            resolve(new Blueprint(data).compile())
          } else {
            reject(`Failed to read file: ${error}`)
          }
        })
      } else {
        reject(`Failed to read file, filepath required`)
      }
    })
  }

  /**
   * Exports API blueprint as either a static html or apib file
   * to the provided file path
   *
   * @param {String} filepath
   * @returns {Promise}
   */
  dist(fixtures, filepath: String): Promise {
    return new Promise((resolve, reject) => {
      const extension = filepath.match(/\.(.*?)$/)

      if (extension) {
        this.read(fixtures)
          .then(consumed => this.marshall(consumed.compiled.content, extension[0]))
          .then(marshalled => {
            fs.writeFile(destination, marshalled, 'utf-8', err => {
              if (!err) {
                resolve(content)
              } else {
                reject(`An error occured while saving file: ${err}`)
              }
            })
          })
      } else {
        reject(`File destinations must contain an extension (.apib or .html)`)
      }
    })
  }

  /**
   * Attempts to marshall API blueprint content into a specific type
   *
   * @param {String} markdown
   * @param {String} filetype 'apib' or 'html'
   * @returns {Promise}
   */
  marshall(markdown: String, filetype: String): Promise {
    return new Promise((resolve, reject) => {
      if (filetype === 'apib') {
        resolve(markdown)
      } else if (filetype === 'json') {
        resolve(Blueprint.fixtures(markdown)) // FIXME
      } else if (filetype === 'html') {
        resolve(null) // TODO
      } else {
        reject(`Unsupported filetype: ${filetype}`)
      }
    })
  }

}

/**
 * Reads in valid API blueprint(s) from Strings or
 * Arrays of Strings and compiles them
 *
 * @param {Array|String} blueprints
 * @returns {Promise}
 */
export function read(blueprints): Promise {
  return new Promise((resolve, reject) => {
    if (_.isArray(blueprints)) {
      resolve(Promise.all(
        blueprints.map(bp => new Blueprint(bp).compile())
      ))
    } else if (_.isString(blueprints)) {
      resolve(new Blueprint(blueprints).compile())
    } else {
      reject('Documents must be represented as a String or Array, got ' + typeof blueprints)
    }
  })
}

/**
 * Globs and reads in valid API blueprint(s) from the filesystem and compiles them
 *
 * @param {Array|String} blueprints
 * @returns {Promise}
 */
export function load(pattern, options): Promise {
  return new Promise((resolve, reject) => {
    glob(pattern, options, (err, files) => {
      if (!err) {
        return Promise.all(
          files.map(file => new Blueprint(file).compile())
        )
      } else {
        reject(`Failed to load file: ${err}`)
      }
    })
  })
}

/**
 * Allows developers to configure and override the default instance of hazy
 */
export var interpolator = hazy

/**
 * A janky regex for finding "JSON" objects in markdown.
 */
export const plainJson = /\{(.*?)\}/gm
<commit_msg>:zap: fix for transclution (use of hercule)<commit_after>'use strict'

import * as hazy from '../node_modules/hazy/lib/hazy.js'
import protagonist from 'protagonist'
import hercule from 'hercule'

import _ from 'lodash'
import fs from 'fs'
import glob from 'glob'

/**
 * Parses, transcludes and compiles API blueprint files
 * and their associated fixtures. Allows compiled
 * API blueprints to be exported to the filesystem.
 */
export class Blueprint {

  /**
   * @param {String} markdown Valid API blueprint markdown
   */
  constructor(markdown: String) {
    this.markdown = markdown

    const statics = ['compile', 'fixtures', 'parse', 'transclude']

    statics.forEach(method => {
      this[method] = () => Blueprint[method](this.markdown)
    })
  }

  /**
   * Compiles API blueprint markdown data by translcuding it and then parsing it for fixtures
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static compile(markdown: String): Promise {
    return Blueprint
      .transclude(markdown)
      .then(embedMd => Blueprint.interpolate(embedMd))
      .then(finalMd => Blueprint.fixtures(finalMd).then(fixtures => {
        return Object.assign(this, {compiled: {fixtures, markdown: finalMd}})
      }))
  }

  /**
   * Extracts JSON fixtures from API blueprint markdown
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static fixtures(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      let fixtures = []
      const jsonStrMatches = markdown.match(plainJson)

      jsonStrMatches && jsonStrMatches.forEach(jsonStr => {
        try {
          const fixture = JSON.parse(jsonStr)

          fixtures.push(fixture)
        } catch (e) {
          if (e instanceof SyntaxError) {
            reject(`Found invalid JSON in API blueprint ${blueprint.filepath}: ${jsonStr}\n\n${e}`)
          } else {
            reject(`Failed to parse JSON fixtures in API blueprint: ${e}`)
          }
        }
      })

      resolve(fixtures)
    })
  }

  /**
   * Parses markdown into a usable object via protagonist
   *
   * @param {String} markdown Valid API blueprint markdown
   */
  static parse(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      if (markdown) {
        protagonist.parse(markdown, (err, blueprint) => {
          if (!err) {
            resolve(blueprint)
          } else {
            reject(`Failed to parse file as valid API blueprint: ${err}`)
          }
        })
      } else {
        reject(`Markdown data required`)
      }
    })
  }

  /**
   * Scans API blueprint markdown for Hercule references and transcludes them
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static transclude(markdown: String): Promise {
    return new Promise((resolve, reject) => {
      if (markdown) {
        hercule.transcludeString(markdown, (transMd) => {
          if (transMd) {
            resolve(transMd)
          } else {
            reject('Failed to parse markdown for Hercule transclusions!')
          }
        })

      } else {
        reject('Valid markdown required for transclusion')
      }
    })
  }

  /**
   * Interpolates markdown with relevant replacements. Uses hazy's random
   * fixture data interpolator by default
   *
   * @param {String} markdown
   * @returns {Promise}
   */
  static interpolate(markdown: String): Promise {
    return new Promise((resolve, reject) => resolve(interpolator.lang.process(markdown)))
  }

  /**
   * Parses and compiles an API blueprint from the file system
   *
   * @param {String} filepath
   * @returns {Promise}
   */
  static src(filepath: String): Promise {
    return new Promise((resolve, reject) => {
      if (filepath) {
        fs.readFile(__dirname + '/' + filepath, 'utf-8', (err, data) => {
          if (!err) {
            resolve(new Blueprint(data).compile())
          } else {
            reject(`Failed to read file: ${error}`)
          }
        })
      } else {
        reject(`Failed to read file, filepath required`)
      }
    })
  }

  /**
   * Exports API blueprint as either a static html or apib file
   * to the provided file path
   *
   * @param {String} filepath
   * @returns {Promise}
   */
  dist(fixtures, filepath: String): Promise {
    return new Promise((resolve, reject) => {
      const extension = filepath.match(/\.(.*?)$/)

      if (extension) {
        this.read(fixtures)
          .then(consumed => this.marshall(consumed.compiled.content, extension[0]))
          .then(marshalled => {
            fs.writeFile(destination, marshalled, 'utf-8', err => {
              if (!err) {
                resolve(content)
              } else {
                reject(`An error occured while saving file: ${err}`)
              }
            })
          })
      } else {
        reject(`File destinations must contain an extension (.apib or .html)`)
      }
    })
  }

  /**
   * Attempts to marshall API blueprint content into a specific type
   *
   * @param {String} markdown
   * @param {String} filetype 'apib' or 'html'
   * @returns {Promise}
   */
  marshall(markdown: String, filetype: String): Promise {
    return new Promise((resolve, reject) => {
      if (filetype === 'apib') {
        resolve(markdown)
      } else if (filetype === 'json') {
        resolve(Blueprint.fixtures(markdown)) // FIXME
      } else if (filetype === 'html') {
        resolve(null) // TODO
      } else {
        reject(`Unsupported filetype: ${filetype}`)
      }
    })
  }

}

/**
 * Reads in valid API blueprint(s) from Strings or
 * Arrays of Strings and compiles them
 *
 * @param {Array|String} blueprints
 * @returns {Promise}
 */
export function read(blueprints): Promise {
  return new Promise((resolve, reject) => {
    if (_.isArray(blueprints)) {
      resolve(Promise.all(
        blueprints.map(bp => new Blueprint(bp).compile())
      ))
    } else if (_.isString(blueprints)) {
      resolve(new Blueprint(blueprints).compile())
    } else {
      reject('Documents must be represented as a String or Array, got ' + typeof blueprints)
    }
  })
}

/**
 * Globs and reads in valid API blueprint(s) from the filesystem and compiles them
 *
 * @param {Array|String} blueprints
 * @returns {Promise}
 */
export function load(pattern, options): Promise {
  return new Promise((resolve, reject) => {
    glob(pattern, options, (err, files) => {
      if (!err) {
        return Promise.all(
          files.map(file => new Blueprint(file).compile())
        )
      } else {
        reject(`Failed to load file: ${err}`)
      }
    })
  })
}

/**
 * Allows developers to configure and override the default instance of hazy
 */
export var interpolator = hazy

/**
 * A janky regex for finding "JSON" objects in markdown.
 */
export const plainJson = /\{(.*?)\}/gm
<|endoftext|>


<commit_before>/*
  backgrid
  http://github.com/wyuenho/backgrid

  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  Licensed under the MIT @license.
*/

/**
   Body is the table body which contains the rows inside a table. Body is
   responsible for refreshing the rows after sorting, insertion and removal.

   @class Backgrid.Body
   @extends Backbone.View
*/
var Body = Backgrid.Body = Backbone.View.extend({

  /** @property */
  tagName: "tbody",

  /**
     Initializer.

     @param {Object} options
     @param {Backbone.Collection} options.collection
     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
     Column metadata.
     @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
     @param {string} [options.emptyText] The text to display in the empty row.

     @throws {TypeError} If options.columns or options.collection is undefined.

     See Backgrid.Row.
  */
  initialize: function (options) {
    Backgrid.requireOptions(options, ["columns", "collection"]);

    this.columns = options.columns;
    if (!(this.columns instanceof Backbone.Collection)) {
      this.columns = new Columns(this.columns);
    }

    this.row = options.row || Row;
    this.rows = this.collection.map(function (model) {
      var row = new this.row({
        columns: this.columns,
        model: model
      });

      return row;
    }, this);

    this.emptyText = options.emptyText;
    this._unshiftEmptyRowMayBe();

    var collection = this.collection;
    this.listenTo(collection, "add", this.insertRow);
    this.listenTo(collection, "remove", this.removeRow);
    this.listenTo(collection, "sort", this.refresh);
    this.listenTo(collection, "reset", this.refresh);
    this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
  },

  _unshiftEmptyRowMayBe: function () {
    if (this.rows.length === 0 && this.emptyText != null) {
      this.rows.unshift(new EmptyRow({
        emptyText: this.emptyText,
        columns: this.columns
      }));
    }
  },

  /**
     This method can be called either directly or as a callback to a
     [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.

     When called directly, it accepts a model or an array of models and an
     option hash just like
     [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
     delegates to it. Once the model is added, a new row is inserted into the
     body and automatically rendered.

     When called as a callback of an `add` event, splices a new row into the
     body and renders it.

     @param {Backbone.Model} model The model to render as a row.
     @param {Backbone.Collection} collection When called directly, this
     parameter is actually the options to
     [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
     @param {Object} options When called directly, this must be null.

     See:

     - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
  */
  insertRow: function (model, collection, options) {

    if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();

    // insertRow() is called directly
    if (!(collection instanceof Backbone.Collection) && !options) {
      this.collection.add(model, (options = collection));
      return;
    }

    options = _.extend({render: true}, options || {});

    var row = new this.row({
      columns: this.columns,
      model: model
    });

    var index = collection.indexOf(model);
    this.rows.splice(index, 0, row);

    var $el = this.$el;
    var $children = $el.children();
    var $rowEl = row.render().$el;

    if (options.render) {
      if (index >= $children.length) {
        $el.append($rowEl);
      }
      else {
        $children.eq(index).before($rowEl);
      }
    }
  },

  /**
     The method can be called either directly or as a callback to a
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
     event.

     When called directly, it accepts a model or an array of models and an
     option hash just like
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
     delegates to it. Once the model is removed, a corresponding row is removed
     from the body.

     When called as a callback of a `remove` event, splices into the rows and
     removes the row responsible for rendering the model.

     @param {Backbone.Model} model The model to remove from the body.
     @param {Backbone.Collection} collection When called directly, this
     parameter is actually the options to
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
     @param {Object} options When called directly, this must be null.

     See:

     - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  */
  removeRow: function (model, collection, options) {

    // removeRow() is called directly
    if (!options) {
      this.collection.remove(model, (options = collection));
      this._unshiftEmptyRowMayBe();
      return;
    }

    if (_.isUndefined(options.render) || options.render) {
      this.rows[options.index].remove();
    }

    this.rows.splice(options.index, 1);
    this._unshiftEmptyRowMayBe();
  },

  /**
     Reinitialize all the rows inside the body and re-render them. Triggers a
     Backbone `backgrid:refresh` event along with the body instance as its event
     parameter when done.
  */
  refresh: function () {
    for (var i = 0; i < this.rows.length; i++) {
      this.rows[i].remove();
    }

    this.rows = this.collection.map(function (model) {
      var row = new this.row({
        columns: this.columns,
        model: model
      });

      return row;
    }, this);
    this._unshiftEmptyRowMayBe();

    this.render();

    Backbone.trigger("backgrid:refresh", this);

    return this;
  },

  /**
     Renders all the rows inside this body. If the collection is empty and
     `options.emptyText` is defined and not null in the constructor, an empty
     row is rendered, otherwise no row is rendered.
  */
  render: function () {
    this.$el.empty();

    var fragment = document.createDocumentFragment();
    for (var i = 0; i < this.rows.length; i++) {
      var row = this.rows[i];
      fragment.appendChild(row.render().el);
    }

    this.el.appendChild(fragment);

    this.delegateEvents();

    return this;
  },

  /**
     Clean up this body and it's rows.

     @chainable
  */
  remove: function () {
    for (var i = 0; i < this.rows.length; i++) {
      var row = this.rows[i];
      row.remove.apply(row, arguments);
    }
    return Backbone.View.prototype.remove.apply(this, arguments);
  },

  /**
     Moves focus to the next renderable and editable cell and return the
     currently editing cell to display mode.

     @param {Backbone.Model} model The originating model
     @param {Backgrid.Column} column The originating model column
     @param {Backgrid.Command} keys The Command object constructed from a DOM
     Event
  */
  moveToNextCell: function (model, column, command) {
    var i = this.collection.indexOf(model);
    var j = this.columns.indexOf(column);

    if (command.moveUp() || command.moveDown() || command.moveLeft() ||
        command.moveRight() || command.save()) {
      var l = this.columns.length;
      var maxOffset = l * this.collection.length;

      if (command.moveUp() || command.moveDown()) {
        var row = this.rows[i + (command.moveUp() ? -1 : 1)];
        if (row) row.cells[j].enterEditMode();
      }
      else if (command.moveLeft() || command.moveRight()) {
        var right = command.moveRight();
        for (var offset = i * l + j + (right ? 1 : -1);
             offset >= 0 && offset < maxOffset;
             right ? offset++ : offset--) {
          var m = ~~(offset / l);
          var n = offset - m * l;
          var cell = this.rows[m].cells[n];
          if (cell.column.get("renderable") && cell.column.get("editable")) {
            cell.enterEditMode();
            break;
          }
        }
      }
    }

    this.rows[i].cells[j].exitEditMode();
  }
});
<commit_msg>Fix moveToNextCell JSDoc param name<commit_after>/*
  backgrid
  http://github.com/wyuenho/backgrid

  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  Licensed under the MIT @license.
*/

/**
   Body is the table body which contains the rows inside a table. Body is
   responsible for refreshing the rows after sorting, insertion and removal.

   @class Backgrid.Body
   @extends Backbone.View
*/
var Body = Backgrid.Body = Backbone.View.extend({

  /** @property */
  tagName: "tbody",

  /**
     Initializer.

     @param {Object} options
     @param {Backbone.Collection} options.collection
     @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
     Column metadata.
     @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
     @param {string} [options.emptyText] The text to display in the empty row.

     @throws {TypeError} If options.columns or options.collection is undefined.

     See Backgrid.Row.
  */
  initialize: function (options) {
    Backgrid.requireOptions(options, ["columns", "collection"]);

    this.columns = options.columns;
    if (!(this.columns instanceof Backbone.Collection)) {
      this.columns = new Columns(this.columns);
    }

    this.row = options.row || Row;
    this.rows = this.collection.map(function (model) {
      var row = new this.row({
        columns: this.columns,
        model: model
      });

      return row;
    }, this);

    this.emptyText = options.emptyText;
    this._unshiftEmptyRowMayBe();

    var collection = this.collection;
    this.listenTo(collection, "add", this.insertRow);
    this.listenTo(collection, "remove", this.removeRow);
    this.listenTo(collection, "sort", this.refresh);
    this.listenTo(collection, "reset", this.refresh);
    this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
  },

  _unshiftEmptyRowMayBe: function () {
    if (this.rows.length === 0 && this.emptyText != null) {
      this.rows.unshift(new EmptyRow({
        emptyText: this.emptyText,
        columns: this.columns
      }));
    }
  },

  /**
     This method can be called either directly or as a callback to a
     [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.

     When called directly, it accepts a model or an array of models and an
     option hash just like
     [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
     delegates to it. Once the model is added, a new row is inserted into the
     body and automatically rendered.

     When called as a callback of an `add` event, splices a new row into the
     body and renders it.

     @param {Backbone.Model} model The model to render as a row.
     @param {Backbone.Collection} collection When called directly, this
     parameter is actually the options to
     [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
     @param {Object} options When called directly, this must be null.

     See:

     - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
  */
  insertRow: function (model, collection, options) {

    if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();

    // insertRow() is called directly
    if (!(collection instanceof Backbone.Collection) && !options) {
      this.collection.add(model, (options = collection));
      return;
    }

    options = _.extend({render: true}, options || {});

    var row = new this.row({
      columns: this.columns,
      model: model
    });

    var index = collection.indexOf(model);
    this.rows.splice(index, 0, row);

    var $el = this.$el;
    var $children = $el.children();
    var $rowEl = row.render().$el;

    if (options.render) {
      if (index >= $children.length) {
        $el.append($rowEl);
      }
      else {
        $children.eq(index).before($rowEl);
      }
    }
  },

  /**
     The method can be called either directly or as a callback to a
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
     event.

     When called directly, it accepts a model or an array of models and an
     option hash just like
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
     delegates to it. Once the model is removed, a corresponding row is removed
     from the body.

     When called as a callback of a `remove` event, splices into the rows and
     removes the row responsible for rendering the model.

     @param {Backbone.Model} model The model to remove from the body.
     @param {Backbone.Collection} collection When called directly, this
     parameter is actually the options to
     [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
     @param {Object} options When called directly, this must be null.

     See:

     - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  */
  removeRow: function (model, collection, options) {

    // removeRow() is called directly
    if (!options) {
      this.collection.remove(model, (options = collection));
      this._unshiftEmptyRowMayBe();
      return;
    }

    if (_.isUndefined(options.render) || options.render) {
      this.rows[options.index].remove();
    }

    this.rows.splice(options.index, 1);
    this._unshiftEmptyRowMayBe();
  },

  /**
     Reinitialize all the rows inside the body and re-render them. Triggers a
     Backbone `backgrid:refresh` event along with the body instance as its event
     parameter when done.
  */
  refresh: function () {
    for (var i = 0; i < this.rows.length; i++) {
      this.rows[i].remove();
    }

    this.rows = this.collection.map(function (model) {
      var row = new this.row({
        columns: this.columns,
        model: model
      });

      return row;
    }, this);
    this._unshiftEmptyRowMayBe();

    this.render();

    Backbone.trigger("backgrid:refresh", this);

    return this;
  },

  /**
     Renders all the rows inside this body. If the collection is empty and
     `options.emptyText` is defined and not null in the constructor, an empty
     row is rendered, otherwise no row is rendered.
  */
  render: function () {
    this.$el.empty();

    var fragment = document.createDocumentFragment();
    for (var i = 0; i < this.rows.length; i++) {
      var row = this.rows[i];
      fragment.appendChild(row.render().el);
    }

    this.el.appendChild(fragment);

    this.delegateEvents();

    return this;
  },

  /**
     Clean up this body and it's rows.

     @chainable
  */
  remove: function () {
    for (var i = 0; i < this.rows.length; i++) {
      var row = this.rows[i];
      row.remove.apply(row, arguments);
    }
    return Backbone.View.prototype.remove.apply(this, arguments);
  },

  /**
     Moves focus to the next renderable and editable cell and return the
     currently editing cell to display mode.

     @param {Backbone.Model} model The originating model
     @param {Backgrid.Column} column The originating model column
     @param {Backgrid.Command} command The Command object constructed from a DOM
     Event
  */
  moveToNextCell: function (model, column, command) {
    var i = this.collection.indexOf(model);
    var j = this.columns.indexOf(column);

    if (command.moveUp() || command.moveDown() || command.moveLeft() ||
        command.moveRight() || command.save()) {
      var l = this.columns.length;
      var maxOffset = l * this.collection.length;

      if (command.moveUp() || command.moveDown()) {
        var row = this.rows[i + (command.moveUp() ? -1 : 1)];
        if (row) row.cells[j].enterEditMode();
      }
      else if (command.moveLeft() || command.moveRight()) {
        var right = command.moveRight();
        for (var offset = i * l + j + (right ? 1 : -1);
             offset >= 0 && offset < maxOffset;
             right ? offset++ : offset--) {
          var m = ~~(offset / l);
          var n = offset - m * l;
          var cell = this.rows[m].cells[n];
          if (cell.column.get("renderable") && cell.column.get("editable")) {
            cell.enterEditMode();
            break;
          }
        }
      }
    }

    this.rows[i].cells[j].exitEditMode();
  }
});
<|endoftext|>


<commit_before>(function() {
  const PREFIX = 'octotree'
      , TOKEN  = 'octotree.github_access_token'
      , SHOWN  = 'octotree.shown'
      // ugly, I know, can it be improved?
      , REGEXP = /([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/

  var $html    = $('html')
    , $sidebar = $('' +
                     '
loading...
' +
                     '

' +
                   '')
    , $tree    = $sidebar.find('.tree')
    , $token   = $('<form>' +
                     '
' +
                       '<input name="token" type="text" placeholder="Enter personal access token"></input>' +
                     '
' +
                     '
' +
                       '<button type="submit">Save</button>' +
                       'Create token | ' +
                       'Help' +
                     '
' +
                     '

' +
                   '</form>')
    , $toggler = $('
☰
')
    , store    = new Storage()

  $(document).ready(function() {
    loadRepo(true)
  })

  function loadRepo(initDom) {
    var repo = getRepoFromPath()
    if (repo) {
      if (initDom) {
        $('body')
          .append($sidebar)
          .append($toggler.click(toggleSidebar))
      }
      fetchData(repo, function(err, tree) {
        if (err) return onFetchError(err)
        renderTree(repo, tree)
      })
    }
  }

  function getRepoFromPath() {
    var match = location.pathname.match(REGEXP)
    if (!match) return false

    // must not be a reserved `username`
    if (~['settings', 'organizations', 'site', 'blog'].indexOf(match[1])) return false

    // TODO: the intention is to hide the sidebar when users navigate to non-code areas (e.g. Issues, Pulls)
    // and show it again when users navigate back to the code area
    // the first part is achieved with the next two lines; but need to implement the second part
    // before activating the entire feature, PR is welcome
    // if match[3] exists, it must be either 'tree' or 'blob'
    // if (match[3] && !~['tree', 'blob'].indexOf(match[3])) return false

    return {
      username : match[1],
      reponame : match[2],
      branch   : $('*[data-master-branch]').data('ref') || 'master'
    }
  }

  function fetchData(repo, done) {
    var github  = new Github({ token: store.get(TOKEN) })
      , api     = github.getRepo(repo.username, repo.reponame)
      , root    = []
      , folders = { '': root }

    api.getTree(encodeURIComponent(repo.branch) + '?recursive=true', function(err, tree) {
      if (err) return done(err)
      tree.forEach(function(item) {
        var path   = item.path
          , index  = path.lastIndexOf('/')
          , name   = path.substring(index + 1)
          , folder = folders[path.substring(0, index)]
          , url    = '/' + repo.username + '/' + repo.reponame + '/' + item.type + '/' + repo.branch + '/' + path

        folder.push(item)
        item.text   = name
        item.icon   = item.type
        if (item.type === 'tree') {
          folders[item.path] = item.children = []
          item.a_attr = { href: '#' }
        }
        else if (item.type === 'blob') {
          item.a_attr = { href: url }
        }
      })

      done(null, sort(root))

      function sort(folder) {
        folder.sort(function(a, b) {
          if (a.type === b.type) return a.text.localeCompare(b.text)
          return a.type === 'tree' ? -1 : 1
        })
        folder.forEach(function(item) {
          if (item.type === 'tree') sort(item.children)
        })
        return folder
      }
    })
  }

  function onFetchError(err) {
    var msg = 'Error: ' + err.error
    if (err.error === 401) msg = 'Invalid token!'
    else if (err.error === 404) msg = 'Private or invalid repository!'
    else if (err.error === 403 && ~err.request.getAllResponseHeaders().indexOf('X-RateLimit-Remaining: 0'))
      msg = 'API limit exceeded!'
    updateSidebar(msg, true)
  }

  function renderTree(repo, tree) {
    $tree
      .empty()
      .jstree({
        core    : { data: tree, animation: 100 },
        plugins : ['wholerow', 'state'],
        state   : { key : PREFIX + '.' + repo.username + '/' + repo.reponame }
      })
      .delegate('.jstree-open>a', 'click.jstree', function() {
        $.jstree.reference(this).close_node(this)
      })
      .delegate('.jstree-closed>a', 'click.jstree', function() {
        $.jstree.reference(this).open_node(this)
      })
      .on('click', function(e) {
        var $target = $(e.target)
        if ($target.is('a.jstree-anchor') && $target.children(':first').hasClass('blob')) {
          $.pjax({
            url: $target.attr('href'),
            container: $('#js-repo-pjax-container')
          })
        }
      })
      .on('ready.jstree', function() {
        updateSidebar(repo.username + ' / ' + repo.reponame + ' [' + repo.branch + ']')
      })
  }

  function updateSidebar(header, askForToken) {
    $sidebar.find('h1').text(header)

    if (askForToken) {
      $tree.empty().append($token.submit(saveToken))
    }

    // Shows sidebar when:
    // 1. ask for access token
    // 2. first time extension is used
    // 3. if it was previously shown
    if (askForToken || store.get(SHOWN) !== false) {
      $html.addClass(PREFIX)
      store.set(SHOWN, true)
    }
  }

  function toggleSidebar() {
    var shown = store.get(SHOWN)
    if (shown) $html.removeClass(PREFIX)
    else $html.addClass(PREFIX)
    store.set(SHOWN, !shown)
  }

  function saveToken(event) {
    event.preventDefault()

    var token = $token.find('[name="token"]').val()
      , $error = $token.find('div.error').text('')

    if (token === '') {
      return $error.text('Token is required')
    }
    store.set(TOKEN, token)
    loadRepo()
  }

  function Storage() {
    this.get = function(key) {
      var val = localStorage.getItem(key)
      try {
        return JSON.parse(val)
      } catch (e) {
        return val
      }
    }
    this.set = function(key, val) {
      return localStorage.setItem(key, JSON.stringify(val))
    }
  }
})()<commit_msg>Add more detailed error messages & stop popping out the tree automatically when requesting token<commit_after>(function() {
  const PREFIX = 'octotree'
      , TOKEN  = 'octotree.github_access_token'
      , SHOWN  = 'octotree.shown'
      // ugly, I know, can it be improved?
      , REGEXP = /([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/

  var $html    = $('html')
    , $sidebar = $('' +
                     '
loading...
' +
                     '

' +
                   '')
    , $tree    = $sidebar.find('.tree')
    , $token   = $('<form>' +
                     '

' +
                     '
' +
                       '<input name="token" type="text" placeholder="Paste access token here"></input>' +
                     '
' +
                     '
' +
                       '<button type="submit">Save</button>' +
                       'Why need access token?' +
                     '
' +
                     '

' +
                   '</form>')
    , $toggler = $('
☰
')
    , store    = new Storage()

  $(document).ready(function() {
    loadRepo(true)
  })

  function loadRepo(initDom) {
    var repo = getRepoFromPath()
    if (repo) {
      if (initDom) {
        $('body')
          .append($sidebar)
          .append($toggler.click(toggleSidebar))
      }
      fetchData(repo, function(err, tree) {
        if (err) return onFetchError(err)
        renderTree(repo, tree)
      })
    }
  }

  function getRepoFromPath() {
    var match = location.pathname.match(REGEXP)
    if (!match) return false

    // must not be a reserved `username`
    if (~['settings', 'organizations', 'site', 'blog'].indexOf(match[1])) return false

    // TODO: the intention is to hide the sidebar when users navigate to non-code areas (e.g. Issues, Pulls)
    // and show it again when users navigate back to the code area
    // the first part is achieved with the next two lines; but need to implement the second part
    // before activating the entire feature, PR is welcome
    // if match[3] exists, it must be either 'tree' or 'blob'
    // if (match[3] && !~['tree', 'blob'].indexOf(match[3])) return false

    return {
      username : match[1],
      reponame : match[2],
      branch   : $('*[data-master-branch]').data('ref') || 'master'
    }
  }

  function fetchData(repo, done) {
    var github  = new Github({ token: store.get(TOKEN) })
      , api     = github.getRepo(repo.username, repo.reponame)
      , root    = []
      , folders = { '': root }

    api.getTree(encodeURIComponent(repo.branch) + '?recursive=true', function(err, tree) {
      if (err) return done(err)
      tree.forEach(function(item) {
        var path   = item.path
          , index  = path.lastIndexOf('/')
          , name   = path.substring(index + 1)
          , folder = folders[path.substring(0, index)]
          , url    = '/' + repo.username + '/' + repo.reponame + '/' + item.type + '/' + repo.branch + '/' + path

        folder.push(item)
        item.text   = name
        item.icon   = item.type
        if (item.type === 'tree') {
          folders[item.path] = item.children = []
          item.a_attr = { href: '#' }
        }
        else if (item.type === 'blob') {
          item.a_attr = { href: url }
        }
      })

      done(null, sort(root))

      function sort(folder) {
        folder.sort(function(a, b) {
          if (a.type === b.type) return a.text.localeCompare(b.text)
          return a.type === 'tree' ? -1 : 1
        })
        folder.forEach(function(item) {
          if (item.type === 'tree') sort(item.children)
        })
        return folder
      }
    })
  }

  function onFetchError(err) {
    var header = 'Error: ' + err.error
      , hasToken = !!store.get(TOKEN)
      , message

    if (err.error === 401) {
      header = 'Invalid token!'
      message = 'The provided token is invalid. Please enter another one.'
    }

    else if (err.error === 404) {
      header = 'Private or invalid repository!'
      if (hasToken) message = 'You are not allowed to access this repository.'
      else message = 'Accessing private repositories requires a GitHub access token. Follow this link to create one and paste it in the textbox below.'
    }

    else if (err.error === 403 && ~err.request.getAllResponseHeaders().indexOf('X-RateLimit-Remaining: 0')) {
      header = 'API limit exceeded!'
      if (hasToken) message = 'Whoa, you have exceeded the API hourly limit, please create a new access token or take a break :).'
      else message = 'You have exceeded the GitHub API hourly limit and need GitHub access token to make extra requests. Follow this link to create one and paste it in the textbox below.'
    }

    updateSidebar(header, message)
  }

  function renderTree(repo, tree) {
    $tree
      .empty()
      .jstree({
        core    : { data: tree, animation: 100 },
        plugins : ['wholerow', 'state'],
        state   : { key : PREFIX + '.' + repo.username + '/' + repo.reponame }
      })
      .delegate('.jstree-open>a', 'click.jstree', function() {
        $.jstree.reference(this).close_node(this)
      })
      .delegate('.jstree-closed>a', 'click.jstree', function() {
        $.jstree.reference(this).open_node(this)
      })
      .on('click', function(e) {
        var $target = $(e.target)
        if ($target.is('a.jstree-anchor') && $target.children(':first').hasClass('blob')) {
          $.pjax({
            url: $target.attr('href'),
            container: $('#js-repo-pjax-container')
          })
        }
      })
      .on('ready.jstree', function() {
        updateSidebar(repo.username + ' / ' + repo.reponame + ' [' + repo.branch + ']')
      })
  }

  function updateSidebar(header, errorMessage) {
    $sidebar.find('h1').text(header)

    if (errorMessage) {
      var token = store.get(TOKEN)
      if (token) $token.find('[name="token"]').val(token)
      $token.find('.message').html(errorMessage)
      $tree.empty().append($token.submit(saveToken))
    }

    // Shows sidebar when:
    // 1. first time extension is used
    // 2. if it was previously shown
    if (store.get(SHOWN) !== false) {
      $html.addClass(PREFIX)
      store.set(SHOWN, true)
    }
  }

  function toggleSidebar() {
    var shown = store.get(SHOWN)
    if (shown) $html.removeClass(PREFIX)
    else $html.addClass(PREFIX)
    store.set(SHOWN, !shown)
  }

  function saveToken(event) {
    event.preventDefault()

    var token = $token.find('[name="token"]').val()
      , $error = $token.find('.error').text('')

    if (token === '') {
      return $error.text('Token is required')
    }
    store.set(TOKEN, token)
    loadRepo()
  }

  function Storage() {
    this.get = function(key) {
      var val = localStorage.getItem(key)
      try {
        return JSON.parse(val)
      } catch (e) {
        return val
      }
    }
    this.set = function(key, val) {
      return localStorage.setItem(key, JSON.stringify(val))
    }
  }
})()<|endoftext|>


<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var gadgets = gadgets || {};

/**
 * @fileoverview General purpose utilities that gadgets can use.
 */

/**
 * @static
 * @class Provides general-purpose utility functions.
 * @name gadgets.util
 */

gadgets.util = function() {
  /**
   * Parses URL parameters into an object.
   * @return {Array.<String>} The parameters
   */
  function parseUrlParams() {
    // Get settings from url, 'hash' takes precedence over 'search' component
    // don't use document.location.hash due to browser differences.
    var query;
    var l = document.location.href;
    var queryIdx = l.indexOf("?");
    var hashIdx = l.indexOf("#");
    if (hashIdx === -1) {
      query = l.substr(queryIdx + 1);
    } else {
      // essentially replaces "#" with "&"
      query = [l.substr(queryIdx + 1, hashIdx - queryIdx - 1), "&",
               l.substr(hashIdx + 1)].join("");
    }
    return query.split("&");
  }

  var parameters = null;
  var features = {};
  var onLoadHandlers = [];

  return /** @scope gadgets.util */ {

    /**
     * Gets the URL parameters.
     *
     * @return {Object} Parameters passed into the query string
     * @member gadgets.util
     * @private Implementation detail.
     */
    getUrlParameters : function () {
      if (parameters !== null) {
        return parameters;
      }
      parameters = {};
      var pairs = parseUrlParams();
      var unesc = window.decodeURIComponent ? decodeURIComponent : unescape;
      for (var i = 0, j = pairs.length; i < j; ++i) {
        var pos = pairs[i].indexOf('=');
        if (pos === -1) {
          continue;
        }
        var argName = pairs[i].substring(0, pos);
        var value = pairs[i].substring(pos + 1);
        // difference to IG_Prefs, is that args doesn't replace spaces in
        // argname. Unclear on if it should do:
        // argname = argname.replace(/\+/g, " ");
        value = value.replace(/\+/g, " ");
        parameters[argName] = unesc(value);
      }
      return parameters;
    },

    /**
     * Creates a closure that is suitable for passing as a callback.
     * Any number of arguments
     * may be passed to the callback;
     * they will be received in the order they are passed in.
     *
     * @param {Object} scope The execution scope; may be null if there is no
     *     need to associate a specific instance of an object with this
     *     callback
     * @param {Function} callback The callback to invoke when this is run;
     *     any arguments passed in will be passed after your initial arguments
     * @param {Object} var_args Initial arguments to be passed to the callback
     *
     * @member gadgets.util
     * @private Implementation detail.
     */
    makeClosure : function (scope, callback, var_args) {
      // arguments isn't a real array, so we copy it into one.
      var tmpArgs = [];
      for (var i = 2, j = arguments.length; i < j; ++i) {
       tmpArgs.push(arguments[i]);
      }
      return function() {
        // append new arguments.
        for (var i = 0, j = arguments.length; i < j; ++i) {
          tmpArgs.push(arguments[i]);
        }
        callback.apply(scope, tmpArgs);
      };
    },

    /**
     * Utility function for generating an "enum" from an array.
     *
     * @param {Array.<String>} values The values to generate.
     * @return {Map<String,String>} An object with member fields to handle
     *   the enum.
     *
     * @private Implementation detail.
     */
    makeEnum : function (values) {
      var obj = {};
      for (var i = 0, v; v = values[i]; ++i) {
        obj[v] = v;
      }
      return obj;
    },

    /**
     * Gets the feature parameters.
     *
     * @param {String} feature The feature to get parameters for
     * @return {Object} The parameters for the given feature, or null
     *
     * @member gadgets.util
     */
    getFeatureParameters : function (feature) {
      return typeof features[feature] === "undefined"
          ? null : features[feature];
    },

    /**
     * Returns whether the current feature is supported.
     *
     * @param {String} feature The feature to test for
     * @return {Boolean} True if the feature is supported
     *
     * @member gadgets.util
     */
    hasFeature : function (feature) {
      return typeof features[feature] !== "undefined";
    },

    /**
     * Registers an onload handler.
     * @param {Function} callback The handler to run
     *
     * @member gadgets.util
     */
    registerOnLoadHandler : function (callback) {
      onLoadHandlers.push(callback);
    },

    /**
     * Runs all functions registered via registerOnLoadHandler.
     * @private Only to be used by the container, not gadgets.
     */
    runOnLoadHandlers : function () {
      for (var i = 0, j = onLoadHandlers.length; i < j; ++i) {
        onLoadHandlers[i]();
      }
    },

    /**
     * Escapes the input using html entities to make it safer.
     *
     * If the input is a string, uses gadgets.util.escapeString.
     * If it is an array, calls escape on each of the array elements
     * if it is an object, will only escape all the mapped keys and values if
     * the opt_escapeObjects flag is set. This operation involves creating an
     * entirely new object so only set the flag when the input is a simple
     * string to string map.
     * Otherwise, does not attempt to modify the input.
     *
     * @param {Object} input The object to escape
     * @param {Boolean} opt_escapeObjects Whether to escape objects.
     * @return {Object} The escaped object
     * @private Only to be used by the container, not gadgets.
     */
    escape : function(input, opt_escapeObjects) {

      if (!input) {
        return input;
      } else if (typeof input == "string") {
        return gadgets.util.escapeString(input);
      } else if (typeof input == "array") {
        for (var i = 0; i < input.length; i++) {
          input[i] = gadgets.util.escape(input[i]);
        }
      } else if (typeof input == "object" && opt_escapeObjects) {
        var newObject = {};
        for (var field in input) if (input.hasOwnProperty(field)) {
          newObject[gadgets.util.escapeString(field)]
              = gadgets.util.escape(input[field], true);
        }
        return newObject;
      }
      return input;
    },

    /**
     * Escapes the input using html entities to make it safer.
     *
     * Currently only escapes < > ' and " All known browsers handle
     * & without issue.
     *
     * Currently not in the spec -- future proposals may change
     * how this is handled.
     *
     * TODO: Parsing the string would probably be more accurate and faster than
     * a bunch of regular expressions.
     *
     * @param {String} str The string to escape
     * @return {String} The escaped string
     */
    escapeString : function(str) {
      return str.replace(/</g, "<")
          .replace(/>/g, ">")
          .replace(/"/g, """)
          .replace(/'/g, "'");
    },

    /**
     * Reverses escapeString
     *
     * @param {String} str The string to unescape.
     */
    unescapeString : function(str) {
      return str.replace(/</g, "<")
                .replace(/>/g, ">")
                .replace(/"/g, '"')
                .replace(/&#39/g, "'");
    },

    /**
     * @param {Object} featureData The features that are supported, and
     *    their parameters.
     * @private Only to be used by the container, not gadgets.
     */
    init : function (featureData) {
      features = featureData;
    }
  };
}();

<commit_msg>Updated escape / unescape routines to catch more problematic characters that manifest in innerHTML assignments that use javascript.<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var gadgets = gadgets || {};

/**
 * @fileoverview General purpose utilities that gadgets can use.
 */

/**
 * @static
 * @class Provides general-purpose utility functions.
 * @name gadgets.util
 */

gadgets.util = function() {
  /**
   * Parses URL parameters into an object.
   * @return {Array.<String>} The parameters
   */
  function parseUrlParams() {
    // Get settings from url, 'hash' takes precedence over 'search' component
    // don't use document.location.hash due to browser differences.
    var query;
    var l = document.location.href;
    var queryIdx = l.indexOf("?");
    var hashIdx = l.indexOf("#");
    if (hashIdx === -1) {
      query = l.substr(queryIdx + 1);
    } else {
      // essentially replaces "#" with "&"
      query = [l.substr(queryIdx + 1, hashIdx - queryIdx - 1), "&",
               l.substr(hashIdx + 1)].join("");
    }
    return query.split("&");
  }

  var parameters = null;
  var features = {};
  var onLoadHandlers = [];

  // Maps code points to the value to replace them with.
  // If the value is "false", the character is removed entirely, otherwise
  // it will be replaced with an html entity.
  var escapeCodePoints = {
   // nul; most browsers truncate because they use c strings under the covers.
   0 : false,
   // new line
   10 : true,
   // carriage return
   13 : true,
   // double quote
   34 : true,
   // single quote
   39 : true,
   // less than
   60 : true,
   // greater than
   62 : true,
   // Backslash
   92 : true,
   // line separator
   8232 : true,
   // paragraph separator
   8233 : true
  };

  /**
   * Regular expression callback that returns strings from unicode code points.
   *
   * @param {Array} match Ignored
   * @param {String} value The codepoint value to convert
   * @return {String} The character corresponding to value.
   */
  function unescapeEntity(match, value) {
    return String.fromCharCode(value);
  }

  return /** @scope gadgets.util */ {

    /**
     * Gets the URL parameters.
     *
     * @return {Object} Parameters passed into the query string
     * @member gadgets.util
     * @private Implementation detail.
     */
    getUrlParameters : function () {
      if (parameters !== null) {
        return parameters;
      }
      parameters = {};
      var pairs = parseUrlParams();
      var unesc = window.decodeURIComponent ? decodeURIComponent : unescape;
      for (var i = 0, j = pairs.length; i < j; ++i) {
        var pos = pairs[i].indexOf('=');
        if (pos === -1) {
          continue;
        }
        var argName = pairs[i].substring(0, pos);
        var value = pairs[i].substring(pos + 1);
        // difference to IG_Prefs, is that args doesn't replace spaces in
        // argname. Unclear on if it should do:
        // argname = argname.replace(/\+/g, " ");
        value = value.replace(/\+/g, " ");
        parameters[argName] = unesc(value);
      }
      return parameters;
    },

    /**
     * Creates a closure that is suitable for passing as a callback.
     * Any number of arguments
     * may be passed to the callback;
     * they will be received in the order they are passed in.
     *
     * @param {Object} scope The execution scope; may be null if there is no
     *     need to associate a specific instance of an object with this
     *     callback
     * @param {Function} callback The callback to invoke when this is run;
     *     any arguments passed in will be passed after your initial arguments
     * @param {Object} var_args Initial arguments to be passed to the callback
     *
     * @member gadgets.util
     * @private Implementation detail.
     */
    makeClosure : function (scope, callback, var_args) {
      // arguments isn't a real array, so we copy it into one.
      var tmpArgs = [];
      for (var i = 2, j = arguments.length; i < j; ++i) {
       tmpArgs.push(arguments[i]);
      }
      return function() {
        // append new arguments.
        for (var i = 0, j = arguments.length; i < j; ++i) {
          tmpArgs.push(arguments[i]);
        }
        callback.apply(scope, tmpArgs);
      };
    },

    /**
     * Utility function for generating an "enum" from an array.
     *
     * @param {Array.<String>} values The values to generate.
     * @return {Map<String,String>} An object with member fields to handle
     *   the enum.
     *
     * @private Implementation detail.
     */
    makeEnum : function (values) {
      var obj = {};
      for (var i = 0, v; v = values[i]; ++i) {
        obj[v] = v;
      }
      return obj;
    },

    /**
     * Gets the feature parameters.
     *
     * @param {String} feature The feature to get parameters for
     * @return {Object} The parameters for the given feature, or null
     *
     * @member gadgets.util
     */
    getFeatureParameters : function (feature) {
      return typeof features[feature] === "undefined"
          ? null : features[feature];
    },

    /**
     * Returns whether the current feature is supported.
     *
     * @param {String} feature The feature to test for
     * @return {Boolean} True if the feature is supported
     *
     * @member gadgets.util
     */
    hasFeature : function (feature) {
      return typeof features[feature] !== "undefined";
    },

    /**
     * Registers an onload handler.
     * @param {Function} callback The handler to run
     *
     * @member gadgets.util
     */
    registerOnLoadHandler : function (callback) {
      onLoadHandlers.push(callback);
    },

    /**
     * Runs all functions registered via registerOnLoadHandler.
     * @private Only to be used by the container, not gadgets.
     */
    runOnLoadHandlers : function () {
      for (var i = 0, j = onLoadHandlers.length; i < j; ++i) {
        onLoadHandlers[i]();
      }
    },

    /**
     * Escapes the input using html entities to make it safer.
     *
     * If the input is a string, uses gadgets.util.escapeString.
     * If it is an array, calls escape on each of the array elements
     * if it is an object, will only escape all the mapped keys and values if
     * the opt_escapeObjects flag is set. This operation involves creating an
     * entirely new object so only set the flag when the input is a simple
     * string to string map.
     * Otherwise, does not attempt to modify the input.
     *
     * @param {Object} input The object to escape
     * @param {Boolean} opt_escapeObjects Whether to escape objects.
     * @return {Object} The escaped object
     * @private Only to be used by the container, not gadgets.
     */
    escape : function(input, opt_escapeObjects) {
      if (!input) {
        return input;
      } else if (typeof input === "string") {
        return gadgets.util.escapeString(input);
      } else if (typeof input === "array") {
        for (var i = 0, j = input.length; i < j; ++i) {
          input[i] = gadgets.util.escape(input[i]);
        }
      } else if (typeof input === "object" && opt_escapeObjects) {
        var newObject = {};
        for (var field in input) if (input.hasOwnProperty(field)) {
          newObject[gadgets.util.escapeString(field)]
              = gadgets.util.escape(input[field], true);
        }
        return newObject;
      }
      return input;
    },

    /**
     * Escapes the input using html entities to make it safer.
     *
     * Currently not in the spec -- future proposals may change
     * how this is handled.
     *
     * TODO: Parsing the string would probably be more accurate and faster than
     * a bunch of regular expressions.
     *
     * @param {String} str The string to escape
     * @return {String} The escaped string
     */
    escapeString : function(str) {
      var out = [], ch, shouldEscape;
      for (var i = 0, j = str.length; i < j; ++i) {
        ch = str.charCodeAt(i);
        shouldEscape = escapeCodePoints[ch];
        if (shouldEscape === true) {
          out.push("&#", ch, ";");
        } else if (shouldEscape !== false) {
          // undefined or null are OK.
          out.push(str.charAt(i));
        }
      }
      return out.join("");
    },

    /**
     * Reverses escapeString
     *
     * @param {String} str The string to unescape.
     */
    unescapeString : function(str) {
      return str.replace(/&#([0-9]+);/g, unescapeEntity);
    },

    /**
     * @param {Object} featureData The features that are supported, and
     *    their parameters.
     * @private Only to be used by the container, not gadgets.
     */
    init : function (featureData) {
      features = featureData;
    }
  };
}();

<|endoftext|>