<commit_before>/*global assignView, assignTemplate, createRegistryWrapper, dataObject, getValue, modelCidAttributeName, viewCidAttributeName */
var _fetch = Backbone.Collection.prototype.fetch,
    _set = Backbone.Collection.prototype.set,
    _replaceHTML = Thorax.View.prototype._replaceHTML,
    collectionCidAttributeName = 'data-collection-cid',
    collectionEmptyAttributeName = 'data-collection-empty',
    collectionElementAttributeName = 'data-collection-element',
    ELEMENT_NODE_TYPE = 1;

Thorax.Collection = Backbone.Collection.extend({
  model: Thorax.Model || Backbone.Model,
  initialize: function() {
    this.cid = _.uniqueId('collection');
    return Backbone.Collection.prototype.initialize.apply(this, arguments);
  },
  isEmpty: function() {
    if (this.length > 0) {
      return false;
    } else {
      return this.length === 0 && this.isPopulated();
    }
  },
  isPopulated: function() {
    return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
  },
  shouldFetch: function(options) {
    return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
  },
  fetch: function(options) {
    options = options || {};
    var success = options.success;
    options.success = function(collection, response) {
      collection._fetched = true;
      success && success(collection, response);
    };
    return _fetch.apply(this, arguments);
  },
  set: function(models, options) {
    this._fetched = !!models;
    return _set.call(this, models, options);
  }
});

_.extend(Thorax.View.prototype, {
  getCollectionViews: function(collection) {
    return _.filter(this.children, function(child) {
      if (!(child instanceof Thorax.CollectionView)) {
        return false;
      }

      return !collection || (child.collection === collection);
    });
  },
  updateFilter: function(collection) {
    _.invoke(this.getCollectionViews(collection), 'updateFilter');
  }
});

Thorax.Collections = {};
createRegistryWrapper(Thorax.Collection, Thorax.Collections);

dataObject('collection', {
  set: 'setCollection',
  bindCallback: onSetCollection,
  defaultOptions: {
    render: undefined,    // Default to deferred rendering
    fetch: true,
    success: false,
    invalid: true,
    change: true          // Wether or not to re-render on model:change
  },
  change: onCollectionReset,
  $el: 'getCollectionElement',
  cidAttrName: collectionCidAttributeName
});

Thorax.CollectionView = Thorax.View.extend({
  _defaultTemplate: Handlebars.VM.noop,
  _collectionSelector: '[' + collectionElementAttributeName + ']',

  // preserve collection element if it was not created with {{collection}} helper
  _replaceHTML: function(html) {
    if (this.collection && this.getObjectOptions(this.collection) && this._renderCount) {
      var element;
      var oldCollectionElement = this._collectionElement;
      element = _replaceHTML.call(this, html);

      this._lookupCollectionElement();

      if (!oldCollectionElement.attr('data-view-cid')) {
        this._collectionElement.replaceWith(oldCollectionElement);
      }
    } else {
      var ret = _replaceHTML.call(this, html);
      this._lookupCollectionElement();

      return ret;
    }
  },

  render: function() {
    var shouldRender = this.shouldRender();

    Thorax.View.prototype.render.apply(this, arguments);
    if (!shouldRender) {
      this.renderCollection();
    }
  },

  //appendItem(model [,index])
  //appendItem(html_string, index)
  //appendItem(view, index)
  appendItem: function(model, index, options) {
    //empty item
    if (!model) {
      return;
    }
    var itemView,
        $el = this._collectionElement,
        collection = this.collection;
    options = _.defaults(options || {}, {
      filter: true
    });
    //if index argument is a view
    index && index.el && (index = $el.children().indexOf(index.el) + 1);
    //if argument is a view, or html string
    if (model.el || _.isString(model)) {
      itemView = model;
      model = false;
    } else {
      index = index || collection.indexOf(model) || 0;
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      itemView = this.renderItem.call(this, model, index);
    }

    if (itemView) {
      if (itemView.cid) {
        itemView.ensureRendered();
        this._addChild(itemView);
      }

      //if the renderer's output wasn't contained in a tag, wrap it in a div
      //plain text, or a mixture of top level text nodes and element nodes
      //will get wrapped
      if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
        itemView = '
' + itemView + '
';
      }
      var itemElement = itemView.$el || $($.trim(itemView)).filter(function() {
        // Only output nodes. DOM || Fruit Loops
        return this.nodeType === ELEMENT_NODE_TYPE || this.type === 'tag';
      });

      if (model) {
        itemElement.attr(modelCidAttributeName, model.cid);
      }
      var previousModel = index > 0 ? collection.at(index - 1) : false;
      if (!previousModel) {
        $el.prepend(itemElement);
      } else {
        //use last() as appendItem can accept multiple nodes from a template
        var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
        last.after(itemElement);
      }

      this.trigger('append', null, function($el) {
        $el.attr(modelCidAttributeName, model.cid);
      });

      if (!options.silent) {
        this.trigger('rendered:item', this, collection, model, itemElement, index);
      }
      if (options.filter) {
        applyItemVisiblityFilter.call(this, model);
      }
    }
    return itemView;
  },

  // updateItem only useful if there is no item view, otherwise
  // itemView.render() provides the same functionality
  updateItem: function(model) {
    var $el = this._collectionElement,
        viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');

    // NOP For views
    if (viewEl.attr(viewCidAttributeName)) {
      return;
    }

    this.removeItem(viewEl);
    this.appendItem(model);
  },

  removeItem: function(model) {
    var viewEl = model;
    if (model.cid) {
      var $el = this._collectionElement;
      viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
    }
    if (!viewEl.length) {
      return false;
    }

    var viewCids = viewEl.find('[' + viewCidAttributeName + ']').map(function(i, el) {
      return $(el).attr(viewCidAttributeName);
    });

    viewEl.remove();

    viewCids.push(viewEl.attr(viewCidAttributeName));
    _.each(viewCids, function(cid) {
      var child = this.children[cid];
      if (child) {
        this._removeChild(child);
      }
    }, this);

    return true;
  },

  renderCollection: function() {
    if (this.collection) {
      if (this.collection.isEmpty()) {
        handleChangeFromNotEmptyToEmpty.call(this);
      } else {
        handleChangeFromEmptyToNotEmpty.call(this);
        this.collection.forEach(function(item, i) {
          this.appendItem(item, i);
        }, this);
      }
      this.trigger('rendered:collection', this, this.collection);
    } else {
      handleChangeFromNotEmptyToEmpty.call(this);
    }
  },
  emptyClass: 'empty',
  renderEmpty: function() {
    if (!this.emptyView) {
      assignView.call(this, 'emptyView', {
        extension: '-empty'
      });
    }
    if (!this.emptyTemplate && !this.emptyView) {
      assignTemplate.call(this, 'emptyTemplate', {
        extension: '-empty',
        required: false
      });
    }
    if (this.emptyView) {
      var viewOptions = {};
      if (this.emptyTemplate) {
        viewOptions.template = this.emptyTemplate;
      }
      var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
      view.ensureRendered();
      return view;
    } else {
      return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
    }
  },
  renderItem: function(model, i) {
    if (!this.itemView) {
      assignView.call(this, 'itemView', {
        extension: '-item',
        required: false
      });
    }
    if (!this.itemTemplate && !this.itemView) {
      assignTemplate.call(this, 'itemTemplate', {
        extension: '-item',
        // only require an itemTemplate if an itemView
        // is not present
        required: !this.itemView
      });
    }
    if (this.itemView) {
      var viewOptions = {
        model: model
      };
      if (this.itemTemplate) {
        viewOptions.template = this.itemTemplate;
      }
      return Thorax.Util.getViewInstance(this.itemView, viewOptions);
    } else {
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i));
    }
  },
  itemContext: function(model /*, i */) {
    return model.attributes;
  },
  appendEmpty: function() {
    var $el = this._collectionElement;
    $el.empty();

    // Using call here to avoid v8 prototype inline optimization bug that helper views
    // expose under Android 4.3 (at minimum)
    // https://twitter.com/kpdecker/status/422149634929082370
    var emptyContent = this.renderEmpty.call(this);
    emptyContent && this.appendItem(emptyContent, 0, {
      silent: true,
      filter: false
    });
    this.trigger('rendered:empty', this, this.collection);
  },
  getCollectionElement: function() {
    return this._collectionElement;
  },
  _lookupCollectionElement: function() {
    var $collectionElement = this.$(this._collectionSelector);
    this._collectionElement = $collectionElement.length ? $collectionElement : this.$el;
  },

  updateFilter: function() {
    applyVisibilityFilter.call(this);
  }
});

Thorax.CollectionView.on({
  collection: {
    reset: onCollectionReset,
    sort: onCollectionReset,
    change: function(model) {
      var options = this.getObjectOptions(this.collection);
      if (options && options.change) {
        this.updateItem(model);
      }
      applyItemVisiblityFilter.call(this, model);
    },
    add: function(model) {
      var $el = this._collectionElement;
      if ($el.length) {
        if (this.collection.length === 1) {
          handleChangeFromEmptyToNotEmpty.call(this);
        }

        var index = this.collection.indexOf(model);
        this.appendItem(model, index);
      }
    },
    remove: function(model) {
      var $el = this._collectionElement;
      this.removeItem(model);
      this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
    }
  }
});

Thorax.View.on({
  collection: {
    invalid: function(collection, message) {
      if (this.getObjectOptions(collection).invalid) {
        this.trigger('invalid', message, collection);
      }
    },
    error: function(collection, resp /*, options */) {
      this.trigger('error', resp, collection);
    }
  }
});

function onCollectionReset(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    this.renderCollection && this.renderCollection();
  }
}

// Even if the view is not a CollectionView
// ensureRendered() to provide similar behavior
// to a model
function onSetCollection(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    // Ensure that something is there if we are going to render the collection.
    this.ensureRendered();
  }
}

function applyVisibilityFilter() {
  if (this.itemFilter) {
    this.collection.forEach(applyItemVisiblityFilter, this);
  }
}

function applyItemVisiblityFilter(model) {
  var $el = this._collectionElement;
  this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
}

