<commit_before>/**
 * Dota 2 module
 * @module Dota2
 */

var steam = require("steam");

const DOTA_APP_ID = 570;

var EventEmitter = require('events').EventEmitter,
    fs = require("fs"),
    path = require("path"),
    util = require("util"),
    bignumber = require("bignumber.js"),
    Protobuf = require('protobufjs'),
    Dota2 = exports;

Protobuf.parse.defaults.keepCase = true;

var folder = fs.readdirSync(__dirname + '/proto');

/**
 * Protobuf schema. See {@link http://dcode.io/protobuf.js/Root.html|Protobufjs#Root}
 * @alias module:Dota2.schema
 */
Dota2.schema = Protobuf.loadSync(folder.map(filename => __dirname + '/proto/' + filename));

/**
 * The Dota 2 client that communicates with the GC
 * @class
 * @alias module:Dota2.Dota2Client
 * @param {Object} steamClient - Node-steam client instanc
 * @param {Boolean} debug - Print debug information to console
 * @param {Boolean} debugMore - Print even more debug information to console
 * @extends {EventEmitter} EventEmitter
 * @fires sourceTVGamesData
 * @fires inventoryUpdate
 * @fires practiceLobbyUpdate
 * @fires lobbyInviteUpdate
 * @fires partyUpdate
 * @fires partyInviteUpdate
 * @fires joinableCustomGameModes
 */
Dota2.Dota2Client = function Dota2Client(steamClient, debug, debugMore) {
    EventEmitter.call(this);
    this.debug = debug || false;
    this.debugMore = debugMore || false;

    /** The current state of the bot's inventory. Contains cosmetics, player cards, ...
    * @type {CSOEconItem[]}
    */
    this.Inventory = [];
    /** The chat channels the bot has joined */
    this.chatChannels = []; // Map channel names to channel data.
    /** The lobby the bot is currently in. Falsy if the bot isn't in a lobby.
    * @type {CSODOTALobby}
    */
    this.Lobby = null;
    /** The currently active lobby invitation. Falsy if the bot has not been invited.
    * @type {CSODOTALobbyInvite}
    */
    this.LobbyInvite = null;
    /** The party the bot is currently in. Falsy if the bot isn't in a party.
    * @type {CSODOTAParty}
    */
    this.Party = null;
    /** The currently active party invitation. Falsy if the bot has not been invited.
    * @type {CSODOTAPartyInvite}
    */
    this.PartyInvite = null;

    var steamUser = new steam.SteamUser(steamClient);
    this._user = steamUser;
    this._client = steamClient;
    this._gc = new steam.SteamGameCoordinator(steamClient, DOTA_APP_ID);
    this._appid = DOTA_APP_ID;

    this._gcReady = false;
    this._gcClientHelloIntervalId = null;
    this._gcConnectionStatus = Dota2.schema.lookupEnum("GCConnectionStatus").GCConnectionStatus_NO_SESSION;
    // node-steam wants this as a simple object, so we can't use CMsgProtoBufHeader
    this._protoBufHeader = {
        "msg": "",
        "proto": {
            "client_steam_id": this._client.steamID,
            "source_app_id": this._appid
        }
    };

    var self = this;
    this._gc.on("message", function fromGC(header, body, callback) {
        /* Routes messages from Game Coordinator to their handlers. */
        callback = callback || null;

        var kMsg = header.msg;
        if (self.debugMore) util.log("Dota2 fromGC: " + Dota2._getMessageName(kMsg));

        if (kMsg in self._handlers) {
            if (callback) {
                self._handlers[kMsg].call(self, body, callback);
            } else {
                self._handlers[kMsg].call(self, body);
            }
        } else {
            self.emit("unhandled", kMsg);
        }
    });

    this._sendClientHello = function() {
        if (self._gcReady) {
            if (self._gcClientHelloIntervalId) {
                clearInterval(self._gcClientHelloIntervalId);
                self._gcClientHelloIntervalId = null;
            }
            return;
        }
        if (self._gcClientHelloCount > 10) {
            if (self.debug) util.log("ClientHello has taken longer than 30 seconds! Reporting timeout...");
            self._gcClientHelloCount = 0;
            self.emit("hellotimeout");
        }

        if (self.debug) util.log("Sending ClientHello");
        if (!self._gc) {
            util.log("Where the fuck is _gc?");
        } else {
            self._protoBufHeader.msg = Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientHello;
            var payload = {
                engine : 1,
                secret_key : "",
                client_session_need : 104
            };
            self._gc.send(
                self._protoBufHeader,
                Dota2.schema.lookupType("CMsgClientHello").encode(payload).finish()
            );
        }

        self._gcClientHelloCount++;
    };
};
util.inherits(Dota2.Dota2Client, EventEmitter);

// Methods
/**
 * Converts a 64bit Steam ID to a Dota2 account ID
 * @alias module:Dota2.Dota2Client.ToAccountID
 * @param {String} steamID - String representation of a 64bit Steam ID
 * @returns {Number} Dota2 account ID corresponding with steamID
 */
Dota2.Dota2Client.prototype.ToAccountID = function(steamID) {
    return new bignumber(steamID).minus('76561197960265728') - 0;
};
/**
 * Converts a Dota2 account ID to a 64bit Steam ID
 * @alias module:Dota2.Dota2Client.ToSteamID
 * @param {String} accid - String representation of a Dota 2 account ID
 * @returns {String} 64bit Steam ID corresponding to the given Dota 2 account ID
 */
Dota2.Dota2Client.prototype.ToSteamID = function(accid) {
    return new bignumber(accid).plus('76561197960265728') + "";
};
/**
 * Reports to Steam that you're playing Dota 2, and then initiates communication with the Game Coordinator.
 * @alias module:Dota2.Dota2Client#launch
 */
Dota2.Dota2Client.prototype.launch = function() {
    /* Reports to Steam that we are running Dota 2. Initiates communication with GC with EMsgGCClientHello */
    if (this.debug) util.log("Launching Dota 2");
    this.AccountID = this.ToAccountID(this._client.steamID);
    this.Party = null;
    this.Lobby = null;
    this.PartyInvite = null;
    this.Inventory = null;
    this._user.gamesPlayed([{
        "game_id": this._appid
    }]);

    // Keep knocking on the GCs door until it accepts us.
    // This is very bad practice and quite trackable.
    // The real client tends to send only one of these.
    // Really we should just send one when the connection status is GC online
    this._gcClientHelloCount = 0;
    this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 6000);

    //Also immediately send clienthello
    setTimeout(this._sendClientHello, 1000);
};
/**
 * Stop sending a heartbeat to the GC and report to steam you're no longer playing Dota 2
 * @alias module:Dota2.Dota2Client#exit
 */
Dota2.Dota2Client.prototype.exit = function() {
    /* Reports to Steam we are not running any apps. */
    if (this.debug) util.log("Exiting Dota 2");

    /* stop knocking if exit comes before ready event */
    if (this._gcClientHelloIntervalId) {
        clearInterval(this._gcClientHelloIntervalId);
        this._gcClientHelloIntervalId = null;
    }
    this._gcReady = false;

    if (this._client.loggedOn) this._user.gamesPlayed([]);
};

Dota2.Dota2Client.prototype.sendToGC = function(type, payload, handler, callback) {
    var self = this;
    if (!this._gcReady) {
        if (this.debug) util.log("GC not ready, please listen for the 'ready' event.");
        if (callback) callback(-1, null);                   // notify user that something went wrong
        return null;
    }
    this._protoBufHeader.msg = type;
    this._gc.send(this._protoBufHeader,                     // protobuf header, same for all messages
                  payload,                                  // payload of the message
                  Dota2._convertCallback.call(self, handler, callback) // let handler treat callback so events are triggered
    );
}

// Handlers

var handlers = Dota2.Dota2Client.prototype._handlers = {};

handlers[Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientWelcome] = function clientWelcomeHandler(message) {
    /* Response to our k_EMsgGCClientHello, now we can execute other GC commands. */

    // Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
    if (this._gcClientHelloIntervalId) {
        clearInterval(this._gcClientHelloIntervalId);
        this._gcClientHelloIntervalId = null;
    }

    if (this.debug) util.log("Received client welcome.");

    // Parse any caches
    this._gcReady = true;
    //this._handleWelcomeCaches(message);
    this.emit("ready");
};

handlers[Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientConnectionStatus] = function gcClientConnectionStatus(message) {
    /* Catch and handle changes in connection status, cuz reasons u know. */
    var status = Dota2.schema.lookupType("CMsgConnectionStatus").decode(message).status;
    if (status) this._gcConnectionStatus = status;

    switch (status) {
        case Dota2.schema.lookupEnum("GCConnectionStatus").GCConnectionStatus_HAVE_SESSION:
            if (this.debug) util.log("GC Connection Status regained.");

            // Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
            if (this._gcClientHelloIntervalId) {
                clearInterval(this._gcClientHelloIntervalId);
                this._gcClientHelloIntervalId = null;

                this._gcReady = true;
                this.emit("ready");
            }
            break;

        default:
            if (this.debug) util.log("GC Connection Status unreliable - " + status);

            // Only execute if !_gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'unready');
            if (!this._gcClientHelloIntervalId) {
                this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 5000); // Continually try regain GC session

                this._gcReady = false;
                this.emit("unready");
            }
            break;
    }
};

require("./handlers/cache");
require("./handlers/inventory");
require("./handlers/chat");
require("./handlers/guild");
require("./handlers/community");
require("./handlers/helper");
require("./handlers/match");
require("./handlers/lobbies");
require("./handlers/parties");
require("./handlers/leagues");
require("./handlers/sourcetv");
require("./handlers/team");
require("./handlers/custom");
require("./handlers/general");
require("./handlers/fantasy");<commit_msg>minor tweak<commit_after>/**
 * Dota 2 module
 * @module Dota2
 */

var steam = require("steam");

const DOTA_APP_ID = 570;

var EventEmitter = require('events').EventEmitter,
    fs = require("fs"),
    path = require("path"),
    util = require("util"),
    bignumber = require("bignumber.js"),
    Protobuf = require('protobufjs'),
    Dota2 = exports;

Protobuf.parse.defaults.keepCase = true;

var folder = fs.readdirSync(__dirname + '/proto');

/**
 * Protobuf schema. See {@link http://dcode.io/protobuf.js/Root.html|Protobufjs#Root}
 * @alias module:Dota2.schema
 */
Dota2.schema = Protobuf.loadSync(folder.map(filename => __dirname + '/proto/' + filename));

/**
 * The Dota 2 client that communicates with the GC
 * @class
 * @alias module:Dota2.Dota2Client
 * @param {Object} steamClient - Node-steam client instanc
 * @param {Boolean} debug - Print debug information to console
 * @param {Boolean} debugMore - Print even more debug information to console
 * @extends {EventEmitter} EventEmitter
 * @fires sourceTVGamesData
 * @fires inventoryUpdate
 * @fires practiceLobbyUpdate
 * @fires lobbyInviteUpdate
 * @fires partyUpdate
 * @fires partyInviteUpdate
 * @fires joinableCustomGameModes
 */
