<commit_before>// String to DOM function
var domify = require('domify')
// classList polyfill for old versions of IE/Firefox
require('classlist-polyfill')

// Object.assign polyfill
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign !== 'function') {
  Object.assign = function (target) {
    if (target == null) {
      throw new TypeError('Cannot convert undefined or null to object')
    }

    target = Object(target)
    for (var index = 1; index < arguments.length; index++) {
      var source = arguments[index]
      if (source != null) {
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key]
          }
        }
      }
    }
    return target
  }
}

// Detect CSS Animation End Support
// https://github.com/limonte/sweetalert2/blob/99bd539f85e15ac170f69d35001d12e092ef0054/src/utils/dom.js#L194
var animationEndEvent = (function () {
  var el = document.createElement('div')
  var eventNames = {
    'WebkitAnimation': 'webkitAnimationEnd',
    'MozAnimation': 'animationend',
    'OAnimation': 'oanimationend',
    'msAnimation': 'MSAnimationEnd',
    'animation': 'animationend'
  }
  for (var i in eventNames) {
    if (el.style[i] !== undefined) {
      return eventNames[i]
    }
  }
  return false
})()

// vex base CSS classes
var baseClassNames = {
  vex: 'vex',
  content: 'vex-content',
  overlay: 'vex-overlay',
  close: 'vex-close',
  closing: 'vex-closing',
  open: 'vex-open'
}

// Private lookup table of all open vex objects, keyed by id
var vexes = {}
var globalId = 1

// vex itself is an object that exposes a simple API to open and close vex objects in various ways
var vex = {
  open: function (opts) {
    // The dialog instance
    var vexInstance = {}

    // Set id
    vexInstance.id = globalId++

    // Store internally
    vexes[vexInstance.id] = vexInstance

    // Set state
    vexInstance.isOpen = true

    // Close function on the vex instance
    // This is how all API functions should close individual vexes
    vexInstance.close = function () {
      // Check state
      if (!this.isOpen) {
        return true
      }

      var options = this.options

      var beforeClose = function () {
        // Call before close callback
        if (options.beforeClose) {
          return options.beforeClose.call(this)
        }
        // Otherwise indicate that it's ok to continue with close
        return true
      }.bind(this)

      var close = function () {
        if (!this.rootEl.parentNode) {
          return
        }
        // Run once
        this.rootEl.removeEventListener(animationEndEvent, close)
        // Remove the dialog from the DOM
        this.rootEl.parentNode.removeChild(this.rootEl)
        // Call after close callback
        if (options.afterClose) {
          options.afterClose.call(this)
        }
        // Remove styling from the body, if no more vexes are open
        if (Object.keys(vexes).length === 0) {
          document.body.classList.remove(baseClassNames.open)
        }
      }.bind(this)

      // If any user-defined validation or anything fails, abort the close
      if (beforeClose() === false) {
        return false
      }

      // Update state
      this.isOpen = false

      // Detect if the content el has any CSS animations defined
      var style = window.getComputedStyle(this.contentEl)
      function hasAnimationPre (prefix) {
        return style.getPropertyValue(prefix + 'animation-name') !== 'none' && style.getPropertyValue(prefix + 'animation-duration') !== '0s'
      }
      var hasAnimation = hasAnimationPre('') || hasAnimationPre('-webkit-') || hasAnimationPre('-moz-') || hasAnimationPre('-o-')

      // Close the vex
      if (animationEndEvent && hasAnimation) {
        // Setup the end event listener, to remove the el from the DOM
        this.rootEl.addEventListener(animationEndEvent, close)
        // Add the closing class to the dialog, showing the close animation
        this.rootEl.classList.add(baseClassNames.closing)
      } else {
        close()
      }

      // Remove from lookup table (prevent memory leaks)
      delete vexes[this.id]

      return true
    }

    // Allow strings as content
    if (typeof opts === 'string') {
      opts = {
        content: opts
      }
    }
    // Store options on instance for future reference
    var options = vexInstance.options = assign({}, vex.defaultOptions, opts)

    // vex root
    var rootEl = vexInstance.rootEl = document.createElement('div')
    rootEl.classList = baseClassNames.vex
    if (options.className) {
      rootEl.classList.add(options.className)
    }

    // Overlay
    var overlayEl = vexInstance.overlayEl = document.createElement('div')
    overlayEl.classList = baseClassNames.overlay
    if (options.overlayClassName) {
      overlayEl.classList.add(options.overlayClassName)
    }
    if (options.overlayClosesOnClick) {
      overlayEl.addEventListener('click', function (e) {
        if (e.target === overlayEl) {
          vexInstance.close()
        }
      })
    }
    rootEl.appendChild(overlayEl)

    // Content
    var contentEl = vexInstance.contentEl = document.createElement('div')
    contentEl.classList = baseClassNames.content
    if (options.contentClassName) {
      contentEl.classList.add(options.contentClassName)
    }
    contentEl.appendChild(options.content instanceof window.Node ? options.content : domify(options.content))
    rootEl.appendChild(contentEl)

    // Close button
    if (options.showCloseButton) {
      var closeEl = vexInstance.closeEl = document.createElement('div')
      closeEl.classList = baseClassNames.close
      if (options.closeClassName) {
        closeEl.classList.add(options.closeClassName)
      }
      closeEl.addEventListener('click', vexInstance.close.bind(vexInstance))
      contentEl.appendChild(closeEl)
    }

    // Add to DOM
    document.querySelector(options.appendLocation).appendChild(rootEl)

    // Call after open callback
    if (options.afterOpen) {
      options.afterOpen.call(vexInstance)
    }

    // Apply styling to the body
    document.body.classList.add(baseClassNames.open)

    // Return the created vex instance
    return vexInstance
  },

  // A top-level vex.close function to close dialogs by reference or id
  close: function (vexOrId) {
    var id
    if (vexOrId.id) {
      id = vexOrId.id
    } else if (typeof vexOrId === 'string') {
      id = vexOrId
    } else {
      throw new TypeError('close requires a vex object or id string')
    }
    if (!vexes[id]) {
      return false
    }
    return vexes[id].close()
  },

  // Close the most recently created/opened vex
  closeTop: function () {
    var ids = Object.keys(vexes)
    if (!ids.length) {
      return false
    }
    return vexes[ids[ids.length - 1]].close()
  },

  // Close every vex!
  closeAll: function () {
    for (var id in vexes) {
      this.close(id)
    }
    return true
  },

  // A getter for the internal lookup table
  getAll: function () {
    return vexes
  },

  // A getter for the internal lookup table
  getById: function (id) {
    return vexes[id]
  }
}

// Close top vex on escape
window.addEventListener('keyup', function (e) {
  if (e.keyCode === 27) {
    vex.closeTop()
  }
})
// Close all vexes on history pop state (useful in single page apps)
window.addEventListener('popstate', vex.closeAll)

vex.defaultOptions = {
  content: '',
  showCloseButton: true,
  escapeButtonCloses: true,
  overlayClosesOnClick: true,
  appendLocation: 'body',
  className: '',
  overlayClassName: '',
  contentClassName: '',
  closeClassName: ''
}

// TODO Loading symbols?

// Plugin system!
vex.registerPlugin = function (plugin, name) {
  var pluginName = name || plugin.pluginName || plugin.name
  if (vex[pluginName]) {
    throw new Error('Plugin ' + name + ' is already registered.')
  }
  vex[pluginName] = plugin(vex)
  for (var prop in plugin) {
    if (plugin.hasOwnProperty(prop) && prop !== 'pluginName' && prop !== 'name') {
      vex[pluginName][prop] = plugin[prop]
    }
  }
}

module.exports = vex
<commit_msg>classList is readonly!<commit_after>// String to DOM function
var domify = require('domify')
// classList polyfill for old versions of IE/Firefox
require('classlist-polyfill')

// Object.assign polyfill
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign !== 'function') {
  Object.assign = function (target) {
    if (target == null) {
      throw new TypeError('Cannot convert undefined or null to object')
    }

    target = Object(target)
    for (var index = 1; index < arguments.length; index++) {
      var source = arguments[index]
      if (source != null) {
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key]
          }
        }
      }
    }
    return target
  }
}

// Detect CSS Animation End Support
// https://github.com/limonte/sweetalert2/blob/99bd539f85e15ac170f69d35001d12e092ef0054/src/utils/dom.js#L194
var animationEndEvent = (function () {
  var el = document.createElement('div')
  var eventNames = {
    'WebkitAnimation': 'webkitAnimationEnd',
    'MozAnimation': 'animationend',
    'OAnimation': 'oanimationend',
    'msAnimation': 'MSAnimationEnd',
    'animation': 'animationend'
  }
  for (var i in eventNames) {
    if (el.style[i] !== undefined) {
      return eventNames[i]
    }
  }
  return false
})()

// vex base CSS classes
var baseClassNames = {
  vex: 'vex',
  content: 'vex-content',
  overlay: 'vex-overlay',
  close: 'vex-close',
  closing: 'vex-closing',
  open: 'vex-open'
}

// Private lookup table of all open vex objects, keyed by id
var vexes = {}
var globalId = 1