function itemShouldBeVisible(model) {
  // Using call here to avoid v8 prototype inline optimization bug that helper views
  // expose under Android 4.3 (at minimum)
  // https://twitter.com/kpdecker/status/422149634929082370
  return this.itemFilter.call(this, model, this.collection.indexOf(model));
}

function handleChangeFromEmptyToNotEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.removeClass(this.emptyClass);
  $el.removeAttr(collectionEmptyAttributeName);
  $el.empty();
}

function handleChangeFromNotEmptyToEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.addClass(this.emptyClass);
  $el.attr(collectionEmptyAttributeName, true);
  this.appendEmpty();
}

//$(selector).collection() helper
$.fn.collection = function(view) {
  if (view && view.collection) {
    return view.collection;
  }
  var $this = $(this),
      collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
      collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
  if (collectionCid) {
    view = $this.view();
    if (view) {
      return view.collection;
    }
  }
  return false;
};
<commit_msg>Rework collection.removeItem to avoid map object<commit_after>/*global assignView, assignTemplate, createRegistryWrapper, dataObject, getValue, modelCidAttributeName, viewCidAttributeName */
var _fetch = Backbone.Collection.prototype.fetch,
    _set = Backbone.Collection.prototype.set,
    _replaceHTML = Thorax.View.prototype._replaceHTML,
    collectionCidAttributeName = 'data-collection-cid',
    collectionEmptyAttributeName = 'data-collection-empty',
    collectionElementAttributeName = 'data-collection-element',
    ELEMENT_NODE_TYPE = 1;

Thorax.Collection = Backbone.Collection.extend({
  model: Thorax.Model || Backbone.Model,
  initialize: function() {
    this.cid = _.uniqueId('collection');
    return Backbone.Collection.prototype.initialize.apply(this, arguments);
  },
  isEmpty: function() {
    if (this.length > 0) {
      return false;
    } else {
      return this.length === 0 && this.isPopulated();
    }
  },
  isPopulated: function() {
    return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
  },
  shouldFetch: function(options) {
    return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
  },
  fetch: function(options) {
    options = options || {};
    var success = options.success;
    options.success = function(collection, response) {
      collection._fetched = true;
      success && success(collection, response);
    };
    return _fetch.apply(this, arguments);
  },
  set: function(models, options) {
    this._fetched = !!models;
    return _set.call(this, models, options);
  }
});

_.extend(Thorax.View.prototype, {
  getCollectionViews: function(collection) {
    return _.filter(this.children, function(child) {
      if (!(child instanceof Thorax.CollectionView)) {
        return false;
      }

      return !collection || (child.collection === collection);
    });
  },
  updateFilter: function(collection) {
    _.invoke(this.getCollectionViews(collection), 'updateFilter');
  }
});

Thorax.Collections = {};
createRegistryWrapper(Thorax.Collection, Thorax.Collections);

dataObject('collection', {
  set: 'setCollection',
  bindCallback: onSetCollection,
  defaultOptions: {
    render: undefined,    // Default to deferred rendering
    fetch: true,
    success: false,
    invalid: true,
    change: true          // Wether or not to re-render on model:change
  },
  change: onCollectionReset,
  $el: 'getCollectionElement',
  cidAttrName: collectionCidAttributeName
});

Thorax.CollectionView = Thorax.View.extend({
  _defaultTemplate: Handlebars.VM.noop,
  _collectionSelector: '[' + collectionElementAttributeName + ']',

  // preserve collection element if it was not created with {{collection}} helper
  _replaceHTML: function(html) {
    if (this.collection && this.getObjectOptions(this.collection) && this._renderCount) {
      var element;
      var oldCollectionElement = this._collectionElement;
      element = _replaceHTML.call(this, html);

      this._lookupCollectionElement();

      if (!oldCollectionElement.attr('data-view-cid')) {
        this._collectionElement.replaceWith(oldCollectionElement);
      }
    } else {
      var ret = _replaceHTML.call(this, html);
      this._lookupCollectionElement();

      return ret;
    }
  },

  render: function() {
    var shouldRender = this.shouldRender();

    Thorax.View.prototype.render.apply(this, arguments);
    if (!shouldRender) {
      this.renderCollection();
    }
  },

  //appendItem(model [,index])
  //appendItem(html_string, index)
  //appendItem(view, index)
  appendItem: function(model, index, options) {
    //empty item
    if (!model) {
      return;
    }
    var itemView,
        $el = this._collectionElement,
        collection = this.collection;
    options = _.defaults(options || {}, {
      filter: true
    });
    //if index argument is a view
    index && index.el && (index = $el.children().indexOf(index.el) + 1);
    //if argument is a view, or html string
    if (model.el || _.isString(model)) {
      itemView = model;
      model = false;
    } else {
      index = index || collection.indexOf(model) || 0;
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      itemView = this.renderItem.call(this, model, index);
    }

    if (itemView) {
      if (itemView.cid) {
        itemView.ensureRendered();
        this._addChild(itemView);
      }

      //if the renderer's output wasn't contained in a tag, wrap it in a div
      //plain text, or a mixture of top level text nodes and element nodes
      //will get wrapped
      if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
        itemView = '
' + itemView + '
';
      }
      var itemElement = itemView.$el || $($.trim(itemView)).filter(function() {
        // Only output nodes. DOM || Fruit Loops
        return this.nodeType === ELEMENT_NODE_TYPE || this.type === 'tag';
      });

      if (model) {
        itemElement.attr(modelCidAttributeName, model.cid);
      }
      var previousModel = index > 0 ? collection.at(index - 1) : false;
      if (!previousModel) {
        $el.prepend(itemElement);
      } else {
        //use last() as appendItem can accept multiple nodes from a template
        var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
        last.after(itemElement);
      }

      this.trigger('append', null, function($el) {
        $el.attr(modelCidAttributeName, model.cid);
      });

      if (!options.silent) {
        this.trigger('rendered:item', this, collection, model, itemElement, index);
      }
      if (options.filter) {
        applyItemVisiblityFilter.call(this, model);
      }
    }
    return itemView;
  },

  // updateItem only useful if there is no item view, otherwise
  // itemView.render() provides the same functionality
  updateItem: function(model) {
    var $el = this._collectionElement,
        viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');

    // NOP For views
    if (viewEl.attr(viewCidAttributeName)) {
      return;
    }

    this.removeItem(viewEl);
    this.appendItem(model);
  },

  removeItem: function(model) {
    var self = this,
        viewEl = model;

    if (model.cid) {
      var $el = this._collectionElement;
      viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
    }
    if (!viewEl.length) {
      return false;
    }

    function cleanCid(viewEl) {
      var cid = viewEl.attr(viewCidAttributeName),
          child = self.children[cid];
      if (child) {
        self._removeChild(child);
      }
    }

    viewEl.find('[' + viewCidAttributeName + ']').each(function(i, el) {
      cleanCid($(el));
    });
    cleanCid(viewEl);

    viewEl.detach();

    return true;
  },

  renderCollection: function() {
    if (this.collection) {
      if (this.collection.isEmpty()) {
        handleChangeFromNotEmptyToEmpty.call(this);
      } else {
        handleChangeFromEmptyToNotEmpty.call(this);
        this.collection.forEach(function(item, i) {
          this.appendItem(item, i);
        }, this);
      }
      this.trigger('rendered:collection', this, this.collection);
    } else {
      handleChangeFromNotEmptyToEmpty.call(this);
    }
  },
  emptyClass: 'empty',
  renderEmpty: function() {
    if (!this.emptyView) {
      assignView.call(this, 'emptyView', {
        extension: '-empty'
      });
    }
    if (!this.emptyTemplate && !this.emptyView) {
      assignTemplate.call(this, 'emptyTemplate', {
        extension: '-empty',
        required: false
      });
    }
    if (this.emptyView) {
      var viewOptions = {};
      if (this.emptyTemplate) {
        viewOptions.template = this.emptyTemplate;
      }
      var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
      view.ensureRendered();
      return view;
    } else {
      return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
    }
  },
  renderItem: function(model, i) {
    if (!this.itemView) {
      assignView.call(this, 'itemView', {
        extension: '-item',
        required: false
      });
    }
    if (!this.itemTemplate && !this.itemView) {
      assignTemplate.call(this, 'itemTemplate', {
        extension: '-item',
        // only require an itemTemplate if an itemView
        // is not present
        required: !this.itemView
      });
    }
    if (this.itemView) {
      var viewOptions = {
        model: model
      };
      if (this.itemTemplate) {
        viewOptions.template = this.itemTemplate;
      }
      return Thorax.Util.getViewInstance(this.itemView, viewOptions);
    } else {
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i));
    }
  },
  itemContext: function(model /*, i */) {
    return model.attributes;
  },
  appendEmpty: function() {
    var $el = this._collectionElement;
    $el.empty();

    // Using call here to avoid v8 prototype inline optimization bug that helper views
    // expose under Android 4.3 (at minimum)
    // https://twitter.com/kpdecker/status/422149634929082370
    var emptyContent = this.renderEmpty.call(this);
    emptyContent && this.appendItem(emptyContent, 0, {
      silent: true,
      filter: false
    });
    this.trigger('rendered:empty', this, this.collection);
  },
  getCollectionElement: function() {
    return this._collectionElement;
  },
  _lookupCollectionElement: function() {
    var $collectionElement = this.$(this._collectionSelector);
    this._collectionElement = $collectionElement.length ? $collectionElement : this.$el;
  },

  updateFilter: function() {
    applyVisibilityFilter.call(this);
  }
});

Thorax.CollectionView.on({
  collection: {
    reset: onCollectionReset,
    sort: onCollectionReset,
    change: function(model) {
      var options = this.getObjectOptions(this.collection);
      if (options && options.change) {
        this.updateItem(model);
      }
      applyItemVisiblityFilter.call(this, model);
    },
    add: function(model) {
      var $el = this._collectionElement;
      if ($el.length) {
        if (this.collection.length === 1) {
          handleChangeFromEmptyToNotEmpty.call(this);
        }

        var index = this.collection.indexOf(model);
        this.appendItem(model, index);
      }
    },
    remove: function(model) {
      var $el = this._collectionElement;
      this.removeItem(model);
      this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
    }
  }
});

Thorax.View.on({
  collection: {
    invalid: function(collection, message) {
      if (this.getObjectOptions(collection).invalid) {
        this.trigger('invalid', message, collection);
      }
    },
    error: function(collection, resp /*, options */) {
      this.trigger('error', resp, collection);
    }
  }
});