Dota2.Dota2Client = function Dota2Client(steamClient, debug, debugMore) {
    EventEmitter.call(this);
    this.debug = debug || false;
    this.debugMore = debugMore || false;

    /** The current state of the bot's inventory. Contains cosmetics, player cards, ...
    * @type {CSOEconItem[]}
    */
    this.Inventory = [];
    /** The chat channels the bot has joined
    * @type {CMsgDOTAJoinChatChannelResponse[]}
    */
    this.chatChannels = []; // Map channel names to channel data.
    /** The lobby the bot is currently in. Falsy if the bot isn't in a lobby.
    * @type {CSODOTALobby}
    */
    this.Lobby = null;
    /** The currently active lobby invitation. Falsy if the bot has not been invited.
    * @type {CSODOTALobbyInvite}
    */
    this.LobbyInvite = null;
    /** The party the bot is currently in. Falsy if the bot isn't in a party.
    * @type {CSODOTAParty}
    */
    this.Party = null;
    /** The currently active party invitation. Falsy if the bot has not been invited.
    * @type {CSODOTAPartyInvite}
    */
    this.PartyInvite = null;

    var steamUser = new steam.SteamUser(steamClient);
    this._user = steamUser;
    this._client = steamClient;
    this._gc = new steam.SteamGameCoordinator(steamClient, DOTA_APP_ID);
    this._appid = DOTA_APP_ID;

    this._gcReady = false;
    this._gcClientHelloIntervalId = null;
    this._gcConnectionStatus = Dota2.schema.lookupEnum("GCConnectionStatus").GCConnectionStatus_NO_SESSION;
    // node-steam wants this as a simple object, so we can't use CMsgProtoBufHeader
    this._protoBufHeader = {
        "msg": "",
        "proto": {
            "client_steam_id": this._client.steamID,
            "source_app_id": this._appid
        }
    };

    var self = this;
    this._gc.on("message", function fromGC(header, body, callback) {
        /* Routes messages from Game Coordinator to their handlers. */
        callback = callback || null;

        var kMsg = header.msg;
        if (self.debugMore) util.log("Dota2 fromGC: " + Dota2._getMessageName(kMsg));

        if (kMsg in self._handlers) {
            if (callback) {
                self._handlers[kMsg].call(self, body, callback);
            } else {
                self._handlers[kMsg].call(self, body);
            }
        } else {
            self.emit("unhandled", kMsg);
        }
    });

    this._sendClientHello = function() {
        if (self._gcReady) {
            if (self._gcClientHelloIntervalId) {
                clearInterval(self._gcClientHelloIntervalId);
                self._gcClientHelloIntervalId = null;
            }
            return;
        }
        if (self._gcClientHelloCount > 10) {
            if (self.debug) util.log("ClientHello has taken longer than 30 seconds! Reporting timeout...");
            self._gcClientHelloCount = 0;
            self.emit("hellotimeout");
        }

        if (self.debug) util.log("Sending ClientHello");
        if (!self._gc) {
            util.log("Where the fuck is _gc?");
        } else {
            self._protoBufHeader.msg = Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientHello;
            var payload = {
                engine : 1,
                secret_key : "",
                client_session_need : 104
            };
            self._gc.send(
                self._protoBufHeader,
                Dota2.schema.lookupType("CMsgClientHello").encode(payload).finish()
            );
        }

        self._gcClientHelloCount++;
    };
};
util.inherits(Dota2.Dota2Client, EventEmitter);

// Methods
/**
 * Converts a 64bit Steam ID to a Dota2 account ID
 * @alias module:Dota2.Dota2Client.ToAccountID
 * @param {String} steamID - String representation of a 64bit Steam ID
 * @returns {Number} Dota2 account ID corresponding with steamID
 */
Dota2.Dota2Client.prototype.ToAccountID = function(steamID) {
    return new bignumber(steamID).minus('76561197960265728') - 0;
};
/**
 * Converts a Dota2 account ID to a 64bit Steam ID
 * @alias module:Dota2.Dota2Client.ToSteamID
 * @param {String} accid - String representation of a Dota 2 account ID
 * @returns {String} 64bit Steam ID corresponding to the given Dota 2 account ID
 */
Dota2.Dota2Client.prototype.ToSteamID = function(accid) {
    return new bignumber(accid).plus('76561197960265728') + "";
};
/**
 * Reports to Steam that you're playing Dota 2, and then initiates communication with the Game Coordinator.
 * @alias module:Dota2.Dota2Client#launch
 */
Dota2.Dota2Client.prototype.launch = function() {
    /* Reports to Steam that we are running Dota 2. Initiates communication with GC with EMsgGCClientHello */
    if (this.debug) util.log("Launching Dota 2");
    this.AccountID = this.ToAccountID(this._client.steamID);
    this.Party = null;
    this.Lobby = null;
    this.PartyInvite = null;
    this.Inventory = null;
    this._user.gamesPlayed([{
        "game_id": this._appid
    }]);

    // Keep knocking on the GCs door until it accepts us.
    // This is very bad practice and quite trackable.
    // The real client tends to send only one of these.
    // Really we should just send one when the connection status is GC online
    this._gcClientHelloCount = 0;
    this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 6000);

    //Also immediately send clienthello
    setTimeout(this._sendClientHello, 1000);
};
/**
 * Stop sending a heartbeat to the GC and report to steam you're no longer playing Dota 2
 * @alias module:Dota2.Dota2Client#exit
 */
Dota2.Dota2Client.prototype.exit = function() {
    /* Reports to Steam we are not running any apps. */
    if (this.debug) util.log("Exiting Dota 2");

    /* stop knocking if exit comes before ready event */
    if (this._gcClientHelloIntervalId) {
        clearInterval(this._gcClientHelloIntervalId);
        this._gcClientHelloIntervalId = null;
    }
    this._gcReady = false;

    if (this._client.loggedOn) this._user.gamesPlayed([]);
};

Dota2.Dota2Client.prototype.sendToGC = function(type, payload, handler, callback) {
    var self = this;
    if (!this._gcReady) {
        if (this.debug) util.log("GC not ready, please listen for the 'ready' event.");
        if (callback) callback(-1, null);                   // notify user that something went wrong
        return null;
    }
    this._protoBufHeader.msg = type;
    this._gc.send(this._protoBufHeader,                     // protobuf header, same for all messages
                  payload,                                  // payload of the message
                  Dota2._convertCallback.call(self, handler, callback) // let handler treat callback so events are triggered
    );
}

// Handlers

var handlers = Dota2.Dota2Client.prototype._handlers = {};

handlers[Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientWelcome] = function clientWelcomeHandler(message) {
    /* Response to our k_EMsgGCClientHello, now we can execute other GC commands. */

    // Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
    if (this._gcClientHelloIntervalId) {
        clearInterval(this._gcClientHelloIntervalId);
        this._gcClientHelloIntervalId = null;
    }

    if (this.debug) util.log("Received client welcome.");

    // Parse any caches
    this._gcReady = true;
    //this._handleWelcomeCaches(message);
    this.emit("ready");
};

handlers[Dota2.schema.lookupEnum("EGCBaseClientMsg").k_EMsgGCClientConnectionStatus] = function gcClientConnectionStatus(message) {
    /* Catch and handle changes in connection status, cuz reasons u know. */
    var status = Dota2.schema.lookupType("CMsgConnectionStatus").decode(message).status;
    if (status) this._gcConnectionStatus = status;

    switch (status) {
        case Dota2.schema.lookupEnum("GCConnectionStatus").GCConnectionStatus_HAVE_SESSION:
            if (this.debug) util.log("GC Connection Status regained.");

            // Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
            if (this._gcClientHelloIntervalId) {
                clearInterval(this._gcClientHelloIntervalId);
                this._gcClientHelloIntervalId = null;

                this._gcReady = true;
                this.emit("ready");
            }
            break;

        default:
            if (this.debug) util.log("GC Connection Status unreliable - " + status);

            // Only execute if !_gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'unready');
            if (!this._gcClientHelloIntervalId) {
                this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 5000); // Continually try regain GC session

                this._gcReady = false;
                this.emit("unready");
            }
            break;
    }
};

require("./handlers/cache");
require("./handlers/inventory");
require("./handlers/chat");
require("./handlers/guild");
require("./handlers/community");
require("./handlers/helper");
require("./handlers/match");
require("./handlers/lobbies");
require("./handlers/parties");
require("./handlers/leagues");
require("./handlers/sourcetv");
require("./handlers/team");
require("./handlers/custom");
require("./handlers/general");
require("./handlers/fantasy");<|endoftext|>


<commit_before>'use strict'

//----------------------------------------------------------
// modules
//----------------------------------------------------------
// node
const p = require('path')

// npm
const P = require('bluebird')
const fs = require('fs-extra')
const md = require('markdown-it')()
const jade = require('jade')
const globby = require('globby')
const co = require('co')
const yaml = require('js-yaml')

// promisification
const read = P.promisify(fs.readFile)
const write = P.promisify(fs.outputFile)

//----------------------------------------------------------
// logic
//----------------------------------------------------------
module.exports = class Jamb {
  constructor(cfg) {
    this._previewDelim = '--MORE--'
    this._yamlDelim = '---'
    this._defaultTemplate = 'page'
    this._opts = {
      jade: {
        basedir: './test/fixtures/templates',
        pretty: true
      }
    }
    // globby opts
    // jade opts
    // markdown-it opts
    return co(
      function* () {
        const posts = yield this.content(cfg.posts)
        const content = this.merge(
          this.sort(yield this.content(cfg.pages)),
          this.sort(posts)
        )
        const templates = yield this.templates(cfg.templates)
        // return content
        return templates
      }.bind(this)
    ).catch(err => {
      console.log(err.stack)
      throw new Error(err)
    })
  }

  //----------------------------------------------------------
  // util fns
  //----------------------------------------------------------
  /**
    Shallow merge two objects with array values.
    @param {Object} to - merge to
    @param {Object} from - merge from
    @returns {Object} merged object
   */
  merge(to, from) {
    Object.keys(from).map(key =>
      to[key]
        ? from[key].map(data => to[key].push(data))
        : to[key] = from[key]
    )
    return to
  }

  /**
    Sort data by template.
    @param {Object[]} dataArr - array of data objects
    @returns {Object} object with template keys and data array values
   */
  sort(dataArr) {
    return dataArr.reduce((accum, data) => {
      const template = data.template || this._defaultTemplate
      if (data.template) delete data.template
      accum[template]
        ? accum[template].push(data)
        : accum[template] = [data]
      return accum
    }, {})
  }

  //----------------------------------------------------------
  // content fns
  //----------------------------------------------------------
  // TODO - JSDOC
  * content(glob) {
    const paths = yield globby(glob)
    return yield P.all(paths.map(path => read(path, 'utf8')))
      .map(strings => this.frontmatter(strings))
      .map(obj => this.preview(obj))
      .map(obj => this.markdown(obj))
  }

  /**
    Parse YAML frontmatter.
    @param {String} rawString - raw string from file
    @returns {Object} object with frontmatter and content
   */
  frontmatter(rawString) {
    const strings = rawString.split(this._yamlDelim)
    const dataObj = yaml.load(strings[1])
    dataObj.content = strings.slice(2).join(this._yamlDelim)
    return dataObj
  }

  /**
    Split content by preview separator.
    @param {Object} data - data object
    @returns {Object} data object
   */
  preview(data) {
    if (data.content.includes(this._previewDelim)) {
      const halves = data.content.split(this._previewDelim)
      data.preview = halves[0]
      data.content = halves[1]
      return data
    }
    return data
  }

  /**
    Render markdown data fields.
    @param {Object} data - data object
    @returns {Object} data object
   */
  markdown(data) {
    ['preview', 'content'].map(field => {
      if (data[field]) data[field] = md.render(data[field]).trim()
    })
    return data
  }

  //----------------------------------------------------------
  // template fns
  //----------------------------------------------------------
  * templates(glob) {
    const paths = yield globby(glob)
    return yield P.all(paths.map(path => read(path, 'utf8')))
      .map(string => jade.compile(string, this._opts.jade))
      .reduce((accum, fn, i) => {
        accum[p.basename(paths[i], '.jade')] = fn
        return accum
      }, {})
  }
}