// vex itself is an object that exposes a simple API to open and close vex objects in various ways
var vex = {
  open: function (opts) {
    // The dialog instance
    var vexInstance = {}

    // Set id
    vexInstance.id = globalId++

    // Store internally
    vexes[vexInstance.id] = vexInstance

    // Set state
    vexInstance.isOpen = true

    // Close function on the vex instance
    // This is how all API functions should close individual vexes
    vexInstance.close = function () {
      // Check state
      if (!this.isOpen) {
        return true
      }

      var options = this.options

      var beforeClose = function () {
        // Call before close callback
        if (options.beforeClose) {
          return options.beforeClose.call(this)
        }
        // Otherwise indicate that it's ok to continue with close
        return true
      }.bind(this)

      var close = function () {
        if (!this.rootEl.parentNode) {
          return
        }
        // Run once
        this.rootEl.removeEventListener(animationEndEvent, close)
        // Remove the dialog from the DOM
        this.rootEl.parentNode.removeChild(this.rootEl)
        // Call after close callback
        if (options.afterClose) {
          options.afterClose.call(this)
        }
        // Remove styling from the body, if no more vexes are open
        if (Object.keys(vexes).length === 0) {
          document.body.classList.remove(baseClassNames.open)
        }
      }.bind(this)

      // If any user-defined validation or anything fails, abort the close
      if (beforeClose() === false) {
        return false
      }

      // Update state
      this.isOpen = false

      // Detect if the content el has any CSS animations defined
      var style = window.getComputedStyle(this.contentEl)
      function hasAnimationPre (prefix) {
        return style.getPropertyValue(prefix + 'animation-name') !== 'none' && style.getPropertyValue(prefix + 'animation-duration') !== '0s'
      }
      var hasAnimation = hasAnimationPre('') || hasAnimationPre('-webkit-') || hasAnimationPre('-moz-') || hasAnimationPre('-o-')

      // Close the vex
      if (animationEndEvent && hasAnimation) {
        // Setup the end event listener, to remove the el from the DOM
        this.rootEl.addEventListener(animationEndEvent, close)
        // Add the closing class to the dialog, showing the close animation
        this.rootEl.classList.add(baseClassNames.closing)
      } else {
        close()
      }

      // Remove from lookup table (prevent memory leaks)
      delete vexes[this.id]

      return true
    }

    // Allow strings as content
    if (typeof opts === 'string') {
      opts = {
        content: opts
      }
    }
    // Store options on instance for future reference
    var options = vexInstance.options = Object.assign({}, vex.defaultOptions, opts)

    // vex root
    var rootEl = vexInstance.rootEl = document.createElement('div')
    rootEl.classList.add(baseClassNames.vex)
    if (options.className) {
      rootEl.classList.add(options.className)
    }

    // Overlay
    var overlayEl = vexInstance.overlayEl = document.createElement('div')
    overlayEl.classList.add(baseClassNames.overlay)
    if (options.overlayClassName) {
      overlayEl.classList.add(options.overlayClassName)
    }
    if (options.overlayClosesOnClick) {
      overlayEl.addEventListener('click', function (e) {
        if (e.target === overlayEl) {
          vexInstance.close()
        }
      })
    }
    rootEl.appendChild(overlayEl)

    // Content
    var contentEl = vexInstance.contentEl = document.createElement('div')
    contentEl.classList.add(baseClassNames.content)
    if (options.contentClassName) {
      contentEl.classList.add(options.contentClassName)
    }
    contentEl.appendChild(options.content instanceof window.Node ? options.content : domify(options.content))
    rootEl.appendChild(contentEl)

    // Close button
    if (options.showCloseButton) {
      var closeEl = vexInstance.closeEl = document.createElement('div')
      closeEl.classList.add(baseClassNames.close)
      if (options.closeClassName) {
        closeEl.classList.add(options.closeClassName)
      }
      closeEl.addEventListener('click', vexInstance.close.bind(vexInstance))
      contentEl.appendChild(closeEl)
    }

    // Add to DOM
    document.querySelector(options.appendLocation).appendChild(rootEl)

    // Call after open callback
    if (options.afterOpen) {
      options.afterOpen.call(vexInstance)
    }

    // Apply styling to the body
    document.body.classList.add(baseClassNames.open)

    // Return the created vex instance
    return vexInstance
  },

  // A top-level vex.close function to close dialogs by reference or id
  close: function (vexOrId) {
    var id
    if (vexOrId.id) {
      id = vexOrId.id
    } else if (typeof vexOrId === 'string') {
      id = vexOrId
    } else {
      throw new TypeError('close requires a vex object or id string')
    }
    if (!vexes[id]) {
      return false
    }
    return vexes[id].close()
  },

  // Close the most recently created/opened vex
  closeTop: function () {
    var ids = Object.keys(vexes)
    if (!ids.length) {
      return false
    }
    return vexes[ids[ids.length - 1]].close()
  },

  // Close every vex!
  closeAll: function () {
    for (var id in vexes) {
      this.close(id)
    }
    return true
  },

  // A getter for the internal lookup table
  getAll: function () {
    return vexes
  },

  // A getter for the internal lookup table
  getById: function (id) {
    return vexes[id]
  }
}

// Close top vex on escape
window.addEventListener('keyup', function (e) {
  if (e.keyCode === 27) {
    vex.closeTop()
  }
})
// Close all vexes on history pop state (useful in single page apps)
window.addEventListener('popstate', vex.closeAll)

vex.defaultOptions = {
  content: '',
  showCloseButton: true,
  escapeButtonCloses: true,
  overlayClosesOnClick: true,
  appendLocation: 'body',
  className: '',
  overlayClassName: '',
  contentClassName: '',
  closeClassName: ''
}

// TODO Loading symbols?

// Plugin system!
vex.registerPlugin = function (plugin, name) {
  var pluginName = name || plugin.pluginName || plugin.name
  if (vex[pluginName]) {
    throw new Error('Plugin ' + name + ' is already registered.')
  }
  vex[pluginName] = plugin(vex)
  for (var prop in plugin) {
    if (plugin.hasOwnProperty(prop) && prop !== 'pluginName' && prop !== 'name') {
      vex[pluginName][prop] = plugin[prop]
    }
  }
}

module.exports = vex
<|endoftext|>


<commit_before>Minionette.View = Backbone.View.extend({
    constructor: function() {
        Backbone.View.apply(this, arguments);

        // Done so we don't bindAll to standard methods.
        _.bindAll(this, '_jqueryRemove', '_close');

        // Keep track of our subviews.
        this._subViews = {};

        // Have the view listenTo the model and collection.
        this._listenToEvents(this.model, this.modelEvents);
        this._listenToEvents(this.collection, this.collectionEvents);
        this.listenTo(this, 'close:before', this._close);
    },

    // A default template that will clear this.$el.
    // Override this in a subclass to something useful.
    template: function() { return ''; },

    // When delegating events, bind this view to jQuery's special remove event.
    // Allows us to clean up the view, even if you remove this.$el with jQuery.
    // http://blog.alexmaccaw.com/jswebapps-memory-management
    delegateEvents: function(events) {
        Backbone.View.prototype.delegateEvents.apply(this, events);
        this.$el.on('remove.delegateEvents' + this.cid, this._jqueryRemove);
    },

    // A useful remove method to that triggers events.
    // Notice we emit "close" events, not "remove".
    close: function() {
        this.trigger('close:before');
        this.remove();
        this.trigger('close');
    },

    // A called on close:before to remove all our subviews.
    _close: function() {
        _.each(this._subViews, function(view) { view.close(); });
    },

    // Assign a subview to an element in my template.
    // `selector` is a dom selector to assign to.
    // `view` is the subview to assign the selector to.
    // `replace` is a boolean.
    //    `False` (default), set view's $el to the selector.
    //    `True`, replace the selector with view's $el.
    // Alternate syntax by passing in an object for selector.
    //    With "selector": subview
    //    Replace will be the second param in this case.
    assign : function (selector, view, replace) {
        var selectors;
        if (_.isObject(selector)) {
            selectors = selector;
            replace = view;
        } else {
            selectors = {};
            selectors[selector] = view;
        }
        if (!selectors) { return; }

        _.each(selectors, function (view, selector) {
            this._subViews[view.cid] = view;
            if (replace) {
                this.$(selector).replaceWith(view.el);
            } else {
                view.setElement(this.$(selector)).render();
            }
        }, this);
    },

    // A proxy method to this.close().
    // This is bindAll-ed to the view instance.
    // Done so that we don't need to bindAll to close().
    _jqueryRemove: function() {
        this.close();
    },

    // Loop through the events given, and listen to
    // entity's event.
    _listenToEvents: function(entity, events) {
        for (var event in events) {
            var method = events[event];
            if (!_.isFunction(method)) { method = this[method]; }
            if (!method) { continue; }

            this.listenTo(entity, event, method);
        }
        return this;
    }
});
<commit_msg>Pass anything arguments passed to #delegateEvents<commit_after>Minionette.View = Backbone.View.extend({
    constructor: function() {
        Backbone.View.apply(this, arguments);

        // Done so we don't bindAll to standard methods.
        _.bindAll(this, '_jqueryRemove', '_close');

        // Keep track of our subviews.
        this._subViews = {};

        // Have the view listenTo the model and collection.
        this._listenToEvents(this.model, this.modelEvents);
        this._listenToEvents(this.collection, this.collectionEvents);
        this.listenTo(this, 'close:before', this._close);
    },

    // A default template that will clear this.$el.
    // Override this in a subclass to something useful.
    template: function() { return ''; },

    // When delegating events, bind this view to jQuery's special remove event.
    // Allows us to clean up the view, even if you remove this.$el with jQuery.
    // http://blog.alexmaccaw.com/jswebapps-memory-management
    delegateEvents: function() {
        Backbone.View.prototype.delegateEvents.apply(this, arguments);
        this.$el.on('remove.delegateEvents' + this.cid, this._jqueryRemove);
    },

    // A useful remove method to that triggers events.
    // Notice we emit "close" events, not "remove".
    close: function() {
        this.trigger('close:before');
        this.remove();
        this.trigger('close');
    },

    // A called on close:before to remove all our subviews.
    _close: function() {
        _.each(this._subViews, function(view) { view.close(); });
    },

    // Assign a subview to an element in my template.
    // `selector` is a dom selector to assign to.
    // `view` is the subview to assign the selector to.
    // `replace` is a boolean.
    //    `False` (default), set view's $el to the selector.
    //    `True`, replace the selector with view's $el.
    // Alternate syntax by passing in an object for selector.
    //    With "selector": subview
    //    Replace will be the second param in this case.
    assign : function (selector, view, replace) {
        var selectors;
        if (_.isObject(selector)) {
            selectors = selector;
            replace = view;
        } else {
            selectors = {};
            selectors[selector] = view;
        }
        if (!selectors) { return; }

        _.each(selectors, function (view, selector) {
            this._subViews[view.cid] = view;
            if (replace) {
                this.$(selector).replaceWith(view.el);
            } else {
                view.setElement(this.$(selector)).render();
            }
        }, this);
    },

    // A proxy method to this.close().
    // This is bindAll-ed to the view instance.
    // Done so that we don't need to bindAll to close().
    _jqueryRemove: function() {
        this.close();
    },

    // Loop through the events given, and listen to
    // entity's event.
    _listenToEvents: function(entity, events) {
        for (var event in events) {
            var method = events[event];
            if (!_.isFunction(method)) { method = this[method]; }
            if (!method) { continue; }

            this.listenTo(entity, event, method);
        }
        return this;
    }
});
<|endoftext|>


<commit_before>/**
 * @file view component
 */

'use strict';

const _ = require('lodash');
const m = require('mithril');
const hbs = require('handlebars');

