/*1* router.js: Base functionality for the router.2*3* (C) 2011, Nodejitsu Inc.4* MIT LICENSE5*6*/78//9// Helper function to turn flatten an array.10//11function _flatten (arr) {12var flat = [];1314for (var i = 0, n = arr.length; i < n; i++) {15flat = flat.concat(arr[i]);16}1718return flat;19}2021//22// Helper function for wrapping Array.every23// in the browser.24//25function _every (arr, iterator) {26for (var i = 0; i < arr.length; i += 1) {27if (iterator(arr[i], i, arr) === false) {28return;29}30}31};3233//34// Helper function for performing an asynchronous every35// in series in the browser and the server.36//37function _asyncEverySeries (arr, iterator, callback) {38if (!arr.length) {39return callback();40}4142var completed = 0;43(function iterate() {44iterator(arr[completed], function (err) {45if (err || err === false) {46callback(err);47callback = function () {};48}49else {50completed += 1;51if (completed === arr.length) {52callback();53}54else {55iterate();56}57}58});59})();60};6162//63// Helper function for expanding "named" matches64// (e.g. `:dog`, etc.) against the given set65// of params:66//67// {68// ':dog': function (str) {69// return str.replace(/:dog/, 'TARGET');70// }71// ...72// }73//74function paramifyString(str, params, mod) {75mod = str;76for (var param in params) {77if (params.hasOwnProperty(param)) {78mod = params[param](str);79if (mod !== str) { break }80}81}8283return mod === str84? '([._a-zA-Z0-9-]+)'85: mod;86}8788//89// Helper function for expanding wildcards (*) and90// "named" matches (:whatever)91//92function regifyString(str, params) {93if (~str.indexOf('*')) {94str = str.replace(/\*/g, '([_\.\(\)!\\ %@&a-zA-Z0-9-]+)');95}9697var captures = str.match(/:([^\/]+)/ig),98length;99100if (captures) {101length = captures.length;102for (var i = 0; i < length; i++) {103str = str.replace(captures[i], paramifyString(captures[i], params));104}105}106107return str;108}109110//111// ### function Router (routes)112// #### @routes {Object} **Optional** Routing table for this instance.113// Constuctor function for the Router object responsible for building114// and dispatching from a given routing table.115//116var Router = exports.Router = function (routes) {117this.params = {};118this.routes = {};119this.methods = ['on', 'after', 'before'];120this.scope = [];121this._methods = {};122123this.configure();124this.mount(routes || {});125};126127//128// ### function configure (options)129// #### @options {Object} **Optional** Options to configure this instance with130// Configures this instance with the specified `options`.131//132Router.prototype.configure = function (options) {133options = options || {};134135for (var i = 0; i < this.methods.length; i++) {136this._methods[this.methods[i]] = true;137}138139this.recurse = options.recurse || this.recurse || false;140this.async = options.async || false;141this.delimiter = options.delimiter || '\/';142this.strict = typeof options.strict === 'undefined' ? true : options.strict;143this.notfound = options.notfound;144this.resource = options.resource;145146// Client only, but browser.js does not include a super implementation147this.history = (options.html5history && this.historySupport) || false;148this.run_in_init = (this.history === true && options.run_handler_in_init !== false);149150//151// TODO: Global once152//153this.every = {154after: options.after || null,155before: options.before || null,156on: options.on || null157};158159return this;160};161162//163// ### function param (token, regex)164// #### @token {string} Token which to replace (e.g. `:dog`, 'cat')165// #### @matcher {string|RegExp} Target to replace the token with.166// Setups up a `params` function which replaces any instance of `token`,167// inside of a given `str` with `matcher`. This is very useful if you168// have a common regular expression throughout your code base which169// you wish to be more DRY.170//171Router.prototype.param = function (token, matcher) {172if (token[0] !== ':') {173token = ':' + token;174}175176var compiled = new RegExp(token, 'g');177this.params[token] = function (str) {178return str.replace(compiled, matcher.source || matcher);179};180};181182//183// ### function on (method, path, route)184// #### @method {string} **Optional** Method to use185// #### @path {Array|string} Path to set this route on.186// #### @route {Array|function} Handler for the specified method and path.187// Adds a new `route` to this instance for the specified `method`188// and `path`.189//190Router.prototype.on = Router.prototype.route = function (method, path, route) {191var self = this;192193if (!route && typeof path == 'function') {194//195// If only two arguments are supplied then assume this196// `route` was meant to be a generic `on`.197//198route = path;199path = method;200method = 'on';201}202203if (Array.isArray(path)) {204return path.forEach(function(p) {205self.on(method, p, route);206});207}208209if (path.source) {210path = path.source.replace(/\\\//ig, '/');211}212213if (Array.isArray(method)) {214return method.forEach(function (m) {215self.on(m.toLowerCase(), path, route);216});217}218219this.insert(method, this.scope.concat(path.split(new RegExp(this.delimiter))), route);220};221222//223// ### function path (path, routesFn)224// #### @path {string|RegExp} Nested scope in which to path225// #### @routesFn {function} Function to evaluate in the new scope226// Evalutes the `routesFn` in the given path scope.227//228Router.prototype.path = function (path, routesFn) {229var self = this,230length = this.scope.length;231232if (path.source) {233path = path.source.replace(/\\\//ig, '/');234}235236path = path.split(new RegExp(this.delimiter));237this.scope = this.scope.concat(path);238239routesFn.call(this, this);240this.scope.splice(length, path.length);241};242243//244// ### function dispatch (method, path[, callback])245// #### @method {string} Method to dispatch246// #### @path {string} Path to dispatch247// #### @callback {function} **Optional** Continuation to respond to for async scenarios.248// Finds a set of functions on the traversal towards249// `method` and `path` in the core routing table then250// invokes them based on settings in this instance.251//252Router.prototype.dispatch = function (method, path, callback) {253var self = this,254fns = this.traverse(method, path, this.routes, ''),255invoked = this._invoked,256after;257258this._invoked = true;259if (!fns || fns.length === 0) {260this.last = [];261if (typeof this.notfound === 'function') {262this.invoke([this.notfound], { method: method, path: path }, callback);263}264265return false;266}267268if (this.recurse === 'forward') {269fns = fns.reverse();270}271272function updateAndInvoke() {273self.last = fns.after;274self.invoke(self.runlist(fns), self, callback);275}276277//278// Builds the list of functions to invoke from this call279// to dispatch conforming to the following order:280//281// 1. Global after (if any)282// 2. After functions from the last call to dispatch283// 3. Global before (if any)284// 4. Global on (if any)285// 5. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)286//287after = this.every && this.every.after288? [this.every.after].concat(this.last)289: [this.last];290291if (after && after.length > 0 && invoked) {292if (this.async) {293this.invoke(after, this, updateAndInvoke);294}295else {296this.invoke(after, this);297updateAndInvoke();298}299300return true;301}302303updateAndInvoke();304return true;305};306307//308// ### function runlist (fns)309// #### @fns {Array} List of functions to include in the runlist310// Builds the list of functions to invoke from this call311// to dispatch conforming to the following order:312//313// 1. Global before (if any)314// 2. Global on (if any)315// 3. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)316//317Router.prototype.runlist = function (fns) {318var runlist = this.every && this.every.before319? [this.every.before].concat(_flatten(fns))320: _flatten(fns);321322if (this.every && this.every.on) {323runlist.push(this.every.on);324}325326runlist.captures = fns.captures;327runlist.source = fns.source;328return runlist;329};330331//332// ### function invoke (fns, thisArg)333// #### @fns {Array} Set of functions to invoke in order.334// #### @thisArg {Object} `thisArg` for each function.335// #### @callback {function} **Optional** Continuation to pass control to for async `fns`.336// Invokes the `fns` synchronously or asynchronously depending on the337// value of `this.async`. Each function must **not** return (or respond)338// with false, or evaluation will short circuit.339//340Router.prototype.invoke = function (fns, thisArg, callback) {341var self = this;342343if (this.async) {344_asyncEverySeries(fns, function apply(fn, next) {345if (Array.isArray(fn)) {346return _asyncEverySeries(fn, apply, next);347}348else if (typeof fn == 'function') {349fn.apply(thisArg, fns.captures.concat(next));350}351}, function () {352//353// Ignore the response here. Let the routed take care354// of themselves and eagerly return true.355//356357if (callback) {358callback.apply(thisArg, arguments);359}360});361}362else {363_every(fns, function apply(fn) {364if (Array.isArray(fn)) {365return _every(fn, apply);366}367else if (typeof fn === 'function') {368return fn.apply(thisArg, fns.captures || null);369}370else if (typeof fn === 'string' && self.resource) {371self.resource[fn].apply(thisArg, fns.captures || null)372}373});374}375};376377//378// ### function traverse (method, path, routes, regexp)379// #### @method {string} Method to find in the `routes` table.380// #### @path {string} Path to find in the `routes` table.381// #### @routes {Object} Partial routing table to match against382// #### @regexp {string} Partial regexp representing the path to `routes`.383// Core routing logic for `director.Router`: traverses the384// specified `path` within `this.routes` looking for `method`385// returning any `fns` that are found.386//387Router.prototype.traverse = function (method, path, routes, regexp) {388var fns = [],389current,390exact,391match,392next,393that;394395//396// Base Case #1:397// If we are dispatching from the root398// then only check if the method exists.399//400if (path === this.delimiter && routes[method]) {401next = [[routes.before, routes[method]].filter(Boolean)];402next.after = [routes.after].filter(Boolean);403next.matched = true;404next.captures = [];405return next;406}407408for (var r in routes) {409//410// We dont have an exact match, lets explore the tree411// in a depth-first, recursive, in-order manner where412// order is defined as:413//414// ['before', 'on', '<method>', 'after']415//416// Remember to ignore keys (i.e. values of `r`) which417// are actual methods (e.g. `on`, `before`, etc), but418// which are not actual nested route (i.e. JSON literals).419//420if (routes.hasOwnProperty(r) && (!this._methods[r] ||421this._methods[r] && typeof routes[r] === 'object' && !Array.isArray(routes[r]))) {422//423// Attempt to make an exact match for the current route424// which is built from the `regexp` that has been built425// through recursive iteration.426//427current = exact = regexp + this.delimiter + r;428429430if (!this.strict) {431exact += '[' + this.delimiter + ']?';432}433434match = path.match(new RegExp('^' + exact));435436if (!match) {437//438// If there isn't a `match` then continue. Here, the439// `match` is a partial match. e.g.440//441// '/foo/bar/buzz'.match(/^\/foo/) // ['/foo']442// '/no-match/route'.match(/^\/foo/) // null443//444continue;445}446447if (match[0] && match[0] == path && routes[r][method]) {448//449// ### Base case 2:450// If we had a `match` and the capture is the path itself,451// then we have completed our recursion.452//453next = [[routes[r].before, routes[r][method]].filter(Boolean)];454next.after = [routes[r].after].filter(Boolean);455next.matched = true;456next.captures = match.slice(1);457458if (this.recurse && routes === this.routes) {459next.push([routes['before'], routes['on']].filter(Boolean));460next.after = next.after.concat([routes['after']].filter(Boolean))461}462463return next;464}465466//467// ### Recursive case:468// If we had a match, but it is not yet an exact match then469// attempt to continue matching against the next portion of the470// routing table.471//472next = this.traverse(method, path, routes[r], current);473474//475// `next.matched` will be true if the depth-first search of the routing476// table from this position was successful.477//478if (next.matched) {479//480// Build the in-place tree structure representing the function481// in the correct order.482//483if (next.length > 0) {484fns = fns.concat(next);485}486487if (this.recurse) {488fns.push([routes[r].before, routes[r].on].filter(Boolean));489next.after = next.after.concat([routes[r].after].filter(Boolean));490491if (routes === this.routes) {492fns.push([routes['before'], routes['on']].filter(Boolean));493next.after = next.after.concat([routes['after']].filter(Boolean))494}495}496497fns.matched = true;498fns.captures = next.captures;499fns.after = next.after;500501//502// ### Base case 2:503// Continue passing the partial tree structure back up the stack.504// The caller for `dispatch()` will decide what to do with the functions.505//506return fns;507}508}509}510511return false;512};513514//515// ### function insert (method, path, route, context)516// #### @method {string} Method to insert the specific `route`.517// #### @path {Array} Parsed path to insert the `route` at.518// #### @route {Array|function} Route handlers to insert.519// #### @parent {Object} **Optional** Parent "routes" to insert into.520// Inserts the `route` for the `method` into the routing table for521// this instance at the specified `path` within the `context` provided.522// If no context is provided then `this.routes` will be used.523//524Router.prototype.insert = function (method, path, route, parent) {525var methodType,526parentType,527isArray,528nested,529part;530531path = path.filter(function (p) {532return p && p.length > 0;533});534535parent = parent || this.routes;536part = path.shift();537if (/\:|\*/.test(part) && !/\\d|\\w/.test(part)) {538part = regifyString(part, this.params);539}540541if (path.length > 0) {542//543// If this is not the last part left in the `path`544// (e.g. `['cities', 'new-york']`) then recurse into that545// child546//547parent[part] = parent[part] || {};548return this.insert(method, path, route, parent[part]);549}550551//552// If there is no part and the path has been exhausted553// and the parent is the root of the routing table,554// then we are inserting into the root and should555// only dive one level deep in the Routing Table.556//557if (!part && !path.length && parent === this.routes) {558methodType = typeof parent[method];559560switch (methodType) {561case 'function':562parent[method] = [parent[method], route];563return;564case 'object':565parent[method].push(route)566return;567case 'undefined':568parent[method] = route;569return;570}571572return;573}574575//576// Otherwise, we are at the end of our insertion so we should577// insert the `route` based on the `method` after getting the578// `parent` of the last `part`.579//580parentType = typeof parent[part];581isArray = Array.isArray(parent[part]);582583if (parent[part] && !isArray && parentType == 'object') {584methodType = typeof parent[part][method];585586switch (methodType) {587case 'function':588parent[part][method] = [parent[part][method], route];589return;590case 'object':591parent[part][method].push(route)592return;593case 'undefined':594parent[part][method] = route;595return;596}597}598else if (parentType == 'undefined') {599nested = {};600nested[method] = route;601parent[part] = nested;602return;603}604605throw new Error('Invalid route context: ' + parentType);606};607608609//610// ### function extend (methods)611// #### @methods {Array} List of method names to extend this instance with612// Extends this instance with simple helper methods to `this.on`613// for each of the specified `methods`614//615Router.prototype.extend = function(methods) {616var self = this,617len = methods.length,618i;619620for (i = 0; i < len; i++) {621(function(method) {622self._methods[method] = true;623self[method] = function () {624var extra = arguments.length === 1625? [method, '']626: [method];627628self.on.apply(self, extra.concat(Array.prototype.slice.call(arguments)));629};630})(methods[i]);631}632};633634//635// ### function mount (routes, context)636// #### @routes {Object} Routes to mount onto this instance637// Mounts the sanitized `routes` onto the root context for this instance.638//639// e.g.640//641// new Router().mount({ '/foo': { '/bar': function foobar() {} } })642//643// yields644//645// { 'foo': 'bar': function foobar() {} } }646//647Router.prototype.mount = function(routes, path) {648if (!routes || typeof routes !== "object" || Array.isArray(routes)) {649return;650}651652var self = this;653path = path || [];654if (!Array.isArray(path)) {655path = path.split(self.delimiter);656}657658function insertOrMount(route, local) {659var rename = route,660parts = route.split(self.delimiter),661routeType = typeof routes[route],662isRoute = parts[0] === "" || !self._methods[parts[0]],663event = isRoute ? "on" : rename;664665if (isRoute) {666rename = rename.slice((rename.match(new RegExp(self.delimiter)) || [''])[0].length);667parts.shift();668}669670if (isRoute && routeType === 'object' && !Array.isArray(routes[route])) {671local = local.concat(parts);672self.mount(routes[route], local);673return;674}675676if (isRoute) {677local = local.concat(rename.split(self.delimiter));678}679680self.insert(event, local, routes[route]);681}682683for (var route in routes) {684if (routes.hasOwnProperty(route)) {685insertOrMount(route, path.slice(0));686}687}688};689690691692