function onCollectionReset(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    this.renderCollection && this.renderCollection();
  }
}

// Even if the view is not a CollectionView
// ensureRendered() to provide similar behavior
// to a model
function onSetCollection(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    // Ensure that something is there if we are going to render the collection.
    this.ensureRendered();
  }
}

function applyVisibilityFilter() {
  if (this.itemFilter) {
    this.collection.forEach(applyItemVisiblityFilter, this);
  }
}

function applyItemVisiblityFilter(model) {
  var $el = this._collectionElement;
  this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
}

function itemShouldBeVisible(model) {
  // Using call here to avoid v8 prototype inline optimization bug that helper views
  // expose under Android 4.3 (at minimum)
  // https://twitter.com/kpdecker/status/422149634929082370
  return this.itemFilter.call(this, model, this.collection.indexOf(model));
}

function handleChangeFromEmptyToNotEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.removeClass(this.emptyClass);
  $el.removeAttr(collectionEmptyAttributeName);
  $el.empty();
}

function handleChangeFromNotEmptyToEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.addClass(this.emptyClass);
  $el.attr(collectionEmptyAttributeName, true);
  this.appendEmpty();
}

//$(selector).collection() helper
$.fn.collection = function(view) {
  if (view && view.collection) {
    return view.collection;
  }
  var $this = $(this),
      collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
      collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
  if (collectionCid) {
    view = $this.view();
    if (view) {
      return view.collection;
    }
  }
  return false;
};
<|endoftext|>


<commit_before>'use strict';

var CC = require('chemcalc');
var removeMFLastPart = require('./util/removeMFLastPart.js');

var alert=alert || function(value) {console.log(value)};

// TODO replace from the value coming from chemcalc
var ELECTRON_MASS=5.4857990946e-4;

function combineMFs (keys, options) {
    var options=options || {};
    options.limit=options.limit || 100000;
    if (!Array.isArray(keys)) return [];

    // we allow String delimited by ". , or ;" instead of an array
    for (var i = 0; i < keys.length; i++) {
        if (!Array.isArray(keys[i])) {
            keys[i] = keys[i].split(/[\.,;]/);
        }
    }

    // we allow ranges in a string ...
    // problem with ranges is that we need to now to what the range applies
    for (var i = 0; i < keys.length; i++) {
        var parts=keys[i];
        var newParts=[];
        for (var j=0; j<parts.length; j++) {
            var part=parts[j];
            var comment=part.replace(/^([^$]*\$|.*)/, '');
            part = part.replace(/\$.*/, '');

            if (~part.indexOf('-')) { // there are ranges ... we are in trouble !
                newParts=newParts.concat(processRange(part,comment));
            } else {
                newParts.push(parts[j]); // the part with the comments !
            }
        }
        keys[i]=newParts;
    }

    var results = [];
    var sizes = new Array(keys.length);
    var currents = new Array(keys.length);
    for (var i = 0; i < keys.length; i++) {
        sizes[i] = keys[i].length - 1;
        currents[i] = 0;
    }
    var position = 0;
    var evolution = 0;

    while (position < currents.length) {
        if (currents[position] < sizes[position]) {
            evolution++;
            appendResult(results, currents, keys);
            currents[position]++;
            for (var i = 0; i < position; i++) {
                currents[i] = 0;
            }
            position = 0;
        } else {
            position++;
        }
        if (evolution>options.limit) {
            alert("You have reached the limit. Only the first "+options.limit+" molecular formula will be processed.")
        }
    }
    appendResult(results, currents, keys);
    return results;
}

module.exports = combineMFs;

var ems={};

function getEM(mf) {
    if (! ems[mf]) {
        // we need to calculate based on the mf
        var info=CC.analyseMF(mf);
        ems[mf]={
            em: info.em,
            charge: info.parts[0].charge || 0
        }
    }
    return {
        em: ems[mf].em,
        charge: ems[mf].charge
    }
}

function getEMFromParts(parts, currents) {
    var charge=0;
    var em=0;

    for (var i = 0; i < parts.length; i++) {
        var part = parts[i][currents[i]];
        if (part) {
            var info=getEM(part);
            charge+=info.charge;
            em+=info.em;
        }
    }
    var msem=em;
    if (charge>0) {
        msem=msem/charge-ELECTRON_MASS;
    } else if (charge<0) {
        msem=msem/(charge*-1)+ELECTRON_MASS;
    } else {
        msem=0;
    }
    return {
        charge: charge,
        em: em,
        msem: msem
    }
}

function appendResult(results, currents, keys) {
    // this script is designed to combine molecular formula
    // that may contain comments after a "$" sign
    // therefore we should put all the comments at the ned
    var result = {mf: ''};
    var comments = [];

    for (var i = 0; i < keys.length; i++) {
        var key = keys[i][currents[i]];
        if (key) {
            result['part' + (i + 1)] = key;
            if (key.indexOf('$') > -1) {
                comments.push(key.replace(/^[^$]*\$/, ''));
                key = key.replace(/\$.*/, '');
            }
            result.mf += key;
        }
        var info=getEMFromParts(keys, currents);
        result.em=info.em;
        result.msem=info.msem;
        result.charge=info.charge;
    }

    if (comments.length > 0) result.mf += '$' + comments.join(' ');

    results.push(result);
}

function processRange(string, comment) {
    var results=[];
    var parts=string.split(/([0-9]+-[0-9]+)/).filter(function(value) { if (value) return value });
    var position=-1;
    var mfs=[];
    for (var i=0; i<parts.length; i++) {
        var part=parts[i];
        if (!~part.search(/[0-9]-[0-9]/)) {
            position++;
            mfs[position]={
                mf:part,
                min:1,
                max:1
            }
        } else {
            mfs[position].min=part.replace(/^(-?[0-9]*)-(-?[0-9]*)/,"$1")>>0;
            mfs[position].max=part.replace(/^(-?[0-9]*)-(-?[0-9]*)/,"$2")>>0;
        }
    }

    var currents = new Array(mfs.length);
    for (var i = 0; i < currents.length; i++) {
        currents[i] = mfs[i].min;
    }
    var position = 0;
    while (position < currents.length) {
        if (currents[position] < mfs[position].max) {
            results.push(getMF(mfs, currents, comment));
            currents[position]++;
            for (var i = 0; i < position; i++) {
                currents[i] = mfs[i].min;
            }
            position = 0;
        } else {
            position++;
        }
    }
    results.push(getMF(mfs, currents, comment));
    return results;
}

function getMF(mfs, currents, comment) {
    var mf="";
    for (var i=0; i<mfs.length; i++) {
        if (currents[i]===0) {
            // TODO we need to remove from currents[i] till we reach another part of the MF
            mf+=removeMFLastPart(mfs[i].mf);
        } else {
            mf+=mfs[i].mf;
            if (currents[i]!==1) {
                mf+=currents[i];
            }
        }
    }
    if (comment) mf+="$"+comment;
    return mf;
}

<commit_msg>Add window.alert<commit_after>'use strict';

var CC = require('chemcalc');
var removeMFLastPart = require('./util/removeMFLastPart.js');

var window = window || {};
window.alert || function(value) {console.log(value)};

// TODO replace from the value coming from chemcalc
var ELECTRON_MASS=5.4857990946e-4;

function combineMFs (keys, options) {
    var options=options || {};
    options.limit=options.limit || 100000;
    if (!Array.isArray(keys)) return [];

    // we allow String delimited by ". , or ;" instead of an array
    for (var i = 0; i < keys.length; i++) {
        if (!Array.isArray(keys[i])) {
            keys[i] = keys[i].split(/[\.,;]/);
        }
    }

    // we allow ranges in a string ...
    // problem with ranges is that we need to now to what the range applies
    for (var i = 0; i < keys.length; i++) {
        var parts=keys[i];
        var newParts=[];
        for (var j=0; j<parts.length; j++) {
            var part=parts[j];
            var comment=part.replace(/^([^$]*\$|.*)/, '');
            part = part.replace(/\$.*/, '');

            if (~part.indexOf('-')) { // there are ranges ... we are in trouble !
                newParts=newParts.concat(processRange(part,comment));
            } else {
                newParts.push(parts[j]); // the part with the comments !
            }
        }
        keys[i]=newParts;
    }

    var results = [];
    var sizes = new Array(keys.length);
    var currents = new Array(keys.length);
    for (var i = 0; i < keys.length; i++) {
        sizes[i] = keys[i].length - 1;
        currents[i] = 0;
    }
    var position = 0;
    var evolution = 0;

    while (position < currents.length) {
        if (currents[position] < sizes[position]) {
            evolution++;
            appendResult(results, currents, keys);
            currents[position]++;
            for (var i = 0; i < position; i++) {
                currents[i] = 0;
            }
            position = 0;
        } else {
            position++;
        }
        if (evolution>options.limit) {
            window.alert("You have reached the limit. Only the first "+options.limit+" molecular formula will be processed.");
            break;
        }
    }
    appendResult(results, currents, keys);
    return results;
}

module.exports = combineMFs;

var ems={};

function getEM(mf) {
    if (! ems[mf]) {
        // we need to calculate based on the mf
        var info=CC.analyseMF(mf);
        ems[mf]={
            em: info.em,
            charge: info.parts[0].charge || 0
        }
    }
    return {
        em: ems[mf].em,
        charge: ems[mf].charge
    }
}

function getEMFromParts(parts, currents) {
    var charge=0;
    var em=0;

    for (var i = 0; i < parts.length; i++) {
        var part = parts[i][currents[i]];
        if (part) {
            var info=getEM(part);
            charge+=info.charge;
            em+=info.em;
        }
    }
    var msem=em;
    if (charge>0) {
        msem=msem/charge-ELECTRON_MASS;
    } else if (charge<0) {
        msem=msem/(charge*-1)+ELECTRON_MASS;
    } else {
        msem=0;
    }
    return {
        charge: charge,
        em: em,
        msem: msem
    }
}

