<commit_before>'use babel';
import {Disposable, CompositeDisposable} from 'atom'
var BlockCursor;

// helper functions

// get the key in namespace
function getConfig(key, namespace = 'block-cursor') {
  return atom.config.get(`${namespace}.${key}`);
}

// set the key in namespace
function setConfig(key, value, namespace = 'block-cursor') {
  return atom.config.set(`${namespace}.${key}`, value);
}

// get a clone of the global config
function getGlobalConfig() {
  var config = assign({}, getConfig('global'));
  assign(config.blinkOn, getConfig('global.blinkOn'));
  assign(config.blinkOff, getConfig('global.blinkOff'));
  return config
}

// set the (own) properties of objs on target
function assign(target, ...objs) {
  for(let obj of objs) {
    for(let key of Object.keys(obj)) {
      target[key] = obj[key];
    }
  }
  return target;
}

// convert a color to a string
function toRGBAString(color) {
  if(typeof color == 'string') return color;
  if(typeof color.toRGBAString == 'function') return color.toRGBAString();
  return `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`;
}

// private API

// keep a reference to the stylesheet
var style;

// create a stylesheet element and
// attach it to the DOM
function setupStylesheet() {
  style = document.createElement('style');
  style.type = 'text/css';
  document.querySelector('head atom-styles').appendChild(style);

  // return a disposable for easy removal :)
  return new Disposable(function() {
    style.parentNode.removeChild(style);
    style = null;
  });
}

// update the stylesheet when config changes
function updateCursorStyles(config) {
  // clear stylesheet
  style.innerHTML = '';
  for(let key of Object.keys(config)) {
    // and add styles for each cursor style
    updateCursorStyle(config[key]);
  }
}

// update the style for a single cursor style
function updateCursorStyle(cursorStyle) {
  // fill the cursor style with global as defaults
  cursorStyle = assign(getGlobalConfig(), cursorStyle);
  var blinkOn = assign({}, getConfig('global.blinkOn'), cursorStyle.blinkOn);
  var blinkOff = assign({}, getConfig('global.blinkOff'), cursorStyle.blinkOff);
  var {selector, scopes, pulseDuration, cursorLineFix} = cursorStyle;

  // if cursor blinking is off, set the secondaryColor the same
  // as primaryColor to prevent cursor blinking in [mini] editors
  if(atom.packages.isPackageActive('cursor-blink-interval') &&
    getConfig('cursorBlinkInterval', 'cursor-blink-interval') == 0)
    blinkOff = assign({}, blinkOn);

  // blink on rule
  blinkOn.selector = selectorForScopes(selector, scopes);
  blinkOn.properties = {
    // blink on background color
    backgroundColor: toRGBAString(blinkOn.backgroundColor),
    // pulse animation duration
    transitionDuration: pulseDuration,
    // cursor line fix
    zIndex: cursorLineFix ? 1 : -1
  };
  // blink on border
  assign(blinkOn.properties, createBorderStyle(blinkOn));
  // end blink on rule

  // blink off rule
  blinkOff.selector = selectorForScopes(selector, scopes, true);
  blinkOff.properties = {};
  if(blinkOff.backgroundColor.alpha == 0 && (blinkOff.borderWidth == 0 ||
    blinkOff.borderStyle == 'none' || blinkOff.borderColor.alpha == 0))
    // better animation performance by animating opacity
    // if blink off cursor is invisible
    blinkOff.properties.opacity = 0;
  else
    // blink off background color
    blinkOff.properties.backgroundColor = toRGBAString(blinkOff.backgroundColor);
    // blink off border
    assign(blinkOff.properties, createBorderStyle(blinkOff));
  // end blink off rule

  style.innerHTML += createCSSRule(blinkOn) + createCSSRule(blinkOff);
}

// create a css properties object for given border properties
function createBorderStyle({borderWidth, borderStyle, borderColor}) {
  var borderString = `${borderWidth}px solid ${toRGBAString(borderColor)}`;
  switch(borderStyle) {
    case 'bordered-box':
      // border on all sides
      return {border: borderString};
    case 'i-beam':
      // border on left side
      return {border: 'none', borderLeft: borderString}
    case 'underline':
      // border on bottom side
      return {border: 'none', borderBottom: borderString};
    default:
      // no border
      return {border: 'none'};
  }
}

// create a css rule from a selector and an
// object containint propertyNames and values
// of the form
// <selector> {
//    <propertyName1>: <value1>;
//    <propertyName2>: <value2>;
//    ...
// }
function createCSSRule({selector, properties}) {
  var cssRule = `${selector} {\n`;
  for(let key of Object.keys(properties)) {
    let cssKey = key.replace(/([A-Z])/g, (match) => `-${match.toLowerCase()}`);
    cssRule += `${cssKey}: ${properties[key]};\n`;
  }
  cssRule += '}\n';
  return cssRule;
}

// creates a css selector for the given scopes
// @param base: selector that selects the atom-text-editor element
// @param scopes: array of scopes to select
// @param blinkOff: create a blink-off selector?
function selectorForScopes(base, scopes, blinkOff = false) {
  var selectors = [];
  for(let scope of scopes) {
    let selector = base;
    if(scope != '*') selector += scope.split('.').map(function mapper(scope) {
      return scope ? `[data-grammar~="${scope}"]` : '';
    }).join('');
    selector += '::shadow .cursors';
    if(blinkOff) selector += '.blink-off';
    selector += ' .cursor';
    selectors.push(selector);
  }
  return selectors.join(',\n');
}

// add a custom cursor to the config. an easy
// shortcut when you want to define a new cursor type
function addCustomCursor() {
  var i = 0;
  while(getConfig(`custom-${i}`)) i++;
  setConfig(`custom-${i}`, getGlobalConfig());
}

// public API

// module.exports = BlockCursor = {
const config = {
  global: {
    type: 'object',
    properties: {
      scopes: {
        type: 'array',
        default: [ '*' ]
      },
      selector: {
        type: 'string',
        default: 'atom-text-editor'
      },
      blinkOn: {
        type: 'object',
        properties: {
          backgroundColor: {
            type: 'color',
            default: '#393939'
          },
          borderWidth: {
            type: 'integer',
            default: 1,
            minimum: 0
          },
          borderStyle: {
            type: 'string',
            default: 'none',
            enum: [ 'none', 'bordered-box', 'i-beam', 'underline' ]
          },
          borderColor: {
            type: 'color',
            default: 'transparent'
          }
        }
      },
      blinkOff: {
        type: 'object',
        properties: {
          backgroundColor: {
            type: 'color',
            default: 'transparent'
          },
          borderWidth: {
            type: 'integer',
            default: 1,
            minimum: 0
          },
          borderStyle: {
            type: 'string',
            default: 'none',
            enum: [ 'none', 'bordered-box', 'i-beam', 'underline' ]
          },
          borderColor: {
            type: 'color',
            default: 'transparent'
          }
        }
      },
      pulseDuration: {
        type: 'integer',
        default: 0,
        minimum: 0
      },
      cursorLineFix: {
        type: 'boolean',
        default: false
      }
    }
  }
};

var disposables;

function activate() {
  disposables = new CompositeDisposable();
  // wait for cursor-blink-interval package to activate
  // if it is loaded
  Promise.resolve(
    atom.packages.isPackageLoaded('cursor-blink-interval') &&
      atom.packages.activatePackage('cursor-blink-interval')
  ).then(function go() {
    disposables.add([
      setupStylesheet(),
      atom.config.observe('block-cursor', updateCursorStyles),
      atom.commands.add('atom-workspace', 'block-cursor:new-custom-cursor', addCustomCursor)
    ]);
  }).catch(function error(error) {
    console.error(error.message);
  });
}

function deactivate() {
  disposables.dispose();
  disposables = null;
}

export {config, activate, deactivate};
<commit_msg>temporary workaround for #20<commit_after>'use babel';
import {Disposable, CompositeDisposable} from 'atom'
var BlockCursor;

// helper functions

// get the key in namespace
function getConfig(key, namespace = 'block-cursor') {
  return atom.config.get(key ? `${namespace}.${key}` : namespace);
}

// set the key in namespace
function setConfig(key, value, namespace = 'block-cursor') {
  return atom.config.set(`${namespace}.${key}`, value);
}

// get a clone of the global config
function getGlobalConfig() {
  var config = assign({}, getConfig('global'));
  assign(config.blinkOn, getConfig('global.blinkOn'));
  assign(config.blinkOff, getConfig('global.blinkOff'));
  return config
}

// set the (own) properties of objs on target
function assign(target, ...objs) {
  for(let obj of objs) {
    for(let key of Object.keys(obj)) {
      target[key] = obj[key];
    }
  }
  return target;
}

// convert a color to a string
function toRGBAString(color) {
  if(typeof color == 'string') return color;
  if(typeof color.toRGBAString == 'function') return color.toRGBAString();
  return `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`;
}

// private API

// keep a reference to the stylesheet
var style;

// create a stylesheet element and
// attach it to the DOM
function setupStylesheet() {
  style = document.createElement('style');
  style.type = 'text/css';
  document.querySelector('head atom-styles').appendChild(style);

  // return a disposable for easy removal :)
  return new Disposable(function() {
    style.parentNode.removeChild(style);
    style = null;
  });
}

// update the stylesheet when config changes
function updateCursorStyles(config) {
  // clear stylesheet
  style.innerHTML = '';
  for(let key of Object.keys(config)) {
    // and add styles for each cursor style
    style.innerHTML += cssForCursorStyle(config[key]);
  }
  // console.log(style.innerHTML);
}

function cssForCursorStyle(cursorStyle) {
  // fill the cursor style with global as defaults
  cursorStyle = assign(getGlobalConfig(), cursorStyle);
  var blinkOn = assign({}, getConfig('global.blinkOn'), cursorStyle.blinkOn);
  var blinkOff = assign({}, getConfig('global.blinkOff'), cursorStyle.blinkOff);
  var {selector, scopes, pulseDuration, cursorLineFix} = cursorStyle;

  // if cursor blinking is off, set the secondaryColor the same
  // as primaryColor to prevent cursor blinking in [mini] editors
  if(atom.packages.isPackageActive('cursor-blink-interval') &&
    getConfig('cursorBlinkInterval', 'cursor-blink-interval') == 0)
    blinkOff = assign({}, blinkOn);

  // blink on rule
  blinkOn.selector = selectorForScopes(selector, scopes);
  blinkOn.properties = {
    // blink on background color
    backgroundColor: toRGBAString(blinkOn.backgroundColor),
    // pulse animation duration
    transitionDuration: pulseDuration,
    // cursor line fix
    // zIndex: cursorLineFix ? 1 : -1 // @TODO: enable this when a solution is found for #20
  };
  // blink on border
  assign(blinkOn.properties, createBorderStyle(blinkOn));
  // end blink on rule

  // blink off rule
  blinkOff.selector = selectorForScopes(selector, scopes, true);
  blinkOff.properties = {};
  if(blinkOff.backgroundColor.alpha == 0 && (blinkOff.borderWidth == 0 ||
    blinkOff.borderStyle == 'none' || blinkOff.borderColor.alpha == 0))
    // better animation performance by animating opacity
    // if blink off cursor is invisible
    blinkOff.properties.opacity = 0;
  else
    // blink off background color
    blinkOff.properties.backgroundColor = toRGBAString(blinkOff.backgroundColor);
    // blink off border
    assign(blinkOff.properties, createBorderStyle(blinkOff));
  // end blink off rule

  return createCSSRule(blinkOn) + createCSSRule(blinkOff);
}