function generateTable(items, list) {
  const contexts = [];
  const th = [];
  _.forEach(items, (item) => {
    let type, exec;
    if (item.template && item.key) {
      type = 'template';
      const cc = hbs.compile(item.template);
      exec = function(data) {return data && data[item.key] && cc(data[item.key])};
    } else if (item.template) {
      type = 'template';
      exec = hbs.compile(item.template);
    } else {
      type = 'key';
      exec = function(data) {return data && data[item.key]};
    }
    contexts.push({type, exec});
    th.push(m('th', item.title || '-'));
  });

  const td = _.map(list, (data) => {
    const inner = [];
    for (let i = 0, len = contexts.length; i < len; i++) {
      const context = contexts[i];
      const value = context.exec(data);
      if (context.type === 'template') {
        inner.push(m('td', m.trust(value))); // HTMLを入れる場合はtrustメソッドを使用する
      } else {
        inner.push(m('td', value));
      }
    }
    return m('tr', inner);
  });

  return m('table', [th, td]);
}

function getParams(url) {
  const params = {};
  _.forEach(url.split('/'), (value) => {
    if (!/^:/.test(value)) {
      return;
    }
    const key = value.slice(1); // sliceで`:`をカットする
    const param = m.route.param(key);
    if (param) {
      params[key] = param;
    }
  });
  return params;
}

function parseApi(api) {
  const arr = api.split(':');
  return {
    method: arr[0],
    url: arr[1]
  };
}

exports.controller = function(schema) {
  const params = getParams(schema.url);
  console.log(params);

  this.items = schema.view.list.items;

  this.list = m.request(parseApi(schema.view.action.read.api));
};

exports.view = function(ctrl) {
  const list = ctrl.list();

  // テーブル表示
  const table = generateTable(ctrl.items, list);

  return m('div', table);
};
<commit_msg>Modify view<commit_after>/**
 * @file view component
 */

'use strict';

const _ = require('lodash');
const m = require('mithril');
const hbs = require('handlebars');

function generateTable(items, list) {
  const contexts = [];
  const th = [];
  _.forEach(items, (item) => {
    let type, exec;
    if (item.template && item.key) {
      type = 'template';
      const cc = hbs.compile(item.template);
      exec = function(data) {return data && data[item.key] && cc(data[item.key])};
    } else if (item.template) {
      type = 'template';
      exec = hbs.compile(item.template);
    } else {
      type = 'key';
      exec = function(data) {return data && data[item.key]};
    }
    contexts.push({type, exec});
    th.push(m('th', item.title || '-'));
  });

  const td = _.map(list, (data) => {
    const inner = [];
    for (let i = 0, len = contexts.length; i < len; i++) {
      const context = contexts[i];
      const value = context.exec(data);
      if (context.type === 'template') {
        inner.push(m('td', m.trust(value))); // HTMLを入れる場合はtrustメソッドを使用する
      } else {
        inner.push(m('td', value));
      }
    }
    return m('tr', inner);
  });

  return m('table', [
    m('thead', th),
    m('tbody', td)
  ]);
}

function getParams(url) {
  const params = {};
  _.forEach(url.split('/'), (value) => {
    if (!/^:/.test(value)) {
      return;
    }
    const key = value.slice(1); // sliceで`:`をカットする
    const param = m.route.param(key);
    if (param) {
      params[key] = param;
    }
  });
  return params;
}

function parseApi(api) {
  const arr = api.split(':');
  return {
    method: arr[0],
    url: arr[1]
  };
}

exports.controller = function(schema) {
  const params = getParams(schema.url);
  console.log(params);

  this.items = schema.view.list.items;

  this.list = m.request(parseApi(schema.view.action.read.api));
};

exports.view = function(ctrl) {
  const list = ctrl.list();

  // テーブル表示
  const table = generateTable(ctrl.items, list);

  return m('div', table);
};
<|endoftext|>


<commit_before>/*globals require, module, setImmediate, setTimeout, console */

'use strict';

var util, Writable, EventEmitter, check, errors, events, terminators, escapes;

// TODO: Consider when to test `walking` and `finished` (currently in `end`)
// TODO: Ensure that we recur from tail positions
// TODO: Make new promisified code work with tests
// TODO: When testing consider gradually adding to available text
// TODO: Test delay argument

util = require('util');
Writable = require('stream').Writable;
EventEmitter = require('events').EventEmitter;
check = require('check-types');
errors = require('./errors');
events = require('./events');

terminators = {
    obj: '}',
    arr: ']'
};

escapes = {
    '"': '"',
    '\\': '\\',
    '/': '/',
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t'
};

module.exports = begin;

function begin (delay) {
    var json, emitter, stream,
        position, scopes, handlers,
        walking, insideString, finished;

    json = '';
    emitter = new EventEmitter();
    stream = new JsonStream(proceed);

    position = {
        index: 0,
        current: {
            line: 1,
            column: 1
        },
        previous: {}
    };
    scopes = [];
    handlers = {
        arr: value,
        obj: property
    };

    stream.on('finish', finish);

    return {
        emitter: emitter,
        stream: stream
    };

    function proceed (chunk) {
        console.log('proceed: ' + chunk);

        json += chunk;

        if (!walking) {
            walking = true;
            defer(value);
        }
    }

    function finish () {
        console.log('finish');

        finished = true;

        if (!walking) {
            end();
        }
    }

    function value () {
        console.log('value');

        ignoreWhitespace().then(function () {
            next().then(handleValue);
        });
    }

    function handleValue (character) {
        console.log('handleValue: ' + character);

        switch (character) {
            case '[':
                return array();
            case '{':
                return object();
            case '"':
                return string();
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '-':
            case '.':
                return number(character);
            case 'f':
                return literalFalse();
            case 'n':
                return literalNull();
            case 't':
                return literalTrue();
            default:
                error(character, 'value', 'previous');
                value();
        }
    }

    function ignoreWhitespace () {
        console.log('ignoreWhitespace');

        var resolve;

        defer(step.bind(null, character()));

        return new Promise(function (r) {
            resolve = r;
        });

        function step (character) {
            console.log('ignoreWhitespace::step: ' + character);

            if (isWhitespace(character)) {
                return next().then(step);
            }

            resolve();
        }
    }

    function next () {
        console.log('next');

        var resolve;

        // TODO: discard old characters to save memory

        isEnd().then(function (atEnd) {
            console.log('next::atEnd: ' + atEnd);

            var result;

            if (atEnd) {
                return end();
            }

            result = character();

            position.index += 1;
            position.previous.line = position.current.line;
            position.previous.column = position.current.column;

            if (result === '\n') {
                position.current.line += 1;
                position.current.column = 1;
            } else {
                position.current.column += 1;
            }

            resolve(result);
        });

        return new Promise(function (r) {
            resolve = r;
        });
    }

    function isEnd () {
        console.log('isEnd');

        var resolve;

        defer(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step () {
            console.log('isEnd::step: ' + walking);

            if (walking) {
                return resolve(position.index === json.length);
            }

            wait(step);
        }
    }

    function wait (after) {
        console.log('wait: ' + after);

        setTimeout(after, delay || 1000);
    }

    function end () {
        console.log('end');

        if (!finished) {
            console.log('end: not finished');
            walking = false;
            return;
        }

        if (insideString) {
            console.log('end: inside string');
            error('EOF', '"', 'current');
        }

        while (scopes.length > 0) {
            console.log('end: ' + scopes.length + ' unterminated scopes');
            error('EOF', terminators[scopes.pop()], 'current');
        }

        console.log('end: emitting event');
        emitter.emit(events.end);
    }

    function error (actual, expected, positionKey) {
        console.log('error: ' + actual + ', ' + expected + ', ' + positionKey);

        emitter.emit(
            events.error,
            errors.create(
                actual,
                expected,
                position[positionKey].line,
                position[positionKey].column
            )
        );
    }

    function character () {
        console.log('character');

        return json[position.index];
    }

    function array () {
        console.log('array');

        scope(events.array, value);
    }

    function scope (event, contentHandler) {
        console.log('scope: ' + event + ', ' + typeof contentHandler);

        emitter.emit(event);
        scopes.push(event);
        endScope().then(function (atScopeEnd) {
            if (!atScopeEnd) {
                defer(contentHandler);
            }
        });
    }

    function object () {
        console.log('object');

        scope(events.object, property);
    }

    function property () {
        console.log('property');

        ignoreWhitespace().then(function () {
            next().then(propertyName);
        });
    }

    function propertyName (character) {
        console.log('propertyName: ' + character);

        checkCharacter(character, '"');
        walkString(events.property).then(function () {
            ignoreWhitespace().then(function () {
                next().then(propertyValue);
            });
        });
    }

    function propertyValue (character) {
        console.log('propertyValue: ' + character);

        checkCharacter(character, ':');
        defer(value);
    }

    function walkString (event) {
        console.log('walkString: ' + event);

        var quoting, string;

        insideString = true;
        quoting = false;
        string = '';

        next().then(step);

        function step (character) {
            console.log('walkString::step: ' + character);

            if (quoting) {
                quoting = false;

                return next().then(function (character) {
                    escape(character).then(function (escaped) {
                        string += escaped;
                        next().then(step);
                    });
                });
            }

            if (character === '\\') {
                quoting = true;
                return next().then(step);
            }

            if (character !== '"') {
                return next().then(function (character) {
                    string += character;
                    next().then(step);
                });
            }

            insideString = false;
            emitter.emit(event, string);
        }
    }

    function escape (character) {
        console.log('escape: ' + character);

        if (escapes[character]) {
            return escapes[character];
        }

        if (character === 'u') {
            return escapeHex();
        }

        error(character, 'escape character', 'previous');

        return '\\' + character;
    }

    function escapeHex () {
        console.log('escapeHex');

        var hexits, resolve;

        hexits = '';

        next().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (index, character) {
            console.log('escapeHex::step: ' + index + ', ' + character);

            if (index === 4) {
                if (hexits.length === 4) {
                    return resolve(String.fromCharCode(parseInt(hexits, 16)));
                }

                error(character, 'hex digit', 'previous');

                return resolve('\\u' + hexits + character);
            }

            if (isHexit(character)) {
                hexits += character;
            }

            next().then(step.bind(null, index + 1));
        }
    }

    function checkCharacter (character, expected) {
        console.log('checkCharacter: ' + character + ', ' + expected);

        if (character !== expected) {
            return error(character, expected, 'previous');
        }
    }

    function endScope (scope) {
        console.log('endScope: ' + scope);

        return new Promise(function (resolve) {
            if (character() !== terminators[scope]) {
                return resolve(false);
            }

            emitter.emit(events.endPrefix + scope);
            scopes.pop();

            next().then(function () {
                defer(endValue);
                resolve(true);
            });
        });
    }

    function endValue () {
        console.log('endValue');

        ignoreWhitespace().then(function () {
            if (scopes.length === 0) {
                return isEnd().then(checkEnd);
            }

            checkScope();
        });

        function checkEnd (atEnd) {
            console.log('checkEnd: ' + atEnd);

            if (!atEnd) {
                error(character(), 'EOF', 'current');
                return defer(value);
            }

            checkScope();
        }

        function checkScope () {
            console.log('checkScope');

            var scope = scopes[scopes.length - 1];

            endScope().then(function (atScopeEnd) {
                if (!atScopeEnd) {
                    next().then(function (character) {
                        checkCharacter(character, ',');
                        defer(handlers[scope]);
                    });
                }
            });
        }
    }

    function string () {
        console.log('string');

        walkString(events.string).then(function () {
            next().then(defer.bind(null, endValue));
        });
    }

    function number (character) {
        console.log('number: ' + character);

        var digits = character;

        walkDigits().then(addDigits.bind(null, checkDecimalPlace));

        function addDigits (step, remainingDigits) {
            console.log('number::addDigits: ' + typeof step + ', ' + remainingDigits);

            digits += remainingDigits;
            next().then(step);
        }

        function checkDecimalPlace (character) {
            console.log('number::checkDecimalPlace: ' + character);

            if (character === '.') {
                digits += character;
                return walkDigits().then(addDigits.bind(null, checkExponent));
            }

            next().then(checkExponent);
        }

        function checkExponent (character) {
            console.log('number::checkExponent: ' + character);

            if (character === 'e' || character === 'E') {
                digits += character;
                return next().then(checkSign);
            }

            endNumber();
        }

        function checkSign (character) {
            console.log('checkSign: ' + character);

            if (character === '+' || character === '-') {
                digits += character;
            }

            walkDigits().then(function (remainingDigits) {
                digits += remainingDigits;
                endNumber();
            });
        }

        function endNumber () {
            console.log('endNumber');

            emitter.emit(events.number, parseFloat(digits));
            defer(endValue);
        }
    }

    function walkDigits () {
        console.log('walkDigits');

        var digits, resolve;

        digits = '';

        isEnd().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (atEnd) {
            console.log('walkDigits::step: ' + atEnd);

            if (atEnd || !isDigit(character())) {
                return resolve(digits);
            }

            next().then(function (character) {
                digits += character;
                isEnd().then(step);
            });
        }
    }

    function literalFalse () {
        console.log('literalFalse');

        literal([ 'a', 'l', 's', 'e' ], false);
    }

    function literal (expectedCharacters, value) {
        console.log('literal: ' + expectedCharacters.join('') + ', ' + value);

        var actual, expected, invalid;

        isEnd().then(step);

        function step (atEnd) {
            console.log('literal::step: ' + atEnd);

            if (expectedCharacters.length === 0 || atEnd) {
                if (invalid) {
                    error(actual, expected, 'previous');
                } else if (expectedCharacters.length > 0) {
                    error('EOF', expectedCharacters.shift(), 'current');
                } else {
                    emitter.emit(events.literal, value);
                }

                return defer(endValue);
            }

            next().then(function (character) {
                console.log('literal::character: ' + character);

                actual = character;
                expected = expectedCharacters.shift();

                if (actual !== expected) {
                    invalid = true;
                    isEnd().then(step);
                }
            });
        }
    }

    function literalNull () {
        console.log('literalNull');

        literal([ 'u', 'l', 'l' ], null);
    }

    function literalTrue () {
        console.log('literalTrue');

        literal([ 'r', 'u', 'e' ], true);
    }
}

function JsonStream (write) {
    console.log('JsonStream: ' + typeof write);

    if (!(this instanceof JsonStream)) {
        return new JsonStream();
    }

    this._write = function (chunk, encoding, callback) {
        console.log('JsonStream::_write: ' + chunk + ', ' + encoding + ', ' + typeof callback);

        write(chunk.toString());
        callback();
    };

    return Writable.call(this);
}

util.inherits(JsonStream, Writable);

function defer (fn) {
    console.log('defer: ' + typeof fn);

    setImmediate(function () {
        try {
            fn();
        } catch (error) {
            /*jshint noempty:false */
        }
    });
}

function isWhitespace (character) {
    console.log('isWhitespace: ' + character);

    switch (character) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            return true;
    }

    return false;
}