function appendResult(results, currents, keys) {
    // this script is designed to combine molecular formula
    // that may contain comments after a "$" sign
    // therefore we should put all the comments at the ned
    var result = {mf: ''};
    var comments = [];

    for (var i = 0; i < keys.length; i++) {
        var key = keys[i][currents[i]];
        if (key) {
            result['part' + (i + 1)] = key;
            if (key.indexOf('$') > -1) {
                comments.push(key.replace(/^[^$]*\$/, ''));
                key = key.replace(/\$.*/, '');
            }
            result.mf += key;
        }
        var info=getEMFromParts(keys, currents);
        result.em=info.em;
        result.msem=info.msem;
        result.charge=info.charge;
    }

    if (comments.length > 0) result.mf += '$' + comments.join(' ');

    results.push(result);
}

function processRange(string, comment) {
    var results=[];
    var parts=string.split(/([0-9]+-[0-9]+)/).filter(function(value) { if (value) return value });
    var position=-1;
    var mfs=[];
    for (var i=0; i<parts.length; i++) {
        var part=parts[i];
        if (!~part.search(/[0-9]-[0-9]/)) {
            position++;
            mfs[position]={
                mf:part,
                min:1,
                max:1
            }
        } else {
            mfs[position].min=part.replace(/^(-?[0-9]*)-(-?[0-9]*)/,"$1")>>0;
            mfs[position].max=part.replace(/^(-?[0-9]*)-(-?[0-9]*)/,"$2")>>0;
        }
    }

    var currents = new Array(mfs.length);
    for (var i = 0; i < currents.length; i++) {
        currents[i] = mfs[i].min;
    }
    var position = 0;
    while (position < currents.length) {
        if (currents[position] < mfs[position].max) {
            results.push(getMF(mfs, currents, comment));
            currents[position]++;
            for (var i = 0; i < position; i++) {
                currents[i] = mfs[i].min;
            }
            position = 0;
        } else {
            position++;
        }
    }
    results.push(getMF(mfs, currents, comment));
    return results;
}

function getMF(mfs, currents, comment) {
    var mf="";
    for (var i=0; i<mfs.length; i++) {
        if (currents[i]===0) {
            // TODO we need to remove from currents[i] till we reach another part of the MF
            mf+=removeMFLastPart(mfs[i].mf);
        } else {
            mf+=mfs[i].mf;
            if (currents[i]!==1) {
                mf+=currents[i];
            }
        }
    }
    if (comment) mf+="$"+comment;
    return mf;
}

<|endoftext|>


<commit_before>var assign = require('object-assign');
var cwd = process.cwd();
var fs = require('fs');
var gat = require('gulp-auto-task');
var path = require('path');
var pkg = require(path.join(cwd, '/package.json'));
var semver = require('semver');
var sh = require('shelljs');

function replace (file, pattern, replacement) {
  var str = fs.readFileSync(file).toString();
  str = str.replace(pattern, replacement);
  fs.writeFileSync(file, str);
}

module.exports = function (done) {
  var opts = assign({
    type: 'patch'
  }, gat.opts());
  var currentVersion = pkg.version;
  var nextVersion = opts.semver || semver.inc(
    currentVersion,
    opts.type
  );

  sh.exec('gulp lint');
  sh.exec('gulp test');
  replace('bower.json', currentVersion, nextVersion);
  replace('package.json', currentVersion, nextVersion);
  sh.exec('gulp build');
  sh.exec('git commit -am "' + currentVersion + ' -> ' + nextVersion + '"');
  sh.exec('git tag -a ' + nextVersion + ' -m ' + nextVersion);
  sh.exec('git push');
  sh.exec('git push --tags');
  sh.exec('npm publish');
  done();
};
<commit_msg>Automatically add / remove any dist and lib files before committing.<commit_after>var assign = require('object-assign');
var cwd = process.cwd();
var fs = require('fs');
var gat = require('gulp-auto-task');
var path = require('path');
var pkg = require(path.join(cwd, '/package.json'));
var semver = require('semver');
var sh = require('shelljs');

function replace (file, pattern, replacement) {
  var str = fs.readFileSync(file).toString();
  str = str.replace(pattern, replacement);
  fs.writeFileSync(file, str);
}

module.exports = function (done) {
  var opts = assign({
    type: 'patch'
  }, gat.opts());
  var currentVersion = pkg.version;
  var nextVersion = opts.semver || semver.inc(
    currentVersion,
    opts.type
  );

  sh.exec('gulp lint');
  sh.exec('gulp test');
  replace('bower.json', currentVersion, nextVersion);
  replace('package.json', currentVersion, nextVersion);
  sh.exec('gulp build');
  sh.exec('git add dist/ lib/');
  sh.exec('git commit -am "' + currentVersion + ' -> ' + nextVersion + '"');
  sh.exec('git tag -a ' + nextVersion + ' -m ' + nextVersion);
  sh.exec('git push');
  sh.exec('git push --tags');
  sh.exec('npm publish');
  done();
};
<|endoftext|>