// function render(content, posts, templates) {
//   const pages = {}
//   Object.keys(content).map(template => {
//     content[template].map(data => {
//       data.posts = posts
//       pages[data.url] = templates[template]({data})
//     })
//   })
//   return pages
// }

// function write(html) {
//   return Object.keys(html).map(url => {
//     const path = url === 'index'
//       ? `${url}.html`
//       : `${url}${p.sep}index.html`
//     fs.outputFileSync(p.join('dist', path), html[url])
//   })
// }

// function blog(cb) {
//   const content = merge(
//     getContentByTemplate('src/content'),
//     getContentByTemplate('src/content/posts')
//   )
//   const posts = getContent('src/content/posts')
//   const templates = getTemplates('src/templates')
//   const html = render(content, posts, templates)
//   write(html)
//   return cb()
// }
<commit_msg>write render method<commit_after>'use strict'

//----------------------------------------------------------
// modules
//----------------------------------------------------------
// node
const p = require('path')

// npm
const P = require('bluebird')
const fs = require('fs-extra')
const md = require('markdown-it')()
const jade = require('jade')
const globby = require('globby')
const co = require('co')
const yaml = require('js-yaml')

// promisification
const read = P.promisify(fs.readFile)
const write = P.promisify(fs.outputFile)

//----------------------------------------------------------
// logic
//----------------------------------------------------------
module.exports = class Jamb {
  constructor(cfg) {
    this._previewDelim = '--MORE--'
    this._yamlDelim = '---'
    this._defaultTemplate = 'page'
    this._opts = {
      jade: {
        basedir: './test/fixtures/templates',
        pretty: true
      }
      // TODO globby opts?
      // TODO markdown-it opts?
    }
    return co(
      function* () {
        const posts = yield this.content(cfg.posts)
        const content = this.merge(
          this.sort(yield this.content(cfg.pages)),
          this.sort(posts)
        )
        const templates = yield this.templates(cfg.templates)
        const html = this.render(content, posts, templates)
        // return content
        // return templates
        // return this.render(content, posts, templates)
      }.bind(this)
    ).catch(err => {
      console.log(err.stack)
      throw new Error(err)
    })
  }

  //----------------------------------------------------------
  // util fns
  //----------------------------------------------------------
  /**
    Shallow merge two objects with array values.
    @param {Object} to - merge to
    @param {Object} from - merge from
    @returns {Object} merged object
   */
  merge(to, from) {
    Object.keys(from).map(key =>
      to[key]
        ? from[key].map(data => to[key].push(data))
        : to[key] = from[key]
    )
    return to
  }

  /**
    Sort data by template.
    @param {Object[]} dataArr - array of data objects
    @returns {Object} object with template keys and data array values
   */
  sort(dataArr) {
    return dataArr.reduce((accum, data) => {
      const template = data.template || this._defaultTemplate
      if (data.template) delete data.template
      accum[template]
        ? accum[template].push(data)
        : accum[template] = [data]
      return accum
    }, {})
  }

  //----------------------------------------------------------
  // content fns
  //----------------------------------------------------------
  // TODO - JSDOC
  * content(glob) {
    const paths = yield globby(glob)
    return yield P.all(paths.map(path => read(path, 'utf8')))
      .map(strings => this.frontmatter(strings))
      .map(obj => this.preview(obj))
      .map(obj => this.markdown(obj))
  }

  /**
    Parse YAML frontmatter.
    @param {String} rawString - raw string from file
    @returns {Object} object with frontmatter and content
   */
  frontmatter(rawString) {
    const strings = rawString.split(this._yamlDelim)
    const dataObj = yaml.load(strings[1])
    dataObj.content = strings.slice(2).join(this._yamlDelim)
    return dataObj
  }

  /**
    Split content by preview separator.
    @param {Object} data - data object
    @returns {Object} data object
   */
  preview(data) {
    if (data.content.includes(this._previewDelim)) {
      const halves = data.content.split(this._previewDelim)
      data.preview = halves[0]
      data.content = halves[1]
      return data
    }
    return data
  }

  /**
    Render markdown data fields.
    @param {Object} data - data object
    @returns {Object} data object
   */
  markdown(data) {
    ['preview', 'content'].map(field => {
      if (data[field]) data[field] = md.render(data[field]).trim()
    })
    return data
  }

  //----------------------------------------------------------
  // template fns
  //----------------------------------------------------------
  // TODO jsdoc
  * templates(glob) {
    const paths = yield globby(glob)
    return yield P.all(paths.map(path => read(path, 'utf8')))
      .map(string => jade.compile(string, this._opts.jade))
      .reduce((accum, fn, i) => {
        accum[p.basename(paths[i], '.jade')] = fn
        return accum
      }, {})
  }

  // TODO jsdoc
  render(content, posts, templates) {
    const html = {}
    Object.keys(content).map(template => {
      content[template].map(data => {
        data.posts = posts
        html[data.url] = templates[template]({data})
      })
    })
    return html
  }
}

// function write(html) {
//   return Object.keys(html).map(url => {
//     const path = url === 'index'
//       ? `${url}.html`
//       : `${url}${p.sep}index.html`
//     fs.outputFileSync(p.join('dist', path), html[url])
//   })
// }

// function blog(cb) {
//   const content = merge(
//     getContentByTemplate('src/content'),
//     getContentByTemplate('src/content/posts')
//   )
//   const posts = getContent('src/content/posts')
//   const templates = getTemplates('src/templates')
//   const html = render(content, posts, templates)
//   write(html)
//   return cb()
// }
<|endoftext|>


<commit_before>var Entity = require('sourced').Entity;
var EventEmitter = require('events').EventEmitter;
var log = require('debug')('sourced-repo-mongo');
var mongo = require('./mongo');
var util = require('util');
var _ = require('lodash');

function Repository (entityType, options) {
  options = options || {};
  EventEmitter.call(this);
  if ( ! mongo.db) {
    throw new Error('mongo has not been initialized. you must call require(\'sourced-repo-mongo/mongo\').connect(config.MONGO_URL); before instantiating a Repository');
  }
  var indices = _.union(options.indices, ['id']);
  var self = this;
  var db = mongo.db;
  self.entityType = entityType;
  self.indices = indices;
  self.snapshotFrequency = options.snapshotFrequency || 10;

  var snapshotCollectionName = util.format('%s.snapshots', entityType.name);
  var snapshots = db.collection(snapshotCollectionName);
  self.snapshots = snapshots;
  var eventCollectionName = util.format('%s.events', entityType.name);
  var events = db.collection(eventCollectionName);
  self.events = events;

  var error = function (err) {
    if (err) return this.emit('error', err);
  };

  self.indices.forEach(function (index) {
    snapshots.ensureIndex(index, error);
    events.ensureIndex(index, error);
  });
  events.ensureIndex({ id: 1, version: 1 }, error);
  snapshots.ensureIndex({ id: 1, version: 1 }, error);
  snapshots.ensureIndex('snapshotVersion', error);

  log('initialized %s entity store', self.entityType.name);

  self.emit('ready');
}

util.inherits(Repository, EventEmitter);

Repository.prototype.commit = function commit (entity, options, cb) {
  if (typeof options === 'function') {
    cb = options;
    options = {};
  }

  var self = this;

  log('committing %s for id %s', this.entityType.name, entity.id);

  this._commitEvents(entity, function _afterCommitEvents (err) {
    if (err) return cb(err);

    self._commitSnapshots(entity, options, function _afterCommitSnapshots (err) {
      if (err) return cb(err);
      self._emitEvents(entity);
      return cb();
    });
  });

};

Repository.prototype.commitAll = function commit (entities, options, cb) {
  if (typeof options === 'function') {
    cb = options;
    options = {};
  }

  var self = this;

  log('committing %s for id %j', this.entityType.name, _.pluck(entities, 'id'));

  this._commitAllEvents(entities, function _afterCommitEvents (err) {
    if (err) return cb(err);
    self._commitAllSnapshots(entities, options, function _afterCommitSnapshots (err) {
      if (err) return cb(err);
      var promises = [];
      entities.forEach(function (entity) {
        promises.push(self._emitEvents(entity));
      });
      return cb();
    });
  });

};

Repository.prototype.get = function get (id, cb) {
  return this._getByIndex('id', id, cb);
};

Repository.prototype._getByIndex = function _getByIndex (index, value, cb) {
  var self = this;

  if (this.indices.indexOf(index) === -1) throw new Error('Cannot get sourced entity type [%s] by index [%s]', this.entityType, index);

  log('getting %s for %s %s', this.entityType.name, index, value);

  var criteria = { };
  criteria[index] = value;

  this.snapshots
    .find(criteria)
    .sort({ version: -1 })
    .limit(-1)
    .toArray(function (err, snapshots) {
      if (err) return cb(err);

      var snapshot = snapshots[0];

      if (snapshot) criteria.version = { $gt: snapshot.version };
      self.events.find(criteria)
        .sort({ version: 1 })
        .toArray(function (err, events) {
          if (err) return cb(err);
          if (snapshot) delete snapshot._id;
          if ( ! snapshot && ! events.length) return cb(null, null);

          var id = (index === 'id') ? value : (snapshot) ? snapshot.id : events[0].id;

          var entity = self._deserialize(id, snapshot, events);
          return cb(null, entity);
        });
  });
};

Repository.prototype.getAll = function getAll (ids, cb) {
  var self = this;

  if (typeof ids === 'function') {
    cb = ids;
    ids = null;
    return this.events.distinct('id', function (err, distinctEventIds) {
      self.getAll(distinctEventIds, cb);
    });
  }

  log('getting %ss for ids %j', this.entityType.name, ids);

  if (ids.length === 0) return cb(null, []);

  this._getAllSnapshots(ids, function _afterGetAllSnapshots (err, snapshots) {
    if (err) return cb(err);
    self._getAllEvents(ids, snapshots, function (err, entities) {
      if (err) return cb(err);
      return cb(null, entities);
    });
  });
};

Repository.prototype._commitEvents = function _commitEvents (entity, cb) {
  var self = this;

  if (entity.newEvents.length === 0) return cb();

  if ( ! entity.id) return cb(new Error('Cannot commit an entity of type [%s] without an [id] property', this.entityType));

  var events = entity.newEvents;
  events.forEach(function _applyIndices (event) {
    if (event && event._id) delete event._id; // mongo will blow up if we try to insert multiple _id keys
    self.indices.forEach(function (index) {
      event[index] = entity[index];
    });
  });
  self.events.insert(events, function (err) {
    if (err) return cb(err);
    log('committed %s.events for id %s', self.entityType.name, entity.id);
    entity.newEvents = [];
    return cb();
  });

};

Repository.prototype._commitAllEvents = function _commitEvents (entities, cb) {
  var self = this;

  var events = [];
  entities.forEach(function (entity) {
    if (entity.newEvents.length === 0) return;

    if ( ! entity.id) return cb(new Error('Cannot commit an entity of type [%s] without an [id] property', self.entityType));

    var evnts = entity.newEvents;
    evnts.forEach(function _applyIndices (event) {
      if (event && event._id) delete event._id; // mongo will blow up if we try to insert multiple _id keys
      self.indices.forEach(function (index) {
        event[index] = entity[index];
      });
    });
    Array.prototype.unshift.apply(events, evnts);
  });

  if (events.length === 0) return cb();

  self.events.insert(events, function (err) {
    if (err) return cb(err);
    log('committed %s.events for ids %j', self.entityType.name, _.pluck(entities, 'id'));
    entities.forEach(function (entity) {
      entity.newEvents = [];
    });
    return cb();
  });

};