// create a css properties object for given border properties
function createBorderStyle({borderWidth, borderStyle, borderColor}) {
  var borderString = `${borderWidth}px solid ${toRGBAString(borderColor)}`;
  switch(borderStyle) {
    case 'bordered-box':
      // border on all sides
      return {border: borderString};
    case 'i-beam':
      // border on left side
      return {border: 'none', borderLeft: borderString}
    case 'underline':
      // border on bottom side
      return {border: 'none', borderBottom: borderString};
    default:
      // no border
      return {border: 'none'};
  }
}

// create a css rule from a selector and an
// object containint propertyNames and values
// of the form
// <selector> {
//    <propertyName1>: <value1>;
//    <propertyName2>: <value2>;
//    ...
// }
function createCSSRule({selector, properties}) {
  var cssRule = `${selector} {\n`;
  for(let key of Object.keys(properties)) {
    let cssKey = key.replace(/([A-Z])/g, (match) => `-${match.toLowerCase()}`);
    cssRule += `\t${cssKey}: ${properties[key]};\n`;
  }
  cssRule += '}\n';
  return cssRule;
}

// creates a css selector for the given scopes
// @param base: selector that selects the atom-text-editor element
// @param scopes: array of scopes to select
// @param blinkOff: create a blink-off selector?
function selectorForScopes(base, scopes, blinkOff = false) {
  var selectors = [];
  for(let scope of scopes) {
    let selector = base;
    if(scope != '*') selector += scope.split('.').map(function mapper(scope) {
      return scope ? `[data-grammar~="${scope}"]` : '';
    }).join('');
    selector += '::shadow .cursors';
    if(blinkOff) selector += '.blink-off';
    selector += ' .cursor';
    selectors.push(selector);
  }
  return selectors.join(',\n');
}

// add a custom cursor to the config. an easy
// shortcut when you want to define a new cursor type
function addCustomCursor() {
  var i = 0;
  while(getConfig(`custom-${i}`)) i++;
  setConfig(`custom-${i}`, getGlobalConfig());
}

// public API

// module.exports = BlockCursor = {
const config = {
  global: {
    type: 'object',
    properties: {
      scopes: {
        type: 'array',
        default: [ '*' ]
      },
      selector: {
        type: 'string',
        default: 'atom-text-editor'
      },
      blinkOn: {
        type: 'object',
        properties: {
          backgroundColor: {
            type: 'color',
            default: '#393939'
          },
          borderWidth: {
            type: 'integer',
            default: 1,
            minimum: 0
          },
          borderStyle: {
            type: 'string',
            default: 'none',
            enum: [ 'none', 'bordered-box', 'i-beam', 'underline' ]
          },
          borderColor: {
            type: 'color',
            default: 'transparent'
          }
        }
      },
      blinkOff: {
        type: 'object',
        properties: {
          backgroundColor: {
            type: 'color',
            default: 'transparent'
          },
          borderWidth: {
            type: 'integer',
            default: 1,
            minimum: 0
          },
          borderStyle: {
            type: 'string',
            default: 'none',
            enum: [ 'none', 'bordered-box', 'i-beam', 'underline' ]
          },
          borderColor: {
            type: 'color',
            default: 'transparent'
          }
        }
      },
      pulseDuration: {
        type: 'integer',
        default: 0,
        minimum: 0
      },
      cursorLineFix: {
        description: 'Temporarily ignored (always true) because of an issue with the tile rendering introduced in Atom 0.209.0.',
        type: 'boolean',
        default: false
      }
    }
  }
};

var disposables;

function activate() {
  // wait for cursor-blink-interval package to activate
  // if it is loaded
  Promise.resolve(
    atom.packages.isPackageLoaded('cursor-blink-interval') &&
      atom.packages.activatePackage('cursor-blink-interval')
  ).then(function go() {
    disposables = new CompositeDisposable(
      setupStylesheet(),
      atom.config.observe('block-cursor', updateCursorStyles),
      atom.commands.add('atom-workspace', 'block-cursor:new-custom-cursor', addCustomCursor)
    );
  }).catch(function error(error) {
    console.error(error.message);
  });
}

function deactivate() {
  disposables.dispose();
  disposables = null;
}

export {config, activate, deactivate};
<|endoftext|>


<commit_before>'use strict';