<commit_before>/*
 * grunt-contrib-coffee
 * http://gruntjs.com/
 *
 * Copyright (c) 2014 Eric Woroshow, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
  var path = require('path');
  var chalk = require('chalk');
  var _ = require('lodash');

  var actionCounts = {
    fileCreated: 0,
    mapCreated: 0
  };

  grunt.registerMultiTask('coffee', 'Compile CoffeeScript files into JavaScript', function() {
    var options = this.options({
      bare: false,
      join: false,
      sourceMap: false,
      joinExt: '.src.coffee',
      separator: grunt.util.linefeed
    });

    options.separator = grunt.util.normalizelf(options.separator);

    this.files.forEach(function(f) {
      var validFiles = removeInvalidFiles(f);

      if (options.sourceMap === true) {
        var paths = createOutputPaths(f.dest);
        // add sourceMapDir to options object
        var fileOptions = _.extend({ sourceMapDir: paths.destDir }, options);
        writeFileAndMap(paths, compileWithMaps(validFiles, fileOptions, paths), fileOptions);
      } else if (options.join === true) {
        writeCompiledFile(f.dest, concatInput(validFiles, options));
      } else {
        writeCompiledFile(f.dest, concatOutput(validFiles, options));
      }
    });

    grunt.log.writeln(actionCounts.fileCreated + ' files created.');
    grunt.log.writeln(actionCounts.mapCreated + ' source map files created.');

  });

  var isLiterate = function(ext) {
    return (ext === ".litcoffee" || ext === ".md");
  };

  var removeInvalidFiles = function(files) {
    return files.src.filter(function(filepath) {
      if (!grunt.file.exists(filepath)) {
        grunt.log.warn('Source file "' + filepath + '" not found.');
        return false;
      } else {
        return true;
      }
    });
  };

  var createOutputPaths = function(destination) {
    var fileName = path.basename(destination, path.extname(destination));
    return {
      dest: destination,
      destName: fileName,
      destDir: appendTrailingSlash(path.dirname(destination)),
      mapFileName: fileName + '.js.map'
    };
  };

  var appendTrailingSlash = function(dirname) {
    if (dirname.length > 0 && dirname.slice(-1) !== path.sep) {
      return dirname + path.sep;
    } else {
      return dirname;
    }
  };

  var compileWithMaps = function(files, options, paths) {
    if (!hasUniformExtensions(files)) {
      return;
    }

    var mapOptions, filepath;

    if (files.length > 1) {
      mapOptions = createOptionsForJoin(files, paths, options.separator, options.joinExt);
    } else {
      mapOptions = createOptionsForFile(files[0], paths);
      filepath = files[0];
    }

    options = _.extend({
      generatedFile: path.basename(paths.dest),
      sourceRoot: mapOptions.sourceRoot,
      sourceFiles: mapOptions.sourceFiles
    }, options);

    var output = compileCoffee(mapOptions.code, options, filepath);
    appendFooter(output, paths, options);
    return output;
  };

  var hasUniformExtensions = function(files) {
    // get all extensions for input files
    var extensions = _.uniq(files.map(path.extname));

    if (extensions.length > 1) {
      grunt.fail.warn('Join and sourceMap options require input files share the same extension (found ' + extensions.join(', ') + ').');
      return false;
    } else {
      return true;
    }
  };

  var createOptionsForJoin = function (files, paths, separator, joinExt) {
    var code = concatFiles(files, separator);
    var targetFileName = paths.destName + joinExt;
    grunt.file.write(paths.destDir + targetFileName, code);

    return {
      code: code,
      sourceFiles: [targetFileName],
      sourceRoot: ''
    };
  };

  var concatFiles = function(files, separator) {
    return files.map(grunt.file.read).join(separator);
  };

  var createOptionsForFile = function(file, paths) {
    return {
      code: grunt.file.read(file),
      sourceFiles: [path.basename(file)],
      sourceRoot: appendTrailingSlash(path.relative(paths.destDir, path.dirname(file)))
    };
  };

  var appendFooter = function(output, paths, options) {
    // We need the sourceMappingURL to be relative to the JS path
    var sourceMappingDir = appendTrailingSlash(path.relative(paths.destDir, options.sourceMapDir));
    // Add sourceMappingURL to file footer
    output.js = output.js + '\n//# sourceMappingURL=' + sourceMappingDir + paths.mapFileName + '\n';
  };

  var concatInput = function(files, options) {
    if (hasUniformExtensions(files)) {
      var code = concatFiles(files, options.separator);
      return compileCoffee(code, options);
    }
  };

  var concatOutput = function(files, options) {
    return files.map(function(filepath) {
      var code = grunt.file.read(filepath);
      return compileCoffee(code, options, filepath);
    }).join(options.separator);
  };

  var compileCoffee = function(code, options, filepath) {
    var coffeeOptions = _.clone(options);
    if (filepath) {
      coffeeOptions.filename = filepath;
      coffeeOptions.literate = isLiterate(path.extname(filepath));
    }

    try {
      return require('coffee-script').compile(code, coffeeOptions);
    } catch (e) {
      if (e.location == null ||
          e.location.first_column == null ||
          e.location.first_line == null) {
        grunt.log.error('Got an unexpected exception ' +
                        'from the coffee-script compiler. ' +
                        'The original exception was: ' +
                        e);
        grunt.log.error('(The coffee-script compiler should not raise *unexpected* exceptions. ' +
                        'You can file this error as an issue of the coffee-script compiler: ' +
                        'https://github.com/jashkenas/coffee-script/issues)');
      } else {
        var firstColumn = e.location.first_column;
        var firstLine = e.location.first_line;
        var codeLine = code.split('\n')[firstLine];
        var errorArrows = chalk.red('>>') + ' ';
        var offendingCharacter;

        if (firstColumn < codeLine.length) {
          offendingCharacter = chalk.red(codeLine[firstColumn]);
        } else {
          offendingCharacter = '';
        }

        grunt.log.error(e);
        grunt.log.error('In file: ' + filepath);
        grunt.log.error('On line: ' + firstLine);
        // log erroneous line and highlight offending character
        // grunt.log.error trims whitespace so we have to use grunt.log.writeln
        grunt.log.writeln(errorArrows + codeLine.substring(0, firstColumn) +
                          offendingCharacter + codeLine.substring(firstColumn + 1));
        grunt.log.writeln(errorArrows + grunt.util.repeat(firstColumn, ' ') +
                          chalk.red('^'));
      }
      grunt.fail.warn('CoffeeScript failed to compile.');
    }
  };

  var writeFileAndMap = function(paths, output, options) {
    if (!output || output.js.length === 0) {
      warnOnEmptyFile(paths.dest);
      return;
    }

    writeCompiledFile(paths.dest, output.js);
    options.sourceMapDir = appendTrailingSlash(options.sourceMapDir);
    writeSourceMapFile(options.sourceMapDir + paths.mapFileName, output.v3SourceMap);
  };

  var warnOnEmptyFile = function(path) {
    grunt.log.warn('Destination "' + path + '" not written because compiled files were empty.');
  };

  var writeFile = function(path, output) {
    if (output.length < 1) {
      warnOnEmptyFile(path);
      return false;
    } else {
      grunt.file.write(path, output);
      return true;
    }
  };

  var writeCompiledFile = function(path, output) {
    if (writeFile(path, output)) {
      actionCounts.fileCreated++;
      grunt.verbose.writeln('File ' + chalk.cyan(path) + ' created.');
    }
  };
  var writeSourceMapFile = function(path, output) {
    if (writeFile(path, output)) {
      actionCounts.mapCreated++;
      grunt.verbose.writeln('File ' + chalk.cyan(path) + ' created (source map).');
    }
  };
};
<commit_msg>Switch to "log.ok". Only show source map log if maps are created.<commit_after>/*
 * grunt-contrib-coffee
 * http://gruntjs.com/
 *
 * Copyright (c) 2014 Eric Woroshow, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
  var path = require('path');
  var chalk = require('chalk');
  var _ = require('lodash');

  var actionCounts = {
    fileCreated: 0,
    mapCreated: 0
  };

  grunt.registerMultiTask('coffee', 'Compile CoffeeScript files into JavaScript', function() {
    var options = this.options({
      bare: false,
      join: false,
      sourceMap: false,
      joinExt: '.src.coffee',
      separator: grunt.util.linefeed
    });

    options.separator = grunt.util.normalizelf(options.separator);

    this.files.forEach(function(f) {
      var validFiles = removeInvalidFiles(f);

      if (options.sourceMap === true) {
        var paths = createOutputPaths(f.dest);
        // add sourceMapDir to options object
        var fileOptions = _.extend({ sourceMapDir: paths.destDir }, options);
        writeFileAndMap(paths, compileWithMaps(validFiles, fileOptions, paths), fileOptions);
      } else if (options.join === true) {
        writeCompiledFile(f.dest, concatInput(validFiles, options));
      } else {
        writeCompiledFile(f.dest, concatOutput(validFiles, options));
      }
    });

    grunt.log.ok(actionCounts.fileCreated + ' files created.');
    if (actionCounts.mapCreated > 0) {
      grunt.log.ok(actionCounts.mapCreated + ' source map files created.');
    }

  });

  var isLiterate = function(ext) {
    return (ext === ".litcoffee" || ext === ".md");
  };

  var removeInvalidFiles = function(files) {
    return files.src.filter(function(filepath) {
      if (!grunt.file.exists(filepath)) {
        grunt.log.warn('Source file "' + filepath + '" not found.');
        return false;
      } else {
        return true;
      }
    });
  };

  var createOutputPaths = function(destination) {
    var fileName = path.basename(destination, path.extname(destination));
    return {
      dest: destination,
      destName: fileName,
      destDir: appendTrailingSlash(path.dirname(destination)),
      mapFileName: fileName + '.js.map'
    };
  };

  var appendTrailingSlash = function(dirname) {
    if (dirname.length > 0 && dirname.slice(-1) !== path.sep) {
      return dirname + path.sep;
    } else {
      return dirname;
    }
  };

  var compileWithMaps = function(files, options, paths) {
    if (!hasUniformExtensions(files)) {
      return;
    }

    var mapOptions, filepath;

    if (files.length > 1) {
      mapOptions = createOptionsForJoin(files, paths, options.separator, options.joinExt);
    } else {
      mapOptions = createOptionsForFile(files[0], paths);
      filepath = files[0];
    }

    options = _.extend({
      generatedFile: path.basename(paths.dest),
      sourceRoot: mapOptions.sourceRoot,
      sourceFiles: mapOptions.sourceFiles
    }, options);

    var output = compileCoffee(mapOptions.code, options, filepath);
    appendFooter(output, paths, options);
    return output;
  };

  var hasUniformExtensions = function(files) {
    // get all extensions for input files
    var extensions = _.uniq(files.map(path.extname));

    if (extensions.length > 1) {
      grunt.fail.warn('Join and sourceMap options require input files share the same extension (found ' + extensions.join(', ') + ').');
      return false;
    } else {
      return true;
    }
  };

  var createOptionsForJoin = function (files, paths, separator, joinExt) {
    var code = concatFiles(files, separator);
    var targetFileName = paths.destName + joinExt;
    grunt.file.write(paths.destDir + targetFileName, code);

    return {
      code: code,
      sourceFiles: [targetFileName],
      sourceRoot: ''
    };
  };

  var concatFiles = function(files, separator) {
    return files.map(grunt.file.read).join(separator);
  };

  var createOptionsForFile = function(file, paths) {
    return {
      code: grunt.file.read(file),
      sourceFiles: [path.basename(file)],
      sourceRoot: appendTrailingSlash(path.relative(paths.destDir, path.dirname(file)))
    };
  };

  var appendFooter = function(output, paths, options) {
    // We need the sourceMappingURL to be relative to the JS path
    var sourceMappingDir = appendTrailingSlash(path.relative(paths.destDir, options.sourceMapDir));
    // Add sourceMappingURL to file footer
    output.js = output.js + '\n//# sourceMappingURL=' + sourceMappingDir + paths.mapFileName + '\n';
  };

  var concatInput = function(files, options) {
    if (hasUniformExtensions(files)) {
      var code = concatFiles(files, options.separator);
      return compileCoffee(code, options);
    }
  };

  var concatOutput = function(files, options) {
    return files.map(function(filepath) {
      var code = grunt.file.read(filepath);
      return compileCoffee(code, options, filepath);
    }).join(options.separator);
  };

  var compileCoffee = function(code, options, filepath) {
    var coffeeOptions = _.clone(options);
    if (filepath) {
      coffeeOptions.filename = filepath;
      coffeeOptions.literate = isLiterate(path.extname(filepath));
    }

    try {
      return require('coffee-script').compile(code, coffeeOptions);
    } catch (e) {
      if (e.location == null ||
          e.location.first_column == null ||
          e.location.first_line == null) {
        grunt.log.error('Got an unexpected exception ' +
                        'from the coffee-script compiler. ' +
                        'The original exception was: ' +
                        e);
        grunt.log.error('(The coffee-script compiler should not raise *unexpected* exceptions. ' +
                        'You can file this error as an issue of the coffee-script compiler: ' +
                        'https://github.com/jashkenas/coffee-script/issues)');
      } else {
        var firstColumn = e.location.first_column;
        var firstLine = e.location.first_line;
        var codeLine = code.split('\n')[firstLine];
        var errorArrows = chalk.red('>>') + ' ';
        var offendingCharacter;

        if (firstColumn < codeLine.length) {
          offendingCharacter = chalk.red(codeLine[firstColumn]);
        } else {
          offendingCharacter = '';
        }

        grunt.log.error(e);
        grunt.log.error('In file: ' + filepath);
        grunt.log.error('On line: ' + firstLine);
        // log erroneous line and highlight offending character
        // grunt.log.error trims whitespace so we have to use grunt.log.writeln
        grunt.log.writeln(errorArrows + codeLine.substring(0, firstColumn) +
                          offendingCharacter + codeLine.substring(firstColumn + 1));
        grunt.log.writeln(errorArrows + grunt.util.repeat(firstColumn, ' ') +
                          chalk.red('^'));
      }
      grunt.fail.warn('CoffeeScript failed to compile.');
    }
  };

  var writeFileAndMap = function(paths, output, options) {
    if (!output || output.js.length === 0) {
      warnOnEmptyFile(paths.dest);
      return;
    }

    writeCompiledFile(paths.dest, output.js);
    options.sourceMapDir = appendTrailingSlash(options.sourceMapDir);
    writeSourceMapFile(options.sourceMapDir + paths.mapFileName, output.v3SourceMap);
  };

  var warnOnEmptyFile = function(path) {
    grunt.log.warn('Destination "' + path + '" not written because compiled files were empty.');
  };

  var writeFile = function(path, output) {
    if (output.length < 1) {
      warnOnEmptyFile(path);
      return false;
    } else {
      grunt.file.write(path, output);
      return true;
    }
  };

  var writeCompiledFile = function(path, output) {
    if (writeFile(path, output)) {
      actionCounts.fileCreated++;
      grunt.verbose.writeln('File ' + chalk.cyan(path) + ' created.');
    }
  };
  var writeSourceMapFile = function(path, output) {
    if (writeFile(path, output)) {
      actionCounts.mapCreated++;
      grunt.verbose.writeln('File ' + chalk.cyan(path) + ' created (source map).');
    }
  };
};
<|endoftext|>


<commit_before>var gulp = require("gulp");
var inject = require("gulp-inject");
var argv = process.argv;

module.exports = function() {

	var argvToString = argv.join(" ");

	var argsToFrontPressFilesMap = {
		"--min": "frontpress.min.js" ,
		"--v1": "frontpress.v1.js" ,
		"--min --v1": "frontpress.v1.min.js"
	};

	var argsToAngularFilesMap = {
		"--min": "angular.min.js"
	};

	function _parseArgsToFilePath(argsToFilesMap, defaultFilePath){
	    var filePath;
	    for(argumentKey in argsToFilesMap){
	    	var argumentKeyList = argumentKey.split(" ");
	        var argumentKeyReversed = argumentKeyList.length == 2 ? [argumentKeyList[1],argumentKeyList[0]].join(" ") : argumentKey;
	        if(argvToString.indexOf(argumentKey) != -1 || argvToString.indexOf(argumentKeyReversed) != -1){
	        	filePath = argsToFilesMap[argumentKey];
	        }
	    }
	    if(!filePath){
	    	filePath = defaultFilePath;
	    }
    	console.log("Injected file: " + filePath);
    	return filePath;
	}

	var frontPressFilePath = _parseArgsToFilePath(argsToFrontPressFilesMap, "frontpress.js");
	var angularFilePath = _parseArgsToFilePath(argsToAngularFilesMap, "angular.js");

    var staticFilesList = [
        "./build/js/"+angularFilePath,
        "./build/js/release/"+frontPressFilePath,
        "./build/js/dev/frontpress.constant.js",
        "./build/js/dev/sample-blog.js",
	];

	console.log(staticFilesList);

	var injectOptions = {
		ignorePath: "build/"
	};

    var injectFiles = gulp.src(staticFilesList, {read: true});

	return gulp.src("src/index.html")
		.pipe(inject(injectFiles, injectOptions))
		.pipe(gulp.dest("./build"));
};
<commit_msg>[#97] - Fix inject log messages<commit_after>var gulp = require("gulp");
var inject = require("gulp-inject");
var argv = process.argv;
var gutil = require('gulp-util');

module.exports = function() {

	var argvToString = argv.join(" ");

	var argsToFrontPressFilesMap = {
		"--min": "frontpress.min.js" ,
		"--v1": "frontpress.v1.js" ,
		"--min --v1": "frontpress.v1.min.js"
	};

	var argsToAngularFilesMap = {
		"--min": "angular.min.js"
	};

	function _parseArgsToFilePath(argsToFilesMap, defaultFilePath){
	    var filePath;
	    for(argumentKey in argsToFilesMap){
	    	var argumentKeyList = argumentKey.split(" ");
	        var argumentKeyReversed = argumentKeyList.length == 2 ? [argumentKeyList[1],argumentKeyList[0]].join(" ") : argumentKey;
	        if(argvToString.indexOf(argumentKey) != -1 || argvToString.indexOf(argumentKeyReversed) != -1){
	        	filePath = argsToFilesMap[argumentKey];
	        }
	    }
	    if(!filePath){
	    	filePath = defaultFilePath;
	    }
		gutil.log("Injected file on index: " + gutil.colors.magenta("'" +filePath+ "'"));
    	return filePath;
	}

	var frontPressFilePath = _parseArgsToFilePath(argsToFrontPressFilesMap, "frontpress.js");
	var angularFilePath = _parseArgsToFilePath(argsToAngularFilesMap, "angular.js");

    var staticFilesList = [
        "./build/js/"+angularFilePath,
        "./build/js/release/"+frontPressFilePath,
        "./build/js/dev/frontpress.constant.js",
        "./build/js/dev/sample-blog.js",
	];

	var injectOptions = {
		ignorePath: "build/"
	};

    var injectFiles = gulp.src(staticFilesList, {read: true});

	return gulp.src("src/index.html")
		.pipe(inject(injectFiles, injectOptions))
		.pipe(gulp.dest("./build"));
};
<|endoftext|>


<commit_before>/*
 * # grunt-jsdoxy
 *
 * forked from Matt McManus grunt-dox https://github.com/punkave/grunt-dox
 *
 * Licensed under the MIT license.
 */