function isHexit (character) {
    console.log('isHexit: ' + character);

    return isDigit(character) ||
           isInRange(character, 'A', 'F') ||
           isInRange(character, 'a', 'f');
}

function isDigit (character) {
    console.log('isDigit: ' + character);

    return isInRange(character, '0', '9');
}

function isInRange (character, lower, upper) {
    console.log('isInRange: ' + character + ', ' + lower + ', ' + upper);

    var code = character.charCodeAt(0);

    return code >= lower.charCodeAt(0) && code <= upper.charCodeAt(0);
}

<commit_msg>Fix bad argument to endScope.<commit_after>/*globals require, module, setImmediate, setTimeout, console */

'use strict';

var util, Writable, EventEmitter, check, errors, events, terminators, escapes;

// TODO: Consider when to test `walking` and `finished` (currently in `end`)
// TODO: Ensure that we recur from tail positions
// TODO: Make new promisified code work with tests
// TODO: When testing consider gradually adding to available text
// TODO: Test delay argument

util = require('util');
Writable = require('stream').Writable;
EventEmitter = require('events').EventEmitter;
check = require('check-types');
errors = require('./errors');
events = require('./events');

terminators = {
    obj: '}',
    arr: ']'
};

escapes = {
    '"': '"',
    '\\': '\\',
    '/': '/',
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t'
};

module.exports = begin;

function begin (delay) {
    var json, emitter, stream,
        position, scopes, handlers,
        walking, insideString, finished;

    json = '';
    emitter = new EventEmitter();
    stream = new JsonStream(proceed);

    position = {
        index: 0,
        current: {
            line: 1,
            column: 1
        },
        previous: {}
    };
    scopes = [];
    handlers = {
        arr: value,
        obj: property
    };

    stream.on('finish', finish);

    return {
        emitter: emitter,
        stream: stream
    };

    function proceed (chunk) {
        console.log('proceed: ' + chunk);

        json += chunk;

        if (!walking) {
            walking = true;
            defer(value);
        }
    }

    function finish () {
        console.log('finish');

        finished = true;

        if (!walking) {
            end();
        }
    }

    function value () {
        console.log('value');

        ignoreWhitespace().then(function () {
            next().then(handleValue);
        });
    }

    function handleValue (character) {
        console.log('handleValue: ' + character);

        switch (character) {
            case '[':
                return array();
            case '{':
                return object();
            case '"':
                return string();
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '-':
            case '.':
                return number(character);
            case 'f':
                return literalFalse();
            case 'n':
                return literalNull();
            case 't':
                return literalTrue();
            default:
                error(character, 'value', 'previous');
                value();
        }
    }

    function ignoreWhitespace () {
        console.log('ignoreWhitespace');

        var resolve;

        defer(step.bind(null, character()));

        return new Promise(function (r) {
            resolve = r;
        });

        function step (character) {
            console.log('ignoreWhitespace::step: ' + character);

            if (isWhitespace(character)) {
                return next().then(step);
            }

            resolve();
        }
    }

    function next () {
        console.log('next');

        var resolve;

        // TODO: discard old characters to save memory

        isEnd().then(function (atEnd) {
            console.log('next::atEnd: ' + atEnd);

            var result;

            if (atEnd) {
                return end();
            }

            result = character();

            position.index += 1;
            position.previous.line = position.current.line;
            position.previous.column = position.current.column;

            if (result === '\n') {
                position.current.line += 1;
                position.current.column = 1;
            } else {
                position.current.column += 1;
            }

            resolve(result);
        });

        return new Promise(function (r) {
            resolve = r;
        });
    }

    function isEnd () {
        console.log('isEnd');

        var resolve;

        defer(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step () {
            console.log('isEnd::step: ' + walking);

            if (walking) {
                return resolve(position.index === json.length);
            }

            wait(step);
        }
    }

    function wait (after) {
        console.log('wait: ' + after);

        setTimeout(after, delay || 1000);
    }

    function end () {
        console.log('end');

        if (!finished) {
            console.log('end: not finished');
            walking = false;
            return;
        }

        if (insideString) {
            console.log('end: inside string');
            error('EOF', '"', 'current');
        }

        while (scopes.length > 0) {
            console.log('end: ' + scopes.length + ' unterminated scopes');
            error('EOF', terminators[scopes.pop()], 'current');
        }

        console.log('end: emitting event');
        emitter.emit(events.end);
    }

    function error (actual, expected, positionKey) {
        console.log('error: ' + actual + ', ' + expected + ', ' + positionKey);

        emitter.emit(
            events.error,
            errors.create(
                actual,
                expected,
                position[positionKey].line,
                position[positionKey].column
            )
        );
    }

    function character () {
        console.log('character');

        return json[position.index];
    }

    function array () {
        console.log('array');

        scope(events.array, value);
    }

    function scope (event, contentHandler) {
        console.log('scope: ' + event + ', ' + typeof contentHandler);

        emitter.emit(event);
        scopes.push(event);
        endScope(event).then(function (atScopeEnd) {
            if (!atScopeEnd) {
                defer(contentHandler);
            }
        });
    }

    function endScope (scope) {
        console.log('endScope: ' + scope);

        return new Promise(function (resolve) {
            if (character() !== terminators[scope]) {
                return resolve(false);
            }

            emitter.emit(events.endPrefix + scope);
            scopes.pop();

            next().then(function () {
                defer(endValue);
                resolve(true);
            });
        });
    }

    function object () {
        console.log('object');

        scope(events.object, property);
    }

    function property () {
        console.log('property');

        ignoreWhitespace().then(function () {
            next().then(propertyName);
        });
    }

    function propertyName (character) {
        console.log('propertyName: ' + character);

        checkCharacter(character, '"');
        walkString(events.property).then(function () {
            ignoreWhitespace().then(function () {
                next().then(propertyValue);
            });
        });
    }

    function propertyValue (character) {
        console.log('propertyValue: ' + character);

        checkCharacter(character, ':');
        defer(value);
    }

    function walkString (event) {
        console.log('walkString: ' + event);

        var quoting, string;

        insideString = true;
        quoting = false;
        string = '';

        next().then(step);

        function step (character) {
            console.log('walkString::step: ' + character);

            if (quoting) {
                quoting = false;

                return next().then(function (character) {
                    escape(character).then(function (escaped) {
                        string += escaped;
                        next().then(step);
                    });
                });
            }

            if (character === '\\') {
                quoting = true;
                return next().then(step);
            }

            if (character !== '"') {
                return next().then(function (character) {
                    string += character;
                    next().then(step);
                });
            }

            insideString = false;
            emitter.emit(event, string);
        }
    }

    function escape (character) {
        console.log('escape: ' + character);

        if (escapes[character]) {
            return escapes[character];
        }

        if (character === 'u') {
            return escapeHex();
        }

        error(character, 'escape character', 'previous');

        return '\\' + character;
    }

    function escapeHex () {
        console.log('escapeHex');

        var hexits, resolve;

        hexits = '';

        next().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (index, character) {
            console.log('escapeHex::step: ' + index + ', ' + character);

            if (index === 4) {
                if (hexits.length === 4) {
                    return resolve(String.fromCharCode(parseInt(hexits, 16)));
                }

                error(character, 'hex digit', 'previous');

                return resolve('\\u' + hexits + character);
            }

            if (isHexit(character)) {
                hexits += character;
            }

            next().then(step.bind(null, index + 1));
        }
    }

    function checkCharacter (character, expected) {
        console.log('checkCharacter: ' + character + ', ' + expected);

        if (character !== expected) {
            return error(character, expected, 'previous');
        }
    }

    function endValue () {
        console.log('endValue');

        ignoreWhitespace().then(function () {
            if (scopes.length === 0) {
                return isEnd().then(checkEnd);
            }

            checkScope();
        });

        function checkEnd (atEnd) {
            console.log('checkEnd: ' + atEnd);

            if (!atEnd) {
                error(character(), 'EOF', 'current');
                return defer(value);
            }

            checkScope();
        }

        function checkScope () {
            console.log('checkScope');

            var scope = scopes[scopes.length - 1];

            endScope().then(function (atScopeEnd) {
                if (!atScopeEnd) {
                    next().then(function (character) {
                        checkCharacter(character, ',');
                        defer(handlers[scope]);
                    });
                }
            });
        }
    }

    function string () {
        console.log('string');

        walkString(events.string).then(function () {
            next().then(defer.bind(null, endValue));
        });
    }

    function number (character) {
        console.log('number: ' + character);

        var digits = character;

        walkDigits().then(addDigits.bind(null, checkDecimalPlace));

        function addDigits (step, remainingDigits) {
            console.log('number::addDigits: ' + typeof step + ', ' + remainingDigits);

            digits += remainingDigits;
            next().then(step);
        }

        function checkDecimalPlace (character) {
            console.log('number::checkDecimalPlace: ' + character);

            if (character === '.') {
                digits += character;
                return walkDigits().then(addDigits.bind(null, checkExponent));
            }

            next().then(checkExponent);
        }

        function checkExponent (character) {
            console.log('number::checkExponent: ' + character);

            if (character === 'e' || character === 'E') {
                digits += character;
                return next().then(checkSign);
            }

            endNumber();
        }

        function checkSign (character) {
            console.log('checkSign: ' + character);

            if (character === '+' || character === '-') {
                digits += character;
            }

            walkDigits().then(function (remainingDigits) {
                digits += remainingDigits;
                endNumber();
            });
        }

        function endNumber () {
            console.log('endNumber');

            emitter.emit(events.number, parseFloat(digits));
            defer(endValue);
        }
    }

    function walkDigits () {
        console.log('walkDigits');

        var digits, resolve;

        digits = '';

        isEnd().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (atEnd) {
            console.log('walkDigits::step: ' + atEnd);

            if (atEnd || !isDigit(character())) {
                return resolve(digits);
            }

            next().then(function (character) {
                digits += character;
                isEnd().then(step);
            });
        }
    }

    function literalFalse () {
        console.log('literalFalse');

        literal([ 'a', 'l', 's', 'e' ], false);
    }

    function literal (expectedCharacters, value) {
        console.log('literal: ' + expectedCharacters.join('') + ', ' + value);

        var actual, expected, invalid;

        isEnd().then(step);

        function step (atEnd) {
            console.log('literal::step: ' + atEnd);

            if (expectedCharacters.length === 0 || atEnd) {
                if (invalid) {
                    error(actual, expected, 'previous');
                } else if (expectedCharacters.length > 0) {
                    error('EOF', expectedCharacters.shift(), 'current');
                } else {
                    emitter.emit(events.literal, value);
                }

                return defer(endValue);
            }

            next().then(function (character) {
                console.log('literal::character: ' + character);

                actual = character;
                expected = expectedCharacters.shift();

                if (actual !== expected) {
                    invalid = true;
                    isEnd().then(step);
                }
            });
        }
    }

    function literalNull () {
        console.log('literalNull');

        literal([ 'u', 'l', 'l' ], null);
    }

    function literalTrue () {
        console.log('literalTrue');

        literal([ 'r', 'u', 'e' ], true);
    }
}

function JsonStream (write) {
    console.log('JsonStream: ' + typeof write);

    if (!(this instanceof JsonStream)) {
        return new JsonStream();
    }

    this._write = function (chunk, encoding, callback) {
        console.log('JsonStream::_write: ' + chunk + ', ' + encoding + ', ' + typeof callback);

        write(chunk.toString());
        callback();
    };

    return Writable.call(this);
}

util.inherits(JsonStream, Writable);

function defer (fn) {
    console.log('defer: ' + typeof fn);

    setImmediate(function () {
        try {
            fn();
        } catch (error) {
            /*jshint noempty:false */
        }
    });
}

function isWhitespace (character) {
    console.log('isWhitespace: ' + character);

    switch (character) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            return true;
    }

    return false;
}