Repository.prototype._commitSnapshots = function _commitSnapshots (entity, options, cb) {
  var self = this;

  if (options.forceSnapshot || entity.version >= entity.snapshotVersion + self.snapshotFrequency) {
    var snapshot = entity.snapshot();
    if (snapshot && snapshot._id) delete snapshot._id; // mongo will blow up if we try to insert multiple _id keys
    self.snapshots.insert(snapshot, function (err) {
      if (err) return cb(err);
      log('committed %s.snapshot for id %s %j', self.entityType.name, entity.id, snapshot);
      return cb(null, entity);
    });
  } else {
    return cb(null, entity);
  }

};

Repository.prototype._commitAllSnapshots = function _commitAllSnapshots (entities, options, cb) {
  var self = this;

  var snapshots = [];
  entities.forEach(function (entity) {
    if (options.forceSnapshot || entity.version >= entity.snapshotVersion + self.snapshotFrequency) {
      var snapshot = entity.snapshot();
      if (snapshot) {
        if (snapshot._id) delete snapshot._id; // mongo will blow up if we try to insert multiple _id keys)
        snapshots.push(snapshot);
      }
    }
  });

  if (snapshots.length === 0) return cb();

  self.snapshots.insert(snapshots, function (err) {
    if (err) return cb(err);
    log('committed %s.snapshot for ids %s %j', self.entityType.name, _.pluck(entities, 'id'), snapshots);
    return cb(null, entities);
  });

};

Repository.prototype._deserialize = function _deserialize (id, snapshot, events) {
  log('deserializing %s entity ', this.entityType.name);
  var entity = new this.entityType(snapshot, events);
  entity.id = id;
  return entity;
};

Repository.prototype._emitEvents = function _emitEvents (entity) {
  var self = this;

  var eventsToEmit = entity.eventsToEmit;
  entity.eventsToEmit = [];
  eventsToEmit.forEach(function (eventToEmit) {
    var args = Array.prototype.slice.call(eventToEmit);
    self.entityType.prototype.emit.apply(entity, args);
  });

  log('emitted local events for id %s', entity.id);

};

Repository.prototype._getAllSnapshots = function _getAllSnapshots (ids, cb) {
  var self = this;

  var match = { $match: { id: { $in: ids } } };
  var group = { $group: { _id: '$id', snapshotVersion: { $last: '$snapshotVersion' } } };

  self.snapshots.aggregate([match, group], function (err, idVersionPairs) {
    if (err) return cb(err);
    var criteria = {};
    if (idVersionPairs.length === 0) {
      return cb(null, []);
    } else if (idVersionPairs.length === 1) {
      criteria = { id: idVersionPairs[0]._id, snapshotVersion: idVersionPairs[0].snapshotVersion };
    } else {
      criteria.$or = [];
      idVersionPairs.forEach(function (pair) {
        var cri = { id: pair._id, snapshotVersion: pair.snapshotVersion };
        criteria.$or.push(cri);
      });
    }
    self.snapshots
      .find(criteria)
      .toArray(function (err, snapshots) {
        if (err) cb(err);
        return cb(null, snapshots);
      });
  });

};

Repository.prototype._getAllEvents = function _getAllEvents (ids, snapshots, cb) {
  var self = this;

  var criteria = { $or: [] };
  ids.forEach(function (id) {
    var snapshot;
    if ( ! (snapshot = _.find(snapshots, function (snapshot) {
      return id === snapshot.id;
    }))) {
      criteria.$or.push({ id: id });
    } else {
      criteria.$or.push({ id: snapshot.id, version: { $gt: snapshot.snapshotVersion } });
    }
  });

  self.events.find(criteria)
    .sort({ id: 1, version: 1 })
    .toArray(function (err, events) {
      if (err) return cb(err);
      if ( ! snapshots.length && ! events.length) return cb(null, null);
      var results = [];
      ids.forEach(function (id) {
        var snapshot = _.find(snapshots, function (snapshot) {
          return snapshot.id === id;
        });
        if (snapshot) delete snapshot._id;
        var evnts = _.filter(events, function (event) {
          return event.id === id;
        });
        var entity = self._deserialize(id, snapshot, evnts);
        results.push(entity);
      });
      return cb(null, results);
    });

};

module.exports.Repository = Repository;<commit_msg>When using $last in a $group stage, the $group stage should follow a $sort stage to have the input documents in a defined order.<commit_after>var Entity = require('sourced').Entity;
var EventEmitter = require('events').EventEmitter;
var log = require('debug')('sourced-repo-mongo');
var mongo = require('./mongo');
var util = require('util');
var _ = require('lodash');

function Repository (entityType, options) {
  options = options || {};
  EventEmitter.call(this);
  if ( ! mongo.db) {
    throw new Error('mongo has not been initialized. you must call require(\'sourced-repo-mongo/mongo\').connect(config.MONGO_URL); before instantiating a Repository');
  }
  var indices = _.union(options.indices, ['id']);
  var self = this;
  var db = mongo.db;
  self.entityType = entityType;
  self.indices = indices;
  self.snapshotFrequency = options.snapshotFrequency || 10;

  var snapshotCollectionName = util.format('%s.snapshots', entityType.name);
  var snapshots = db.collection(snapshotCollectionName);
  self.snapshots = snapshots;
  var eventCollectionName = util.format('%s.events', entityType.name);
  var events = db.collection(eventCollectionName);
  self.events = events;

  var error = function (err) {
    if (err) return this.emit('error', err);
  };

  self.indices.forEach(function (index) {
    snapshots.ensureIndex(index, error);
    events.ensureIndex(index, error);
  });
  events.ensureIndex({ id: 1, version: 1 }, error);
  snapshots.ensureIndex({ id: 1, version: 1 }, error);
  snapshots.ensureIndex('snapshotVersion', error);

  log('initialized %s entity store', self.entityType.name);

  self.emit('ready');
}

util.inherits(Repository, EventEmitter);

Repository.prototype.commit = function commit (entity, options, cb) {
  if (typeof options === 'function') {
    cb = options;
    options = {};
  }

  var self = this;

  log('committing %s for id %s', this.entityType.name, entity.id);

  this._commitEvents(entity, function _afterCommitEvents (err) {
    if (err) return cb(err);

    self._commitSnapshots(entity, options, function _afterCommitSnapshots (err) {
      if (err) return cb(err);
      self._emitEvents(entity);
      return cb();
    });
  });

};

Repository.prototype.commitAll = function commit (entities, options, cb) {
  if (typeof options === 'function') {
    cb = options;
    options = {};
  }

  var self = this;

  log('committing %s for id %j', this.entityType.name, _.pluck(entities, 'id'));

  this._commitAllEvents(entities, function _afterCommitEvents (err) {
    if (err) return cb(err);
    self._commitAllSnapshots(entities, options, function _afterCommitSnapshots (err) {
      if (err) return cb(err);
      var promises = [];
      entities.forEach(function (entity) {
        promises.push(self._emitEvents(entity));
      });
      return cb();
    });
  });

};

Repository.prototype.get = function get (id, cb) {
  return this._getByIndex('id', id, cb);
};

Repository.prototype._getByIndex = function _getByIndex (index, value, cb) {
  var self = this;

  if (this.indices.indexOf(index) === -1) throw new Error('Cannot get sourced entity type [%s] by index [%s]', this.entityType, index);

  log('getting %s for %s %s', this.entityType.name, index, value);

  var criteria = { };
  criteria[index] = value;

  this.snapshots
    .find(criteria)
    .sort({ version: -1 })
    .limit(-1)
    .toArray(function (err, snapshots) {
      if (err) return cb(err);

      var snapshot = snapshots[0];

      if (snapshot) criteria.version = { $gt: snapshot.version };
      self.events.find(criteria)
        .sort({ version: 1 })
        .toArray(function (err, events) {
          if (err) return cb(err);
          if (snapshot) delete snapshot._id;
          if ( ! snapshot && ! events.length) return cb(null, null);

          var id = (index === 'id') ? value : (snapshot) ? snapshot.id : events[0].id;

          var entity = self._deserialize(id, snapshot, events);
          return cb(null, entity);
        });
  });
};

Repository.prototype.getAll = function getAll (ids, cb) {
  var self = this;

  if (typeof ids === 'function') {
    cb = ids;
    ids = null;
    return this.events.distinct('id', function (err, distinctEventIds) {
      self.getAll(distinctEventIds, cb);
    });
  }

  log('getting %ss for ids %j', this.entityType.name, ids);

  if (ids.length === 0) return cb(null, []);

  this._getAllSnapshots(ids, function _afterGetAllSnapshots (err, snapshots) {
    if (err) return cb(err);
    self._getAllEvents(ids, snapshots, function (err, entities) {
      if (err) return cb(err);
      return cb(null, entities);
    });
  });
};

Repository.prototype._commitEvents = function _commitEvents (entity, cb) {
  var self = this;

  if (entity.newEvents.length === 0) return cb();

  if ( ! entity.id) return cb(new Error('Cannot commit an entity of type [%s] without an [id] property', this.entityType));

  var events = entity.newEvents;
  events.forEach(function _applyIndices (event) {
    if (event && event._id) delete event._id; // mongo will blow up if we try to insert multiple _id keys
    self.indices.forEach(function (index) {
      event[index] = entity[index];
    });
  });
  self.events.insert(events, function (err) {
    if (err) return cb(err);
    log('committed %s.events for id %s', self.entityType.name, entity.id);
    entity.newEvents = [];
    return cb();
  });

};

Repository.prototype._commitAllEvents = function _commitEvents (entities, cb) {
  var self = this;

  var events = [];
  entities.forEach(function (entity) {
    if (entity.newEvents.length === 0) return;

    if ( ! entity.id) return cb(new Error('Cannot commit an entity of type [%s] without an [id] property', self.entityType));

    var evnts = entity.newEvents;
    evnts.forEach(function _applyIndices (event) {
      if (event && event._id) delete event._id; // mongo will blow up if we try to insert multiple _id keys
      self.indices.forEach(function (index) {
        event[index] = entity[index];
      });
    });
    Array.prototype.unshift.apply(events, evnts);
  });

  if (events.length === 0) return cb();

  self.events.insert(events, function (err) {
    if (err) return cb(err);
    log('committed %s.events for ids %j', self.entityType.name, _.pluck(entities, 'id'));
    entities.forEach(function (entity) {
      entity.newEvents = [];
    });
    return cb();
  });

};

Repository.prototype._commitSnapshots = function _commitSnapshots (entity, options, cb) {
  var self = this;

  if (options.forceSnapshot || entity.version >= entity.snapshotVersion + self.snapshotFrequency) {
    var snapshot = entity.snapshot();
    if (snapshot && snapshot._id) delete snapshot._id; // mongo will blow up if we try to insert multiple _id keys
    self.snapshots.insert(snapshot, function (err) {
      if (err) return cb(err);
      log('committed %s.snapshot for id %s %j', self.entityType.name, entity.id, snapshot);
      return cb(null, entity);
    });
  } else {
    return cb(null, entity);
  }

};

Repository.prototype._commitAllSnapshots = function _commitAllSnapshots (entities, options, cb) {
  var self = this;

  var snapshots = [];
  entities.forEach(function (entity) {
    if (options.forceSnapshot || entity.version >= entity.snapshotVersion + self.snapshotFrequency) {
      var snapshot = entity.snapshot();
      if (snapshot) {
        if (snapshot._id) delete snapshot._id; // mongo will blow up if we try to insert multiple _id keys)
        snapshots.push(snapshot);
      }
    }
  });

  if (snapshots.length === 0) return cb();

  self.snapshots.insert(snapshots, function (err) {
    if (err) return cb(err);
    log('committed %s.snapshot for ids %s %j', self.entityType.name, _.pluck(entities, 'id'), snapshots);
    return cb(null, entities);
  });

};