var exec = require('child_process').exec,
    fs = require('fs'),
    path = require('path'),
    rimraf = require('rimraf'),
    jade = require('jade');

module.exports = function(grunt) {

  grunt.registerMultiTask('jsdoxy', 'Generate jsdoxy output ', function() {

    var dir = this.filesSrc,
        dest = this.data.dest,
        done = this.async(),
        doxPath = path.resolve(__dirname,'../'),
        _opts = this.options(),
        _args = [],
        outputFile = dest + "output.json";

    if(!fs.existsSync(dest)) {
      fs.mkdirSync(dest);
    }
    if(!fs.existsSync(outputFile)) {
      fs.writeFileSync(outputFile, "");
    }

    // Absolute path to jsdoxy
    var jsdoxy = [doxPath, 'bin', 'jsdoxy'].join(path.sep);

    // Cleanup any existing docs
    rimraf.sync(dest);

    _args.push('<');
    _args.push(dir);
    _args.push('>');
    _args.push(dest);

    // Set options to arguments
    // if(_opts.title){
    //   // _args.push('--title');
    //   _args.push('"' + _opts.title + '"');
    // }

    if (_opts.template) {
      _args.push('--template');
      _args.push('"' + _opts.template + '"');
    }

    // Pass through ignore params if set
    // if (this.data.ignore) {
    //   _args.push('--ignore');
    //   this.data.ignore.forEach(function(ignorePath) {
    //     _args.push(doxPath + ignorePath);
    //   });

    // }

    exec(jsdoxy + ' < ' + dir.join(' ') + " > " + outputFile, {maxBuffer: 5000*1024}, function(error, stout, sterr){
      if (error) { grunt.log.error("jsdoxy ERROR:  "+ error + "\n" + error.stack); }
      if (!error) {
        grunt.log.ok('Directory "' + dir + '" doxxed by jsdoxy.');
        done();
      }
    });
  });

};<commit_msg>output file location<commit_after>/*
 * # grunt-jsdoxy
 *
 * forked from Matt McManus grunt-dox https://github.com/punkave/grunt-dox
 *
 * Licensed under the MIT license.
 */

var exec = require('child_process').exec,
    fs = require('fs'),
    path = require('path'),
    rimraf = require('rimraf'),
    jade = require('jade');

module.exports = function(grunt) {

  grunt.registerMultiTask('jsdoxy', 'Generate jsdoxy output ', function() {

    var dir = this.filesSrc,
        dest = this.data.dest,
        done = this.async(),
        doxPath = path.resolve(__dirname,'../'),
        _opts = this.options(),
        _args = [],
        outputFile = path.resolve(dest, "output.json");

    if(!fs.existsSync(dest)) {
      fs.mkdirSync(dest);
    }
    if(!fs.existsSync(outputFile)) {
      fs.writeFileSync(outputFile, "");
    }

    // Absolute path to jsdoxy
    var jsdoxy = [doxPath, 'bin', 'jsdoxy'].join(path.sep);

    // Cleanup any existing docs
    rimraf.sync(dest);

    // _args.push('<');
    // _args.push(dir);
    // _args.push('>');
    // _args.push(dest);

    // Set options to arguments
    // if(_opts.title){
    //   // _args.push('--title');
    //   _args.push('"' + _opts.title + '"');
    // }

    if (!_opts.template) {
      grunt.log.error("jsdoxy ERROR: missing jade `template` option");
      return;
      // _args.push('--template');
      // _args.push('"' + _opts.template + '"');
    }

    // Pass through ignore params if set
    // if (this.data.ignore) {
    //   _args.push('--ignore');
    //   this.data.ignore.forEach(function(ignorePath) {
    //     _args.push(doxPath + ignorePath);
    //   });

    // }

    exec(jsdoxy + ' < ' + dir.join(' ') + " > " + outputFile, {maxBuffer: 5000*1024}, function(error, stout, sterr){
      if (error) { grunt.log.error("jsdoxy ERROR:  "+ error + "\n" + error.stack); }
      if (!error) {
        grunt.log.ok('Directory "' + dir + '" doxxed by jsdoxy.');
        done();
      }
    });
  });

};<|endoftext|>


<commit_before>var _path = require('path');

var util = require('../lib/util'),
	notify = require('grunt-notify/lib/notify-lib'),
	guessProjectName = require('grunt-notify/lib/util/guessProjectName');

module.exports = function(grunt){
	'use strict';

	var defaults = {
		success: {
			title: 'SUCCESS - TypeScript compile',
			message: guessProjectName(),
			image: _path.join(__dirname, '..', 'images', 'success.png')
		},
		failed: {
			title: 'FAILED - TypeScript compile',
			message: guessProjectName(),
			image: _path.join(__dirname, '..', 'images', 'failed.png')
		}
	};

	grunt.registerMultiTask('typescript-notify', function(){

		// Merge the notification configuration together
		// using the standard multitask merge behaviour.
		var taskOptions = this.options();

		grunt.event.on('grunt-typescript', function(e){

			var notifyKey = 'success';
			if (!e.success){
				notifyKey = 'failed';
			}

			var options = util.extend({}, defaults[notifyKey], taskOptions[notifyKey]);

			// On Windows double-escape the backslashes so the image path works.
			if (_path.sep == '\\'){
				options.image = options.image.replace(/\\/g, '\\\\');
			}
			console.log('!options=%j', options);

			// Send the growl notification.
			notify(options);
		});

	});

};
<commit_msg>refactor(tasks): turn off console logging<commit_after>var _path = require('path');

var util = require('../lib/util'),
	notify = require('grunt-notify/lib/notify-lib'),
	guessProjectName = require('grunt-notify/lib/util/guessProjectName');

module.exports = function(grunt){
	'use strict';

	var defaults = {
		success: {
			title: 'SUCCESS - TypeScript compile',
			message: guessProjectName(),
			image: _path.join(__dirname, '..', 'images', 'success.png')
		},
		failed: {
			title: 'FAILED - TypeScript compile',
			message: guessProjectName(),
			image: _path.join(__dirname, '..', 'images', 'failed.png')
		}
	};

	grunt.registerMultiTask('typescript-notify', function(){

		// Merge the notification configuration together
		// using the standard multitask merge behaviour.
		var taskOptions = this.options();

		grunt.event.on('grunt-typescript', function(e){

			var notifyKey = 'success';
			if (!e.success){
				notifyKey = 'failed';
			}

			var options = util.extend({}, defaults[notifyKey], taskOptions[notifyKey]);

			// On Windows double-escape the backslashes so the image path works.
			if (_path.sep == '\\'){
				options.image = options.image.replace(/\\/g, '\\\\');
			}
			// console.log('!options=%j', options);

			// Send the growl notification.
			notify(options);
		});

	});

};
<|endoftext|>