function isHexit (character) {
    console.log('isHexit: ' + character);

    return isDigit(character) ||
           isInRange(character, 'A', 'F') ||
           isInRange(character, 'a', 'f');
}

function isDigit (character) {
    console.log('isDigit: ' + character);

    return isInRange(character, '0', '9');
}

function isInRange (character, lower, upper) {
    console.log('isInRange: ' + character + ', ' + lower + ', ' + upper);

    var code = character.charCodeAt(0);

    return code >= lower.charCodeAt(0) && code <= upper.charCodeAt(0);
}

<|endoftext|>


<commit_before>import { Core, Container, Playback, version } from '@clappr/core'
import ContextMenuPlugin from './context_menu'

describe('Context Menu Plugin', () => {
  test('is loaded on core plugins array', () => {
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)

    expect(core.getPlugin(plugin.name).name).toEqual('context_menu')
  })

  test('is compatible with the latest Clappr core version', () => {
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)

    expect(core.getPlugin(plugin.name).supportedVersion).toEqual({ min: version })
  })

  test('only unbind events when is necessary', () => {
    const core = new Core({})
    const playback = new Playback({})
    const container = new Container({ playerId: 1, playback })
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    jest.spyOn(plugin, 'stopListening')

    core.activeContainer = container

    expect(plugin.stopListening).not.toHaveBeenCalled()

    core.activeContainer = container

    expect(plugin.stopListening).toHaveBeenCalled()
  })

  test('shows at context menu event trigger occurs', () => {
    const core = new Core({})
    const playback = new Playback({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    core.activeContainer = new Container({ playerId: 1, playback })
    jest.spyOn(plugin, 'show')

    const evt = new Event('contextmenu', { bubbles: true, cancelable: true })
    core.activeContainer.trigger('container:contextmenu', evt)

    expect(plugin.show).toHaveBeenCalled()
  })

  test('creates cache elements to not have unnecessary re-render cicles', () => {
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    jest.spyOn(plugin, 'render')
    jest.spyOn(plugin, 'cacheElements')
    plugin.render()

    expect(plugin.render).toHaveBeenCalledTimes(1)
    expect(plugin.cacheElements).not.toHaveBeenCalled()

    plugin.render()

    expect(plugin.render).toHaveBeenCalledTimes(2)
    expect(plugin.cacheElements).not.toHaveBeenCalled()
  })

  test('only accept absolute size format', () => {
    const customSize = { height: 360, width: 640 }
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    plugin.registerPlayerResize({ height: '100%', width: '100%' })

    expect(plugin.playerSize).toBeUndefined()
    core.resize(customSize)

    expect(plugin.playerSize).toEqual(customSize)
  })

  test('respect player size limits to render', () => {
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    core.trigger('core:resize', { height: 360, width: 640 })
    plugin.show(100, 100)

    expect(plugin.el.style.left).toEqual('100px')

    plugin.show(1000, 2000)

    expect(plugin.el.style.left).toEqual('480px')
  })

  test('is destroyed when Core is destroyed too', () => {
    const core = new Core({})
    const plugin = new ContextMenuPlugin(core)
    core.addPlugin(plugin)
    jest.spyOn(plugin, 'destroy')
    core.destroy()

    expect(plugin.destroy).toHaveBeenCalled()
  })

  describe('with no configuration', () => {
    test('render default items', () => {
      const core = new Core({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      expect(plugin.menuOptions).toEqual(plugin.defaultMenuItems)
    })
  })

  describe('with menuItems configured', () => {
    const contextMenuOptions = { menuItems: [] }

    test('discard empty config', () => {
      const core = new Core({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      expect(plugin.menuOptions).toEqual(plugin.defaultMenuItems)
    })

    test('discard unrecognized config', () => {
      contextMenuOptions.menuItems.push('xpto')
      const core = new Core({ source: 'test.mp4', contextMenu: contextMenuOptions })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      expect(plugin.menuOptions).toEqual(plugin.defaultMenuItems)
    })

    test('apply custom menuItem config', () => {
      contextMenuOptions.menuItems.push('playerVersion')
      const core = new Core({ source: 'test.mp4', contextMenu: contextMenuOptions })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      expect(plugin.menuOptions).toEqual([{ label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true }])
    })
  })

  describe('with extraOptions configured', () => {
    const extraOption = { name: 'test', label: 'Test Label' }
    const contextMenuOptions = { extraOptions: [extraOption] }

    test('use setted extram options', () => {
      const core = new Core({ source: 'test.mp4', contextMenu: contextMenuOptions })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      expect(plugin.menuOptions).toContain(extraOption)
    })

    test('trigger callback at click on extra options', () => {
      let callbackCalled = false
      extraOption.callback = () => { callbackCalled = true }

      const core = new Core({ source: 'test.mp4', contextMenu: contextMenuOptions })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      const evt = new Event('click', { bubbles: true, cancelable: false })
      const extraOptionElement = plugin.el.querySelector('[data-test]')
      extraOptionElement.dispatchEvent(evt)

      expect(callbackCalled).toBeTruthy()
    })
  })

  describe('with customStyle configured', () => {
    test('apply style correctly', () => {
      const customStyle = {
        list: { 'background-color': 'gray' },
        items: { color: 'yellow' },
      }
      const contextMenuOptions = { customStyle }

      const core = new Core({ source: 'test.mp4', contextMenu: contextMenuOptions })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      jest.spyOn(plugin.$list, 'css')
      jest.spyOn(plugin.$listItem, 'css')
      plugin.addCustomStyle(customStyle)

      expect(plugin.$list.css).toHaveBeenCalledWith({ 'background-color': 'gray' })
      expect(plugin.$listItem.css).toHaveBeenCalledWith({ color: 'yellow' })
    })
  })

  describe('player version feature', () => {
    test('show correct Clappr version', () => {
      const core = new Core({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      const playerVersion = plugin.el.querySelector('[data-playerversion]')

      expect(playerVersion.innerHTML).toEqual(`Clappr Player v${version}`)
    })
  })

  describe('loop feature', () => {
    test('show current loop status', () => {
      const core = new Core({ source: 'test.mp4', loop: true })
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)

      const loop = plugin.el.querySelector('[data-loop]')

      expect([...loop.classList]).toContain('on')
    })

    test('update loop playback property value', () => {
      const core = new Core({})
      const playback = new Playback({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      core.activeContainer = new Container({ playerId: 1, playback })

      const loop = plugin.el.querySelector('[data-loop]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      loop.dispatchEvent(evt)

      expect([...loop.classList]).toContain('on')
    })
  })

  describe('copyURL feature', () => {
    test('copy url website at Clappr as embeded', () => {
      const core = new Core({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      jest.spyOn(plugin, 'copyToClipboard').mockImplementation()

      const copyURL = plugin.el.querySelector('[data-copyURL]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)

      expect(plugin.copyToClipboard).toHaveBeenCalledWith(window.location.href, plugin.$el)
    })
  })

  describe('CopyURLCurrentTime feature', () => {
    test('copy url without query string and put current video time', () => {
      const core = new Core({})
      const playback = new Playback({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      core.activeContainer = new Container({ playerId: 1, playback })
      jest.spyOn(plugin, 'copyToClipboard').mockImplementation()
      jest.spyOn(plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}?t=1`

      expect(plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, plugin.$el)
    })

    test('copy url with other query string and put current video time', () => {
      const core = new Core({})
      const playback = new Playback({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      core.activeContainer = new Container({ playerId: 1, playback })
      jest.spyOn(plugin, 'copyToClipboard').mockImplementation()
      window.history.pushState({}, '', `${window.location.href}?other_query_string=0`)
      jest.spyOn(plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}&t=1`

      expect(plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, plugin.$el)
    })

    test('copy url with resume at query string and put current video time', () => {
      const core = new Core({})
      const playback = new Playback({})
      const plugin = new ContextMenuPlugin(core)
      core.addPlugin(plugin)
      core.activeContainer = new Container({ playerId: 1, playback })
      jest.spyOn(plugin, 'copyToClipboard').mockImplementation()
      const sitePageURL = window.location.href
      window.history.pushState({}, '', `${window.location.href}?t=0`)
      jest.spyOn(plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${sitePageURL}?t=1`

      expect(plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, plugin.$el)
    })
  })
})
<commit_msg>test(src): avoid overwhelmed code<commit_after>import { Events, Core, Container, Playback, version } from '@clappr/core'
import ContextMenuPlugin from './context_menu'

const setupTest = (options = {}, fullSetup = false) => {
  const core = new Core(options)
  const plugin = new ContextMenuPlugin(core)
  core.addPlugin(plugin)

  const response = { core, plugin }
  fullSetup && (response.container = new Container({ playerId: 1, playback: new Playback({}) }))

  return response
}

describe('Context Menu Plugin', function() {
  beforeEach(() => {
    jest.clearAllMocks()
    const response = setupTest({}, true)
    this.core = response.core
    this.container = response.container
    this.core.activeContainer = this.container
    this.playback = this.container.playback
    this.plugin = response.plugin
  })

  test('is loaded on core plugins array', () => {
    expect(this.core.getPlugin(this.plugin.name).name).toEqual('context_menu')
  })

  test('is compatible with the latest Clappr core version', () => {
    expect(this.core.getPlugin(this.plugin.name).supportedVersion).toEqual({ min: version })
  })

  test('only unbind events when is necessary', () => {
    jest.spyOn(this.plugin, 'stopListening')

    expect(this.plugin.stopListening).not.toHaveBeenCalled()

    this.core.activeContainer = this.container

    expect(this.plugin.stopListening).toHaveBeenCalled()
  })

  test('shows at context menu event trigger occurs', () => {
    jest.spyOn(this.plugin, 'show')
    const evt = new Event('contextmenu', { bubbles: true, cancelable: true })
    this.core.activeContainer.trigger(Events.CONTAINER_CONTEXTMENU, evt)

    expect(this.plugin.show).toHaveBeenCalled()
  })

  test('creates cache elements to not have unnecessary re-render cycles', () => {
    jest.spyOn(this.plugin, 'render')
    jest.spyOn(this.plugin, 'cacheElements')
    this.plugin.render()

    expect(this.plugin.render).toHaveBeenCalledTimes(1)
    expect(this.plugin.cacheElements).not.toHaveBeenCalled()

    this.plugin.render()

    expect(this.plugin.render).toHaveBeenCalledTimes(2)
    expect(this.plugin.cacheElements).not.toHaveBeenCalled()
  })

  test('only accept absolute size format', () => {
    const customSize = { height: 360, width: 640 }
    this.plugin.registerPlayerResize({ height: '100%', width: '100%' })

    expect(this.plugin.playerSize).toBeUndefined()
    this.core.resize(customSize)

    expect(this.plugin.playerSize).toEqual(customSize)
  })

  test('respect player size limits to render', () => {
    this.core.trigger(Events.CORE_RESIZE, { height: 360, width: 640 })
    this.plugin.show(100, 100)

    expect(this.plugin.el.style.left).toEqual('100px')

    this.plugin.show(1000, 2000)

    expect(this.plugin.el.style.left).toEqual('480px')
  })

  test('is destroyed when Core is destroyed too', () => {
    jest.spyOn(this.plugin, 'destroy')
    this.core.destroy()

    expect(this.plugin.destroy).toHaveBeenCalled()
  })

  describe('with no configuration', () => {
    test('render default items', () => {
      expect(this.plugin.menuOptions).toEqual(this.plugin.defaultMenuItems)
    })
  })

  describe('with menuItems configured', () => {
    const contextMenuOptions = { menuItems: [] }

    test('discard empty config', () => {
      const { plugin } = setupTest()

      expect(plugin.menuOptions).toEqual(plugin.defaultMenuItems)
    })

    test('discard unrecognized config', () => {
      contextMenuOptions.menuItems.push('invalid')
      const { plugin } = setupTest({ source: 'test.mp4', contextMenu: contextMenuOptions })

      expect(plugin.menuOptions).toEqual(plugin.defaultMenuItems)
    })

    test('apply custom menuItem config', () => {
      contextMenuOptions.menuItems.push('playerVersion')
      const { plugin } = setupTest({ source: 'test.mp4', contextMenu: contextMenuOptions })

      expect(plugin.menuOptions).toEqual([{ label: `Clappr Player v${version}`, name: 'playerVersion', noAction: true }])
    })
  })

  describe('with extraOptions configured', () => {
    const extraOption = { name: 'test', label: 'Test Label' }
    const contextMenuOptions = { extraOptions: [extraOption] }

    test('use set extra options', () => {
      const { plugin } = setupTest({ source: 'test.mp4', contextMenu: contextMenuOptions })

      expect(plugin.menuOptions).toContain(extraOption)
    })

    test('trigger callback at click on extra options', () => {
      let callbackCalled = false
      extraOption.callback = () => { callbackCalled = true }
      const { plugin } = setupTest({ source: 'test.mp4', contextMenu: contextMenuOptions })

      const evt = new Event('click', { bubbles: true, cancelable: false })
      const extraOptionElement = plugin.el.querySelector('[data-test]')
      extraOptionElement.dispatchEvent(evt)

      expect(callbackCalled).toBeTruthy()
    })
  })

  describe('with customStyle configured', () => {
    test('apply style correctly', () => {
      const customStyle = {
        list: { 'background-color': 'gray' },
        items: { color: 'yellow' },
      }
      const contextMenuOptions = { customStyle }
      const { plugin } = setupTest({ source: 'test.mp4', contextMenu: contextMenuOptions })
      jest.spyOn(plugin.$list, 'css')
      jest.spyOn(plugin.$listItem, 'css')
      plugin.addCustomStyle(customStyle)

      expect(plugin.$list.css).toHaveBeenCalledWith({ 'background-color': 'gray' })
      expect(plugin.$listItem.css).toHaveBeenCalledWith({ color: 'yellow' })
    })
  })

  describe('player version feature', () => {
    test('show correct Clappr version', () => {
      const playerVersion = this.plugin.el.querySelector('[data-playerversion]')

      expect(playerVersion.innerHTML).toEqual(`Clappr Player v${version}`)
    })
  })

  describe('loop feature', () => {
    test('show current loop status', () => {
      const { plugin } = setupTest({ source: 'test.mp4', loop: true })
      const loop = plugin.el.querySelector('[data-loop]')

      expect([...loop.classList]).toContain('on')
    })

    test('update loop playback property value', () => {
      const loop = this.plugin.el.querySelector('[data-loop]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      loop.dispatchEvent(evt)

      expect([...loop.classList]).toContain('on')
    })
  })

  describe('copyURL feature', () => {
    test('copy url website at Clappr as embedded', () => {
      jest.spyOn(this.plugin, 'copyToClipboard').mockImplementation()
      const copyURL = this.plugin.el.querySelector('[data-copyURL]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(window.location.href, this.plugin.$el)
    })
  })

  describe('CopyURLCurrentTime feature', () => {
    test('copy url without query string and put current video time', () => {
      jest.spyOn(this.plugin, 'copyToClipboard').mockImplementation()
      jest.spyOn(this.plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    test('copy url with other query string and put current video time', () => {
      jest.spyOn(this.plugin, 'copyToClipboard').mockImplementation()
      window.history.pushState({}, '', `${window.location.href}?other_query_string=0`)
      jest.spyOn(this.plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${window.location.href}&t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })

    test('copy url with resume at query string and put current video time', () => {
      jest.spyOn(this.plugin, 'copyToClipboard').mockImplementation()
      const sitePageURL = window.location.href
      window.history.pushState({}, '', `${window.location.href}?t=0`)
      jest.spyOn(this.plugin.container, 'getCurrentTime').mockImplementation(() => 1)

      const copyURL = this.plugin.el.querySelector('[data-copyURLCurrentTime]')
      const evt = new Event('click', { bubbles: true, cancelable: false })
      copyURL.dispatchEvent(evt)
      const expectedURL = `${sitePageURL}?t=1`

      expect(this.plugin.copyToClipboard).toHaveBeenCalledWith(expectedURL, this.plugin.$el)
    })
  })
})
<|endoftext|>


<commit_before>'use strict';

SoundJSDirectorGroupProto.play = function playGroup () {
  // TODO
  return this;
};

SoundJSDirectorGroupProto.pause = function pauseGroup () {
  // TODO
  return this;
};

SoundJSDirectorGroupProto.resume = function resumeGroup () {
  // TODO
  return this;
};

SoundJSDirectorGroupProto.stop = function stopGroup () {
  // TODO
  return this;
};

<commit_msg>Started groupped playing method<commit_after>'use strict';

// TODO: add easings to all of current methods.

SoundJSDirectorGroupProto.play = function playGroup (options) {
  // TODO: Pass group options to every sounds considering every group of sound.
  this.eachSound(function (sound) {
    // TODO: pass arguments multiplied with all instance groups.
    sound.play(options);
  }, this);
  return this;
};

SoundJSDirectorGroupProto.pause = function pauseGroup () {
  // TODO
  return this;
};

SoundJSDirectorGroupProto.resume = function resumeGroup () {
  // TODO
  return this;
};

SoundJSDirectorGroupProto.stop = function stopGroup () {
  // TODO
  return this;
};

<|endoftext|>


<commit_before>'use strict';

/**
 * Module that contains some useful helper functions.
 */

const Util = {
	youtubeTitleRegExps: [
		// Artist "Track", Artist: "Track", Artist - "Track", etc.
		{
			pattern: /(.+?)(\s|-|—|:)+\s*"(.+?)"/,
			groups: { artist: 1, track: 3 }
		},
		// Artist「Track」 (Japanese tracks)
		{
			pattern: /(.+?)「(.+?)」/,
			groups: { artist: 1, track: 2 }
		},
		// Track (... by Artist)
		{
			pattern: /([\w\d][\w\d\s]*?)\s+\([^)]*\s*by\s*([^)]+)+\)/,
			groups: { artist: 2, track: 1 }
		}
	],

	/**
	 * Extract artist and track from Youtube video title.
	 * @param  {String} videoTitle Youtube video title
	 * @param  {Array} separators Array of separators
	 * @return {Object} Object contains artist and track fields
	 */
	processYoutubeVideoTitle(videoTitle) {
		if (!videoTitle) {
			return this.makeEmptyArtistTrack();
		}

		// Remove [genre] or 【genre】 from the beginning of the title
		let title = videoTitle.replace(/^((\[[^\]]+\])|(【[^】]+】))\s*-*\s*/i, '');

		let [artist, track] = [null, null];

		// Try to match one of the regexps
		for (let regExp of Util.youtubeTitleRegExps) {
			let artistTrack = title.match(regExp.pattern);
			if (artistTrack) {
				artist = artistTrack[regExp.groups.artist];
				track = artistTrack[regExp.groups.track];
				break;
			}
		}

		// No match? Try splitting, then.
		if (artist === null && track === null) {
			({ artist, track } = this.splitArtistTrack(title));
		}

		return { artist, track };
	},

	/**
	 * Parse given video URL and return video ID.
	 * @param  {String} videoUrl Video URL
	 * @return {String} Video ID
	 */
	getYoutubeVideoIdFromUrl(videoUrl) {
		if (!videoUrl) {
			return null;
		}

		let match = videoUrl.match(Util.videoIdRegExp);
		if (match) {
			return match[7];
		}

		return null;
	},

	/**
	 * Normalize given URL. Currently it only normalizes
	 * protocol-relative links.
	 * @param  {String} url URL, which is possibly protocol-relative
	 * @return {String} Normalized URL
	 */
	normalizeUrl(url) {
		if (!url) {
			return null;
		}

		return url.startsWith('//') ? location.protocol + url : url;
	},

	/**
	 * Convert given time-string into seconds.
	 * @param  {String} str Time-string in h:m:s format
	 * @return {Number} Seconds
	 */
	stringToSeconds(str) {
		if (!str) {
			return 0;
		}

		let s = str.toString().trim();
		let val = 0;
		let seconds = 0;

		let isNegative = s.startsWith('-');
		if (isNegative) {
			s = s.substr(1);
		}

		for (let i = 0; i < 3; i++) {
			let idx = s.lastIndexOf(':');
			if (idx > -1) {
				val = parseInt(s.substr(idx + 1), 10);
				seconds += val * Math.pow(60, i);
				s = s.substr(0, idx);
			} else {
				val = parseInt(s, 10);
				seconds += val * Math.pow(60, i);
				break;
			}
		}

		if (isNegative) {
			seconds = -seconds;
		}

		return seconds || 0;
	},

	/**
	 * Find first occurence of possible separator in given string
	 * and return separator's position and size in chars or null.
	 * @param  {String} str String contains separator
	 * @param  {Array} separators Array of separators
	 * @return {Object} Object contains position and width of separator
	 */
	findSeparator(str, separators = null) {
		if (str === null || str.length === 0) {
			return null;
		}

		if (!separators) {
			separators = this.separators;
		}

		for (let sep of separators) {
			let index = str.indexOf(sep);
			if (index > -1) {
				return { index, length: sep.length };
			}
		}

		return null;
	},

	/**
	 * Join array of artist name into a string. The array must contain objects
	 * that have 'textContent' property (DOM node).
	 * @param  {Array} artists Array of DOM nodes
	 * @return {String} String joined by separator
	 */
	joinArtists(artists) {
		return artists.map((artist) => {
			return artist.textContent;
		}).join(this.ARTIST_SEPARATOR);
	},

	/**
	 * Split string to artist and track.
	 * @param  {String} str String contains artist and track
	 * @param  {Array} separators Array of separators
	 * @param  {Boolean} swap Swap artist and track values
	 * @return {Object} Object contains artist and track fields
	 */
	splitArtistTrack(str, separators = null, swap = false) {
		let [artist, track] = this.splitString(str, separators, swap);
		return { artist, track };
	},

	/**
	 * Split string to current time and duration.
	 * @param  {String} str String contains current time and duration
	 * @param  {String} sep Separator
	 * @param  {Boolean} swap Swap currentTime and duration values
	 * @return {Object} Array ontains 'currentTime' and 'duration' fields
	 */
	splitTimeInfo(str, sep = '/', swap = false) {
		let [currentTime, duration] = this.splitString(str, [sep], swap);
		if (currentTime) {
			currentTime = this.stringToSeconds(currentTime);
		}
		if (duration) {
			duration = this.stringToSeconds(duration);
		}

		return { currentTime, duration };
	},

	/**
	 * Split string to two ones using array of separators.
	 * @param  {String} str Any string
	 * @param  {Array} separators Array of separators
	 * @param  {Boolean} swap Swap values
	 * @return {Array} Array of strings splitted by separator
	 */
	splitString(str, separators = null, swap = false) {
		let first = null;
		let second = null;

		if (str) {
			let separator = this.findSeparator(str, separators);

			if (separator !== null) {
				first = str.substr(0, separator.index);
				second = str.substr(separator.index + separator.length);

				if (swap) {
					[second, first] = [first, second];
				}
			}
		}

		return [first, second];
	},

	/**
	 * Verify time value and return time as a Number object.
	 * Return null value if time value is not a number.
	 * @param  {Any} time Time value
	 * @return {Number} time value as a Number object
	 */
	escapeBadTimeValues(time) {
		if (typeof time !== 'number') {
			return null;
		}
		if (isNaN(time) || !isFinite(time)) {
			return null;
		}
		return Math.round(time);
	},

	/**
	 * Extract track art URL from CSS property.
	 * @param  {String} cssProperty CSS property
	 * @return {String} Track art URL
	 */
	extractUrlFromCssProperty(cssProperty) {
		let match = /url\((['"]?)(.*)\1\)/.exec(cssProperty);
		if (match) {
			return match[2].trim();
		}
		return null;
	},

	/**
	 * Inject script into document.
	 * @param {String} scriptUrl script URL
	 */
	injectScriptIntoDocument(scriptUrl) {
		let script = document.createElement('script');
		script.src = scriptUrl;
		script.onload = function() {
			this.parentNode.removeChild(this);
		};
		(document.head || document.documentElement).appendChild(script);
	},

	/**
	 * Returns a function, that, when invoked, will only be triggered
	 * at most once during a given window of time.
	 *
	 * Taken from Underscore library.
	 *
	 * @param  {Function} func Function to be throttled
	 * @param  {Number} wait Time before function calls
	 * @param  {Object} options Options
	 * @return {Function} Throttled function
	 */
	throttle(func, wait, options = {}) {
		let context;
		let args;
		let result;
		let timeout = null;
		let previous = 0;

		function later() {
			previous = options.leading === false ? 0 : Date.now();
			timeout = null;
			result = func.apply(context, args);
			if (!timeout) {
				context = args = null;
			}
		}
		return function() {
			let now = Date.now();
			if (!previous && options.leading === false) {
				previous = now;
			}
			let remaining = wait - (now - previous);
			context = this;
			args = arguments;
			if (remaining <= 0 || remaining > wait) {
				if (timeout) {
					clearTimeout(timeout);
					timeout = null;
				}
				previous = now;
				result = func.apply(context, args);
				if (!timeout) {
					context = args = null;
				}
			} else if (!timeout && options.trailing !== false) {
				timeout = setTimeout(later, remaining);
			}
			return result;
		};
	},

	/**
	 * Check if given 'artistTrack' object is empty. The object means empty
	 * if its 'artist' and 'track' properties are undefined, null or empty.
	 * @param  {Object}  artistTrack Object contains artist and track info
	 * @return {Boolean} True if object is empty; false otherwise
	 */
	isArtistTrackEmpty(artistTrack) {
		return !(artistTrack.artist && artistTrack.track);
	},

	/**
	 * Default array of separators used to split artist and track info.
	 * Push new separators in the implementation if required.
	 *
	 * @type {Array}
	 */
	separators: [
		' -- ', '--', ' ~ ', ' - ', ' – ', ' — ',
		' // ', '-', '–', '—', ':', '|', '///', '/'
	],

	/**
	 * Create an object that contains no artist and track info.
	 * @return {Object} Object contains no artist and track info
	 */
	makeEmptyArtistTrack() {
		return { artist: null, track: null };
	},

	/**
	 * Read connector option from storage.
	 * @param  {String} connector Connector name
	 * @param  {String} key Option key
	 * @return {Any} Option value
	 */
	getOption(connector, key) {
		return new Promise((resolve) => {
			chrome.storage.sync.get('Connectors', (data) => {
				if (data && data.Connectors && data.Connectors[connector]) {
					resolve(data.Connectors[connector][key]);
				}
			});
		});
	},

	/**
	 * Regular expression used to get Youtube video ID from URL. It covers
	 * default, shortened and embed URLs.
	 * @type {RegExp}
	 */
	videoIdRegExp: /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?.*v=))([^#&?]*).*/,

	/**
	 * Separator used to join array of artist names into a single string.
	 * @type {String}
	 */
	ARTIST_SEPARATOR: ', ',
};

/**
 * Export Util object if script is executed in Node.js context.
 */
if (typeof module !== 'undefined') {
	module.exports = Util;
}
<commit_msg>Return `undefined` if no option in storage<commit_after>'use strict';

/**
 * Module that contains some useful helper functions.
 */

const Util = {
	youtubeTitleRegExps: [
		// Artist "Track", Artist: "Track", Artist - "Track", etc.
		{
			pattern: /(.+?)(\s|-|—|:)+\s*"(.+?)"/,
			groups: { artist: 1, track: 3 }
		},
		// Artist「Track」 (Japanese tracks)
		{
			pattern: /(.+?)「(.+?)」/,
			groups: { artist: 1, track: 2 }
		},
		// Track (... by Artist)
		{
			pattern: /([\w\d][\w\d\s]*?)\s+\([^)]*\s*by\s*([^)]+)+\)/,
			groups: { artist: 2, track: 1 }
		}
	],

	/**
	 * Extract artist and track from Youtube video title.
	 * @param  {String} videoTitle Youtube video title
	 * @param  {Array} separators Array of separators
	 * @return {Object} Object contains artist and track fields
	 */
	processYoutubeVideoTitle(videoTitle) {
		if (!videoTitle) {
			return this.makeEmptyArtistTrack();
		}

		// Remove [genre] or 【genre】 from the beginning of the title
		let title = videoTitle.replace(/^((\[[^\]]+\])|(【[^】]+】))\s*-*\s*/i, '');

		let [artist, track] = [null, null];

		// Try to match one of the regexps
		for (let regExp of Util.youtubeTitleRegExps) {
			let artistTrack = title.match(regExp.pattern);
			if (artistTrack) {
				artist = artistTrack[regExp.groups.artist];
				track = artistTrack[regExp.groups.track];
				break;
			}
		}

		// No match? Try splitting, then.
		if (artist === null && track === null) {
			({ artist, track } = this.splitArtistTrack(title));
		}

		return { artist, track };
	},

	/**
	 * Parse given video URL and return video ID.
	 * @param  {String} videoUrl Video URL
	 * @return {String} Video ID
	 */
	getYoutubeVideoIdFromUrl(videoUrl) {
		if (!videoUrl) {
			return null;
		}

		let match = videoUrl.match(Util.videoIdRegExp);
		if (match) {
			return match[7];
		}

		return null;
	},

	/**
	 * Normalize given URL. Currently it only normalizes
	 * protocol-relative links.
	 * @param  {String} url URL, which is possibly protocol-relative
	 * @return {String} Normalized URL
	 */
	normalizeUrl(url) {
		if (!url) {
			return null;
		}

		return url.startsWith('//') ? location.protocol + url : url;
	},

	/**
	 * Convert given time-string into seconds.
	 * @param  {String} str Time-string in h:m:s format
	 * @return {Number} Seconds
	 */
	stringToSeconds(str) {
		if (!str) {
			return 0;
		}

		let s = str.toString().trim();
		let val = 0;
		let seconds = 0;

		let isNegative = s.startsWith('-');
		if (isNegative) {
			s = s.substr(1);
		}

		for (let i = 0; i < 3; i++) {
			let idx = s.lastIndexOf(':');
			if (idx > -1) {
				val = parseInt(s.substr(idx + 1), 10);
				seconds += val * Math.pow(60, i);
				s = s.substr(0, idx);
			} else {
				val = parseInt(s, 10);
				seconds += val * Math.pow(60, i);
				break;
			}
		}

		if (isNegative) {
			seconds = -seconds;
		}

		return seconds || 0;
	},

	/**
	 * Find first occurence of possible separator in given string
	 * and return separator's position and size in chars or null.
	 * @param  {String} str String contains separator
	 * @param  {Array} separators Array of separators
	 * @return {Object} Object contains position and width of separator
	 */
	findSeparator(str, separators = null) {
		if (str === null || str.length === 0) {
			return null;
		}

		if (!separators) {
			separators = this.separators;
		}

		for (let sep of separators) {
			let index = str.indexOf(sep);
			if (index > -1) {
				return { index, length: sep.length };
			}
		}

		return null;
	},

	/**
	 * Join array of artist name into a string. The array must contain objects
	 * that have 'textContent' property (DOM node).
	 * @param  {Array} artists Array of DOM nodes
	 * @return {String} String joined by separator
	 */
	joinArtists(artists) {
		return artists.map((artist) => {
			return artist.textContent;
		}).join(this.ARTIST_SEPARATOR);
	},

	/**
	 * Split string to artist and track.
	 * @param  {String} str String contains artist and track
	 * @param  {Array} separators Array of separators
	 * @param  {Boolean} swap Swap artist and track values
	 * @return {Object} Object contains artist and track fields
	 */
	splitArtistTrack(str, separators = null, swap = false) {
		let [artist, track] = this.splitString(str, separators, swap);
		return { artist, track };
	},

	/**
	 * Split string to current time and duration.
	 * @param  {String} str String contains current time and duration
	 * @param  {String} sep Separator
	 * @param  {Boolean} swap Swap currentTime and duration values
	 * @return {Object} Array ontains 'currentTime' and 'duration' fields
	 */
	splitTimeInfo(str, sep = '/', swap = false) {
		let [currentTime, duration] = this.splitString(str, [sep], swap);
		if (currentTime) {
			currentTime = this.stringToSeconds(currentTime);
		}
		if (duration) {
			duration = this.stringToSeconds(duration);
		}

		return { currentTime, duration };
	},

	/**
	 * Split string to two ones using array of separators.
	 * @param  {String} str Any string
	 * @param  {Array} separators Array of separators
	 * @param  {Boolean} swap Swap values
	 * @return {Array} Array of strings splitted by separator
	 */
	splitString(str, separators = null, swap = false) {
		let first = null;
		let second = null;

		if (str) {
			let separator = this.findSeparator(str, separators);

			if (separator !== null) {
				first = str.substr(0, separator.index);
				second = str.substr(separator.index + separator.length);

				if (swap) {
					[second, first] = [first, second];
				}
			}
		}

		return [first, second];
	},

	/**
	 * Verify time value and return time as a Number object.
	 * Return null value if time value is not a number.
	 * @param  {Any} time Time value
	 * @return {Number} time value as a Number object
	 */
	escapeBadTimeValues(time) {
		if (typeof time !== 'number') {
			return null;
		}
		if (isNaN(time) || !isFinite(time)) {
			return null;
		}
		return Math.round(time);
	},

	/**
	 * Extract track art URL from CSS property.
	 * @param  {String} cssProperty CSS property
	 * @return {String} Track art URL
	 */
	extractUrlFromCssProperty(cssProperty) {
		let match = /url\((['"]?)(.*)\1\)/.exec(cssProperty);
		if (match) {
			return match[2].trim();
		}
		return null;
	},

	/**
	 * Inject script into document.
	 * @param {String} scriptUrl script URL
	 */
	injectScriptIntoDocument(scriptUrl) {
		let script = document.createElement('script');
		script.src = scriptUrl;
		script.onload = function() {
			this.parentNode.removeChild(this);
		};
		(document.head || document.documentElement).appendChild(script);
	},

	/**
	 * Returns a function, that, when invoked, will only be triggered
	 * at most once during a given window of time.
	 *
	 * Taken from Underscore library.
	 *
	 * @param  {Function} func Function to be throttled
	 * @param  {Number} wait Time before function calls
	 * @param  {Object} options Options
	 * @return {Function} Throttled function
	 */
	throttle(func, wait, options = {}) {
		let context;
		let args;
		let result;
		let timeout = null;
		let previous = 0;

		function later() {
			previous = options.leading === false ? 0 : Date.now();
			timeout = null;
			result = func.apply(context, args);
			if (!timeout) {
				context = args = null;
			}
		}
		return function() {
			let now = Date.now();
			if (!previous && options.leading === false) {
				previous = now;
			}
			let remaining = wait - (now - previous);
			context = this;
			args = arguments;
			if (remaining <= 0 || remaining > wait) {
				if (timeout) {
					clearTimeout(timeout);
					timeout = null;
				}
				previous = now;
				result = func.apply(context, args);
				if (!timeout) {
					context = args = null;
				}
			} else if (!timeout && options.trailing !== false) {
				timeout = setTimeout(later, remaining);
			}
			return result;
		};
	},

	/**
	 * Check if given 'artistTrack' object is empty. The object means empty
	 * if its 'artist' and 'track' properties are undefined, null or empty.
	 * @param  {Object}  artistTrack Object contains artist and track info
	 * @return {Boolean} True if object is empty; false otherwise
	 */
	isArtistTrackEmpty(artistTrack) {
		return !(artistTrack.artist && artistTrack.track);
	},

	/**
	 * Default array of separators used to split artist and track info.
	 * Push new separators in the implementation if required.
	 *
	 * @type {Array}
	 */
	separators: [
		' -- ', '--', ' ~ ', ' - ', ' – ', ' — ',
		' // ', '-', '–', '—', ':', '|', '///', '/'
	],

	/**
	 * Create an object that contains no artist and track info.
	 * @return {Object} Object contains no artist and track info
	 */
	makeEmptyArtistTrack() {
		return { artist: null, track: null };
	},

	/**
	 * Read connector option from storage.
	 * @param  {String} connector Connector name
	 * @param  {String} key Option key
	 * @return {Any} Option value
	 */
	getOption(connector, key) {
		return new Promise((resolve) => {
			chrome.storage.sync.get('Connectors', (data) => {
				if (data && data.Connectors && data.Connectors[connector]) {
					resolve(data.Connectors[connector][key]);
				} else {
					resolve(undefined);
				}
			});
		});
	},

	/**
	 * Regular expression used to get Youtube video ID from URL. It covers
	 * default, shortened and embed URLs.
	 * @type {RegExp}
	 */
	videoIdRegExp: /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?.*v=))([^#&?]*).*/,

	/**
	 * Separator used to join array of artist names into a single string.
	 * @type {String}
	 */
	ARTIST_SEPARATOR: ', ',
};

/**
 * Export Util object if script is executed in Node.js context.
 */
if (typeof module !== 'undefined') {
	module.exports = Util;
}
<|endoftext|>