starChat.View = (function () {
    var View = function (sessionClass) {
        this.sessionClass_ = sessionClass;

        initialize(this);
    };
    function initialize(self) {
        self.session_ = new self.sessionClass_();

        // TODO: Model に相当するクラスを作る?
        // TODO: いずれこれらの変数も private (_ 終わり) にする
        self.channels = [];
        self.channelName = '';
        self.messageScrollTops = {};
        self.userNames = {};
        self.isEdittingChannels = false;

        self.lastChannelName_ = '';
        self.newMessages_ = {};
        self.messageIdsAlreadyInSection_ = {};
        self.dirtyFlags_ = {};
        self.startTime_ = null;
        self.endTime_ = null;
        self.oldMessages_ = {};
        self.isBlinkingTitle_ = false;

        self.title_ = 'StarChat (β)';
        document.title = self.title_;
        stopBlinkingTitle(self);
    }
    function startBlinkingTitle(self, anotherTitle) {
        if (self.isBlinkingTitle_) {
            return;
        }
        self.isBlinkingTitle_ = true;
        function loop (i) {
            if (!self.isBlinkingTitle_) {
                stopBlinkingTitle(self);
                return;
            }
            if (starChat.isFocused()) {
                stopBlinkingTitle(self);
                return;
            }
            document.title = {
                0: self.title_,
                1: anotherTitle,
            }[i];
            setTimeout(function () {
                loop(1 - i);
            }, 1000);
        }
        loop(0);
    }
    function stopBlinkingTitle(self) {
        self.isBlinkingTitle_ = false;
        document.title = self.title_;
    }
    var updateViewChannels = (function () {
        var lastSessionId = 0;
        return function (self) {
            var channels = self.channels.sort(function (a, b) {
                if (a.name > b.name) {
                    return 1;
                }
                if (a.name < b.name) {
                    return -1;
                }
                return 0;
            });
            if (self.channelName) {
                self.dirtyFlags_[self.channelName] = false;
            }
            (function () {
                var ul = $('#channels ul');
                ul.find('li').filter(function (i) {
                    var channelName = $(this).attr('data-channel-name');
                    return channels.every(function (channel) {
                        return channel.name !== channelName;
                    });
                }).remove();
                var existChannelNames = $.map(ul.find('li'), function (e) {
                    return $(e).attr('data-channel-name');
                });
                var newChannels = channels.filter(function (channel) {
                    return existChannelNames.every(function (name) {
                        return name !== channel.name;
                    });
                });
                // TODO: sort
                newChannels.forEach(function (channel) {
                    var a = $('');
                    var name = channel.name;
                    var href = '#channels/' + encodeURIComponent(channel.name);
                    a.attr('href', href);
                    a.text(name);
                    var icon = $('').click(function () {
                        return self.clickChannelDel_(channel);
                    });
                    var li = $('

').attr('data-channel-name', channel.name);
                    li.append(a).append(icon);
                    ul.append(li);
                });
                ul.find('li').each(function () {
                    var e = $(this);
                    var channelName = e.attr('data-channel-name');
                    e.find('a').toggleClass('dirty', self.dirtyFlags_[channelName] === true);
                });
                lastSessionId = self.session_.id();
            })();
            if (self.isEdittingChannels) {
                $('#channels li img[data-tool-id="delete"]').show().css('display', 'inline');
            } else {
                $('#channels li img[data-tool-id="delete"]').hide();
            }
        }
    })();
    function getSectionElement(self) {
        if (!self.channelName) {
            return null;
        }
        var sections = $('#messages > section').filter(function (i) {
            return $(this).attr('data-channel-name') === self.channelName &&
                (!self.isShowingOldLogs() &&
                 !$(this).attr('data-start-time') &&
                 !$(this).attr('data-end-time')) ||
                (self.isShowingOldLogs() &&
                 parseInt($(this).attr('data-start-time')) === self.startTime_ &&
                 parseInt($(this).attr('data-end-time'))   === self.endTime_);
        });
        if (sections.length === 1) {
            var section = sections;
            section.find('input[name="year"]').val('');
            section.find('input[name="month"]').val('');
            section.find('input[name="day"]').val('');
            return section;
        }
        if (2 <= sections.length) {
            throw 'invalid sections';
        }
        var section = $('

');
        var channelName = self.channelName;
        section.attr('data-channel-name', channelName);
        if (self.isShowingOldLogs()) {
            section.attr('data-start-time', self.startTime_);
            section.attr('data-end-time',   self.endTime_);
        }
        if (!self.isShowingOldLogs()) {
            section.scroll(function () {
                self.messageScrollTops[channelName] = section.scrollTop();
            });
        }
        var inputYear   = $('<input type="number" name="year" min="0" max="9999" value="" />');
        var inputMonth  = $('<input type="number" name="month" min="1" max="12" value="" />');
        var inputDay    = $('<input type="number" name="day" min="1" max="31" value="" />');
        var inputSubmit = $('<input type="submit" value="Show" />');
        var oldLogsP    = $('

').append('Old Logs: ');
        oldLogsP.append(inputYear).append('-').append(inputMonth).append('-').append(inputDay);
        oldLogsP.append(inputSubmit);
        var oldLogsForm = $('<form action="." method="get"></form>');
        oldLogsForm.append(oldLogsP);
        inputSubmit.click(function () {
            var year  = inputYear.val();
            var month = inputMonth.val();
            var day   = inputDay.val();
            var startTime = (new Date(year, month - 1, day)).getTime() / 1000;
            var endTime   = startTime + 60 * 60 * 24;
            var fragment = 'channels/' + encodeURIComponent(channelName) +
                '/old_logs/by_time_span/' +
                encodeURIComponent(startTime) + ',' + encodeURIComponent(endTime);
            location.hash = fragment;
            return false;
        });
        section.append(oldLogsForm);
        $('#messages h2').after(section);
        return section;
    }
    function messageToElement(message, keywords) {
        var messageTR = $('

');
        messageTR.addClass('message');
        var userNameTD = $('	').text(message.user_name);
        userNameTD.addClass('userName');
        messageTR.append(userNameTD);
        var bodyTD = $('	').addClass('body').text(message.body);
        var emphasizedNum = 0;
        keywords.forEach(function (keyword) {
            emphasizedNum += starChat.emphasizeKeyword(bodyTD, keyword);
        });
        messageTR.append(bodyTD);
        var time = new Date();
        time.setTime(message.created_at * 1000);
        var h = time.getHours() + '';
        var m = time.getMinutes() + '';
        if (h.length < 2) {
            h = '0' + h;
        }
        if (m.length < 2) {
            m = '0' + m;
        }
        var timeStr = h + ':' + m;
        var createdAtTD = $('	');
        var createdAtTime = $('<time></time>').text(timeStr).attr('data-unix-time', message.created_at);;
        createdAtTD.append(createdAtTime).addClass('createdAt');
        messageTR.append(createdAtTD);
        messageTR.attr('data-message-id', message.id);
        messageTR.data('emphasizedNum', emphasizedNum);
        return messageTR;
    }
    function updateViewMessages(self) {
        if (self.channelName) {
            if (self.isShowingOldLogs()) {
                var d = new Date(self.startTime_ * 1000);
                if ((self.endTime_ - self.startTime_) === 60 * 60 * 24 &&
                    d.getHours() === 0 &&
                    d.getMinutes() === 0 &&
                    d.getSeconds() === 0) {
                    var startTime = starChat.toISO8601(new Date(self.startTime_ * 1000), 'date');
                    var oldLogs = '(Old Logs: ' + startTime + ')';
                } else {
                    var startTime = starChat.toISO8601(new Date(self.startTime_ * 1000));
                    var endTime   = starChat.toISO8601(new Date(self.endTime_   * 1000));
                    var oldLogs = '(Old Logs: ' + startTime + '/' + endTime + ')';
                }
                $('#messages h2').text(self.channelName + ' ' + oldLogs);
            } else {
                $('#messages h2').text(self.channelName);
            }
        } else {
            $('#messages h2').text("\u00a0");
        }
        if (!self.isShowingOldLogs()) {
            $('#messages > section').filter(function (i) {
                return $(this).attr('data-start-time') || $(this).attr('data-end-time');
            }).remove();
        }
        if (!self.channelName) {
            $('#messages > section').hide();
            self.lastChannelName_ = '';
            return;
        }
        var section = getSectionElement(self);
        // isBottom should be gotten before appending new message elements
        var diff = section.outerHeight() -
            (section.get(0).scrollHeight - section.scrollTop())
        var isBottom = diff < 20;
        $('#messages > section').each(function (i) {
            var e = $(this);
            if (e.attr('data-channel-name') === self.channelName &&
                (!self.isShowingOldLogs() ||
                 (parseInt(e.attr('data-start-time')) === self.startTime_ &&
                  parseInt(e.attr('data-end-time'))   === self.endTime_))) {
                e.show();
            } else {
                e.hide();
            }
        });
        // TODO: sort by id
        var table = section.find('table.messages');
        if (table.length === 0) {
            table = $('

').addClass('messages');
            section.append(table);
        }
        if (!self.isShowingOldLogs()) {
            if (self.channelName in self.newMessages_) {
                var msgs = self.newMessages_[self.channelName];
                msgs.forEach(function (message) {
                    if (message.id in self.messageIdsAlreadyInSection_) {
                        return;
                    }
                    self.messageIdsAlreadyInSection_[message.id] = true;
                    var userName = self.session().userName();
                    if (message.user_name !== userName) {
                        var e = messageToElement(message, [userName]);
                    } else {
                        var e = messageToElement(message, []);
                    }
                    table.append(e);
                    var emphasizedNum = e.data('emphasizedNum');
                    if (0 < emphasizedNum && !starChat.isFocused()) {
                        startBlinkingTitle(self, '(*) ' + self.title_);
                    }
                });
                self.newMessages_[self.channelName] = [];
            }
        } else {
            var key = self.startTime_ + '_' + self.endTime_;
            if (self.channelName in self.oldMessages_ &&
                key in self.oldMessages_[self.channelName]) {
                // TODO: Refactoring
                section.find('section.message').remove();
                var msgs = self.oldMessages_[self.channelName][key];
                msgs.forEach(function (message) {
                    table.append(messageToElement(message, []));
                });
            }
        }
        if (!self.isShowingOldLogs() && !section.is(':animated')) {
            if (self.lastChannelName_ === self.channelName) {
                if (isBottom) {
                    section.animate({scrollTop: section.get(0).scrollHeight}, {
                        duration: 1000,
                    });
                }
            } else {
                if (!self.lastChannelName_ ||
                    !(self.channelName in self.messageScrollTops)) {
                    section.scrollTop(section.get(0).scrollHeight);
                } else {
                    section.scrollTop(self.messageScrollTops[self.channelName]);
                }
            }
            self.messageScrollTops[self.channelName] = section.scrollTop();
            self.lastChannelName_ = self.channelName;
        }
    }
    function updateViewUsers(self) {
        var userNamesObj = self.userNames[self.channelName];
        if (!userNamesObj) {
            userNamesObj = {};
        }
        var userNames = Object.keys(userNamesObj).sort();
        var ul = $('#users ul');
        ul.empty();
        userNames.forEach(function (userName) {
            var li = $('

');
            li.text(userName);
            ul.append(li);
        });
    }
    View.prototype.update = function () {
        if (this.session_.isLoggedIn()) {
            $('#logInForm').hide();
            $('#logOutLink span').text(this.session_.userName());
            $('#logOutLink').show();
            $('#main input').removeAttr('disabled');
            if (this.channelName && !this.isShowingOldLogs()) {
                $('#postMessageForm input').removeAttr('disabled');
            } else {
                $('#postMessageForm input').attr('disabled', 'disabled');
            }
        } else {
            $('#logInForm').show();
            $('#logOutLink').hide();
            $('#main input').attr('disabled', 'disabled');
        }
        updateViewChannels(this);
        updateViewMessages(this);
        updateViewUsers(this);
        $('img[data-image-icon-name]').each(function () {
            var e = $(this);
            if (e.attr('src')) {
                return true;
            }
            var iconName = e.attr('data-image-icon-name');
            e.attr('src', starChat.Icons[iconName]);
        });
        $(window).resize();
    };
    View.prototype.logIn = function (userName, password) {
        this.session_ = new this.sessionClass_($.now(), userName, password);
    };
    View.prototype.logOut = function () {
        this.session_ = new this.sessionClass_();
        initialize(this);
    };
    View.prototype.session = function () {
        return this.session_;
    };
    View.prototype.addNewMessage = function (channelName, message, setDirtyFlag) {
        if (!this.newMessages_[channelName]) {
            this.newMessages_[channelName] = [];
        }
        if (message.id in this.messageIdsAlreadyInSection_) {
            return;
        }
        this.newMessages_[channelName].push(message);
        if (setDirtyFlag && channelName !== this.channelName) {
            this.setDirtyFlag(channelName, true);
        }
    };
    View.prototype.setDirtyFlag = function (channelName, value) {
        this.dirtyFlags_[channelName] = value;
    };
    // TODO: 関数名直すべき?
    View.prototype.clickChannelDel = function (func) {
        this.clickChannelDel_ = func;
        return this;
    };
    View.prototype.resetTimeSpan = function () {
        this.startTime_ = null;
        this.endTime_   = null;
    };
    View.prototype.setTimeSpan = function (startTime, endTime) {
        this.startTime_ = startTime;
        this.endTime_   = endTime;
    };
    View.prototype.isShowingOldLogs = function () {
        return $.isNumeric(this.startTime_) && $.isNumeric(this.endTime_);
    };
    View.prototype.setOldMessages = function (channelName, startTime, endTime, messages) {
        if (!(channelName in this.oldMessages_)) {
            this.oldMessages_[channelName] = {};
        }
        var key = startTime + '_' + endTime;
        this.oldMessages_[channelName][key] = messages;
    };
    return View;
})();
<commit_msg>Refactoring<commit_after>'use strict';

starChat.View = (function () {
    var View = function (sessionClass) {
        this.sessionClass_ = sessionClass;

        initialize(this);
    };
    function initialize(self) {
        self.session_ = new self.sessionClass_();

        // TODO: Model に相当するクラスを作る?
        // TODO: いずれこれらの変数も private (_ 終わり) にする
        self.channels = [];
        self.channelName = '';
        self.messageScrollTops = {};
        self.userNames = {};
        self.isEdittingChannels = false;

        self.lastChannelName_ = '';
        self.newMessages_ = {};
        self.messageIdsAlreadyInSection_ = {};
        self.dirtyFlags_ = {};
        self.startTime_ = null;
        self.endTime_ = null;
        self.oldMessages_ = {};
        self.isBlinkingTitle_ = false;

        self.title_ = 'StarChat (β)';
        document.title = self.title_;
        stopBlinkingTitle(self);
    }
    function startBlinkingTitle(self, anotherTitle) {
        if (self.isBlinkingTitle_) {
            return;
        }
        self.isBlinkingTitle_ = true;
        function loop (i) {
            if (!self.isBlinkingTitle_) {
                stopBlinkingTitle(self);
                return;
            }
            if (starChat.isFocused()) {
                stopBlinkingTitle(self);
                return;
            }
            document.title = {
                0: self.title_,
                1: anotherTitle,
            }[i];
            setTimeout(function () {
                loop(1 - i);
            }, 1000);
        }
        loop(0);
    }
    function stopBlinkingTitle(self) {
        self.isBlinkingTitle_ = false;
        document.title = self.title_;
    }
    var updateViewChannels = (function () {
        var lastSessionId = 0;
        return function (self) {
            var channels = self.channels.sort(function (a, b) {
                if (a.name > b.name) {
                    return 1;
                }
                if (a.name < b.name) {
                    return -1;
                }
                return 0;
            });
            if (self.channelName) {
                self.dirtyFlags_[self.channelName] = false;
            }
            (function () {
                var ul = $('#channels ul');
                ul.find('li').filter(function (i) {
                    var channelName = $(this).attr('data-channel-name');
                    return channels.every(function (channel) {
                        return channel.name !== channelName;
                    });
                }).remove();
                var existChannelNames = $.map(ul.find('li'), function (e) {
                    return $(e).attr('data-channel-name');
                });
                var newChannels = channels.filter(function (channel) {
                    return existChannelNames.every(function (name) {
                        return name !== channel.name;
                    });
                });
                // TODO: sort
                newChannels.forEach(function (channel) {
                    var a = $('');
                    var name = channel.name;
                    var href = '#channels/' + encodeURIComponent(channel.name);
                    a.attr('href', href);
                    a.text(name);
                    var icon = $('').click(function () {
                        return self.clickChannelDel_(channel);
                    });
                    var li = $('

').attr('data-channel-name', channel.name);
                    li.append(a).append(icon);
                    ul.append(li);
                });
                ul.find('li').each(function () {
                    var e = $(this);
                    var channelName = e.attr('data-channel-name');
                    e.find('a').toggleClass('dirty', self.dirtyFlags_[channelName] === true);
                });
                lastSessionId = self.session_.id();
            })();
            if (self.isEdittingChannels) {
                $('#channels li img[data-tool-id="delete"]').show().css('display', 'inline');
            } else {
                $('#channels li img[data-tool-id="delete"]').hide();
            }
        }
    })();
    function getSectionElement(self) {
        if (!self.channelName) {
            return $('#messages > section[data-channel-name=""]');
        }
        var sections = $('#messages > section').filter(function (i) {
            return $(this).attr('data-channel-name') === self.channelName &&
                (!self.isShowingOldLogs() &&
                 !$(this).attr('data-start-time') &&
                 !$(this).attr('data-end-time')) ||
                (self.isShowingOldLogs() &&
                 parseInt($(this).attr('data-start-time')) === self.startTime_ &&
                 parseInt($(this).attr('data-end-time'))   === self.endTime_);
        });
        if (sections.length === 1) {
            var section = sections;
            section.find('input[name="year"]').val('');
            section.find('input[name="month"]').val('');
            section.find('input[name="day"]').val('');
            return section;
        }
        if (2 <= sections.length) {
            throw 'invalid sections';
        }
        var section = $('

');
        var channelName = self.channelName;
        section.attr('data-channel-name', channelName);
        if (self.isShowingOldLogs()) {
            section.attr('data-start-time', self.startTime_);
            section.attr('data-end-time',   self.endTime_);
        }
        if (!self.isShowingOldLogs()) {
            section.scroll(function () {
                self.messageScrollTops[channelName] = section.scrollTop();
            });
        }
        var inputYear   = $('<input type="number" name="year" min="0" max="9999" value="" />');
        var inputMonth  = $('<input type="number" name="month" min="1" max="12" value="" />');
        var inputDay    = $('<input type="number" name="day" min="1" max="31" value="" />');
        var inputSubmit = $('<input type="submit" value="Show" />');
        var oldLogsP    = $('

').append('Old Logs: ');
        oldLogsP.append(inputYear).append('-').append(inputMonth).append('-').append(inputDay);
        oldLogsP.append(inputSubmit);
        var oldLogsForm = $('<form action="." method="get"></form>');
        oldLogsForm.append(oldLogsP);
        inputSubmit.click(function () {
            var year  = inputYear.val();
            var month = inputMonth.val();
            var day   = inputDay.val();
            var startTime = (new Date(year, month - 1, day)).getTime() / 1000;
            var endTime   = startTime + 60 * 60 * 24;
            var fragment = 'channels/' + encodeURIComponent(channelName) +
                '/old_logs/by_time_span/' +
                encodeURIComponent(startTime) + ',' + encodeURIComponent(endTime);
            location.hash = fragment;
            return false;
        });
        section.append(oldLogsForm);
        $('#messages h2').after(section);
        return section;
    }
    function messageToElement(message, keywords) {
        var messageTR = $('

');
        messageTR.addClass('message');
        var userNameTD = $('	').text(message.user_name);
        userNameTD.addClass('userName');
        messageTR.append(userNameTD);
        var bodyTD = $('	').addClass('body').text(message.body);
        var emphasizedNum = 0;
        keywords.forEach(function (keyword) {
            emphasizedNum += starChat.emphasizeKeyword(bodyTD, keyword);
        });
        messageTR.append(bodyTD);
        var time = new Date();
        time.setTime(message.created_at * 1000);
        var h = time.getHours() + '';
        var m = time.getMinutes() + '';
        if (h.length < 2) {
            h = '0' + h;
        }
        if (m.length < 2) {
            m = '0' + m;
        }
        var timeStr = h + ':' + m;
        var createdAtTD = $('	');
        var createdAtTime = $('<time></time>').text(timeStr).attr('data-unix-time', message.created_at);;
        createdAtTD.append(createdAtTime).addClass('createdAt');
        messageTR.append(createdAtTD);
        messageTR.attr('data-message-id', message.id);
        messageTR.data('emphasizedNum', emphasizedNum);
        return messageTR;
    }
    function updateViewMessages(self) {
        if (self.channelName) {
            if (self.isShowingOldLogs()) {
                var d = new Date(self.startTime_ * 1000);
                if ((self.endTime_ - self.startTime_) === 60 * 60 * 24 &&
                    d.getHours() === 0 &&
                    d.getMinutes() === 0 &&
                    d.getSeconds() === 0) {
                    var startTime = starChat.toISO8601(new Date(self.startTime_ * 1000), 'date');
                    var oldLogs = '(Old Logs: ' + startTime + ')';
                } else {
                    var startTime = starChat.toISO8601(new Date(self.startTime_ * 1000));
                    var endTime   = starChat.toISO8601(new Date(self.endTime_   * 1000));
                    var oldLogs = '(Old Logs: ' + startTime + '/' + endTime + ')';
                }
                $('#messages h2').text(self.channelName + ' ' + oldLogs);
            } else {
                $('#messages h2').text(self.channelName);
            }
        } else {
            $('#messages h2').text("\u00a0");
        }
        if (!self.isShowingOldLogs()) {
            $('#messages > section').filter(function (i) {
                return $(this).attr('data-start-time') || $(this).attr('data-end-time');
            }).remove();
        }
        if (!self.channelName) {
            $('#messages > section').hide();
            self.lastChannelName_ = '';
        }
        var section = getSectionElement(self);
        $('#messages > section').each(function () {
            var e = $(this);
            if (e.get(0) === section.get(0)) {
                e.show();
            } else {
                e.hide();
            }
        });
        if (!self.channelName) {
            return;
        }

        // isBottom should be gotten before appending new message elements
        var diff = section.outerHeight() -
            (section.get(0).scrollHeight - section.scrollTop())
        var isBottom = diff < 20;

        // TODO: sort by id
        var table = section.find('table.messages');
        if (table.length === 0) {
            table = $('

').addClass('messages');
            section.append(table);
        }
        if (!self.isShowingOldLogs()) {
            if (self.channelName in self.newMessages_) {
                var msgs = self.newMessages_[self.channelName];
                msgs.forEach(function (message) {
                    if (message.id in self.messageIdsAlreadyInSection_) {
                        return;
                    }
                    self.messageIdsAlreadyInSection_[message.id] = true;
                    var userName = self.session().userName();
                    if (message.user_name !== userName) {
                        var e = messageToElement(message, [userName]);
                    } else {
                        var e = messageToElement(message, []);
                    }
                    table.append(e);
                    var emphasizedNum = e.data('emphasizedNum');
                    if (0 < emphasizedNum && !starChat.isFocused()) {
                        startBlinkingTitle(self, '(*) ' + self.title_);
                    }
                });
                self.newMessages_[self.channelName] = [];
            }
        } else {
            var key = self.startTime_ + '_' + self.endTime_;
            if (self.channelName in self.oldMessages_ &&
                key in self.oldMessages_[self.channelName]) {
                // TODO: Refactoring
                section.find('section.message').remove();
                var msgs = self.oldMessages_[self.channelName][key];
                msgs.forEach(function (message) {
                    table.append(messageToElement(message, []));
                });
            }
        }
        if (!self.isShowingOldLogs() && !section.is(':animated')) {
            if (self.lastChannelName_ === self.channelName) {
                if (isBottom) {
                    section.animate({scrollTop: section.get(0).scrollHeight}, {
                        duration: 1000,
                    });
                }
            } else {
                if (!self.lastChannelName_ ||
                    !(self.channelName in self.messageScrollTops)) {
                    section.scrollTop(section.get(0).scrollHeight);
                } else {
                    section.scrollTop(self.messageScrollTops[self.channelName]);
                }
            }
            self.messageScrollTops[self.channelName] = section.scrollTop();
            self.lastChannelName_ = self.channelName;
        }
    }
    function updateViewUsers(self) {
        var userNamesObj = self.userNames[self.channelName];
        if (!userNamesObj) {
            userNamesObj = {};
        }
        var userNames = Object.keys(userNamesObj).sort();
        var ul = $('#users ul');
        ul.empty();
        userNames.forEach(function (userName) {
            var li = $('

');
            li.text(userName);
            ul.append(li);
        });
    }
    View.prototype.update = function () {
        if (this.session_.isLoggedIn()) {
            $('#logInForm').hide();
            $('#logOutLink span').text(this.session_.userName());
            $('#logOutLink').show();
            $('#main input').removeAttr('disabled');
            if (this.channelName && !this.isShowingOldLogs()) {
                $('#postMessageForm input').removeAttr('disabled');
            } else {
                $('#postMessageForm input').attr('disabled', 'disabled');
            }
        } else {
            $('#logInForm').show();
            $('#logOutLink').hide();
            $('#main input').attr('disabled', 'disabled');
        }
        updateViewChannels(this);
        updateViewMessages(this);
        updateViewUsers(this);
        $('img[data-image-icon-name]').each(function () {
            var e = $(this);
            if (e.attr('src')) {
                return true;
            }
            var iconName = e.attr('data-image-icon-name');
            e.attr('src', starChat.Icons[iconName]);
        });
        $(window).resize();
    };
    View.prototype.logIn = function (userName, password) {
        this.session_ = new this.sessionClass_($.now(), userName, password);
    };
    View.prototype.logOut = function () {
        this.session_ = new this.sessionClass_();
        initialize(this);
    };
    View.prototype.session = function () {
        return this.session_;
    };
    View.prototype.addNewMessage = function (channelName, message, setDirtyFlag) {
        if (!this.newMessages_[channelName]) {
            this.newMessages_[channelName] = [];
        }
        if (message.id in this.messageIdsAlreadyInSection_) {
            return;
        }
        this.newMessages_[channelName].push(message);
        if (setDirtyFlag && channelName !== this.channelName) {
            this.setDirtyFlag(channelName, true);
        }
    };
    View.prototype.setDirtyFlag = function (channelName, value) {
        this.dirtyFlags_[channelName] = value;
    };
    // TODO: 関数名直すべき?
    View.prototype.clickChannelDel = function (func) {
        this.clickChannelDel_ = func;
        return this;
    };
    View.prototype.resetTimeSpan = function () {
        this.startTime_ = null;
        this.endTime_   = null;
    };
    View.prototype.setTimeSpan = function (startTime, endTime) {
        this.startTime_ = startTime;
        this.endTime_   = endTime;
    };
    View.prototype.isShowingOldLogs = function () {
        return $.isNumeric(this.startTime_) && $.isNumeric(this.endTime_);
    };
    View.prototype.setOldMessages = function (channelName, startTime, endTime, messages) {
        if (!(channelName in this.oldMessages_)) {
            this.oldMessages_[channelName] = {};
        }
        var key = startTime + '_' + endTime;
        this.oldMessages_[channelName][key] = messages;
    };
    return View;
})();
<|endoftext|>


<commit_before>// load env vars first
import dotenv from 'dotenv'
dotenv.load()

import 'newrelic'
import 'babel-core/polyfill'
import 'isomorphic-fetch'

import express from 'express'
import path from 'path'
import fs from 'fs'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match } from 'redux-router/server'
import { ReduxRouter } from 'redux-router'
import { Provider } from 'react-redux'
import store from './src/store_server'
import { updateStrings as updateTimeAgoStrings } from './src/vendor/time_ago_in_words'

updateTimeAgoStrings({ about: '' })

const app = express()
let indexStr = ''
// grab out the index.html string first thing
fs.readFile(path.join(__dirname, './public/index.html'), 'utf-8', (err, data) => {
  if (!err) {
    indexStr = data
  }
})

// Assets
app.use(express.static('public'))
app.use('/static', express.static('public/static'))

// Auth token
// Get the access token object.
const credentials = {
  clientID: process.env.AUTH_CLIENT_ID,
  clientSecret: process.env.AUTH_CLIENT_SECRET,
  site: process.env.AUTH_DOMAIN,
  tokenPath: '/api/oauth/token',
  headers: {
    'Accept': 'application/json',
  },
}

// Initialize the OAuth2 Library
const oauth2 = require('simple-oauth2')(credentials)
const tokenConfig = {}
let token = null

// Get the access token object for the client
oauth2.client
  .getToken(tokenConfig)
  .then((result) => {
    if (result.errors) {
      console.log('Unable to get access token', result)
      process.exit(1)
    }
    token = oauth2.accessToken.create(result)
  })
  .catch((error) => {
    console.log('Access Token error', error.message)
    process.exit(1)
  })

app.get('/token', (req, res) => {
  if (token.expired()) {
    token.refresh().then((result) => {
      if (result.errors) {
        console.log('Access Token error', result)
        res.status(401).send(result.errors)
      } else {
        token = result
        res.status(200).send(token)
      }
    }).catch((error) => {
      console.log('Access Token error', error.message)
      res.status(401).send()
    })
  } else {
    res.status(200).send(token)
  }
})

// Return promises for initial loads
function preRender(routerState) {
  const promises = routerState.components.map(component => (component && component.preRender) ? component.preRender(store, routerState) : null).filter(component => !!component)
  return Promise.all(promises)
}

function renderFromServer(req, res) {
  store.dispatch(match(req.url, (error, redirectLocation, routerState) => {
    if (error) {
      console.log('error', error)
    }
    if (redirectLocation) {
      console.log('handle redirect')
    }
    if (!routerState) { return }
    preRender(routerState).then(() => {
      const InitialComponent = (
        <Provider store={store}>
          <ReduxRouter />
        </Provider>
      )
      const componentHTML = renderToString(InitialComponent)
      const state = store.getState()
      state.authentication.isLoggedIn = false
      const initialStateTag = `<script id="initial-state">window.__INITIAL_STATE__ = ${JSON.stringify(state)}</script>`
      indexStr = indexStr.replace('

', `<div id="root">${componentHTML}</div>${initialStateTag}`)
      res.send(indexStr)
    }).catch((err) => {
      // this will give you a js error like:
      // `window is not defined`
      console.log('ERROR', err)
    })
  }))
}

const loggedOutPaths = {
  find: /\/find$/,
  root: /\/$/,
  recent: /\/recent$/,
  trending: /\/trending$/,
}

app.use((req, res) => {
  let isLoggedOutPath = false
  for (const re in loggedOutPaths) {
    if (req.url.match(loggedOutPaths[re])) {
      isLoggedOutPath = true
      break
    }
  }
  console.log('url', req.url, isLoggedOutPath)
  if (isLoggedOutPath) {
    renderFromServer(req, res)
  } else {
    res.send(indexStr)
  }
})

const port = process.env.PORT || 6660
app.listen(port, (err) => {
  if (err) {
    console.log(err)
    return
  }
  console.log('Listening at http://localhost:' + port)
})

<commit_msg>Only load .env if we are in test env.<commit_after>// load env vars first in test
if (process.env.NODE_ENV === 'test') {
  require('dotenv').load()
}

import 'newrelic'
import 'babel-core/polyfill'
import 'isomorphic-fetch'

import express from 'express'
import path from 'path'
import fs from 'fs'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match } from 'redux-router/server'
import { ReduxRouter } from 'redux-router'
import { Provider } from 'react-redux'
import store from './src/store_server'
import { updateStrings as updateTimeAgoStrings } from './src/vendor/time_ago_in_words'

updateTimeAgoStrings({ about: '' })

const app = express()
let indexStr = ''
// grab out the index.html string first thing
fs.readFile(path.join(__dirname, './public/index.html'), 'utf-8', (err, data) => {
  if (!err) {
    indexStr = data
  }
})

// Assets
app.use(express.static('public'))
app.use('/static', express.static('public/static'))

// Auth token
// Get the access token object.
const credentials = {
  clientID: process.env.AUTH_CLIENT_ID,
  clientSecret: process.env.AUTH_CLIENT_SECRET,
  site: process.env.AUTH_DOMAIN,
  tokenPath: '/api/oauth/token',
  headers: {
    'Accept': 'application/json',
  },
}

// Initialize the OAuth2 Library
const oauth2 = require('simple-oauth2')(credentials)
const tokenConfig = {}
let token = null

// Get the access token object for the client
oauth2.client
  .getToken(tokenConfig)
  .then((result) => {
    if (result.errors) {
      console.log('Unable to get access token', result)
      process.exit(1)
    }
    token = oauth2.accessToken.create(result)
  })
  .catch((error) => {
    console.log('Access Token error', error.message)
    process.exit(1)
  })

app.get('/token', (req, res) => {
  if (token.expired()) {
    token.refresh().then((result) => {
      if (result.errors) {
        console.log('Access Token error', result)
        res.status(401).send(result.errors)
      } else {
        token = result
        res.status(200).send(token)
      }
    }).catch((error) => {
      console.log('Access Token error', error.message)
      res.status(401).send()
    })
  } else {
    res.status(200).send(token)
  }
})

// Return promises for initial loads
function preRender(routerState) {
  const promises = routerState.components.map(component => (component && component.preRender) ? component.preRender(store, routerState) : null).filter(component => !!component)
  return Promise.all(promises)
}

function renderFromServer(req, res) {
  store.dispatch(match(req.url, (error, redirectLocation, routerState) => {
    if (error) {
      console.log('error', error)
    }
    if (redirectLocation) {
      console.log('handle redirect')
    }
    if (!routerState) { return }
    preRender(routerState).then(() => {
      const InitialComponent = (
        <Provider store={store}>
          <ReduxRouter />
        </Provider>
      )
      const componentHTML = renderToString(InitialComponent)
      const state = store.getState()
      state.authentication.isLoggedIn = false
      const initialStateTag = `<script id="initial-state">window.__INITIAL_STATE__ = ${JSON.stringify(state)}</script>`
      indexStr = indexStr.replace('

', `<div id="root">${componentHTML}</div>${initialStateTag}`)
      res.send(indexStr)
    }).catch((err) => {
      // this will give you a js error like:
      // `window is not defined`
      console.log('ERROR', err)
    })
  }))
}

const loggedOutPaths = {
  find: /\/find$/,
  root: /\/$/,
  recent: /\/recent$/,
  trending: /\/trending$/,
}

app.use((req, res) => {
  let isLoggedOutPath = false
  for (const re in loggedOutPaths) {
    if (req.url.match(loggedOutPaths[re])) {
      isLoggedOutPath = true
      break
    }
  }
  console.log('url', req.url, isLoggedOutPath)
  if (isLoggedOutPath) {
    renderFromServer(req, res)
  } else {
    res.send(indexStr)
  }
})

const port = process.env.PORT || 6660
app.listen(port, (err) => {
  if (err) {
    console.log(err)
    return
  }
  console.log('Listening at http://localhost:' + port)
})

<|endoftext|>


<commit_before>/* global require, module, process, console, __dirname */
/* jshint -W097 */
'use strict';

var
  serverRootDir = __dirname + '/../../',
  mocha         = require('mocha'),
  expect        = require('chai').expect,
  request       = require('supertest'),
  _             = require('underscore'),
  app           = require(serverRootDir + 'app').app;

describe('api/media', function() {

  var firstRecord;

  // -- GET ALL ---------------------------------

  describe('Get all media objects -> GET /api/media', function() {

    it('responds with json', function(done) {
      request(app)
        .get('/api/media')
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });

    it('returns an array of media objects', function(done) {
      request(app)
        .get('/api/media')
        .set('Accept', 'application/json')
        .end(function(err, res) {

          expect(err).to.not.exist;

          expect(_.isArray(res.body)).to.be.true;

          expect(res.body).to.have.length.of.at.least(2);

          // save a reference for later usage
          firstRecord = res.body[0];

          expect(firstRecord).to.have.property('_id');
          expect(firstRecord).to.have.property('name');
          expect(firstRecord).to.have.property('description');
          expect(firstRecord).to.have.property('url');
          expect(firstRecord).to.have.property('active');

          done();
        });
    });
  });

  // -- GET ONE ---------------------------------

  describe('Get one media object -> GET /api/media/:id', function() {

    it('responds with json', function(done) {
      request(app)
        .get('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .get('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

    it('returns a Media object', function(done) {
      request(app)
        .get('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .end(function(err, res) {

          expect(err).to.not.exist;

          var model = res.body;

          expect(model).to.be.an('object');

          expect(model).to.have.property('_id');
          expect(model).to.have.property('name');
          expect(model).to.have.property('description');
          expect(model).to.have.property('url');
          expect(model).to.have.property('active');

          expect(model._id).not.to.be.null;

          done();
        });
    });

  });

  // -- CREATE ----------------------------------
  describe('Create a new media object -> POST /api/media', function() {

/*
    it('responds with json', function(done) {
      request(app)
        .post('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });
*/

  });

  // -- UPDATE ----------------------------------
  describe('Update a media object -> PUT /api/media/:id', function() {

    /*
    it('responds with json', function(done) {
      request(app)
        .put('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });
    */

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .put('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

  });

  // -- DELETE ----------------------------------
  describe('Delete a media object -> DELETE /api/media/:id', function() {

    var deletedModel;

    it('responds with json', function(done) {
      request(app)
        .delete('/api/media/a-non-existing-record-id')
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(404, done);
    });

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .delete('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

    it('returns the deleted model', function(done) {
      request(app)
        .delete('/api/media/'+firstRecord._id)
        .end(function(err, res) {

          expect(err).to.not.exist;

          deletedModel = res.body;

          expect(deletedModel).to.be.an('object');

          expect(deletedModel).to.have.property('_id');
          expect(deletedModel).to.have.property('name');
          expect(deletedModel).to.have.property('description');
          expect(deletedModel).to.have.property('url');
          expect(deletedModel).to.have.property('active');

          expect(deletedModel._id).not.to.be.null;

          done();
        });
    });

    it('deletes the requested model', function(done) {
      request(app).get('/api/media/'+deletedModel._id).expect(404, done);
    });

  });

});
<commit_msg>Reset the dev. database before executing the api tests<commit_after>/* global require, module, process, console, __dirname */
/* jshint -W097 */
'use strict';

var
  serverRootDir = __dirname + '/../../',
  mocha         = require('mocha'),
  expect        = require('chai').expect,
  request       = require('supertest'),
  _             = require('underscore'),
  app           = require(serverRootDir + 'app').app;

describe('api/media', function() {

  var firstRecord;

  before(function(done) {
    // populate the database (the dev. one, this is only for development purposes!)
    var exec = require('child_process').exec;

    this.timeout(2000);
    exec('grunt mongo:populate', done);
  });

  // -- GET ALL ---------------------------------

  describe('Get all media objects -> GET /api/media', function() {

    it('responds with json', function(done) {
      request(app)
        .get('/api/media')
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });

    it('returns an array of media objects', function(done) {
      request(app)
        .get('/api/media')
        .set('Accept', 'application/json')
        .end(function(err, res) {

          expect(err).to.not.exist;

          expect(_.isArray(res.body)).to.be.true;

          expect(res.body).to.have.length.of.at.least(2);

          // save a reference for later usage
          firstRecord = res.body[0];

          expect(firstRecord).to.have.property('_id');
          expect(firstRecord).to.have.property('name');
          expect(firstRecord).to.have.property('description');
          expect(firstRecord).to.have.property('url');
          expect(firstRecord).to.have.property('active');

          done();
        });
    });
  });

  // -- GET ONE ---------------------------------

  describe('Get one media object -> GET /api/media/:id', function() {

    it('responds with json', function(done) {
      request(app)
        .get('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .get('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

    it('returns a Media object', function(done) {
      request(app)
        .get('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .end(function(err, res) {

          expect(err).to.not.exist;

          var model = res.body;

          expect(model).to.be.an('object');

          expect(model).to.have.property('_id');
          expect(model).to.have.property('name');
          expect(model).to.have.property('description');
          expect(model).to.have.property('url');
          expect(model).to.have.property('active');

          expect(model._id).not.to.be.null;

          done();
        });
    });

  });

  // -- CREATE ----------------------------------

  describe('Create a new media object -> POST /api/media', function() {

/*
    it('responds with json', function(done) {
      request(app)
        .post('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });
*/

  });

  // -- UPDATE ----------------------------------

  describe('Update a media object -> PUT /api/media/:id', function() {

    /*
    it('responds with json', function(done) {
      request(app)
        .put('/api/media/'+firstRecord._id)
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(200, done);
    });
    */

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .put('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

  });

  // -- DELETE ----------------------------------

  describe('Delete a media object -> DELETE /api/media/:id', function() {

    var deletedModel;

    it('responds with json', function(done) {
      request(app)
        .delete('/api/media/a-non-existing-record-id')
        .set('Accept', 'application/json')
        .expect('Content-Type', /application\/json/)
        .expect(404, done);
    });

    it('retuns a 404 error if the model does not exist', function(done) {
      request(app)
        .delete('/api/media/a-non-existing-record-id')
        .expect(404, done);
    });

    it('returns the deleted model', function(done) {
      request(app)
        .delete('/api/media/'+firstRecord._id)
        .end(function(err, res) {

          expect(err).to.not.exist;

          deletedModel = res.body;

          expect(deletedModel).to.be.an('object');

          expect(deletedModel).to.have.property('_id');
          expect(deletedModel).to.have.property('name');
          expect(deletedModel).to.have.property('description');
          expect(deletedModel).to.have.property('url');
          expect(deletedModel).to.have.property('active');

          expect(deletedModel._id).not.to.be.null;

          done();
        });
    });

    it('deletes the requested model', function(done) {
      request(app).get('/api/media/'+deletedModel._id).expect(404, done);
    });

  });

});
<|endoftext|>


<commit_before>/*
 * app.js: Core Application object for managing plugins and features in broadway
 *
 * (C) 2011, Nodejitsu Inc.
 * MIT LICENSE
 *
 */

var utile = require('utile'),
    async = utile.async,
    events = require('eventemitter2'),
    common = require('./common'),
    constants = require('./constants'),
    features = require('./features');

var App = exports.App = function (options) {
  //
  // Setup options and `App` constants.
  //
  var self       = this;
  options        = options || {};
  this.root      = options.root;
  this.delimiter = options.delimiter || constants.DELIMITER;

  //
  // Inherit from `EventEmitter2`
  //
  events.EventEmitter2.call(this, {
    delimiter: this.DELIMITER,
    wildcard: true
  });

  //
  // Setup other relevant options such as the plugins
  // for this instance.
  //
  this.options      = options;
  this.plugins      = options.plugins   || {};
  this.initializers = {};
  this.use(this.plugins.core || require('./plugins/core'), options);
};

//
// Inherit from `EventEmitter2`.
//
utile.inherits(App, events.EventEmitter2);

//
// ### function init (options, callback)
// #### @options {Object} **Optional** Options to initialize this instance with
// #### @callback {function} Continuation to respond to when complete.
// Initializes this instance by the following procedure:
//
// 1. Initializes all plugins (starting with `core`).
// 2. Creates all directories in `this.config.directories` (if any).
// 3. Ensures the files in the core directory structure conform to the
//    features required by this application.
//
App.prototype.init = function (options, callback) {
  if (!callback && typeof options === 'function') {
    callback = options;
    options = {};
  }

  var self = this;

  function onComplete() {
    self.initializers['all'] = true;
    callback();
  }

  function ensureFeatures (err) {
    return err
      ? onError(err)
      : features.ensure(this, onComplete);
  }

  function initPlugin(plugin, next) {
    if (typeof self.initializers[plugin] === 'function') {
      return self.initializers[plugin].call(self, function (err) {
        if (err) {
          return next(err);
        }

        self.initializers[plugin] = true;
        next();
      });
    }

    next();
  }

  //
  // Emit and respond with any errors that may short
  // circuit the process.
  //
  function onError(err) {
    self.emit('error:init', err);
    callback(err);
  }

  //
  // Initialize plugins, then create directories and
  // ensure features for this instance.
  //
  async.forEach(Object.keys(this.initializers), initPlugin, ensureFeatures);
};

//
// ### function use(plugin, callback)
// Attachs the plugin with the specific name to this `App` instance.
//
App.prototype.use = function (plugin, options) {
  options = options || {};
  var name = plugin.name;
  if (this.plugins[name] && this.plugins[name].detach) {
    this.plugins[name].detach(this);
  }

  //
  // Setup state on this instance for the specified plugin
  //
  this.plugins[name]      = plugin;
  this.options[name]      = options;
  this.initializers[name] = plugin.init || true;

  //
  // Attach the specified plugin to this instance, extending
  // the `App` with new functionality.
  //
  this.plugins[name].attach.call(this, options);
};

//
// ### function inspect ()
// Inspects the modules and features used by the current
// application directory structure
//
App.prototype.inspect = function () {

};<commit_msg>[api minor] Dont take options in `.init`<commit_after>/*
 * app.js: Core Application object for managing plugins and features in broadway
 *
 * (C) 2011, Nodejitsu Inc.
 * MIT LICENSE
 *
 */

var utile = require('utile'),
    async = utile.async,
    events = require('eventemitter2'),
    common = require('./common'),
    constants = require('./constants'),
    features = require('./features');

var App = exports.App = function (options) {
  //
  // Setup options and `App` constants.
  //
  var self       = this;
  options        = options || {};
  this.root      = options.root;
  this.delimiter = options.delimiter || constants.DELIMITER;

  //
  // Inherit from `EventEmitter2`
  //
  events.EventEmitter2.call(this, {
    delimiter: this.DELIMITER,
    wildcard: true
  });

  //
  // Setup other relevant options such as the plugins
  // for this instance.
  //
  this.options      = options;
  this.plugins      = options.plugins   || {};
  this.initializers = {};
  this.use(this.plugins.core || require('./plugins/core'), options);
};

//
// Inherit from `EventEmitter2`.
//
utile.inherits(App, events.EventEmitter2);

//
// ### function init (options, callback)
// #### @callback {function} Continuation to respond to when complete.
// Initializes this instance by the following procedure:
//
// 1. Initializes all plugins (starting with `core`).
// 2. Creates all directories in `this.config.directories` (if any).
// 3. Ensures the files in the core directory structure conform to the
//    features required by this application.
//
App.prototype.init = function (callback) {
  var self = this;

  function onComplete() {
    self.initializers['all'] = true;
    callback();
  }

  function ensureFeatures (err) {
    return err
      ? onError(err)
      : features.ensure(this, onComplete);
  }

  function initPlugin(plugin, next) {
    if (typeof self.initializers[plugin] === 'function') {
      return self.initializers[plugin].call(self, function (err) {
        if (err) {
          return next(err);
        }

        self.initializers[plugin] = true;
        next();
      });
    }

    next();
  }

  //
  // Emit and respond with any errors that may short
  // circuit the process.
  //
  function onError(err) {
    self.emit('error:init', err);
    callback(err);
  }

  //
  // Initialize plugins, then create directories and
  // ensure features for this instance.
  //
  async.forEach(Object.keys(this.initializers), initPlugin, ensureFeatures);
};

//
// ### function use(plugin, callback)
// Attachs the plugin with the specific name to this `App` instance.
//
App.prototype.use = function (plugin, options) {
  options = options || {};
  var name = plugin.name;
  if (this.plugins[name] && this.plugins[name].detach) {
    this.plugins[name].detach(this);
  }

  //
  // Setup state on this instance for the specified plugin
  //
  this.plugins[name]      = plugin;
  this.options[name]      = options;
  this.initializers[name] = plugin.init || true;

  if (this.options[name]) {
    options = common.mixin({}, options, this.options[name]);
  }

  //
  // Attach the specified plugin to this instance, extending
  // the `App` with new functionality.
  //
  this.plugins[name].attach.call(this, options);
};

//
// ### function inspect ()
// Inspects the modules and features used by the current
// application directory structure
//
App.prototype.inspect = function () {

};<|endoftext|>


<commit_before>"use strict";

var hooks           = require("./hooks");
var async           = require("./async");
var config          = require("./config");
var messages        = require("./connect-utils");
var utils           = require("./utils");
var logger          = require("./logger");

var _               = require("lodash");
var EE              = require("easy-extender");
var filePath        = require("path");

/**
 * Required internal plugin, all can be overridden.
 */
var defaultPlugins = {
    "logger":        logger,
    "socket":        require("./sockets"),
    "file:watcher":  require("./file-watcher"),
    "server":        require("./server"),
    "tunnel":        require("./tunnel"),
    "client:script": require("browser-sync-client")
};

/**
 * @constructor
 */
var BrowserSync = function (emitter) {

    this.cwd      = process.cwd();
    this.active   = false;
    this.paused   = false;
    this.config   = config;

    this.events   = emitter;

    this._reloadQueue   = [];
    this._browserReload = false;

    // Plugin management
    this.pluginManager = new EE(defaultPlugins, hooks);

    this._userPlugins  = [];

    this.clientEvents  = [
        "scroll",
        "input:text",
        "input:toggles",
        "form:submit",
        "form:reset",
        "click"
    ];
};

/**
 * @param {Map} options
 * @param {Function} cb
 * @returns {BrowserSync}
 */
BrowserSync.prototype.init = function (options, cb) {

    var bs = this;
    bs.cb  = cb || function () {/*noop*/};

    // verify/update options
    utils.verifyConfig(options, bs.cb);
    bs.options = utils.updateOptions(options);
    bs.pluginManager.init();

    // set loggers
    bs.logger   = bs.pluginManager.get("logger")(bs.events, bs);
    bs.debugger = bs.logger.clone({useLevelPrefixes: true});
    bs.debug    = bs.debugger.debug;

    var asyncTasks = [
        {
            step: "Finding an empty port",
            fn: async.getEmptyPort
        },
        {
            step: "Checking online status",
            fn: async.getOnlineStatus
        },
        {
            step: "Set Urls and other options that rely on port/online status",
            fn: async.setOptions
        },
        {
            step: "Setting Internal Events",
            fn: async.setInternalEvents
        },
        {
            step: "Setting file watchers",
            fn: async.setFileWatchers
        },
        {
            step: "Starting the Server",
            fn: async.startServer
        },
        {
            step: "Starting the HTTPS Tunnel",
            fn: async.startTunnel
        },
        {
            step: "Starting the web-socket server",
            fn: async.startSockets
        },
        {
            step: "Init user plugins",
            fn: async.initUserPlugins
        }
    ];

    require("async").eachSeries(asyncTasks, function (item, cb) {
        bs.debug("Starting Step: " + item.step);
        item.fn(bs, function (err, out) {
            if (err) {
                return cb(err);
            }
            if (out) {
                if (out.options) {
                    Object.keys(out.options).forEach(function (key) {
                        bs.setOption(key, out.options[key]);
                    });
                }
                if (out.optionsIn) {
                    out.optionsIn.forEach(function (item) {
                        bs.setOptionIn(item.path, item.value);
                    });
                }
                if (out.instance) {
                    Object.keys(out.instance).forEach(function (key) {
                        bs[key] = out.instance[key];
                    });
                }
            }
            bs.debug("{green:Step Complete: " + item.step);
            cb();
        });
    }, function () {

        bs.events.emit("init", bs);

        /**
         * This is no-longer needed as the Callback now only resolves
         * when everything (including slow thing, like the tunnel) is ready.
         * It's here purely for back-compat.
         */
        bs.events.emit("service:running", {
            options: bs.options,
            baseDir: bs.options.getIn(["server", "baseDir"]),
            type:    bs.options.get("mode"),
            port:    bs.options.get("port"),
            url:     bs.options.getIn(["urls", "local"]),
            urls:    bs.options.get("urls").toJS(),
            tunnel:  bs.options.getIn(["urls", "tunnel"])
        });

        bs.cb(null, bs);
    });

    return this;
};

/**
 * @param module
 * @param opts
 * @param cb
 */
BrowserSync.prototype.registerPlugin = function (module, opts, cb) {

    this.pluginManager.registerPlugin(module, opts, cb);

    if (module["plugin:name"]) {
        this._userPlugins.push(module);
    }
};

/**
 * @param {Function} [filter]
 */
BrowserSync.prototype.getUserPlugins = function (filter) {

    filter = filter || function () {
        return true;
    };

    /**
     * Transform Plugins option
     */
    this.userPlugins = this._userPlugins.filter(filter).map(function (plugin) {
        return {
            name: plugin["plugin:name"],
            active: plugin._enabled
        };
    });

    return this.userPlugins;
};

/**
 * Get middleware
 * @returns {*}
 */
BrowserSync.prototype.getMiddleware = function (type) {

    var types = {
        "connector": messages.socketConnector(this.options.get("port"), this.options.get("socket").toJS(), this.options, true),
        "socket-js": require("./snippet").utils.getSocketScript()
    };

    if (type in types) {
        return function (req, res) {
            res.setHeader("Content-Type", "text/javascript");
            res.end(types[type]);
        };
    }
};

/**
 * @param path
 * @param props
 */
BrowserSync.prototype.serveFile = function (path, props) {
    if (this.app) {
        this.app.use(path, function (req, res) {
            res.setHeader("Content-Type", props.type);
            res.end(props.content);
        });
    }
};

/**
 * Middleware for socket connection (external usage)
 * @param port
 * @param opts
 * @returns {*}
 */
BrowserSync.prototype.getSocketConnector = function (port, opts) {
    return function (req, res) {
        res.setHeader("Content-Type", "text/javascript");
        res.end(messages.socketConnector(port, opts, null, true));
    };
};

/**
 * Callback helper
 * @param err
 * @param [data]
 */
BrowserSync.prototype.callback = function (err, data) {

    if (_.isFunction(this.cb)) {
        this.cb(err, data, this);
    }

    this.events.emit("service:ready");
};

/**
 * Callback helper
 * @param name
 */
BrowserSync.prototype.getOption = function (name) {
    this.debug("Getting option: {magenta:%s", name);
    return this.options.get(name);
};

/**
 * Callback helper
 * @param path
 */
BrowserSync.prototype.getOptionIn = function (path) {
    this.debug("Getting option via path: {magenta:%s", path);
    return this.options.getIn(path);
};

/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getOptions = function () {
    return this.options;
};

/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getLogger = logger.getLogger;

/**
 * @param {String} name
 * @param {*} value
 * @returns {BrowserSync.options|*}
 */
BrowserSync.prototype.setOption = function (name, value, silent) {
    this.debug("Setting Option: {cyan:%s} - {magenta:%s", name, value.toString());
    this.options = this.options.set(name, value);
    if (!silent) {
        this.events.emit("options:set", {path: name, value: value, options: this.options});
    }
    return this.options;
};

BrowserSync.prototype.setOptionIn = function (path, value, silent) {
    this.debug("Setting Option: {cyan:%s} - {magenta:%s", path.join("."), value.toString());
    this.options = this.options.setIn(path, value);
    if (!silent) {
        this.events.emit("options:set", {path: path, value: value, options: this.options});
    }
    return this.options;
};

/**
 * Set multiple options with mutations
 * @param fn
 * @returns {Map|*}
 */
BrowserSync.prototype.setMany = function (fn, silent) {
    this.debug("Setting multiple Options");
    this.options = this.options.withMutations(fn);
    if (!silent) {
        this.events.emit("options:set", {options: this.options.toJS()});
    }
    return this.options;
};

/**
 * Handle Browser Reloads
 */
BrowserSync.prototype.doBrowserReload = function () {
    var bs = this;
    if (bs._browserReload) {
        return;
    }
    bs._browserReload = setTimeout(function () {
        bs.io.sockets.emit("browser:reload");
        clearTimeout(bs._browserReload);
        bs._browserReload = false;
    }, this.options.get("reloadDelay"));
};

/**
 * Handle a queue of reloads
 * @param {Object} data
 */
BrowserSync.prototype.doFileReload = function (data) {

    var bs = this;

    bs._reloadQueue = bs._reloadQueue || [];
    bs._reloadQueue.push(data);

    if (bs._reloadTimer) {
        return;
    }

    bs._reloadTimer = setTimeout(function () {
        if (utils.willCauseReload(bs._reloadQueue.map(function (item) { return item.path; }), bs.options.get("injectFileTypes").toJS())) {
            bs.io.sockets.emit("browser:reload");
        } else {
            bs._reloadQueue.forEach(function (item) {
                bs.io.sockets.emit("file:reload", item);
            });
        }
        clearTimeout(bs._reloadTimer);
        bs._reloadTimer = undefined;
        bs._reloadQueue = [];
    }, bs.options.get("reloadDelay"));
};

/**
 * Instance Cleanup
 */
BrowserSync.prototype.cleanup = function (cb) {

    if (!this.active) {
        return;
    }

    // Close any servers
    if (this.server) {
        this.debug("Closing server...");
        this.server.close();
    }

    // Stop any file watching
    if (this.watchers) {
        this.debug("Stopping watchers...");
        _.each(this.watchers, function (item) {
            item.watcher.end();
        });
    }

    // Remove all event listeners
    if (this.events) {
        this.debug("Removing event listeners...");
        this.events.removeAllListeners();
    }

    // Reset the flag
    this.debug("Setting {magenta:active: false");
    this.active = false;
    this.paused = false;

    this.pluginManager.plugins       = {};
    this.pluginManager.pluginOptions = {};

    this._userPlugins                = [];
    this.userPlugins                 = [];
    this._reloadTimer                = undefined;
    this._reloadQueue                = undefined;

    if (_.isFunction(cb)) {
        cb(null, this);
    }
};

/**
 * @param {Object} data
 * @param {Object} options
 * @returns {{assetFileName: String}}
 */
BrowserSync.prototype.changeFile = function (data, options) {

    var path = data.path;
    var fileName = filePath.basename(path);

    var fileExtension = utils.getFileExtension(path);

    var obj = {
        assetFileName: fileName,
        fileExtension: fileExtension
    };

    var message = "inject";

    // RELOAD page
    if (!_.contains(options.injectFileTypes, fileExtension)) {
        obj.url = path;
        message = "reload";
    }

    obj.cwd = this.cwd;
    obj.path = path;
    obj.type = message;
    obj.log  = data.log;

    // emit the event through socket
    this.events.emit("file:reload", obj);

    return obj;
};

module.exports = BrowserSync;
<commit_msg>Use opts object for option setting<commit_after>"use strict";

var hooks           = require("./hooks");
var async           = require("./async");
var config          = require("./config");
var messages        = require("./connect-utils");
var utils           = require("./utils");
var logger          = require("./logger");

var _               = require("lodash");
var EE              = require("easy-extender");
var filePath        = require("path");

/**
 * Required internal plugin, all can be overridden.
 */
var defaultPlugins = {
    "logger":        logger,
    "socket":        require("./sockets"),
    "file:watcher":  require("./file-watcher"),
    "server":        require("./server"),
    "tunnel":        require("./tunnel"),
    "client:script": require("browser-sync-client")
};

/**
 * @constructor
 */
var BrowserSync = function (emitter) {

    this.cwd      = process.cwd();
    this.active   = false;
    this.paused   = false;
    this.config   = config;

    this.events   = emitter;

    this._reloadQueue   = [];
    this._browserReload = false;

    // Plugin management
    this.pluginManager = new EE(defaultPlugins, hooks);

    this._userPlugins  = [];

    this.clientEvents  = [
        "scroll",
        "input:text",
        "input:toggles",
        "form:submit",
        "form:reset",
        "click"
    ];
};

/**
 * @param {Map} options
 * @param {Function} cb
 * @returns {BrowserSync}
 */
BrowserSync.prototype.init = function (options, cb) {

    var bs = this;
    bs.cb  = cb || function () {/*noop*/};

    // verify/update options
    utils.verifyConfig(options, bs.cb);
    bs.options = utils.updateOptions(options);
    bs.pluginManager.init();

    // set loggers
    bs.logger   = bs.pluginManager.get("logger")(bs.events, bs);
    bs.debugger = bs.logger.clone({useLevelPrefixes: true});
    bs.debug    = bs.debugger.debug;

    var asyncTasks = [
        {
            step: "Finding an empty port",
            fn: async.getEmptyPort
        },
        {
            step: "Checking online status",
            fn: async.getOnlineStatus
        },
        {
            step: "Set Urls and other options that rely on port/online status",
            fn: async.setOptions
        },
        {
            step: "Setting Internal Events",
            fn: async.setInternalEvents
        },
        {
            step: "Setting file watchers",
            fn: async.setFileWatchers
        },
        {
            step: "Starting the Server",
            fn: async.startServer
        },
        {
            step: "Starting the HTTPS Tunnel",
            fn: async.startTunnel
        },
        {
            step: "Starting the web-socket server",
            fn: async.startSockets
        },
        {
            step: "Init user plugins",
            fn: async.initUserPlugins
        }
    ];

    require("async").eachSeries(asyncTasks, function (item, cb) {
        bs.debug("Starting Step: " + item.step);
        item.fn(bs, function (err, out) {
            if (err) {
                return cb(err);
            }
            if (out) {
                if (out.options) {
                    Object.keys(out.options).forEach(function (key) {
                        bs.setOption(key, out.options[key]);
                    });
                }
                if (out.optionsIn) {
                    out.optionsIn.forEach(function (item) {
                        bs.setOptionIn(item.path, item.value);
                    });
                }
                if (out.instance) {
                    Object.keys(out.instance).forEach(function (key) {
                        bs[key] = out.instance[key];
                    });
                }
            }
            bs.debug("{green:Step Complete: " + item.step);
            cb();
        });
    }, function () {

        bs.events.emit("init", bs);

        /**
         * This is no-longer needed as the Callback now only resolves
         * when everything (including slow thing, like the tunnel) is ready.
         * It's here purely for back-compat.
         */
        bs.events.emit("service:running", {
            options: bs.options,
            baseDir: bs.options.getIn(["server", "baseDir"]),
            type:    bs.options.get("mode"),
            port:    bs.options.get("port"),
            url:     bs.options.getIn(["urls", "local"]),
            urls:    bs.options.get("urls").toJS(),
            tunnel:  bs.options.getIn(["urls", "tunnel"])
        });

        bs.cb(null, bs);
    });

    return this;
};

/**
 * @param module
 * @param opts
 * @param cb
 */
BrowserSync.prototype.registerPlugin = function (module, opts, cb) {

    this.pluginManager.registerPlugin(module, opts, cb);

    if (module["plugin:name"]) {
        this._userPlugins.push(module);
    }
};

/**
 * @param {Function} [filter]
 */
BrowserSync.prototype.getUserPlugins = function (filter) {

    filter = filter || function () {
        return true;
    };

    /**
     * Transform Plugins option
     */
    this.userPlugins = this._userPlugins.filter(filter).map(function (plugin) {
        return {
            name: plugin["plugin:name"],
            active: plugin._enabled
        };
    });

    return this.userPlugins;
};

/**
 * Get middleware
 * @returns {*}
 */
BrowserSync.prototype.getMiddleware = function (type) {

    var types = {
        "connector": messages.socketConnector(this.options.get("port"), this.options.get("socket").toJS(), this.options, true),
        "socket-js": require("./snippet").utils.getSocketScript()
    };

    if (type in types) {
        return function (req, res) {
            res.setHeader("Content-Type", "text/javascript");
            res.end(types[type]);
        };
    }
};

/**
 * @param path
 * @param props
 */
BrowserSync.prototype.serveFile = function (path, props) {
    if (this.app) {
        this.app.use(path, function (req, res) {
            res.setHeader("Content-Type", props.type);
            res.end(props.content);
        });
    }
};

/**
 * Middleware for socket connection (external usage)
 * @param port
 * @param opts
 * @returns {*}
 */
BrowserSync.prototype.getSocketConnector = function (port, opts) {
    return function (req, res) {
        res.setHeader("Content-Type", "text/javascript");
        res.end(messages.socketConnector(port, opts, null, true));
    };
};

/**
 * Callback helper
 * @param err
 * @param [data]
 */
BrowserSync.prototype.callback = function (err, data) {

    if (_.isFunction(this.cb)) {
        this.cb(err, data, this);
    }

    this.events.emit("service:ready");
};

/**
 * Callback helper
 * @param name
 */
BrowserSync.prototype.getOption = function (name) {
    this.debug("Getting option: {magenta:%s", name);
    return this.options.get(name);
};

/**
 * Callback helper
 * @param path
 */
BrowserSync.prototype.getOptionIn = function (path) {
    this.debug("Getting option via path: {magenta:%s", path);
    return this.options.getIn(path);
};

/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getOptions = function () {
    return this.options;
};

/**
 * @returns {BrowserSync.options}
 */
BrowserSync.prototype.getLogger = logger.getLogger;

/**
 * @param {String} name
 * @param {*} value
 * @returns {BrowserSync.options|*}
 */
BrowserSync.prototype.setOption = function (name, value, opts) {
    opts = opts || {};
    this.debug("Setting Option: {cyan:%s} - {magenta:%s", name, value.toString());
    this.options = this.options.set(name, value);
    if (!opts.silent) {
        this.events.emit("options:set", {path: name, value: value, options: this.options});
    }
    return this.options;
};

BrowserSync.prototype.setOptionIn = function (path, value, opts) {
    opts = opts || {};
    this.debug("Setting Option: {cyan:%s} - {magenta:%s", path.join("."), value.toString());
    this.options = this.options.setIn(path, value);
    if (!opts.silent) {
        this.events.emit("options:set", {path: path, value: value, options: this.options});
    }
    return this.options;
};

/**
 * Set multiple options with mutations
 * @param fn
 * @returns {Map|*}
 */
BrowserSync.prototype.setMany = function (fn, opts) {
    opts = opts || {};
    this.debug("Setting multiple Options");
    this.options = this.options.withMutations(fn);
    if (!opts.silent) {
        this.events.emit("options:set", {options: this.options.toJS()});
    }
    return this.options;
};

/**
 * Handle Browser Reloads
 */
BrowserSync.prototype.doBrowserReload = function () {
    var bs = this;
    if (bs._browserReload) {
        return;
    }
    bs._browserReload = setTimeout(function () {
        bs.io.sockets.emit("browser:reload");
        clearTimeout(bs._browserReload);
        bs._browserReload = false;
    }, this.options.get("reloadDelay"));
};

/**
 * Handle a queue of reloads
 * @param {Object} data
 */
BrowserSync.prototype.doFileReload = function (data) {

    var bs = this;

    bs._reloadQueue = bs._reloadQueue || [];
    bs._reloadQueue.push(data);

    if (bs._reloadTimer) {
        return;
    }

    bs._reloadTimer = setTimeout(function () {
        if (utils.willCauseReload(bs._reloadQueue.map(function (item) { return item.path; }), bs.options.get("injectFileTypes").toJS())) {
            bs.io.sockets.emit("browser:reload");
        } else {
            bs._reloadQueue.forEach(function (item) {
                bs.io.sockets.emit("file:reload", item);
            });
        }
        clearTimeout(bs._reloadTimer);
        bs._reloadTimer = undefined;
        bs._reloadQueue = [];
    }, bs.options.get("reloadDelay"));
};

/**
 * Instance Cleanup
 */
BrowserSync.prototype.cleanup = function (cb) {

    if (!this.active) {
        return;
    }

    // Close any servers
    if (this.server) {
        this.debug("Closing server...");
        this.server.close();
    }

    // Stop any file watching
    if (this.watchers) {
        this.debug("Stopping watchers...");
        _.each(this.watchers, function (item) {
            item.watcher.end();
        });
    }

    // Remove all event listeners
    if (this.events) {
        this.debug("Removing event listeners...");
        this.events.removeAllListeners();
    }

    // Reset the flag
    this.debug("Setting {magenta:active: false");
    this.active = false;
    this.paused = false;

    this.pluginManager.plugins       = {};
    this.pluginManager.pluginOptions = {};

    this._userPlugins                = [];
    this.userPlugins                 = [];
    this._reloadTimer                = undefined;
    this._reloadQueue                = undefined;

    if (_.isFunction(cb)) {
        cb(null, this);
    }
};

/**
 * @param {Object} data
 * @param {Object} options
 * @returns {{assetFileName: String}}
 */
BrowserSync.prototype.changeFile = function (data, options) {

    var path = data.path;
    var fileName = filePath.basename(path);

    var fileExtension = utils.getFileExtension(path);

    var obj = {
        assetFileName: fileName,
        fileExtension: fileExtension
    };

    var message = "inject";

    // RELOAD page
    if (!_.contains(options.injectFileTypes, fileExtension)) {
        obj.url = path;
        message = "reload";
    }

    obj.cwd = this.cwd;
    obj.path = path;
    obj.type = message;
    obj.log  = data.log;

    // emit the event through socket
    this.events.emit("file:reload", obj);

    return obj;
};

module.exports = BrowserSync;
<|endoftext|>