<commit_before>var path = require('path'),
    vow = require('vow'),
    vfs = require('enb/lib/fs/async-fs'),
    postcss = require('postcss'),
    atImport = require('postcss-import'),
    url = require('postcss-url'),
    stylus = require('stylus'),
    autoprefixer = require('autoprefixer-core'),
    nib = require('nib'),
    EOL = require('os').EOL,
    csswring = require('csswring');

/**
 * @class StylusTech
 * @augments {BaseTech}
 * @classdesc
 *
 * Builds CSS from Stylus and CSS sources.

 *
 * Files processing in 3 steps:

 * 1. Prepares list of @import sources (contain .styl and .css source code).

 * 2. Expands Stylus @import and processes it with the [Stylus renderer]{@link https://github.com/stylus/stylus}.

 * 3. Expands the remaining CSS @import and processes the received common CSS using
 * [Postcss]{@link https://github.com/postcss/postcss}.

 *
 * Important: `prefix`, `includes`, `useNib` options are enabled only for Stylus source code.

 *
 * @param {Object}          [options]                                 Options
 * @param {String}          [options.filesTarget='?.files']           Path to target with
 *                                                                    [FileList]{@link http://bit.ly/1GTUOj0}
 * @param {String[]}        [options.sourceSuffixes=['styl', 'css']]  Files with specified suffixes involved in
 *                                                                    the assembly. Default: `styl`, `css`
 * @param {String|Boolean}  [options.url='rebase']                    Rebases or inlines url():

 *                                                                    - `rebase` – resolves a path relative to the
 *                                                                    bundle directory.

 *                                                                    - `inline` – inlines assets using base64 encoding.
 * @param {Boolean}         [options.comments=true]                   Adds CSS comment with path to source to a code
 *                                                                    block (above and below).

 * @param {String|Boolean}  [options.imports='include']               Allows to include(expand) @import or leave without
 *                                                                    changes.
 * @param {Boolean|String}  [options.sourcemap=false]                 Builds sourcemap:

 *                                                                    - `true` – builds ?.css.map.

 *                                                                    - `inline` – builds and inlining sourcemap into
 *                                                                    bundled CSS file.
 * @param {Boolean|Object}  [options.autoprefixer=false]              Adds vendor prefixes using autoprefixer:

 *                                                                    - `true` – enables autoprefixer and defines what
 *                                                                    prefixes should be used based on
 *                                                                    [CanIUse]{@link http://caniuse.com} data.

 *                                                                    - `{browsers: ['last 2 versions']}` – allows to
 *                                                                    set custom browsers.
 * @param {Boolean}         [options.compress=false]                  Minifies styles. Supports sourcemap.
 * @param {String}          [options.prefix='']                       Adds prefix to CSS classes.

 *                                                                    Important: Available for Stylus only.
 * @param {String[]}        [options.includes=[]]                     Adds additional path to resolve a path in @import
 *                                                                    and url().

 *                                                                    [Stylus: include]{@link http://bit.ly/1IpsoTh}
 *

 *                                                                    Important: Available for Stylus only.
 * @param {Boolean}         [options.useNib=false]                    Allows to use Nib library for Stylus.

 *                                                                    Important: Available for Stylus only.
 *
 * @example
 * // Styles in file system before build:
 * // blocks/
 * // ├── block1.styl
 * // └── block2.css
 * //
 * // After build:
 * // bundle/
 * // └── bundle.css
 *
 * var stylusTech = require('enb-stylus/techs/stylus'),
 *     FileProvideTech = require('enb/techs/file-provider'),
 *     bem = require('enb-bem-techs');
 *
 * module.exports = function(config) {
 *     config.node('bundle', function(node) {
 *         // get FileList
 *         node.addTechs([
 *             [FileProvideTech, { target: '?.bemdecl.js' }],
 *             [bem.levels, levels: ['blocks']],
 *             bem.deps,
 *             bem.files
 *         ]);
 *
 *         // build css file
 *         node.addTech(stylusTech);
 *         node.addTarget('?.css');
 *     });
 * };
 */
module.exports = require('enb/lib/build-flow').create()
    .name('stylus')
    .target('target', '?.css')
    .defineOption('url', 'rebase')
    .defineOption('comments', true)
    .defineOption('imports', 'include')
    .defineOption('sourcemap', false)
    .defineOption('autoprefixer', false)
    .defineOption('compress', false)
    .defineOption('prefix', '')
    .defineOption('includes', [])
    .defineOption('useNib', false)
    .useFileList(['styl', 'css'])
    .builder(function (sourceFiles) {
        var node = this.node,
            filename = node.resolvePath(path.basename(this._target)),
            stylesImports = this._prepareImports(sourceFiles);

        return this._processStylus(filename, stylesImports)
            .spread(function (css, sourcemap) {
                return this._processCss(filename, css, sourcemap);
            }, this)
            .then(function (result) {
                return this._writeMap(filename + '.map', result.map)
                    .then(function () {
                        return result.css;
                    });
            }, this);
    })

    .methods(/** @lends StylusTech.prototype */{

        /**
         * Prepares the list of @import sources.
         *
         * @private
         * @param {Array} sourceFiles – paths to the files that contain styles for processing
         * @see [FileList]{@link https://github.com/enb-make/enb/blob/master/lib/file-list.js}
         * @returns {String} – list of @import
         */
        _prepareImports: function (sourceFiles) {
            var added = {},
                node = this.node;

            return sourceFiles
                .filter(function (file) {
                    /**
                     * This code is used when block has a lot of files that include styles.
                     *
                     * Case #1:
                     * blocks/
                     * ├── block.styl
                     * └── block.css
                     * Will be used `.styl`
                     *
                     * Case #2:
                     * blocks/
                     * ├── block.styl
                     * ├── block.ie.styl
                     * └── block.css
                     * Will be used `.styl` and `.ie.styl`
                     *
                     * Case #3:
                     * blocks/
                     * ├── block.css
                     * ├── block.ie.css
                     * Will be used `.css` and `.ie.css`
                     */
                    var basename = file.fullname.substring(0, file.fullname.lastIndexOf('.'));

                    if (added[basename]) {
                        return false;
                    }

                    added[basename] = true;

                    return true;
                })
                .map(function (file) {
                    var url = node.relativePath(file.fullname),
                        pre = '',
                        post = '';

                    if (this._comments) {
                        pre = '/* ' + url + ':begin */' + EOL;
                        post = '/* ' + url + ':end */' + EOL;
                    }

                    return pre + '@import "' + url + '";' + EOL + post;
                }, this).join(EOL);
        },

        /**
         * Configure Stylus renderer.
         * Could be used for overriding render options.
         *
         * @protected
         * @param {Object} renderer – instance of Stylus renderer class
         * @returns {Object} – instance of Stylus renderer class
         */
        _configureRenderer: function (renderer) {
            return renderer;
        },

        /**
         * Process Stylus files.
         *
         * @private
         * @param {String} filename – filename of the target
         * @param {String} stylesImports – list of @import to process
         * @returns {Promise[]} – promise with sourcemap of Stylus file and content with expanded @import with
         * Stylus sources and list of @import with CSS sources, that would expand on next step
         */
        _processStylus: function (filename, stylesImports) {
            var map = !!this._sourcemap;

            if (map) {
                map = {
                    basePath: path.dirname(filename),
                    inline: false,
                    comment: false
                };
            }

            var renderer = stylus(stylesImports)
                .set('prefix', this._prefix)
                .set('filename', filename)
                .set('sourcemap', map);

            // rebase url() in all cases on stylus level
            if (['rebase', 'inline'].indexOf(this._url) !== -1) {
                renderer
                    .set('resolve url', true)
                    // set `nocheck` for fixed github.com/stylus/stylus/issues/1951
                    .define('url', stylus.resolver({ nocheck: true }));
            }

            if (this._includes) {
                this._includes.forEach(function (path) {
                    renderer.include(path);
                });
            }

            if (this._useNib) {
                renderer
                    .use(nib())
                    .import(nib.path + '/nib');
            }

            var defer = vow.defer();

            this._configureRenderer(renderer).render(function (err, css) {
                err ? defer.reject(err) : defer.resolve([css, renderer.sourcemap]);
            });

            return defer.promise();
        },

        /**
         * Process CSS files.
         *
         * @private
         * @param {String} filename – filename of the target
         * @param {String} css – list of CSS @import to process and CSS code received after rendering Stylus source code
         * @param {Object} sourcemap – sourcemap of Stylus source code
         * @returns {Promise} – promise with processed css and sourcemap (options)
         */
        _processCss: function (filename, css, sourcemap) {
            var processor = postcss(),
                urlMethod = this._url,

                // base opts to resolve urls
                opts = {
                    from: filename,
                    to: filename
                };

            // add options to build sourcemap
            if (this._sourcemap) {
                opts.map = {
                    prev: JSON.stringify(sourcemap),
                    inline: this._sourcemap === 'inline',
                    annotation: true
                };
            }

            // expand imports with css
            if (this._imports === 'include') {
                processor.use(atImport());
            }

            // rebase or inline urls in css
            if (['rebase', 'inline'].indexOf(urlMethod) > -1) {
                processor.use(url({ url: urlMethod }));
            }

            // use autoprefixer
            if (this._autoprefixer) {
                processor.use(
                    (this._autoprefixer.browsers ?
                        autoprefixer({ browsers: this._autoprefixer.browsers }) :
                        autoprefixer)
                );
            }

            // compress css
            if (this._compress) {
                processor.use(csswring());
            }

            return processor.process(css, opts);
        },

        /**
         * Write sourcemap to file system in target directory
         *
         * @private
         * @param {String} filename – filename for map, e.g. /Users/project/bundle/bundle.css.map
         * @param {Object} data – data of sourcemap
         * @returns {Promise}
         */
        _writeMap: function (filename, data) {
            if (this._sourcemap && this._sourcemap !== 'inline') {
                return vfs.write(filename, JSON.stringify(data));
            }

            return vow.resolve();
        }
    })
    .createTech();