Repository.prototype._deserialize = function _deserialize (id, snapshot, events) {
  log('deserializing %s entity ', this.entityType.name);
  var entity = new this.entityType(snapshot, events);
  entity.id = id;
  return entity;
};

Repository.prototype._emitEvents = function _emitEvents (entity) {
  var self = this;

  var eventsToEmit = entity.eventsToEmit;
  entity.eventsToEmit = [];
  eventsToEmit.forEach(function (eventToEmit) {
    var args = Array.prototype.slice.call(eventToEmit);
    self.entityType.prototype.emit.apply(entity, args);
  });

  log('emitted local events for id %s', entity.id);

};

Repository.prototype._getAllSnapshots = function _getAllSnapshots (ids, cb) {
  var self = this;

  var match = { $match: { id: { $in: ids } } };
  var sort = { $sort: { snapshotVersion: 1 } };
  var group = { $group: { _id: '$id', snapshotVersion: { $last: '$snapshotVersion' } } };

  self.snapshots.aggregate([match, sort, group], function (err, idVersionPairs) {
    if (err) return cb(err);
    var criteria = {};
    if (idVersionPairs.length === 0) {
      return cb(null, []);
    } else if (idVersionPairs.length === 1) {
      criteria = { id: idVersionPairs[0]._id, snapshotVersion: idVersionPairs[0].snapshotVersion };
    } else {
      criteria.$or = [];
      idVersionPairs.forEach(function (pair) {
        var cri = { id: pair._id, snapshotVersion: pair.snapshotVersion };
        criteria.$or.push(cri);
      });
    }
    self.snapshots
      .find(criteria)
      .toArray(function (err, snapshots) {
        if (err) cb(err);
        return cb(null, snapshots);
      });
  });

};

Repository.prototype._getAllEvents = function _getAllEvents (ids, snapshots, cb) {
  var self = this;

  var criteria = { $or: [] };
  ids.forEach(function (id) {
    var snapshot;
    if ( ! (snapshot = _.find(snapshots, function (snapshot) {
      return id === snapshot.id;
    }))) {
      criteria.$or.push({ id: id });
    } else {
      criteria.$or.push({ id: snapshot.id, version: { $gt: snapshot.snapshotVersion } });
    }
  });

  self.events.find(criteria)
    .sort({ id: 1, version: 1 })
    .toArray(function (err, events) {
      if (err) return cb(err);
      if ( ! snapshots.length && ! events.length) return cb(null, null);
      var results = [];
      ids.forEach(function (id) {
        var snapshot = _.find(snapshots, function (snapshot) {
          return snapshot.id === id;
        });
        if (snapshot) delete snapshot._id;
        var evnts = _.filter(events, function (event) {
          return event.id === id;
        });
        var entity = self._deserialize(id, snapshot, evnts);
        results.push(entity);
      });
      return cb(null, results);
    });

};

module.exports.Repository = Repository;
<|endoftext|>


<commit_before>'use strict';

var nextNode = require('next-node');

var win = window;
var doc = document;

/**
 * Get a Selection's Range
 * @see http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
 * @param {Selection} sel
 * @return {Range|null}
 */
function getRange(sel) {
	sel = sel || win.getSelection();
	return sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
}

/**
 * Restore a Selection Range
 * @see http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
 * @param {Range} saved
 */
function setRange(saved) {
	if (!saved) return;
	// new selection
	var sel = win.getSelection();
	sel.removeAllRanges();
	sel.addRange(saved);
}

/**
 * Get a Selection Rectangle
 * @see http://stackoverflow.com/questions/12603397/calculate-width-height-of-the-selected-text-javascript
 * @see http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page
 * @param {Selection} sel
 * @return {Object|Boolean}
 */
function getRect(sel) {
	sel = sel || win.getSelection();
	if (!sel.rangeCount) return false;

	var range = sel.getRangeAt(0).cloneRange();

	// on Webkit 'range.getBoundingClientRect()' sometimes return 0/0/0/0 - but 'range.getClientRects()' works
	var rects = range.getClientRects ? range.getClientRects() : [];
	for (var i = 0; i < rects.length; ++i) {
		var rect = rects[i];
		if (rect.left && rect.top && rect.right && rect.bottom) {
			return {
				// Modern browsers return floating-point numbers
				left: parseInt(rect.left),
				top: parseInt(rect.top),
				width: parseInt(rect.right - rect.left),
				height: parseInt(rect.bottom - rect.top)
			};
		}
	}

	return false;
}

/**
 * Get all Nodes within a selected Range.
 * @see http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div
 * @param  {Range} range
 * @return {Array}
 */
function getNodes(range) {
	range = range || getRange();
	if (!range) return [];

	var node = range.startContainer;
	var endNode = range.endContainer;

	// Special case for a range that is contained within a single node
	if (node == endNode) return [node];

	// Iterate nodes until we hit the end container
	var nodes = [];
	while (node && node != endNode) {
		nodes.push(node = nextNode(node));
	}

	// Add partially selected nodes at the start of the range
	node = range.startContainer;
	while (node && node != range.commonAncestorContainer) {
		nodes.unshift(node);
		node = node.parentNode;
	}

	return nodes;
}

/**
 * Get the inner HTML content of a selection.
 * @see http://stackoverflow.com/questions/4652734/return-html-from-a-user-selected-text/4652824#4652824
 * @param {Selection} sel
 * @return {String}
 */
function getHTML(sel) {
	sel = sel || win.getSelection();
	if (!sel.rangeCount || sel.isCollapsed) return null;

	var len = sel.rangeCount;
	var container = doc.createElement('div');

	for (var i = 0; i < len; ++i) {
		container.appendChild(sel.getRangeAt(i).cloneContents());
	}

	return container.innerHTML;
};

/**
 * Is there a Selection active?
 * @param {Selection} sel
 * @return {Boolean}
 */
function isCollapsed(sel) {
	return (sel || win.getSelection()).isCollapsed;
}

/**
 * Collapse a Selection to its beginning.
 * @param {Selection} sel
 * @see  http://stackoverflow.com/questions/8513368/collapse-selection-to-start-of-selection-not-div
 */
function collapseStart(sel) {
	(sel || win.getSelection()).collapseToStart();
}

/**
 * Collapse a Selection to its endpoint.
 * @param {Selection} sel
 */
function collapseEnd(sel) {
	(sel || win.getSelection()).collapseToEnd();
}

/**
 * Is the Selection within given container Node?
 * @param {Node} container  The container node
 * @param {Selection} sel
 * @return {Boolean}
 */
function isWithin(container, sel) {
	sel = sel || win.getSelection();
	return container.contains(sel.anchorNode) && container.contains(sel.focusNode);
}

/**
 * Force/Restrict a Selection to the container & its children only.
 * @param {Node} container
 * @param {Selection} sel
 */
function forceWithin(container, sel) {
	sel = sel || win.getSelection();
	var range = doc.createRange();

	range.selectNodeContents(container);
	range.collapse(false);
	sel.removeAllRanges();
	sel.addRange(range);
}
<commit_msg>declare exports object<commit_after>'use strict';

var nextNode = require('next-node');

var win = window;
var doc = document;

/**
 * Get a Selection's Range
 * @see http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
 * @param {Selection} sel
 * @return {Range|null}
 */
function getRange(sel) {
	sel = sel || win.getSelection();
	return sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
}

/**
 * Restore a Selection Range
 * @see http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
 * @param {Range} saved
 */
function setRange(saved) {
	if (!saved) return;
	// new selection
	var sel = win.getSelection();
	sel.removeAllRanges();
	sel.addRange(saved);
}

/**
 * Get a Selection Rectangle
 * @see http://stackoverflow.com/questions/12603397/calculate-width-height-of-the-selected-text-javascript
 * @see http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page
 * @param {Selection} sel
 * @return {Object|Boolean}
 */
function getRect(sel) {
	sel = sel || win.getSelection();
	if (!sel.rangeCount) return false;

	var range = sel.getRangeAt(0).cloneRange();

	// on Webkit 'range.getBoundingClientRect()' sometimes return 0/0/0/0 - but 'range.getClientRects()' works
	var rects = range.getClientRects ? range.getClientRects() : [];
	for (var i = 0; i < rects.length; ++i) {
		var rect = rects[i];
		if (rect.left && rect.top && rect.right && rect.bottom) {
			return {
				// Modern browsers return floating-point numbers
				left: parseInt(rect.left),
				top: parseInt(rect.top),
				width: parseInt(rect.right - rect.left),
				height: parseInt(rect.bottom - rect.top)
			};
		}
	}

	return false;
}

/**
 * Get all Nodes within a selected Range.
 * @see http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div
 * @param  {Range} range
 * @return {Array}
 */
function getNodes(range) {
	range = range || getRange();
	if (!range) return [];

	var node = range.startContainer;
	var endNode = range.endContainer;

	// Special case for a range that is contained within a single node
	if (node == endNode) return [node];

	// Iterate nodes until we hit the end container
	var nodes = [];
	while (node && node != endNode) {
		nodes.push(node = nextNode(node));
	}

	// Add partially selected nodes at the start of the range
	node = range.startContainer;
	while (node && node != range.commonAncestorContainer) {
		nodes.unshift(node);
		node = node.parentNode;
	}

	return nodes;
}

/**
 * Get the inner HTML content of a selection.
 * @see http://stackoverflow.com/questions/4652734/return-html-from-a-user-selected-text/4652824#4652824
 * @param {Selection} sel
 * @return {String}
 */
function getHTML(sel) {
	sel = sel || win.getSelection();
	if (!sel.rangeCount || sel.isCollapsed) return null;

	var len = sel.rangeCount;
	var container = doc.createElement('div');

	for (var i = 0; i < len; ++i) {
		container.appendChild(sel.getRangeAt(i).cloneContents());
	}

	return container.innerHTML;
};

/**
 * Is there a Selection active?
 * @param {Selection} sel
 * @return {Boolean}
 */
function isCollapsed(sel) {
	return (sel || win.getSelection()).isCollapsed;
}

/**
 * Collapse a Selection to its beginning.
 * @param {Selection} sel
 * @see  http://stackoverflow.com/questions/8513368/collapse-selection-to-start-of-selection-not-div
 */
function collapseStart(sel) {
	(sel || win.getSelection()).collapseToStart();
}

/**
 * Collapse a Selection to its endpoint.
 * @param {Selection} sel
 */
function collapseEnd(sel) {
	(sel || win.getSelection()).collapseToEnd();
}

/**
 * Is the Selection within given container Node?
 * @param {Node} container  The container node
 * @param {Selection} sel
 * @return {Boolean}
 */
function isWithin(container, sel) {
	sel = sel || win.getSelection();
	return container.contains(sel.anchorNode) && container.contains(sel.focusNode);
}

/**
 * Force/Restrict a Selection to the container & its children only.
 * @param {Node} container
 * @param {Selection} sel
 */
function forceWithin(container, sel) {
	sel = sel || win.getSelection();
	var range = doc.createRange();

	range.selectNodeContents(container);
	range.collapse(false);
	sel.removeAllRanges();
	sel.addRange(range);
}

module.exports = {
	getRange: getRange,
	setRange: setRange,
	getRect: getRect,
	getNodes: getNodes,
	getHTML: getHTML,
	isCollapsed: isCollapsed,
	collapseStart: collapseStart,
	collapseEnd: collapseEnd,
	isWithin: isWithin,
	forceWithin: forceWithin
};
<|endoftext|>


<commit_before>'use strict';

var util = require('./lib/util');

exports = module.exports = Parth;

function Parth(o){
  o = o || {};
  if(this instanceof Parth){
    this.regex = [];
    this.regex.master = /(?:[])/;
    return this;
  }

  return new Parth(o);
}

