PHP Classes

File: src/js/Dromeo.js

Recommend this page to a friend!
  Classes of Nikos M.   Dromeo PHP Router Library   src/js/Dromeo.js   Download  
File: src/js/Dromeo.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Dromeo PHP Router Library
Route HTTP requests to functions with URL patterns
Author: By
Last change: v.1.2.0, contd

* namedroutes are passed on to top router
* .off() is handled recursively in subrouters
* various edits and changes
v.1.2.0 in progress

* onGroup method to group routes under common (literal) prefix (in progress)
* handle edge case in makePattern
* original matched extracts contain same defaults (if given)
* fix typo in PHP,JS in clearRoute
* update tests
Date: 2 years ago
Size: 47,646 bytes
 

Contents

Class file image Download
/** * * Dromeo * Simple and Flexible Pattern Routing Framework for PHP, JavaScript, Python * @version: 1.2.0 * * https://github.com/foo123/Dromeo * **/ !function(root, name, factory) { "use strict"; var m; if (('undefined'!==typeof Components)&&('object'===typeof Components.classes)&&('object'===typeof Components.classesByID)&&Components.utils&&('function'===typeof Components.utils['import'])) /* XPCOM */ (root.EXPORTED_SYMBOLS = [name]) && (root[name] = factory.call(root)); else if (('object'===typeof module)&&module.exports) /* CommonJS */ module.exports = factory.call(root); else if (('function'===typeof(define))&&define.amd&&('function'===typeof(require))&&('function'===typeof(require.specified))&&require.specified(name)) /* AMD */ define(name,['require','exports','module'],function() {return factory.call(root);}); else if (!(name in root)) /* Browser/WebWorker/.. */ (root[name] = (m=factory.call(root)))&&('function'===typeof(define))&&define.amd&&define(function() {return m;} ); }( /* current root */ 'undefined' !== typeof self ? self : this, /* module name */ "Dromeo", /* module factory */ function ModuleFactory__Dromeo(undef) { "use strict"; var __version__ = "1.2.0", // http://en.wikipedia.org/wiki/List_of_HTTP_status_codes HTTP_STATUS = { // 1xx Informational 100: "Continue" ,101: "Switching Protocols" ,102: "Processing" ,103: "Early Hints" // 2xx Success ,200: "OK" ,201: "Created" ,202: "Accepted" ,203: "Non-Authoritative Information" ,204: "No Content" ,205: "Reset Content" ,206: "Partial Content" ,207: "Multi-Status" ,208: "Already Reported" ,226: "IM Used" // 3xx Redirection ,300: "Multiple Choices" ,301: "Moved Permanently" ,302: "Found" //Previously "Moved temporarily" ,303: "See Other" ,304: "Not Modified" ,305: "Use Proxy" ,306: "Switch Proxy" ,307: "Temporary Redirect" ,308: "Permanent Redirect" // 4xx Client Error ,400: "Bad Request" ,401: "Unauthorized" ,402: "Payment Required" ,403: "Forbidden" ,404: "Not Found" ,405: "Method Not Allowed" ,406: "Not Acceptable" ,407: "Proxy Authentication Required" ,408: "Request Timeout" ,409: "Conflict" ,410: "Gone" ,411: "Length Required" ,412: "Precondition Failed" ,413: "Request Entity Too Large" ,414: "Request-URI Too Long" ,415: "Unsupported Media Type" ,416: "Requested Range Not Satisfiable" ,417: "Expectation Failed" ,418: "I'm a teapot" ,419: "Authentication Timeout" ,422: "Unprocessable Entity" ,423: "Locked" ,424: "Failed Dependency" ,426: "Upgrade Required" ,428: "Precondition Required" ,429: "Too Many Requests" ,431: "Request Header Fields Too Large" ,440: "Login Timeout" ,444: "No Response" ,449: "Retry With" ,450: "Blocked by Windows Parental Controls" ,451: "Unavailable For Legal Reasons" ,494: "Request Header Too Large" ,495: "Cert Error" ,496: "No Cert" ,497: "HTTP to HTTPS" ,498: "Token expired/invalid" ,499: "Client Closed Request" // 5xx Server Error ,500: "Internal Server Error" ,501: "Not Implemented" ,502: "Bad Gateway" ,503: "Service Unavailable" ,504: "Gateway Timeout" ,505: "HTTP Version Not Supported" ,506: "Variant Also Negotiates" ,507: "Insufficient Storage" ,508: "Loop Detected" ,509: "Bandwidth Limit Exceeded" ,510: "Not Extended" ,511: "Network Authentication Required" ,520: "Origin Error" ,521: "Web server is down" ,522: "Connection timed out" ,523: "Proxy Declined Request" ,524: "A timeout occurred" ,598: "Network read timeout error" ,599: "Network connect timeout error" }, _patternOr = /^([^|]+\|.+)$/, _nested = /\[([^\]]*?)\]$/, _group = /\((\d+)\)$/, trim_re = /^\s+|\s+$/g, re_escape = /([*+\[\]\(\)?^$\/\\:.])/g, // auxilliaries PROTO = 'prototype', OP = Object[PROTO], AP = Array[PROTO], FP = Function[PROTO], toString = OP.toString, HAS = OP.hasOwnProperty, isNode = ('undefined' !== typeof global) && ('[object global]' == toString.call(global)), trim = String[PROTO].trim ? function(s) {return s.trim();} : function(s) {return s.replace(trim_re, '');}, // adapted from https://github.com/kvz/phpjs uriParser = { php: /^(?:([^:\/?#]+):)?(?:\/\/()(?:(?:()(?:([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?()(?:(()(?:(?:[^?#\/]*\/)*)()(?:[^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/\/?)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // Added one optional slash to post-scheme to catch file:/// (should restrict this) }, uriComponent = ['source', 'scheme', 'authority', 'userInfo', 'user', 'pass', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'] ; function length(s) { return s.length > 0; } function esc_regex(s) { return s.replace(re_escape, '\\$1'); } function is_array(o) { return '[object Array]' === toString.call(o); } function is_obj(o) { return ('[object Object]' === toString.call(o)) && ('function' === typeof o.constructor) && ('Object' === o.constructor.name); } function is_string(o) { return ('string' === typeof o) || ('[object String]' === toString.call(o)); } function is_number(o) { return ('number' === typeof o) || ('[object Number]' === toString.call(o)); } function is_callable(o) { return "function" === typeof o; } function extend(o1, o2, deep) { var k, v; deep = true === deep; if (o2) { for (k in o2) { if (!HAS.call(o2, k)) continue; v = o2[k]; if (is_number(v)) o1[k] = 0+v; else if (is_string(v)) o1[k] = v.slice(); else if (is_array(v)) o1[k] = deep ? extend(new Array(v.length), v, deep) : v; else if (is_obj(v)) o1[k] = deep ? extend({}, v, deep) : v; else o1[k] = v; } } return o1; } function parse_url(s, component, mode/*, queryKey*/) { var m = uriParser[mode || 'php'].exec(s), uri = {}, i = 14//, parser, name ; while (i--) { if (m[i]) uri[uriComponent[i]] = m[i] } if (HAS.call(uri, 'port')) uri['port'] = parseInt(uri['port'], 10); if (component) { return uri[component.replace('PHP_URL_', '').toLowerCase()] || null; } /*if ( 'php' !== mode ) { name = queryKey || 'queryKey'; parser = /(?:^|&)([^&=]*)=?([^&]*)/g; uri[ name ] = { }; uri[ uriComponent[12] ].replace(parser, function ($0, $1, $2) { if ($1) {uri[name][$1] = $2;} }); }*/ if (uri.source) delete uri.source; return uri; } function rawurldecode(str) { return decodeURIComponent(String(str)); } function rawurlencode(str) { return encodeURIComponent(String(str)) .split('!').join('%21') .split("'").join('%27') .split('(').join('%28') .split(')').join('%29') .split('*').join('%2A') //.split('~').join('%7E') ; } function urldecode(str) { return rawurldecode(String(str).split('+').join('%20')); } function urlencode(str) { return rawurlencode(str).split('%20').join('+'); } function parse_str(str) { var strArr = str.replace(/^&+|&+$/g, '').split('&'), sal = strArr.length, i, j, ct, p, lastObj, obj, chr, tmp, key, value, postLeftBracketPos, keys, keysLen, lastkey, array = {}, possibleLists = [], prevkey, prevobj ; for (i=0; i<sal; ++i) { tmp = strArr[i].split('='); key = rawurldecode(trim(tmp[0])); value = (tmp.length < 2) ? '' : rawurldecode(trim(tmp[1])); j = key.indexOf('\x00'); if (j > -1) key = key.slice(0, j); if (key && ('[' !== key.charAt(0))) { keys = []; postLeftBracketPos = 0; for (j=0; j<key.length; ++j) { if (('[' === key.charAt(j)) && !postLeftBracketPos) { postLeftBracketPos = j + 1; } else if (']' === key.charAt(j)) { if (postLeftBracketPos) { if (!keys.length) { keys.push(key.slice(0, postLeftBracketPos - 1)); } keys.push(key.substr(postLeftBracketPos, j - postLeftBracketPos)); postLeftBracketPos = 0; if ('[' !== key.charAt(j + 1)) break; } } } if (!keys.length) keys = [key]; for (j=0; j<keys[0].length; ++j) { chr = keys[0].charAt(j); if (' ' === chr || '.' === chr || '[' === chr) { keys[0] = keys[0].substr(0, j) + '_' + keys[0].substr(j + 1); } if ('[' === chr) break; } obj = array; key = null; lastObj = obj; lastkey = keys.length ? trim(keys[ keys.length-1 ].replace(/^['"]|['"]$/g, '')) : null; for (j=0, keysLen=keys.length; j<keysLen; ++j) { prevkey = key; key = keys[j].replace(/^['"]|['"]$/g, ''); prevobj = lastObj; lastObj = obj; if ('' !== trim(key) || 0 === j) { if (!HAS.call(obj, key)) obj[key] = (j+1 === keysLen-1) && (''===lastkey) ? [] : {}; obj = obj[key]; } else { // To insert new dimension /*ct = -1; for ( p in obj ) { if ( HAS.call(obj,p) ) { if ( +p > ct && p.match(/^\d+$/g) ) { ct = +p; } } } key = ct + 1;*/ key = true; } } if (true === key) { lastObj.push(value); } else { if (key == +key) possibleLists.push({key:prevkey, obj:prevobj}); lastObj[key] = value; } } } for(i=possibleLists.length-1; i>=0; --i) { // safe to pass multiple times same obj, it is possible obj = possibleLists[i].key ? possibleLists[i].obj[possibleLists[i].key] : possibleLists[i].obj; if (is_numeric_array(obj)) { obj = array_values(obj); if (possibleLists[i].key) possibleLists[i].obj[possibleLists[i].key] = obj; else array = obj; } } return array; } function array_keys(o) { if ('function' === typeof Object.keys) return Object.keys(o); var v, k, l; if (is_array(o)) { v = new Array(l=o.length); for (k=0; k<l; ++k) { v[k] = String(k); } } else { v = []; for (k in o) { if (HAS.call(o, k)) v.push(k); } } return v; } function array_values(o) { if (is_array(o)) return o; if ('function' === typeof Object.values) return Object.values(o); var v = [], k; for (k in o) { if (HAS.call(o, k)) v.push(o[k]); } return v; } function is_numeric_array(o) { if (is_array(o)) return true; if (is_obj(o)) { var k = array_keys(o), i, l = k.length; for (i=0; i<l; ++i) { if (i !== +k[i]) return false; } return true; } return false; } function in_array(v, a, strict) { var i, l = a.length; if (true === strict) { // Array.indexOf uses strict equality return (0 < l) && (-1 !== a.indexOf(v)); /*for(i=0; i<l; i++) if ( v===a[i] ) return true;*/ } else { for (i=0; i<l; ++i) { if (v == a[i]) return true; } } return false; } // adapted from https://github.com/kvz/phpjs function http_build_query_helper(key, val, arg_separator, PHP_QUERY_RFC3986) { var k, tmp, encode = PHP_QUERY_RFC3986 ? rawurlencode : urlencode; if (true === val) val = "1"; else if (false === val) val = "0"; if (null != val) { if ('object' === typeof val) { tmp = []; for (k in val) { if (HAS.call(val, k) && (null != val[k])) { tmp.push(http_build_query_helper(key + "[" + k + "]", val[k], arg_separator, PHP_QUERY_RFC3986)); } } return tmp.join(arg_separator); } else { return encode(key) + "=" + encode(val); } } else { return ''; } } function http_build_query(data, arg_separator, PHP_QUERY_RFC3986) { var value, key, query, tmp = []; if (arguments.length < 2) arg_separator = "&"; if (arguments.length < 3) PHP_QUERY_RFC3986 = false; for (key in data) { if (!HAS.call(data, key)) continue; value = data[key]; query = http_build_query_helper(key, value, arg_separator, PHP_QUERY_RFC3986); if ('' != query) tmp.push(query); } return tmp.join(arg_separator); } function split(s, d1, d2) { if ((d1 === d2) || !d2) { return s.split(d1); } else { var parts = [], part, i; s = s.split(d1); for (i=0; i<s.length; ++i) { part = s[i]; part = part.split(d2); parts.push(part[0]); if (part.length > 1) parts.push(part[1]); } return parts; } } function offset(i) { return function(m) { return i; }; } function matched(i) { return function(m) { return m[i] ? m[i].length : 0; }; } function index(offsets) { return function(m) { return offsets.reduce(function(i, offset) { return i + offset(m); }, 0); }; } function makePattern(_delims, _patterns, pattern) { var i, l, isPattern, p, m, numGroups = 0, offsets, types = {}, tpl, tplPattern, pat; pattern = split(pattern, _delims[2], _delims[3]); p = []; tpl = []; offsets = []; tplPattern = null; l = pattern.length; isPattern = false; for (i=0; i<l; ++i) { if (isPattern) { if (pattern[i].length) { if (HAS.call(_patterns, pattern[i])) { p.push('(' + _patterns[pattern[i]][0] + ')'); ++numGroups; // typecaster if (_patterns[pattern[i]][1]) types[numGroups] = _patterns[pattern[i]][1]; if (null == tplPattern) tplPattern = p[p.length-1]; offsets.push([numGroups]); } else if ((m = pattern[i].match(_patternOr))) { p.push('(' + m[1].split('|').filter(length).map(esc_regex).join('|') + ')'); ++numGroups; if (null == tplPattern) tplPattern = p[p.length-1]; offsets.push([numGroups]); } else if (pattern[i].length) { p.push('(' + esc_regex(pattern[i]) + ')'); ++numGroups; if (null == tplPattern) tplPattern = p[p.length-1]; offsets.push([numGroups]); } } tpl.push(true); isPattern = false; } else { if (pattern[i].length) { p.push(esc_regex(pattern[i])); tpl.push(pattern[i]); offsets.push(pattern[i].length); } isPattern = true; } } if (1 === p.length && 1 === numGroups) { types[0] = types[1] ? types[1] : null; pat = p.join(''); return [pat, numGroups, types, tpl, tplPattern ? tplPattern : pat, offsets]; } else { types[0] = null; pat = '(' + p.join('') + ')'; return [pat, numGroups+1, types, tpl, tplPattern ? tplPattern : pat, offsets]; } } function makeRoute(_delims, _patterns, route, method, prefix) { var parts, part, i, l, isOptional, isCaptured, isPattern, pattern, p, m, numGroups, patternTypecaster, captures, captureName, capturePattern, captureIndex, tpl, currOffset, offsets, offsetCapture, done ; if (0 > route.indexOf(_delims[0])) { // literal route return [route, prefix && prefix.length ? prefix+route : route, {}, method, true, [route]]; } parts = split(route, _delims[0], _delims[1]); l = parts.length; isPattern = false; pattern = ''; currOffset = 0; offsets = []; numGroups = 0; captures = {}; tpl = []; if (prefix && prefix.length) { pattern += esc_regex(prefix); currOffset = prefix.length; } for (i=0; i<l; ++i) { part = parts[i]; if (isPattern) { isOptional = false; isCaptured = false; patternTypecaster = null; offsetCapture = []; // http://abc.org/{%ALFA%:user}{/%NUM%:?id(1)} p = part.split(_delims[4]); if (!p[0].length) { // http://abc.org/{:user}/{:?id} // assume pattern is %PART% p[0] = _delims[2] + 'PART' + _delims[3]; } capturePattern = makePattern(_delims, _patterns, p[0]); if (p.length > 1) { captureName = trim(p[1]); isOptional = (captureName.length && '?' === captureName.charAt(0)); if (isOptional) captureName = captureName.slice(1); if ((m = captureName.match(_group))) { captureName = captureName.slice(0, -m[0].length); captureIndex = parseInt(m[1], 10); patternTypecaster = HAS.call(capturePattern[2], captureIndex) ? capturePattern[2][captureIndex] : null; if (captureIndex > 0 && captureIndex < capturePattern[1]) { done = false; offsetCapture = capturePattern[5].reduce(function(offsetCapture, o) { if (is_array(o)) { if (o[0] >= captureIndex) { done = true; } } if (!done) { offsetCapture.push(is_array(o) ? matched(o[0]+numGroups+1) : offset(o)); } return offsetCapture; }, []); captureIndex += numGroups + 1; } else { captureIndex = numGroups + 1; } } else { patternTypecaster = capturePattern[2][0] ? capturePattern[2][0] : null; captureIndex = numGroups + 1; } isCaptured = (captureName.length > 0); } pattern += capturePattern[0]; if (isOptional) pattern += '?'; if (isCaptured) captures[captureName] = [captureIndex, patternTypecaster, index(offsets.concat(offsetCapture))]; if (isCaptured) tpl.push({ name : captureName, optional : isOptional, re : new RegExp('^' + capturePattern[4] + '$'), tpl : capturePattern[3] }); currOffset = 0; offsets.push(matched(numGroups + 1)); numGroups += capturePattern[1]; isPattern = false; } else { pattern += esc_regex(part); currOffset += part.length; tpl.push(part); offsets.push(offset(currOffset)); isPattern = true; } } return [route, new RegExp('^' + pattern + '$'), captures, method, false, tpl]; } function to_key(route, method) { return method.join(',') + '->' + route; } function to_method(method) { method = method ? (method.map ? method.map(function(x){return x.toLowerCase()}) : [String(method).toLowerCase()]) : ['*']; if (in_array('*', method)) method = ['*']; method.sort(); return method; } function insertRoute(self, route, oneOff) { if ( route && is_string(route.route) /*&& route.route.length*/ && route.handler && is_callable(route.handler) ) { oneOff = (true === oneOff); var handler = route.handler, defaults = route.defaults || {}, types = route.types || null, name = route.name || null, method = to_method(route.method), h, r, i, l, key; route = self.key + route.route; key = to_key(route, method); r = null; for (i=0,l=self._routes.length; i<l; ++i) { if (key === self._routes[i].key) { r = self._routes[i]; break; } } if (!r) { r = new Route(self._delims, self._patterns, route, method, name, self._prefix); self._routes.push(r); self._addNamedRoute(r); } r.handlers.push([ handler, defaults, types, oneOff, 0 ]); } } function clearRoute(self, key) { var i, l = self._routes.length, route; for (i=l-1; i>=0; --i) { if (key === self._routes[i].key) { route = self._routes[i]; self._routes.splice(i, 1); self._delNamedRoute(route); route.dispose(); } } } function Route(delims, patterns, route, method, name, prefix) { var self = this; self.__args__ = [delims, patterns]; self.isParsed = false; // lazy init self.handlers = []; self.route = null != route ? String(route) : ''; self.prefix = null != prefix ? String(prefix) : ''; self.method = method; self.pattern = null; self.captures = null; self.literal = false; self.namespace = null; self.tpl = null; self.name = null != name ? String(name) : null; self.key = to_key(self.route, self.method); } Route.to_key = to_key; Route[PROTO] = { constructor: Route, __args__: null, isParsed: false, handlers: null, route: null, prefix: null, pattern: null, captures: null, tpl: null, method: null, literal: null, namespace: null, name: null, key: null, dispose: function() { var self = this; self.__args__ = null; self.isParsed = null; self.handlers = null; self.route = null; self.prefix = null; self.pattern = null; self.captures = null; self.tpl = null; self.method = null; self.literal = null; self.namespace = null; self.name = null; self.key = null; return self; }, parse: function() { var self = this; if (self.isParsed) return self; var r = makeRoute(self.__args__[0], self.__args__[1], self.route, self.method, self.prefix); self.pattern = r[1]; self.captures = r[2]; self.tpl = r[5]; self.literal = true === r[4]; self.__args__ = null; self.isParsed = true; return self; }, match: function(route, method) { var self = this; method = method || '*'; if (!in_array(method, self.method) && ('*' !== self.method[0])) return null; if (!self.isParsed) self.parse(); // lazy init route = String(route); return self.literal ? (route === self.pattern ? [] : null) : route.match(self.pattern); }, make: function(params, strict) { var self = this, out = '', i, l, j, k, param, part, tpl; params = params || {}; strict = true === strict; if (!self.isParsed) self.parse(); // lazy init tpl = self.tpl; for (i=0,l=tpl.length; i<l; ++i) { if (is_string(tpl[i])) { out += tpl[i]; } else { if (!HAS.call(params, tpl[i].name) || (null == params[tpl[i].name])) { if (tpl[i].optional) { continue; } else { throw new ReferenceError('Dromeo: Route "'+self.name+'" (Pattern: "'+self.route+'") missing parameter "'+tpl[i].name+'"!'); } } else { param = String(params[tpl[i].name]); if (strict && !tpl[i].re.test(param)) { throw new ReferenceError('Dromeo: Route "'+self.name+'" (Pattern: "'+self.route+'") parameter "'+tpl[i].name+'" value "'+param+'" does not match pattern!'); } part = tpl[i].tpl; for (j=0,k=part.length; j<k; ++j) { out += true === part[j] ? param : part[j]; } } } } return out; }, sub: function(match, data, type, originalInput, originalKey) { var self = this, v, g, i, groupIndex, groupTypecaster, groupMatchIndex, givenInput, isDifferentInput, hasOriginal, odata, matchedValue, matchedOriginalValue, typecaster; if (!self.isParsed || self.literal) return self; givenInput = match[0]; isDifferentInput = is_string(originalInput) && (originalInput !== givenInput); hasOriginal = is_string(originalKey); odata = hasOriginal ? {} : null; for (v in self.captures) { if (!HAS.call(self.captures, v)) continue; g = self.captures[v]; groupIndex = g[0]; groupTypecaster = g[1]; groupMatchIndex = g[2]; if (match[groupIndex]) { matchedValue = match[groupIndex]; if (isDifferentInput) { // if original input is given, // find index and get match from original input (eg with original case) i = groupMatchIndex(match); // match index matchedOriginalValue = originalInput.slice(i, i+matchedValue.length); } else { // else what matched matchedOriginalValue = matchedValue; } if (type && HAS.call(type, v) && type[v]) { typecaster = type[v]; if (is_string(typecaster) && HAS.call(Dromeo.TYPES, typecaster)) typecaster = Dromeo.TYPES[typecaster]; data[v] = is_callable(typecaster) ? typecaster(matchedValue) : matchedValue; if (hasOriginal) odata[v] = is_callable(typecaster) ? typecaster(matchedOriginalValue) : matchedOriginalValue; } else if (groupTypecaster) { typecaster = groupTypecaster; data[v] = is_callable(typecaster) ? typecaster(matchedValue) : matchedValue; if (hasOriginal) odata[v] = is_callable(typecaster) ? typecaster(matchedOriginalValue) : matchedOriginalValue; } else { data[v] = matchedValue; if (hasOriginal) odata[v] = matchedOriginalValue; } } else if (!HAS.call(data, v)) { data[v] = null; if (hasOriginal) odata[v] = null; } else if (hasOriginal) { odata[v] = data[v]; } } if (hasOriginal) data[String(originalKey)] = odata; return self; } }; function Dromeo(prefix, group, top) { var self = this; // constructor factory method if (!(self instanceof Dromeo)) return new Dromeo(prefix, group, top); self._delims = ['{', '}', '%', '%', ':']; self._patterns = {}, self.definePattern('ALPHA', '[a-zA-Z\\-_]+'); self.definePattern('ALNUM', '[a-zA-Z0-9\\-_]+'); self.definePattern('NUMBR', '[0-9]+'); self.definePattern('INT', '[0-9]+', 'INT'); self.definePattern('PART', '[^\\/?#]+'); self.definePattern('VAR', '[^=?&#\\/]+', 'VAR'); self.definePattern('QUERY', '\\?[^?#]+'); self.definePattern('FRAGMENT', '#[^?#]+'); self.definePattern('URLENCODED', '[^\\/?#]+', 'URLENCODED'); self.definePattern('ALL', '.+'); self._routes = []; self._named_routes = {}; self._fallback = false; self._top = top instanceof Dromeo ? top : self; self.key = self === self._top ? '' : self._top.key + String(group); self._prefix = null == prefix ? '' : String(prefix); }; // default typecasters function type_to_int(v) { return parseInt(v, 10) || 0; } function type_to_str(v) { return is_string(v) ? v : '' + String(v); } function type_to_urldecode(v) { return urldecode(v); } function type_to_array(v) { return is_array(v) ? v : [v]; } function type_to_params(v) { return is_string(v) ? Dromeo.unglue_params(v) : v; } Dromeo.VERSION = __version__; Dromeo.HTTP_STATUS = HTTP_STATUS; Dromeo.Route = Route; Dromeo.to_method = to_method; Dromeo.TYPES = { 'INTEGER' : type_to_int ,'STRING' : type_to_str ,'URLDECODE': type_to_urldecode ,'ARRAY' : type_to_array ,'PARAMS' : type_to_params }; // aliases Dromeo.TYPES['INT'] = Dromeo.TYPES['INTEGER']; Dromeo.TYPES['STR'] = Dromeo.TYPES['STRING']; Dromeo.TYPES['VAR'] = Dromeo.TYPES['URLDECODE']; Dromeo.TYPES['URLENCODED'] = Dromeo.TYPES['PARAMS']; // build/glue together a uri component from a params object Dromeo.glue_params = function(params) { var component = ''; // http://php.net/manual/en/function.http-build-query.php if (params) component += http_build_query(params, '&', true); return component; }; // unglue/extract params object from uri component Dromeo.unglue_params = function(s) { var PARAMS = s ? parse_str(s) : {}; return PARAMS; }; // parse and extract uri components and optional query/fragment params Dromeo.parse_components = function(s, query_p, fragment_p) { var self = this, COMPONENTS = {}; if (s) { if (arguments.length < 3 || null == fragment_p) fragment_p = 'fragment_params'; if (arguments.length < 2 || null == query_p) query_p = 'query_params'; COMPONENTS = parse_url(s); if (query_p) { if (COMPONENTS['query']) COMPONENTS[query_p] = self.unglue_params(COMPONENTS['query']); else COMPONENTS[query_p] = {}; } if (fragment_p) { if (COMPONENTS['fragment']) COMPONENTS[fragment_p] = self.unglue_params(COMPONENTS['fragment']); else COMPONENTS[fragment_p] = {}; } } return COMPONENTS; }; // build a url from baseUrl plus query/hash params Dromeo.build_components = function(baseUrl, query, hash, q, h) { var self = this, url = '' + baseUrl; if (arguments.length < 5 || null == h) h = '#'; if (arguments.length < 4 || null == q) q = '?'; if (query) url += q + self.glue_params(query); if (hash) url += h + self.glue_params(hash); return url; }; Dromeo.defType = function(type, caster) { if (type && is_callable(caster)) Dromeo.TYPES[type] = caster; }; Dromeo.TYPE = function(type) { if (type && HAS.call(Dromeo.TYPES, type)) return Dromeo.TYPES[type]; return null; }; Dromeo[PROTO] = { constructor: Dromeo, _delims: null, _patterns: null, _routes: null, _named_routes: null, _fallback: false, _prefix: '', _top: null, key: '', dispose: function() { var self = this, i, l; self._top = null; self._delims = null; self._patterns = null; self._fallback = null; self._prefix = null; if (self._routes) { for (i=0,l=self._routes.length; i<l; ++i) { self._routes[i].dispose(); } } self._routes = null; self._named_routes = null; return self; }, top: function() { return this._top; }, isTop: function() { return (null == this._top) || (this === this._top); }, clone: function(group) { var self = this, cloned = new Dromeo(self._prefix, group, self), className, args; cloned.defineDelimiters(self._delims); for (className in self._patterns) { if (!HAS.call(self._patterns, className)) continue; args = self._patterns[className]; cloned.definePattern(className, args[0], 1 < args.length ? args[1] : null); } return cloned; }, reset: function() { var self = this; self._routes = []; self._named_routes = {}; self._fallback = false; return self; }, /*debug: function() { console.log('Routes: '); console.log(this._routes); console.log('Fallback: '); console.log(this._fallback); },*/ defineDelimiters: function(delims) { var self = this, _delims = self._delims, l; if (delims) { l = delims.length; if (l > 0 && delims[0]) _delims[0] = delims[0]; if (l > 1 && delims[1]) _delims[1] = delims[1]; if (l > 2 && delims[2]) _delims[2] = delims[2]; if (l > 3 && delims[3]) _delims[3] = delims[3]; if (l > 4 && delims[4]) _delims[4] = delims[4]; } return self; }, definePattern: function(className, subPattern, typecaster) { var self = this; if ( typecaster && is_string(typecaster) && typecaster.length && HAS.call(Dromeo.TYPES, typecaster) ) typecaster = Dromeo.TYPES[typecaster]; if (!typecaster || !is_callable(typecaster)) typecaster = null; self._patterns[className] = [subPattern, typecaster]; return self; }, dropPattern: function(className) { var self = this, patterns = self._patterns; if (HAS.call(patterns, className)) delete patterns[className]; return self; }, defineType: function(type, caster) { Dromeo.defType(type, caster); return this; }, // build/glue together a uri component from a params object glue: function(params) { return Dromeo.glue_params(params); }, // unglue/extract params object from uri component unglue: function(s) { return Dromeo.unglue_params(s); }, // parse and extract uri components and optional query/fragment params parse: function(s, query_p, fragment_p) { return Dromeo.parse_components(s, query_p, fragment_p); }, // build a url from baseUrl plus query/hash params build: function(baseUrl, query, hash, q, h) { return Dromeo.build_components(baseUrl, query, hash, q, h); }, redirect: function(url, response, statusCode, statusMsg) { // node redirection based on http module // http://nodejs.org/api/http.html#http_http if (url) { if (!isNode) { document.location.href = url; // make sure document is reloaded in case only hash changes //document.location.reload(true); } else if (response) { if (arguments.length < 3) statusCode = 302; if (arguments.length < 4) statusMsg = true; if (statusMsg) { if (true === statusMsg) statusMsg = Dromeo.HTTP_STATUS[statusCode] || ''; response.writeHead(statusCode, statusMsg, {"Location": url}); } else { response.writeHead(statusCode, {"Location": url}); } response.end(); } } return this; }, onGroup: function(groupRoute, handler) { var self = this, groupRouter; groupRoute = String(groupRoute); if (groupRoute.length && is_callable(handler)) { groupRouter = self.clone(groupRoute); self._routes.push(groupRouter); handler(groupRouter); } return self; }, on: function(/* var args here .. */) { var self = this, args = arguments, args_len = args.length, routes ; if (1 === args_len) { routes = is_array(args[0]) ? args[0] : [args[0]]; } else if (2 === args_len && is_string(args[0]) && is_callable(args[1])) { routes = [{ route: args[0], handler: args[1], method: '*', defaults: {}, types: null }]; } else { routes = args; } for (var i=0; i<routes.length; ++i) { insertRoute(self, routes[i], false); } return self; }, one: function(/* var args here .. */) { var self = this, args = arguments, args_len = args.length, routes ; if (1 === args_len) { routes = is_array(args[0]) ? args[0] : [args[0]]; } else if (2 === args_len && is_string(args[0]) && is_callable(args[1])) { routes = [{ route: args[0], handler: args[1], method: '*', defaults: {}, types: null }]; } else { routes = args; } for (var i=0; i<routes.length; ++i) { insertRoute(self, routes[i], true); } return self; }, off: function(route, handler, method) { var self = this, routes = self._routes, i, r, l, key; if (!route) return self; if (null == method) method = '*'; if (is_obj(route)) { handler = route.handler || handler; method = route.method || method; route = route.route; if (!route) return self; route = String(route); key = to_key(route, to_method(method)); r = null; for (i=0,l=routes.length; i<l; ++i) { if (routes[i] instanceof Dromeo) { routes[i].off(route, handler, method); } else { if (key === routes[i].key) { r = routes[i]; break; } } } if (!r) return self; if (handler && is_callable(handler)) { l = r.handlers.length; for (i=l-1; i>=0; --i) { if (handler === r.handlers[i][0]) r.handlers.splice(i, 1); } if (!r.handlers.length) clearRoute(self, key); } else { clearRoute(self, key); } } else if (is_string(route) && route.length) { route = String(route); key = to_key(route, to_method(method)); r = null; for (i=0,l=routes.length; i<l; ++i) { if (routes[i] instanceof Dromeo) { if (route === routes[i].key) { r = routes[i]; break; } else { routes[i].off(route, handler, method); } } else { if (key === routes[i].key) { r = routes[i]; break; } } } if (!r) return self; if (r instanceof Dromeo) { routes.splice(i, 1); r.dispose(); } else { if (handler && is_callable(handler)) { l = r.handlers.length; for (i=l-1; i>=0; --i) { if (handler === r.handlers[i][0]) r.handlers.splice(i, 1); } if (!r.handlers.length) clearRoute(self, key); } else { clearRoute(self, key); } } } return self; }, fallback: function(handler) { var self = this; if (1 > arguments.length) handler = false; if (false === handler || null === handler || is_callable(handler)) self._fallback = handler; return self; }, make: function(named_route, params, strict) { var routes = this._named_routes; return HAS.call(routes, named_route) ? routes[named_route].make(params, strict) : null; }, route: function(r, method, breakOnFirstMatch, originalR, originalKey) { var self = this, proceed, prefix, routes, route, params, defaults, type, to_remove, i, l, lh, h, match, handlers, handler, found; ; if (!self.isTop() && !self._routes.length) return false; proceed = true; found = false; r = null != r ? String(r) : ''; prefix = self._prefix + self.key; if (prefix.length) { proceed = (prefix === r.slice(0, prefix.length)); } if (proceed) { breakOnFirstMatch = false !== breakOnFirstMatch; method = null != method ? String(method).toLowerCase() : '*'; routes = self._routes.slice(); // copy, avoid mutation l = routes.length; for (i=0; i<l; ++i) { route = routes[i]; if (route instanceof Dromeo) { // group router match = route.route(r, method, breakOnFirstMatch, originalR, originalKey); if (!match) continue; found = true; } else { // simple route match = route.match(r, method); if (null == match) continue; found = true; // copy handlers, avoid mutation during calls handlers = route.handlers.slice(); // make calls to_remove = []; lh = handlers.length; for (h=0; h<lh; ++h) { handler = handlers[h]; // handler is oneOff and already called if (handler[3] && handler[4]) { to_remove.unshift(h); continue; } defaults = handler[1]; type = handler[2]; params = { route: r, method: method, pattern: route.route, fallback: false, data: extend({}, defaults, true) }; if (is_string(originalR)) params['route_original'] = originalR; route.sub(match, params.data, type, originalR, originalKey); handler[4] = 1; // handler called if (handler[3]) to_remove.unshift(h); handler[0](params); } // remove called oneOffs for (h=0,lh=to_remove.length; h<lh; ++h) { route.handlers.splice(to_remove[h], 1); } if (!route.handlers.length) { clearRoute(self, route.key); } } if (breakOnFirstMatch) return true; } if (found) return true; } if (self._fallback && self.isTop()) { self._fallback({ route: r, method: method, pattern: null, fallback: true, data: null }); } return false; }, _addNamedRoute: function(route) { var self = this; if (self.isTop()) { if ((route instanceof Dromeo.Route) && route.name && route.name.length) { self._named_routes[route.name] = route; } } else { self.top()._addNamedRoute(route); } return self; }, _delNamedRoute: function(route) { var self = this; if (self.isTop()) { if ((route instanceof Dromeo.Route) && route.name && HAS.call(self._named_routes, route.name)) { delete self._named_routes[route.name]; } } else { self.top()._delNamedRoute(route); } return self; } }; // export it return Dromeo; });