<commit_msg>Updated jsdoc<commit_after>var path = require('path'),
    vow = require('vow'),
    vfs = require('enb/lib/fs/async-fs'),
    postcss = require('postcss'),
    atImport = require('postcss-import'),
    url = require('postcss-url'),
    stylus = require('stylus'),
    autoprefixer = require('autoprefixer-core'),
    nib = require('nib'),
    EOL = require('os').EOL,
    csswring = require('csswring');

/**
 * @class StylusTech
 * @augments {BaseTech}
 * @classdesc
 *
 * Builds CSS from Stylus and CSS sources.

 *
 * Files processing in 3 steps:

 * 1. Prepares list of @import sources (contain .styl and .css source code).

 * 2. Expands Stylus @import and processes it with the [Stylus renderer]{@link https://github.com/stylus/stylus}.

 * 3. Expands the remaining CSS @import and processes the received common CSS using
 * [Postcss]{@link https://github.com/postcss/postcss}.

 *
 * Important: `prefix`, `includes`, `useNib` options are enabled only for Stylus source code.

 *
 * @param {Object}          [options]                                 Options
 * @param {String}          [options.filesTarget='?.files']           Path to target with
 *                                                                    [FileList]{@link http://bit.ly/1GTUOj0}
 * @param {String[]}        [options.sourceSuffixes=['styl', 'css']]  Files with specified suffixes involved in
 *                                                                    the assembly. Default: `styl`, `css`
 * @param {String|Boolean}  [options.url='rebase']                    Rebases or inlines url():

 *                                                                    - `rebase` – resolves a path relative to the
 *                                                                    bundle directory.

 *                                                                    - `inline` – inlines assets using base64 encoding.
 * @param {Boolean}         [options.comments=true]                   Adds CSS comment with path to source to a code
 *                                                                    block (above and below).

 * @param {String|Boolean}  [options.imports='include']               Allows to include(expand) @import or leave without
 *                                                                    changes.
 * @param {Boolean|String}  [options.sourcemap=false]                 Builds sourcemap:

 *                                                                    - `true` – builds ?.css.map.

 *                                                                    - `inline` – builds and inlining sourcemap into
 *                                                                    bundled CSS file.
 * @param {Boolean|Object}  [options.autoprefixer=false]              Adds vendor prefixes using autoprefixer:

 *                                                                    - `true` – enables autoprefixer and defines what
 *                                                                    prefixes should be used based on
 *                                                                    [CanIUse]{@link http://caniuse.com} data.

 *                                                                    - `{browsers: ['last 2 versions']}` – allows to
 *                                                                    set custom browsers.
 * @param {Boolean}         [options.compress=false]                  Minifies styles. Supports sourcemap.
 * @param {String}          [options.prefix='']                       Adds prefix to CSS classes.

 *                                                                    Important: Available for Stylus only.
 * @param {String[]}        [options.includes=[]]                     Adds additional path to resolve a path in @import
 *                                                                    and url().

 *                                                                    [Stylus: include]{@link http://bit.ly/1IpsoTh}
 *

 *                                                                    Important: Available for Stylus only.
 * @param {Boolean}         [options.useNib=false]                    Allows to use Nib library for Stylus.

 *                                                                    Important: Available for Stylus only.
 *
 * @example
 * // Styles in file system before build:
 * // blocks/
 * // ├── block1.styl
 * // └── block2.css
 * //
 * // After build:
 * // bundle/
 * // └── bundle.css
 *
 * var stylusTech = require('enb-stylus/techs/stylus'),
 *     FileProvideTech = require('enb/techs/file-provider'),
 *     bemTechs = require('enb-bem-techs');
 *
 * module.exports = function(config) {
 *     config.node('bundle', function(node) {
 *         // get FileList
 *         node.addTechs([
 *             [FileProvideTech, { target: '?.bemdecl.js' }],
 *             [bemTechs.levels, levels: ['blocks']],
 *             bemTechs.deps,
 *             bemTechs.files
 *         ]);
 *
 *         // build css file
 *         node.addTech(stylusTech);
 *         node.addTarget('?.css');
 *     });
 * };
 */
module.exports = require('enb/lib/build-flow').create()
    .name('stylus')
    .target('target', '?.css')
    .defineOption('url', 'rebase')
    .defineOption('comments', true)
    .defineOption('imports', 'include')
    .defineOption('sourcemap', false)
    .defineOption('autoprefixer', false)
    .defineOption('compress', false)
    .defineOption('prefix', '')
    .defineOption('includes', [])
    .defineOption('useNib', false)
    .useFileList(['styl', 'css'])
    .builder(function (sourceFiles) {
        var node = this.node,
            filename = node.resolvePath(path.basename(this._target)),
            stylesImports = this._prepareImports(sourceFiles);

        return this._processStylus(filename, stylesImports)
            .spread(function (css, sourcemap) {
                return this._processCss(filename, css, sourcemap);
            }, this)
            .then(function (result) {
                return this._writeMap(filename + '.map', result.map)
                    .then(function () {
                        return result.css;
                    });
            }, this);
    })

    .methods(/** @lends StylusTech.prototype */{

        /**
         * Prepares the list of @import sources.
         *
         * @private
         * @param {Array} sourceFiles – paths to the files that contain styles for processing
         * @see [FileList]{@link https://github.com/enb-make/enb/blob/master/lib/file-list.js}
         * @returns {String} – list of @import
         */
        _prepareImports: function (sourceFiles) {
            var added = {},
                node = this.node;

            return sourceFiles
                .filter(function (file) {
                    /**
                     * This code is used when block has a lot of files that include styles.
                     *
                     * Case #1:
                     * blocks/
                     * ├── block.styl
                     * └── block.css
                     * Will be used `.styl`
                     *
                     * Case #2:
                     * blocks/
                     * ├── block.styl
                     * ├── block.ie.styl
                     * └── block.css
                     * Will be used `.styl` and `.ie.styl`
                     *
                     * Case #3:
                     * blocks/
                     * ├── block.css
                     * ├── block.ie.css
                     * Will be used `.css` and `.ie.css`
                     */
                    var basename = file.fullname.substring(0, file.fullname.lastIndexOf('.'));

                    if (added[basename]) {
                        return false;
                    }

                    added[basename] = true;

                    return true;
                })
                .map(function (file) {
                    var url = node.relativePath(file.fullname),
                        pre = '',
                        post = '';

                    if (this._comments) {
                        pre = '/* ' + url + ':begin */' + EOL;
                        post = '/* ' + url + ':end */' + EOL;
                    }

                    return pre + '@import "' + url + '";' + EOL + post;
                }, this).join(EOL);
        },

        /**
         * Configure Stylus renderer.
         * Could be used for overriding render options.
         *
         * @protected
         * @param {Object} renderer – instance of Stylus renderer class
         * @returns {Object} – instance of Stylus renderer class
         */
        _configureRenderer: function (renderer) {
            return renderer;
        },

        /**
         * Process Stylus files.
         *
         * @private
         * @param {String} filename – filename of the target
         * @param {String} stylesImports – list of @import to process
         * @returns {Promise[]} – promise with sourcemap of Stylus file and content with expanded @import with
         * Stylus sources and list of @import with CSS sources, that would expand on next step
         */
        _processStylus: function (filename, stylesImports) {
            var map = !!this._sourcemap;

            if (map) {
                map = {
                    basePath: path.dirname(filename),
                    inline: false,
                    comment: false
                };
            }

            var renderer = stylus(stylesImports)
                .set('prefix', this._prefix)
                .set('filename', filename)
                .set('sourcemap', map);

            // rebase url() in all cases on stylus level
            if (['rebase', 'inline'].indexOf(this._url) !== -1) {
                renderer
                    .set('resolve url', true)
                    // set `nocheck` for fixed github.com/stylus/stylus/issues/1951
                    .define('url', stylus.resolver({ nocheck: true }));
            }

            if (this._includes) {
                this._includes.forEach(function (path) {
                    renderer.include(path);
                });
            }

            if (this._useNib) {
                renderer
                    .use(nib())
                    .import(nib.path + '/nib');
            }

            var defer = vow.defer();

            this._configureRenderer(renderer).render(function (err, css) {
                err ? defer.reject(err) : defer.resolve([css, renderer.sourcemap]);
            });

            return defer.promise();
        },

        /**
         * Process CSS files.
         *
         * @private
         * @param {String} filename – filename of the target
         * @param {String} css – list of CSS @import to process and CSS code received after rendering Stylus source code
         * @param {Object} sourcemap – sourcemap of Stylus source code
         * @returns {Promise} – promise with processed css and sourcemap (options)
         */
        _processCss: function (filename, css, sourcemap) {
            var processor = postcss(),
                urlMethod = this._url,

                // base opts to resolve urls
                opts = {
                    from: filename,
                    to: filename
                };

            // add options to build sourcemap
            if (this._sourcemap) {
                opts.map = {
                    prev: JSON.stringify(sourcemap),
                    inline: this._sourcemap === 'inline',
                    annotation: true
                };
            }

            // expand imports with css
            if (this._imports === 'include') {
                processor.use(atImport());
            }

            // rebase or inline urls in css
            if (['rebase', 'inline'].indexOf(urlMethod) > -1) {
                processor.use(url({ url: urlMethod }));
            }

            // use autoprefixer
            if (this._autoprefixer) {
                processor.use(
                    (this._autoprefixer.browsers ?
                        autoprefixer({ browsers: this._autoprefixer.browsers }) :
                        autoprefixer)
                );
            }

            // compress css
            if (this._compress) {
                processor.use(csswring());
            }

            return processor.process(css, opts);
        },

        /**
         * Write sourcemap to file system in target directory
         *
         * @private
         * @param {String} filename – filename for map, e.g. /Users/project/bundle/bundle.css.map
         * @param {Object} data – data of sourcemap
         * @returns {Promise}
         */
        _writeMap: function (filename, data) {
            if (this._sourcemap && this._sourcemap !== 'inline') {
                return vfs.write(filename, JSON.stringify(data));
            }

            return vow.resolve();
        }
    })
    .createTech();
<|endoftext|>