/** * * 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 -1) key = key.slice(0, j); if (key && ('[' !== key.charAt(0))) { keys = []; postLeftBracketPos = 0; for (j=0; j 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 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 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 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=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 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=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=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