// ## parth.add(path)
// > path to a regex adding it to parth.store
// > TODO: regexp input
//
// arguments
//  - path, type `string`
//
// returns
//  - null for non-supported types
//  - regular expression from the path
//
//

var paramRE = /(^|\W)\:([^(?#/.: ]+)(\([^)]*?\)+)?/g;

Parth.prototype.add = function(p, o){
  o = o || { }; if(!util.boil(p, o)){ return null; }

  var index, regex;
  o.depth = util.boil.argv(o.path).length;

  if(!this.regex[o.depth]){
    regex = this.regex;
    index = regex.length;
    while(index < o.depth){
      index = regex.push([]);
      regex[index-1].master = /(?:[])/;
    }
  }
  regex = this.regex[o.depth-1];

  // default and custom regexes indexes
  var sep, cus = 0, def = 0;
  o.regex = o.path.replace(/\S+/g, function(stem){
    sep = (stem.match(/\//) || stem.match(/\./) || ' ')[0].trim();
    return stem.replace(paramRE, function($0, $1, $2, $3){
      if($3){ cus++; } else { def++; }
      return $1 + ($3 || '([^'+sep+' ]+)');
    });
  }).replace(/[^()]+(?=\(|$)/g, function($0){
      // escape separation tokens outside parens
      return $0.replace(/[\/\.]/g, '\\$&');
    });

  // attach relevant info
  o.regex = new RegExp(o.regex, 'i');
  o.regex.path = o.path; o.regex.def = def; o.regex.cus = cus;

  // reorder them
  regex.push(o.regex);
  regex = regex.sort(function(a, b){
    return (a.def - b.cus) - (b.def - a.cus);
  });

  // ## sum up all learned
  // - void groups and
  // - make one regex per depth
  // - make a giant regex for everything
  //
  //
  regex.master = new RegExp('(' +
    regex.map(function(re){
      var group = re.source.replace(/\((?=[^?])/g, '(?:');
      if(re.def + re.cus){
         return '(?:^' + util.escapeRegExp(re.path) + '$)|(?:^' + group + ')';
      } else {
        return '^' + group;
      }
    }).join(')|(') + ')',
  'i');

  // THE GIANT REGEXP
  // -----------Oooo---
  // -----------(----)---
  // ------------)--/----
  // ------------(_/-
  // ----oooO----
  // ----(---)----
  // -----\--(--
  // ------\_)-
  //
  this.regex.master = new RegExp('(' +
    this.regex.map(function(re){
      return re.master.source.replace(/\((?=[^?])/g, '(?:');
    }).reverse().join(')|(') + ')',
  'i');

  return o.regex;
};

// ## parth.match(path[, options])
// > take a string or array, return the matching path
//
// arguments
//  - path, type `string`
//  - options, type `object` optional holding all extra information
//
// return
//  - null for non-supported types or not matching path
//  - regex with for the matching path
//
Parth.prototype.match = function(p, o){
  o = o || {}; o.notFound = true;
  if(!util.boil(p, o) || !this.regex.master.test(o.path)){
    return null;
  }

  var index = this.regex.length-1;
  var parth = o.path.match(this.regex.master).slice(1);
    o.depth = index - parth.indexOf(parth.join(''));
  var found = o.path.match(this.regex[o.depth].master).slice(1);
  var regex = this.regex[o.depth][found.indexOf(found.join(''))];

  o.match = found.join('');
  o.params = {_: o.path.match(regex).slice(1)};

  index = 0;
  regex.path.replace(paramRE, function($0, $1, $2){
    var p = o.params._[index], num = Number(p);
    o.params[$2] = util.isNaN(num) ? p : num;
    o.params._[index++] = $2;
  });

  o.notFound = o.path.replace(o.match, '') || false;
  return regex;
};
<commit_msg>minor improvements<commit_after>'use strict';

var util = require('./lib/util');

exports = module.exports = Parth;

function Parth(o){
  o = o || {};
  if(this instanceof Parth){
    this.regex = [];
    this.regex.master = /(?:[])/;
    return this;
  }

  return new Parth(o);
}

// ## parth.add(path)
// > path to a regex adding it to parth.store
// > TODO: regexp input
//
// arguments
//  - path, type `string`
//
// returns
//  - null for non-supported types
//  - regular expression from the path
//
//

var paramRE = /(^|\W)\:([^(?#/.: ]+)(\([^)]*?\)+)?/g;

Parth.prototype.add = function(p, o){
  o = o || { }; if(!util.boil(p, o)){ return null; }

  var index, regex = this.regex;
  o.depth = util.boil.argv(o.path).length;

  if(!regex[o.depth-1]){
    index = regex.length;
    while(index < o.depth){
      index = regex.push([]);
      regex[index-1].master = /(?:[])/;
    }
  }
  regex = regex[o.depth-1];

  // default and custom regexes indexes
  var sep, cus = 0, def = 0;
  o.regex = o.path.replace(/\S+/g, function(stem){
    sep = (stem.match(/\//) || stem.match(/\./) || ' ')[0].trim();
    return stem.replace(paramRE, function($0, $1, $2, $3){
      if($3){ cus++; } else { def++; }
      return $1 + ($3 || '([^'+sep+' ]+)');
    });
  }).replace(/[^()]+(?=\(|$)/g, function($0){
      // escape separation tokens outside parens
      return $0.replace(/[\/\.]/g, '\\$&');
    });

  // attach relevant info
  o.regex = new RegExp(o.regex, 'i');
  o.regex.path = o.path; o.regex.depth = o.depth-1;
  o.regex.def = def; o.regex.cus = cus;

  // reorder them
  regex.push(o.regex);
  regex = regex.sort(function(a, b){
    return (a.def - b.cus) - (b.def - a.cus);
  });

  // ## sum up all learned
  // - void groups and
  // - make one regex per depth
  // - make a giant regex for everything
  //
  //
  regex.master = new RegExp('(' +
    regex.map(function(re){
      var group = re.source.replace(/\((?=[^?])/g, '(?:');
      if(re.def + re.cus){
         return '(?:^' + util.escapeRegExp(re.path) + '$)|(?:^' + group + ')';
      } else {
        return '^' + group;
      }
    }).join(')|(') + ')',
  'i');

  // THE GIANT REGEXP
  // -----------Oooo---
  // -----------(----)---
  // ------------)--/----
  // ------------(_/-
  // ----oooO----
  // ----(---)----
  // -----\--(--
  // ------\_)-
  //
  this.regex.master = new RegExp('(' +
    this.regex.map(function(re){
      return re.master.source.replace(/\((?=[^?])/g, '(?:');
    }).reverse().join(')|(') + ')',
  'i');

  return o.regex;
};

// ## parth.match(path[, options])
// > take a string or array, return the matching path
//
// arguments
//  - path, type `string`
//  - options, type `object` optional holding all extra information
//
// return
//  - null for non-supported types or not matching path
//  - regex with for the matching path
//
Parth.prototype.match = function(p, o){
  o = o || {}; o.notFound = true;
  if(!util.boil(p, o)){ return null; }
  if(!this.regex.master.test(o.path)){ return null; }

  var index = this.regex.length-1;
  var parth = o.path.match(this.regex.master).slice(1);
    o.depth = index - parth.indexOf(parth.join(''));
  var found = o.path.match(this.regex[o.depth].master).slice(1);
    o.match = found.join('');
  var regex = this.regex[o.depth][found.indexOf(o.match)];

  o.params = {_: o.path.match(regex).slice(1)};

  index = 0;
  regex.path.replace(paramRE, function($0, $1, $2){
    var p = o.params._[index], num = Number(p);
    o.params[$2] = util.isNaN(num) ? p : num;
    o.params._[index++] = $2;
  });

  o.notFound = o.path.replace(o.match, '') || false;
  return regex;
};
<|endoftext|>


<commit_before>/**
 * Expose `pathtoRegexp`.
 */
module.exports = pathtoRegexp;

var PATH_REGEXP = new RegExp([
  // Match already escaped characters that would otherwise incorrectly appear
  // in future matches. This allows the user to escape special characters that
  // shouldn't be transformed.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
  // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
  '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?',
  // Match regexp special characters that should always be escaped.
  '([.+*?=^!:${}()[\\]|\\/])'
].join('|'), 'g');

/**
 * Escape the capturing group by escaping special characters and meaning.
 *
 * @param  {String} group
 * @return {String}
 */
function escapeGroup (group) {
  return group.replace(/([=!:$\/()])/g, '\\$1');
}

/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array should be passed in, which will contain the placeholder key
 * names. For example `/user/:id` will then contain `["id"]`.
 *
 * @param  {(String|RegExp|Array)} path
 * @param  {Array}                 keys
 * @param  {Object}                options
 * @return {RegExp}
 */
function pathtoRegexp (path, keys, options) {
  keys = keys || [];
  options = options || {};

  var strict = options.strict;
  var end = options.end !== false;
  var flags = options.sensitive ? '' : 'i';
  var index = 0;

  if (path instanceof RegExp) {
    // Match all capturing groups of a regexp.
    var groups = path.source.match(/\((?!\?)/g) || [];

    // Map all the matches to their numeric keys and push into the keys.
    keys.push.apply(keys, groups.map(function (match, index) {
      return {
        name:      index,
        delimiter: null,
        optional:  false,
        repeat:    false
      };
    }));

    // Return the source back to the user.
    return path;
  }

  if (Array.isArray(path)) {
    // Map array parts into regexps and return their source. We also pass
    // the same keys and options instance into every generation to get
    // consistent matching groups before we join the sources together.
    path = path.map(function (value) {
      return pathtoRegexp(value, keys, options).source;
    });

    // Generate a new regexp instance by joining all the parts together.
    return new RegExp('(?:' + path.join('|') + ')', flags);
  }

  // Alter the path string into a usable regexp.
  path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape) {
    // Avoiding re-escaping escaped characters.
    if (escaped) {
      return escaped;
    }

    // Escape regexp special characters.
    if (escape) {
      return '\\' + escape;
    }

    keys.push({
      name:      key || index++,
      delimiter: prefix || '/',
      optional:  suffix === '?' || suffix === '*',
      repeat:    suffix === '+' || suffix === '*'
    });

    // Escape the prefix character.
    prefix = prefix ? '\\' + prefix : '';

    // Match using the custom capturing group, or fallback to capturing
    // everything up to the next slash (or next period if the param was
    // prefixed with a period).
    capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?');

    // More complex regexp is required for suffix support.
    if (suffix) {
      if (suffix === '+') {
        return prefix + '(' + capture + '(?:' + prefix + capture + ')*)'
      }

      if (suffix === '*') {
        return '(?:' + prefix + '(' + capture + '(?:' + prefix + capture + ')*|' + capture + '))?';
      }

      return '(?:' + prefix + '(' + capture + '))?';
    }

    // Basic parameter support.
    return prefix + '(' + capture + ')';
  });

  if (path[path.length - 1] !== '/') {
    // If we are doing a non-ending match, we need to prompt the matching groups
    // to match as much as possible. To do this, we add a positive lookahead for
    // the next path fragment or the end. However, if the regexp already ends
    // in a path fragment, we'll run into problems.
    if (!end) {
      path += '(?=\\/|$)';
    }

    // Allow trailing slashes to be matched in non-strict, ending mode.
    if (end && !strict) {
      path += '\\/?';
    }
  }

  return new RegExp('^' + path + (end ? '$' : ''), flags);
};
<commit_msg>Improve repeated parameter regexp generation<commit_after>/**
 * Expose `pathtoRegexp`.
 */
module.exports = pathtoRegexp;

var PATH_REGEXP = new RegExp([
  // Match already escaped characters that would otherwise incorrectly appear
  // in future matches. This allows the user to escape special characters that
  // shouldn't be transformed.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
  // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
  '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?',
  // Match regexp special characters that should always be escaped.
  '([.+*?=^!:${}()[\\]|\\/])'
].join('|'), 'g');

/**
 * Escape the capturing group by escaping special characters and meaning.
 *
 * @param  {String} group
 * @return {String}
 */
function escapeGroup (group) {
  return group.replace(/([=!:$\/()])/g, '\\$1');
}

/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array should be passed in, which will contain the placeholder key
 * names. For example `/user/:id` will then contain `["id"]`.
 *
 * @param  {(String|RegExp|Array)} path
 * @param  {Array}                 keys
 * @param  {Object}                options
 * @return {RegExp}
 */
function pathtoRegexp (path, keys, options) {
  keys = keys || [];
  options = options || {};

  var strict = options.strict;
  var end = options.end !== false;
  var flags = options.sensitive ? '' : 'i';
  var index = 0;

  if (path instanceof RegExp) {
    // Match all capturing groups of a regexp.
    var groups = path.source.match(/\((?!\?)/g) || [];

    // Map all the matches to their numeric keys and push into the keys.
    keys.push.apply(keys, groups.map(function (match, index) {
      return {
        name:      index,
        delimiter: null,
        optional:  false,
        repeat:    false
      };
    }));

    // Return the source back to the user.
    return path;
  }

  if (Array.isArray(path)) {
    // Map array parts into regexps and return their source. We also pass
    // the same keys and options instance into every generation to get
    // consistent matching groups before we join the sources together.
    path = path.map(function (value) {
      return pathtoRegexp(value, keys, options).source;
    });

    // Generate a new regexp instance by joining all the parts together.
    return new RegExp('(?:' + path.join('|') + ')', flags);
  }

  // Alter the path string into a usable regexp.
  path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape) {
    // Avoiding re-escaping escaped characters.
    if (escaped) {
      return escaped;
    }

    // Escape regexp special characters.
    if (escape) {
      return '\\' + escape;
    }

    var repeat   = suffix === '+' || suffix === '*';
    var optional = suffix === '?' || suffix === '*';

    keys.push({
      name:      key || index++,
      delimiter: prefix || '/',
      optional:  optional,
      repeat:    repeat
    });

    // Escape the prefix character.
    prefix = prefix ? '\\' + prefix : '';

    // Match using the custom capturing group, or fallback to capturing
    // everything up to the next slash (or next period if the param was
    // prefixed with a period).
    capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?');

    // Allow parameters to be repeated more than once.
    if (repeat) {
      capture = capture + '(?:' + prefix + capture + ')*';
    }

    // Allow a parameter to be optional.
    if (optional) {
      return '(?:' + prefix + '(' + capture + '))?';
    }

    // Basic parameter support.
    return prefix + '(' + capture + ')';
  });

  if (path[path.length - 1] !== '/') {
    // If we are doing a non-ending match, we need to prompt the matching groups
    // to match as much as possible. To do this, we add a positive lookahead for
    // the next path fragment or the end. However, if the regexp already ends
    // in a path fragment, we'll run into problems.
    if (!end) {
      path += '(?=\\/|$)';
    }

    // Allow trailing slashes to be matched in non-strict, ending mode.
    if (end && !strict) {
      path += '\\/?';
    }
  }

  return new RegExp('^' + path + (end ? '$' : ''), flags);
};
<|endoftext|>


<commit_before>var fs = require("fs");
var _ = require("lodash");
require.resolve = function (request) {
	return module._getFilename(request);
};

module.exports = function (options) {
	options = _.assign({}, {
		clean:     true,
		config:    {},
		css:       false,
		selector:  "body",
		dest:      fs.absolute("")
	}, options);

	return function (done) {
		var page = this;

		var easy = require.resolve("easy-mathjax");
		page.injectJs(easy);

		page.on("mathjax:css", function (css) {
			fs.write(options.dest + "mathjax.css", css, "w");
		});

		page.on("mathjax:font", function (font, data) {
			fs.write(options.dest + "/fonts/mathjax/" + font, data);
		});

		page.on("mathjax:end", function () {
			done();
		});

		page.evaluate(function (options) {

			(function () {
				/* global window, EasyMathJax, XMLHttpRequest */
				var task = new EasyMathJax(options);

				var onFont = function (font, url) {
					// Synchronous XMLHttpRequest for easy flow.
					var xhr = new XMLHttpRequest();
					xhr.open("GET", url, false);
					xhr.responseType = "blob";
					xhr.send();
					window.callPhantom(["mathjax:font", font, xhr.response]);
				};

				var onRender = function () {
					console.log("Cleaning up...");
					if (options.css) {
						var css = task.css();
						for (var font in css.fonts) {
							if (css.fonts.hasOwnProperty(font)) {
								onFont(font, css.fonts[font]);
							}
						}
						window.callPhantom(["mathjax:css", css.contents]);
					}
					if (options.clean) {
						// task.clean();
					}
					window.callPhantom(["mathjax:end"]);
				};
				console.log("Loading MathJax...");
				task.inject(function () {
					console.log("Rendering Math...");
					task.render(options.selector, onRender);
				});
			})();
		}, options);
	};
};
<commit_msg>Refactor to improve clarity of execution flow.<commit_after>var fs = require("fs");
var _ = require("lodash");
require.resolve = function (request) {
	return module._getFilename(request);
};

module.exports = function (options) {
	options = _.assign({}, {
		clean:     true,
		config:    {},
		css:       false,
		selector:  "body",
		dest:      fs.absolute("")
	}, options);

	return function (done) {
		var page = this;

		var easy = require.resolve("easy-mathjax");
		page.injectJs(easy);

		page.on("mathjax:css", function (css) {
			fs.write(options.dest + "mathjax.css", css, "w");
		});

		page.on("mathjax:font", function (font, data) {
			fs.write(options.dest + "/fonts/mathjax/" + font, data);
		});

		page.on("mathjax:end", function () {
			done();
		});

		page.evaluate(function (options) {

			(function () {
				/* global window, EasyMathJax, XMLHttpRequest */
				var task = new EasyMathJax(options);

				var onFont = function (font, url) {
					// Synchronous XMLHttpRequest for easy flow.
					var xhr = new XMLHttpRequest();
					xhr.open("GET", url, false);
					xhr.responseType = "blob";
					xhr.send();
					window.callPhantom(["mathjax:font", font, xhr.response]);
				};

				var onRender = function () {
					console.log("Cleaning up...");
					if (options.css) {
						var css = task.css();
						for (var font in css.fonts) {
							if (css.fonts.hasOwnProperty(font)) {
								onFont(font, css.fonts[font]);
							}
						}
						window.callPhantom(["mathjax:css", css.contents]);
					}
					if (options.clean) {
						// task.clean();
					}
					window.callPhantom(["mathjax:end"]);
				};

				var onReady = function () {
					console.log("Rendering Math...");
					task.render(options.selector, onRender);
				};

				console.log("Loading MathJax...");
				task.inject(onReady);
			})();
		}, options);
	};
};
<|endoftext|>


<commit_before>'use strict';

var CONFIG_FILE = '/.cartridgerc';
var MATCH_REGEX = /(\[\/\/\]: <> \(Modules start\)\s)([^[]*)(\[\/\/\]: <> \(Modules end\)\s)/g;

var del      = require('del');
var path     = require('path');
var chalk    = require('chalk');
var template = require('lodash/template');
var Promise  = require('bluebird');
var fs       = Promise.promisifyAll(require('fs-extra'));
var inArray = require('in-array');

var paths = {
	project:   path.resolve('../../'),
	config:    path.resolve('../../', '_config'),
	readme:    path.resolve('../../', 'readme.md'),
	cartridge: path.resolve('../../_cartridge')
};

// Checks if the project has been set up with Cartridge
function hasCartridgeInstalled() {
	try {
		fs.accessSync(paths.project + CONFIG_FILE, fs.R_OK | fs.W_OK);
	} catch(err) {
		return false;
	}

	console.log('config file');
	console.log(paths.project + CONFIG_FILE);

	return true;
}

function insertModulesInToReadme(readmeContents) {
	return readmeContents.replace(MATCH_REGEX, this.renderedModules);
}

function updateReadme(renderedModuleTemplate) {

	return fs.readFileAsync(paths.readme, 'utf8')
		.bind({
			renderedModules: renderedModuleTemplate
		})
		.then(insertModulesInToReadme)
		.then(function(fileContent){
			return fs.writeFileAsync(paths.readme, fileContent)
		})
		.catch(function(err) {
			console.log('updateReadme error');
			console.error(err);
			process.exit(1);
		});
}

function compileTemplate(rawTemplate) {
	var compiledTemplate = template(rawTemplate);

	return compiledTemplate(this)
}

function updateReadmeModules(rcData) {
	var filePath = path.join(paths.cartridge, 'modules.tpl');

	return fs.readFileAsync(filePath, 'utf8')
		.bind(rcData)
		.then(compileTemplate)
		.then(updateReadme)
		.catch(function(err){
			console.log('updateReadmeModules error');
			console.error(err);
			console.trace();
			process.exit(1);
		});
}

function ensureProjectConfigPaths(projectConfig) {
	if(!projectConfig.hasOwnProperty('paths')) {
		projectConfig.paths = {
			src:   {},
			dest: {}
		};
	}

	if(!projectConfig.paths.hasOwnProperty('src')) {
		projectConfig.paths.src = {};
	}

	if(!projectConfig.paths.hasOwnProperty('dest')) {
		projectConfig.paths.dest = {};
	}

	return projectConfig;
}

function getModuleId(modules, moduleName) {
	var i, moduleIndex;

	// Yes so check that this one doesn't already exist
	for(i = 0; i < modules.length; i++) {

		if(modules[i].name == moduleName) {
			moduleIndex = i;
			break;
		}
	}

	if(moduleIndex === undefined) {
		moduleIndex = modules.length;
	}

	return moduleIndex;
}

function addModule(rcContent) {
	// This is bound to the module data
	var i, moduleIndex;

	// Check if any modules have been added to the rc yet
	if(!rcContent.hasOwnProperty('modules')) {
		// No so create the array
		rcContent.modules = [];
		moduleIndex = 0;
	} else {
		moduleIndex = getModuleId(rcContent.modules, this.name);
	}

	// Set the data
	rcContent.modules[moduleIndex] = this;

	return rcContent;
}

function writeJsonFile(fileContent) {
	return fs.writeJsonAsync(this, fileContent)
		.then(function(){
			return fileContent;
		});
}

module.exports = function(packageConfig) {
	var cartridgeApi = {};

	cartridgeApi.exitIfDevEnvironment = function() {
		if(process.env.NODE_ENV === 'development') {

			cartridgeApi.logMessage('NODE_ENV is set to ' + chalk.underline('development'));
			cartridgeApi.logMessage('Skipping postinstall.js for ' + chalk.underline(packageConfig.name));
			cartridgeApi.logMessage('');

			process.exit(0);
		}
	}

	cartridgeApi.ensureCartridgeExists = function ensureCartridgeExists() {
		if(!hasCartridgeInstalled()) {
			console.error(chalk.red('Cartridge is not set up in this directory. Please set it up first before installing this module'));
			process.exit(1);
		}
	};

	// Adds the specified module to the .cartridgerc file
	cartridgeApi.addToRc = function addToRc() {
		var filePath = path.join(paths.project,  CONFIG_FILE);

		var moduleConfig = {
			name:    packageConfig.name,
			version: packageConfig.version,
			site:    packageConfig.homepage,
			task:    packageConfig.name + '/' + packageConfig.main
		};

		cartridgeApi.logMessage('Adding ' + packageConfig.name + ' to .cartridgerc');

		return fs.readJsonAsync(filePath)
			.bind(moduleConfig)
			.then(addModule)
			.bind(filePath)
			.then(writeJsonFile)
			.then(updateReadmeModules)
			.then(function() {
				cartridgeApi.logMessage('Finished: adding ' + packageConfig.name + ' to .cartridgerc');
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('addToRc error');
				console.error(err);
				process.exit(1);
			});
	};

	// Removes the specified module from the .cartridgerc file
	cartridgeApi.removeFromRc = function removeFromRc(moduleName) {
		var filePath = path.join(paths.project,  CONFIG_FILE);

		cartridgeApi.logMessage('Removing ' + moduleName + ' from .cartridgerc');

		return fs.readJsonAsync(filePath)
			.then(function(data) {
				for (var i = 0; i < data.modules.length; i++) {
					if(data.modules[i].name === moduleName) {
						data.modules.splice(i, 1);
					}
				}

				return data;
			})
			.bind(filePath)
			.then(writeJsonFile)
			.then(updateReadmeModules)
			.then(function() {
				cartridgeApi.logMessage('Finished: Removing ' + packageConfig.name + ' from .cartridgerc');
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('removeFromRc error');
				console.error(err);
				process.exit(1);
			});
	};

	// Modify the project configuration (project.json) with a transform function
	cartridgeApi.modifyProjectConfig = function modifyProjectConfig(transform) {
		var filePath = path.join(paths.config,  '/project.json');

		cartridgeApi.logMessage('Updating project config for ' + packageConfig.name);

		return fs.readJsonAsync(paths.config + '/project.json')
			.then(ensureProjectConfigPaths)
			.then(transform)
			.bind(filePath)
			.then(writeJsonFile)
			.then(function(){
				cartridgeApi.logMessage('Finished: modifying project config for ' + packageConfig.name);
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('modifyProjectConfig error');
				console.error(err);
				process.exit(1);
			});
	};

	// Add configuration files to the project _config directory for this module
	cartridgeApi.addModuleConfig = function addModuleConfig(configPath) {
		var configFileName = path.basename(configPath)
		var toPath = path.join(paths.config, configFileName);
		var destinationFileList = fs.readdirSync(paths.config);
		var configFileExists = inArray(destinationFileList, configFileName);

		if(configFileExists) {
			cartridgeApi.logMessage('Skipping: Copying config file as it already exists');
			return Promise.resolve();
		}

		return fs.copyAsync(configPath, toPath)
			.then(function(){
				cartridgeApi.logMessage('Finished: adding ' + packageConfig.name + ' config files');
				return Promise.resolve();
			});
	};

	// Remove configuration files from the project _config directory for this module
	cartridgeApi.removeModuleConfig = function removeModuleConfig(configPath) {
		var configFileName = path.basename(configPath)
		var projectModuleConfigPath = path.join(paths.config, configFileName);

		return fs.removeAsync(projectModuleConfigPath)
			.then(function(){
				cartridgeApi.logMessage('Finished: Removed ' + packageConfig.name + ' config files');
				return Promise.resolve();
			});
	};

	cartridgeApi.finishInstall = function finishInstall(packageDetails) {
		cartridgeApi.logMessage('Finished post install of ' + packageConfig.name);
		process.exit(0);
	};

	cartridgeApi.logMessage = function logMessage(message) {
		console.log(message);
	};

	return cartridgeApi;
};
<commit_msg>feat: Add finishUninstall()<commit_after>'use strict';

var CONFIG_FILE = '/.cartridgerc';
var MATCH_REGEX = /(\[\/\/\]: <> \(Modules start\)\s)([^[]*)(\[\/\/\]: <> \(Modules end\)\s)/g;

var del      = require('del');
var path     = require('path');
var chalk    = require('chalk');
var template = require('lodash/template');
var Promise  = require('bluebird');
var fs       = Promise.promisifyAll(require('fs-extra'));
var inArray = require('in-array');

var paths = {
	project:   path.resolve('../../'),
	config:    path.resolve('../../', '_config'),
	readme:    path.resolve('../../', 'readme.md'),
	cartridge: path.resolve('../../_cartridge')
};

// Checks if the project has been set up with Cartridge
function hasCartridgeInstalled() {
	try {
		fs.accessSync(paths.project + CONFIG_FILE, fs.R_OK | fs.W_OK);
	} catch(err) {
		return false;
	}

	console.log('config file');
	console.log(paths.project + CONFIG_FILE);

	return true;
}

function insertModulesInToReadme(readmeContents) {
	return readmeContents.replace(MATCH_REGEX, this.renderedModules);
}

function updateReadme(renderedModuleTemplate) {

	return fs.readFileAsync(paths.readme, 'utf8')
		.bind({
			renderedModules: renderedModuleTemplate
		})
		.then(insertModulesInToReadme)
		.then(function(fileContent){
			return fs.writeFileAsync(paths.readme, fileContent)
		})
		.catch(function(err) {
			console.log('updateReadme error');
			console.error(err);
			process.exit(1);
		});
}

function compileTemplate(rawTemplate) {
	var compiledTemplate = template(rawTemplate);

	return compiledTemplate(this)
}

function updateReadmeModules(rcData) {
	var filePath = path.join(paths.cartridge, 'modules.tpl');

	return fs.readFileAsync(filePath, 'utf8')
		.bind(rcData)
		.then(compileTemplate)
		.then(updateReadme)
		.catch(function(err){
			console.log('updateReadmeModules error');
			console.error(err);
			console.trace();
			process.exit(1);
		});
}

function ensureProjectConfigPaths(projectConfig) {
	if(!projectConfig.hasOwnProperty('paths')) {
		projectConfig.paths = {
			src:   {},
			dest: {}
		};
	}

	if(!projectConfig.paths.hasOwnProperty('src')) {
		projectConfig.paths.src = {};
	}

	if(!projectConfig.paths.hasOwnProperty('dest')) {
		projectConfig.paths.dest = {};
	}

	return projectConfig;
}

function getModuleId(modules, moduleName) {
	var i, moduleIndex;

	// Yes so check that this one doesn't already exist
	for(i = 0; i < modules.length; i++) {

		if(modules[i].name == moduleName) {
			moduleIndex = i;
			break;
		}
	}

	if(moduleIndex === undefined) {
		moduleIndex = modules.length;
	}

	return moduleIndex;
}

function addModule(rcContent) {
	// This is bound to the module data
	var i, moduleIndex;

	// Check if any modules have been added to the rc yet
	if(!rcContent.hasOwnProperty('modules')) {
		// No so create the array
		rcContent.modules = [];
		moduleIndex = 0;
	} else {
		moduleIndex = getModuleId(rcContent.modules, this.name);
	}

	// Set the data
	rcContent.modules[moduleIndex] = this;

	return rcContent;
}

function writeJsonFile(fileContent) {
	return fs.writeJsonAsync(this, fileContent)
		.then(function(){
			return fileContent;
		});
}

module.exports = function(packageConfig) {
	var cartridgeApi = {};

	cartridgeApi.exitIfDevEnvironment = function() {
		if(process.env.NODE_ENV === 'development') {

			cartridgeApi.logMessage('NODE_ENV is set to ' + chalk.underline('development'));
			cartridgeApi.logMessage('Skipping postinstall.js for ' + chalk.underline(packageConfig.name));
			cartridgeApi.logMessage('');

			process.exit(0);
		}
	}

	cartridgeApi.ensureCartridgeExists = function ensureCartridgeExists() {
		if(!hasCartridgeInstalled()) {
			console.error(chalk.red('Cartridge is not set up in this directory. Please set it up first before installing this module'));
			process.exit(1);
		}
	};

	// Adds the specified module to the .cartridgerc file
	cartridgeApi.addToRc = function addToRc() {
		var filePath = path.join(paths.project,  CONFIG_FILE);

		var moduleConfig = {
			name:    packageConfig.name,
			version: packageConfig.version,
			site:    packageConfig.homepage,
			task:    packageConfig.name + '/' + packageConfig.main
		};

		cartridgeApi.logMessage('Adding ' + packageConfig.name + ' to .cartridgerc');

		return fs.readJsonAsync(filePath)
			.bind(moduleConfig)
			.then(addModule)
			.bind(filePath)
			.then(writeJsonFile)
			.then(updateReadmeModules)
			.then(function() {
				cartridgeApi.logMessage('Finished: adding ' + packageConfig.name + ' to .cartridgerc');
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('addToRc error');
				console.error(err);
				process.exit(1);
			});
	};

	// Removes the specified module from the .cartridgerc file
	cartridgeApi.removeFromRc = function removeFromRc(moduleName) {
		var filePath = path.join(paths.project,  CONFIG_FILE);

		cartridgeApi.logMessage('Removing ' + moduleName + ' from .cartridgerc');

		return fs.readJsonAsync(filePath)
			.then(function(data) {
				for (var i = 0; i < data.modules.length; i++) {
					if(data.modules[i].name === moduleName) {
						data.modules.splice(i, 1);
					}
				}

				return data;
			})
			.bind(filePath)
			.then(writeJsonFile)
			.then(updateReadmeModules)
			.then(function() {
				cartridgeApi.logMessage('Finished: Removing ' + packageConfig.name + ' from .cartridgerc');
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('removeFromRc error');
				console.error(err);
				process.exit(1);
			});
	};

	// Modify the project configuration (project.json) with a transform function
	cartridgeApi.modifyProjectConfig = function modifyProjectConfig(transform) {
		var filePath = path.join(paths.config,  '/project.json');

		cartridgeApi.logMessage('Updating project config for ' + packageConfig.name);

		return fs.readJsonAsync(paths.config + '/project.json')
			.then(ensureProjectConfigPaths)
			.then(transform)
			.bind(filePath)
			.then(writeJsonFile)
			.then(function(){
				cartridgeApi.logMessage('Finished: modifying project config for ' + packageConfig.name);
				return Promise.resolve();
			})
			.catch(function(err){
				console.log('modifyProjectConfig error');
				console.error(err);
				process.exit(1);
			});
	};

	// Add configuration files to the project _config directory for this module
	cartridgeApi.addModuleConfig = function addModuleConfig(configPath) {
		var configFileName = path.basename(configPath)
		var toPath = path.join(paths.config, configFileName);
		var destinationFileList = fs.readdirSync(paths.config);
		var configFileExists = inArray(destinationFileList, configFileName);

		if(configFileExists) {
			cartridgeApi.logMessage('Skipping: Copying config file as it already exists');
			return Promise.resolve();
		}

		return fs.copyAsync(configPath, toPath)
			.then(function(){
				cartridgeApi.logMessage('Finished: adding ' + packageConfig.name + ' config files');
				return Promise.resolve();
			});
	};

	// Remove configuration files from the project _config directory for this module
	cartridgeApi.removeModuleConfig = function removeModuleConfig(configPath) {
		var configFileName = path.basename(configPath)
		var projectModuleConfigPath = path.join(paths.config, configFileName);

		return fs.removeAsync(projectModuleConfigPath)
			.then(function(){
				cartridgeApi.logMessage('Finished: Removed ' + packageConfig.name + ' config files');
				return Promise.resolve();
			});
	};

	cartridgeApi.finishInstall = function finishInstall(packageDetails) {
		cartridgeApi.logMessage('Finished: post install of ' + packageConfig.name);
		process.exit(0);
	};

	cartridgeApi.finishUninstall = function finishUninstall(packageDetails) {
		cartridgeApi.logMessage('Finished: post uninstall of ' + packageConfig.name);
		process.exit(0);
	};

	cartridgeApi.logMessage = function logMessage(message) {
		console.log(message);
	};

	return cartridgeApi;
};
<|endoftext|>