├── .gitignore ├── lib ├── context-processors │ ├── index.js │ └── pathAware.js ├── constants.js ├── utils.js ├── templatingEngine.js ├── server.js └── routes │ └── route-pattern.js ├── index.js ├── LICENSE ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /lib/context-processors/index.js: -------------------------------------------------------------------------------- 1 | const pathAware = require('./pathAware'); 2 | 3 | /** 4 | * @type {{pathAware: Object}} 5 | */ 6 | module.exports = { 7 | pathAware 8 | }; 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * @fileOverview Nova Server. 4 | * @author Pablo Alecio 5 | */ 6 | const server = require('./lib/server'); 7 | const contextProcessors = require('./lib/context-processors'); 8 | 9 | /** 10 | * @type {{server: {start: Function}, contextProcessors: {pathAware: Object}}} 11 | */ 12 | module.exports = { 13 | server, 14 | contextProcessors 15 | }; 16 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Nova constants. 3 | * @author Pablo Alecio 4 | */ 5 | 'use strict'; 6 | 7 | /** 8 | * Exports a frozen object with the project constants. 9 | * @module lib/constants 10 | * @type {Object} 11 | */ 12 | module.exports = Object.freeze({ 13 | BLANK: '', 14 | ASTERISK: '*', 15 | HTML: 'html', 16 | JSON_TYPE: 'json', 17 | TEXT_TYPE: 'text/html' 18 | }); 19 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Outputs args to the console, adding '> Nova Server:' as prefix. 5 | * @param args The arguments to log. 6 | */ 7 | const log = (...args) => console.log('> Nova Server:', ...args); 8 | 9 | /** 10 | * Deep clones obj and returns the clone. 11 | * @param obj The object to clone 12 | */ 13 | const cloneObject = obj => 14 | (typeof obj === 'object' && JSON.parse(JSON.stringify(obj))) || null; 15 | 16 | module.exports = { 17 | log, 18 | cloneObject 19 | }; 20 | -------------------------------------------------------------------------------- /lib/templatingEngine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Contains the logic for the Templating Engine. 3 | * @author Pablo Alecio 4 | */ 5 | 'use strict'; 6 | 7 | const handlebars = require('handlebars'); 8 | 9 | /** 10 | * Compiles the markup using the contentModel 11 | * @param {string} markup 12 | * @param {Object} contentModel 13 | * @returns {string} the compiled markup 14 | */ 15 | const compileTemplate = (markup, contentModel) => { 16 | const template = handlebars.compile(markup); 17 | return template(contentModel); 18 | }; 19 | 20 | /** 21 | * @module lib/templatingEngine 22 | * @type { compileTemplate } 23 | */ 24 | module.exports = { compileTemplate }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Pablo Alecio 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@palecio/nova-server", 3 | "version": "1.5.0", 4 | "description": "Lightweight web server using Expressjs that enables building web APIs with Nova.", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/PaleSC2/nova-server.git" 10 | }, 11 | "keywords": [ 12 | "nova", 13 | "server", 14 | "webserver", 15 | "rest", 16 | "api", 17 | "expressjs", 18 | "microservices" 19 | ], 20 | "engines": { 21 | "node": ">=12.11.0", 22 | "npm": ">=6.11.3" 23 | }, 24 | "author": { 25 | "name": "Pablo Alecio", 26 | "email": "paleciop@gmail.com" 27 | }, 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/PaleSC2/nova-server/issues" 31 | }, 32 | "homepage": "", 33 | "dependencies": { 34 | "@palecio/nova-core": "^1.4.1", 35 | "axios": "^0.19.2", 36 | "body-parser": "^1.17.2", 37 | "express": "^4.15.4", 38 | "handlebars": "^4.7.6" 39 | }, 40 | "prettier": { 41 | "singleQuote": true 42 | }, 43 | "devDependencies": { 44 | "nodemon": "^2.0.4", 45 | "prettier": "^1.19.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Nova Server 2 | Lightweight web server using Expressjs that enables building web APIs with Nova. 3 | 4 | ## Install 5 | 6 | ``` 7 | npm install @palecio/nova-server 8 | ``` 9 | 10 | ## Create your own Nova server 11 | ``` 12 | const serverConfig = { 13 | contextProcessorPaths: '', 14 | port: , //Default is 9001 15 | baseURLPath: , //Default is '/api' 16 | allowedHostnames: , // hostnames allowed to make cross domain requests 17 | baseContentModel: { // The base content model 18 | config: {} //an object with configs that will be accessible to the Context Processors 19 | } 20 | }; 21 | 22 | server.start(serverConfig).then(() => { 23 | console.log('Nova Server Running!'); 24 | }); 25 | ``` 26 | 27 | ## Example Context Processor 28 | ``` 29 | const contextProcessor = require("nova-core").pathAwareContextProcessor; 30 | 31 | module.exports = contextProcessor.extend({ 32 | patterns: ["/my-path"], //Express-like routes 33 | priority: 10, //The higher the number, the sooner it runs, no priority means the CP doesn't have any dependencies so it'll run in parallel 34 | process(executionContext, contentModel) { 35 | //do something with the content model here 36 | } 37 | }); 38 | ``` 39 | 40 | ### execution context TODO 41 | ### access request TODO 42 | ### httpHeaders context TODO 43 | ### templatingEngine TODO 44 | ### debugMode TODO 45 | -------------------------------------------------------------------------------- /lib/context-processors/pathAware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Path Aware Context Processor, meant to be extended with {@link extend} 3 | * @author Pablo Alecio 4 | */ 5 | 'use strict'; 6 | 7 | const RoutePattern = require('../routes/route-pattern'); 8 | const contextProcessor = require('@palecio/nova-core').contextProcessor; 9 | 10 | /** 11 | * @module lib/context-processors/pathAwareContextProcessor 12 | * @type {Object} 13 | */ 14 | module.exports = contextProcessor.extend({ 15 | /** 16 | * The context processor name. 17 | */ 18 | name: 'Path Aware', 19 | /** 20 | * A list of URL patterns which the request path has to match in order for this Context Processor to be executed. 21 | */ 22 | patterns: [], 23 | /** 24 | * Gets the pattern that matches thePath. 25 | * @param thePath the path. 26 | * @returns {string} The pattern that matched the path. 27 | */ 28 | getMatchingPattern(thePath) { 29 | for (let i = 0; i < this.patterns.length; i++) { 30 | const routePattern = RoutePattern.fromString(this.patterns[i]); 31 | const match = routePattern.matches(thePath); 32 | if (match) { 33 | return this.patterns[i]; 34 | } 35 | } 36 | return ''; 37 | }, 38 | /** 39 | * Gets the path from executionContext. First, it looks for a path property, if that fails, it uses the request path. 40 | * @param {Object} executionContext The Execution Context. 41 | * @returns {string} the path of the execution context. 42 | */ 43 | getPath(executionContext) { 44 | return ( 45 | executionContext.path || 46 | (executionContext.request && executionContext.request.baseUrl) || 47 | '' 48 | ); 49 | }, 50 | /** 51 | * Gets the match object containing the variables which matched the path. 52 | * @param executionContext The Execution Context. 53 | * @returns {Object} The match. 54 | */ 55 | getMatch(executionContext) { 56 | const thePath = this.getPath(executionContext); 57 | const pattern = this.getMatchingPattern(thePath); 58 | const routePattern = RoutePattern.fromString(pattern); 59 | return routePattern.match(thePath); 60 | }, 61 | /** 62 | * Checks if this Context Processor has to be executed based on the path in the execution context. 63 | * @param {Object} executionContext The execution context. 64 | * @returns {boolean} True if the path sent as argument matches any of the patterns; false otherwise. 65 | */ 66 | accepts(executionContext) { 67 | const thePath = this.getPath(executionContext); 68 | return !!this.getMatchingPattern(thePath); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Nova Server. 3 | * @author Pablo Alecio 4 | */ 5 | 'use strict'; 6 | 7 | const express = require('express'); 8 | const bodyParser = require('body-parser'); 9 | const nova = require('@palecio/nova-core'); 10 | const templatingEngine = require('./templatingEngine'); 11 | 12 | const { ASTERISK, HTML, JSON_TYPE, TEXT_TYPE } = require('./constants'); 13 | const utils = require('./utils'); 14 | 15 | const app = express(); 16 | const router = express.Router(); 17 | const { log, cloneObject } = utils; 18 | const DEFAULT_NOVA_PORT = 9001; 19 | const DEFAULT_BASE_URL_PATH = '/api'; 20 | 21 | /** 22 | * @async 23 | * @param configuration {Object} the server configuration object 24 | */ 25 | const start = async function start(configuration = {}) { 26 | const { 27 | port = DEFAULT_NOVA_PORT, // The port where the express server will start 28 | baseURLPath = DEFAULT_BASE_URL_PATH, // The route URL prefix. Default is '/api' 29 | allowedHostnames = ['localhost'], // The host names that are allowed to make requests to this server 30 | contextProcessors = {}, // An array of literal Context Processors or a single literal Context Processor 31 | contextProcessorPaths = [], // An array of paths or a single path where the Context Processors are located in the file system 32 | baseContentModel = {} // The starting Content Model 33 | } = configuration; 34 | 35 | const paths = Array.isArray(contextProcessorPaths) 36 | ? contextProcessorPaths 37 | : [contextProcessorPaths]; 38 | 39 | /** 40 | * The Context Processor engine 41 | * @async 42 | * @type {Function} 43 | */ 44 | const cpe = await nova.fetchContextProcessorEngine({ 45 | contextProcessors, 46 | paths 47 | }); 48 | 49 | /** 50 | * A function that executes the Context Processors based on the data in the execution context 51 | * @async 52 | * @param executionContext the Execution Context 53 | * @param contentModel the Content Model 54 | * @returns {Promise} returns a promise which in turn returns the final Content Model 55 | */ 56 | const executeContextProcessors = async function executeContextProcessors( 57 | executionContext, 58 | contentModel 59 | ) { 60 | return cpe.execute(executionContext, contentModel); 61 | }; 62 | 63 | /** 64 | * CORS support 65 | * @param req the request 66 | * @param res the response 67 | * @param next the next function 68 | */ 69 | const allowCrossDomain = function(req, res, next) { 70 | const origin = req.headers.origin; 71 | if ( 72 | origin && 73 | allowedHostnames.some(hostname => origin.includes(hostname)) 74 | ) { 75 | res.header('Access-Control-Allow-Origin', req.headers.origin); 76 | res.header('Access-Control-Allow-Headers', '*'); 77 | res.header('Access-Control-Allow-Credentials', 'true'); 78 | } 79 | next(); 80 | }; 81 | 82 | /** 83 | * The function that handles all the requests made to the server 84 | * @param request the request object 85 | * @param response the response object 86 | */ 87 | const handleRequest = function handleRequest(request, response) { 88 | executeContextProcessors( 89 | { path: request.path, request }, 90 | cloneObject(baseContentModel) 91 | ) 92 | .then(contentModel => { 93 | if (contentModel) { 94 | const body = request.body; 95 | const contentType = request.headers['content-type']; 96 | const headers = contentModel.httpHeaders; 97 | let responseBody = ''; 98 | 99 | if (contentModel.debug && request.query.debug) { 100 | response.type(HTML); 101 | return response.send(contentModel.debug.visualRepresentation); 102 | } 103 | 104 | if (headers && typeof headers === 'object') { 105 | response.set(headers); 106 | delete contentModel.httpHeaders; 107 | } 108 | 109 | if (body && contentType === 'text/html') { 110 | response.type(HTML); 111 | responseBody = templatingEngine.compileTemplate(body, contentModel); 112 | } else { 113 | response.type(JSON_TYPE); 114 | responseBody = contentModel; 115 | } 116 | response.send(responseBody); 117 | } else { 118 | response.status(404).send('Not Found'); 119 | } 120 | }) 121 | .catch(error => { 122 | console.error(error); 123 | const status = 500; 124 | response 125 | .status(status) 126 | .type(JSON_TYPE) 127 | .send({ code: status, message: error.message }); 128 | }); 129 | }; 130 | 131 | app.use(allowCrossDomain); // CORS support 132 | router.use(bodyParser.json()); // to parse the request body as json 133 | router.use(bodyParser.text({ type: TEXT_TYPE })); // to parse the request as text 134 | router.all(ASTERISK, handleRequest); // handle ALL requests to the server 135 | app.use(baseURLPath, router); // use 'router' to handle all requests made to the baseURLPath 136 | app.listen(port, function() { 137 | log(`Started on port ${port}`); 138 | }); 139 | }; 140 | 141 | /** 142 | * @type {{start: Function}} server 143 | */ 144 | module.exports = { 145 | start 146 | }; 147 | -------------------------------------------------------------------------------- /lib/routes/route-pattern.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | 3 | // # Utility functions 4 | // 5 | // ## Shallow merge two or more objects, e.g. 6 | // merge({a: 1, b: 2}, {a: 2}, {a: 3}) => {a: 3, b: 2} 7 | function merge() { 8 | return [].slice.call(arguments).reduce(function(merged, source) { 9 | for (var prop in source) { 10 | merged[prop] = source[prop]; 11 | } 12 | return merged; 13 | }, {}); 14 | } 15 | 16 | // Split a location string into different parts, e.g.: 17 | // splitLocation("/foo/bar?fruit=apple#some-hash") => { 18 | // path: "/foo/bar", queryString: "fruit=apple", hash: "some-hash" 19 | // } 20 | function splitLocation(location) { 21 | var re = /([^\?#]*)?(\?[^#]*)?(#.*)?$/; 22 | var match = re.exec(location); 23 | return { 24 | path: match[1] || '', 25 | queryString: (match[2] && match[2].substring(1)) || '', 26 | hash: (match[3] && match[3].substring(1)) || '' 27 | }; 28 | } 29 | 30 | // # QueryStringPattern 31 | // The QueryStringPattern holds a compiled version of the query string part of a route string, i.e. 32 | // ?foo=:foo&fruit=:fruit 33 | var QueryStringPattern = (function() { 34 | // The RoutePattern constructor 35 | // Takes a route string or regexp as parameter and provides a set of utility functions for matching against a 36 | // location path 37 | function QueryStringPattern(options) { 38 | // The query parameters specified 39 | this.params = options.params; 40 | 41 | // if allowWildcards is set to true, unmatched query parameters will be ignored 42 | this.allowWildcards = options.allowWildcards; 43 | 44 | // The original route string (optional) 45 | this.routeString = options.routeString; 46 | } 47 | 48 | QueryStringPattern.prototype.matches = function(queryString) { 49 | var givenParams = (queryString || '') 50 | .split('&') 51 | .reduce(function(params, pair) { 52 | var parts = pair.split('='), 53 | name = parts[0], 54 | value = parts[1]; 55 | if (name) params[name] = value; 56 | return params; 57 | }, {}); 58 | 59 | var requiredParam, 60 | requiredParams = [].concat(this.params); 61 | while ((requiredParam = requiredParams.shift())) { 62 | if (!givenParams.hasOwnProperty(requiredParam.key)) return false; 63 | if ( 64 | requiredParam.value && 65 | givenParams[requiredParam.key] != requiredParam.value 66 | ) 67 | return false; 68 | } 69 | if (!this.allowWildcards && this.params.length) { 70 | if (Object.getOwnPropertyNames(givenParams).length > this.params.length) 71 | return false; 72 | } 73 | return true; 74 | }; 75 | 76 | QueryStringPattern.prototype.match = function(queryString) { 77 | if (!this.matches(queryString)) return null; 78 | 79 | var data = { 80 | params: [], 81 | namedParams: {}, 82 | namedQueryParams: {} 83 | }; 84 | 85 | if (!queryString) { 86 | return data; 87 | } 88 | 89 | // Create a mapping from each key in params to their named param 90 | var namedParams = this.params.reduce(function(names, param) { 91 | names[param.key] = param.name; 92 | return names; 93 | }, {}); 94 | 95 | var parsedQueryString = querystring.parse(queryString); 96 | Object.keys(parsedQueryString).forEach(function(key) { 97 | var value = parsedQueryString[key]; 98 | data.params.push(value); 99 | if (namedParams[key]) { 100 | data.namedQueryParams[namedParams[key]] = data.namedParams[ 101 | namedParams[key] 102 | ] = value; 103 | } 104 | }); 105 | return data; 106 | }; 107 | 108 | QueryStringPattern.fromString = function(routeString) { 109 | var options = { 110 | routeString: routeString, 111 | allowWildcards: false, 112 | params: [] 113 | }; 114 | 115 | // Extract named parameters from the route string 116 | // Construct an array with some metadata about each of the named parameters 117 | routeString.split('&').forEach(function(pair) { 118 | if (!pair) return; 119 | 120 | var parts = pair.split('='), 121 | name = parts[0], 122 | value = parts[1] || ''; 123 | 124 | var wildcard = false; 125 | 126 | var param = { key: name }; 127 | 128 | // Named parameters starts with ":" 129 | if (value.charAt(0) == ':') { 130 | // Thus the name of the parameter is whatever comes after ":" 131 | param.name = value.substring(1); 132 | } else if (name == '*' && value == '') { 133 | // If current param is a wildcard parameter, the options are flagged as accepting wildcards 134 | // and the current parameter is not added to the options' list of params 135 | wildcard = options.allowWildcards = true; 136 | } else { 137 | // The value is an exact match, i.e. the route string 138 | // page=search&q=:query will match only when the page parameter is "search" 139 | param.value = value; 140 | } 141 | if (!wildcard) { 142 | options.params.push(param); 143 | } 144 | }); 145 | return new QueryStringPattern(options); 146 | }; 147 | 148 | return QueryStringPattern; 149 | })(); 150 | 151 | // # PathPattern 152 | // The PathPattern holds a compiled version of the path part of a route string, i.e. 153 | // /some/:dir 154 | var PathPattern = (function() { 155 | // These are the regexps used to construct a regular expression from a route pattern string 156 | // Based on route patterns in Backbone.js 157 | var pathParam = /:\w+/g, 158 | splatParam = /\*\w+/g, 159 | namedParams = /(:[^\/\.]+)|(\*\w+)/g, 160 | subPath = /\*/g, 161 | escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 162 | 163 | // The PathPattern constructor 164 | // Takes a route string or regexp as parameter and provides a set of utility functions for matching against a 165 | // location path 166 | function PathPattern(options) { 167 | // The route string are compiled to a regexp (if it isn't already) 168 | this.regexp = options.regexp; 169 | 170 | // The query parameters specified in the path part of the route 171 | this.params = options.params; 172 | 173 | // The original routestring (optional) 174 | this.routeString = options.routeString; 175 | } 176 | 177 | PathPattern.prototype.matches = function(pathname) { 178 | return this.regexp.test(pathname); 179 | }; 180 | 181 | // Extracts all matched parameters 182 | PathPattern.prototype.match = function(pathname) { 183 | if (!this.matches(pathname)) return null; 184 | 185 | // The captured data from pathname 186 | var data = { 187 | params: [], 188 | namedParams: {} 189 | }; 190 | 191 | // Using a regexp to capture named parameters on the pathname (the order of the parameters is significant) 192 | (this.regexp.exec(pathname) || []).slice(1).forEach(function(value, idx) { 193 | if (value !== undefined) { 194 | value = decodeURIComponent(value); 195 | } 196 | 197 | data.namedParams[this.params[idx]] = value; 198 | data.params.push(value); 199 | }, this); 200 | 201 | return data; 202 | }; 203 | 204 | PathPattern.routePathToRegexp = function(path) { 205 | path = path 206 | .replace(escapeRegExp, '\\$&') 207 | .replace(pathParam, '([^/]+)') 208 | .replace(splatParam, '(.*)?') 209 | .replace(subPath, '.*?') 210 | .replace(/\/?$/, '/?'); 211 | return new RegExp('^/?' + path + '$'); 212 | }; 213 | 214 | // This compiles a route string into a set of options which a new PathPattern is created with 215 | PathPattern.fromString = function(routeString) { 216 | // Whatever comes after ? and # is ignored 217 | routeString = routeString.split(/\?|#/)[0]; 218 | 219 | // Create the options object 220 | // Keep the original routeString and a create a regexp for the pathname part of the url 221 | var options = { 222 | routeString: routeString, 223 | regexp: PathPattern.routePathToRegexp(routeString), 224 | params: (routeString.match(namedParams) || []).map(function(param) { 225 | return param.substring(1); 226 | }) 227 | }; 228 | 229 | // Options object are created, now instantiate the PathPattern 230 | return new PathPattern(options); 231 | }; 232 | 233 | return PathPattern; 234 | })(); 235 | 236 | // # RegExpPattern 237 | // The RegExpPattern is just a simple wrapper around a regex, used to provide a similar api as the other route patterns 238 | var RegExpPattern = (function() { 239 | // The RegExpPattern constructor 240 | // Wraps a regexp and provides a *Pattern api for it 241 | function RegExpPattern(regex) { 242 | this.regex = regex; 243 | } 244 | 245 | RegExpPattern.prototype.matches = function(loc) { 246 | return this.regex.test(loc); 247 | }; 248 | 249 | // Extracts all matched parameters 250 | RegExpPattern.prototype.match = function(location) { 251 | if (!this.matches(location)) return null; 252 | 253 | var loc = splitLocation(location); 254 | 255 | return { 256 | params: this.regex.exec(location).slice(1), 257 | queryParams: querystring.parse(loc.queryString), 258 | namedParams: {} 259 | }; 260 | }; 261 | 262 | return RegExpPattern; 263 | })(); 264 | 265 | // # RoutePattern 266 | // The RoutePattern combines the PathPattern and the QueryStringPattern so it can represent a full location 267 | // (excluding the scheme + domain part) 268 | // It also allows for having path-like routes in the hash part of the location 269 | // Allows for route strings like: 270 | // /some/:page?param=:param&foo=:foo#:bookmark 271 | // /some/:page?param=:param&foo=:foo#/:section/:bookmark 272 | // 273 | // Todo: maybe allow for parameterization of the kind of route pattern to use for the hash? 274 | // Maybe use the QueryStringPattern for cases like 275 | // /some/:page?param=:param&foo=:foo#?onlyCareAbout=:thisPartOfTheHash&* 276 | // Need to test how browsers handles urls like that 277 | var RoutePattern = (function() { 278 | // The RoutePattern constructor 279 | // Takes a route string or regexp as parameter and provides a set of utility functions for matching against a 280 | // location path 281 | function RoutePattern(options) { 282 | // The route string are compiled to a regexp (if it isn't already) 283 | this.pathPattern = options.pathPattern; 284 | this.queryStringPattern = options.queryStringPattern; 285 | this.hashPattern = options.hashPattern; 286 | 287 | // The original routestring (optional) 288 | this.routeString = options.routeString; 289 | } 290 | 291 | RoutePattern.prototype.matches = function(location) { 292 | // Whatever comes after ? and # is ignored 293 | var loc = splitLocation(location); 294 | 295 | return ( 296 | (!this.pathPattern || this.pathPattern.matches(loc.path)) && 297 | (!this.queryStringPattern || 298 | this.queryStringPattern.matches(loc.queryString)) && 299 | (!this.hashPattern || this.hashPattern.matches(loc.hash)) 300 | ); 301 | }; 302 | 303 | // Extracts all matched parameters 304 | RoutePattern.prototype.match = function(location) { 305 | if (!this.matches(location)) return null; 306 | 307 | // Whatever comes after ? and # is ignored 308 | var loc = splitLocation(location), 309 | match, 310 | pattern; 311 | 312 | var data = { 313 | params: [], 314 | namedParams: {}, 315 | pathParams: {}, 316 | queryParams: querystring.parse(loc.queryString), 317 | namedQueryParams: {}, 318 | hashParams: {} 319 | }; 320 | 321 | var addMatch = function(match) { 322 | data.params = data.params.concat(match.params); 323 | data.namedParams = merge(data.namedParams, match.namedParams); 324 | }; 325 | 326 | if ((pattern = this.pathPattern)) { 327 | match = pattern.match(loc.path); 328 | if (match) addMatch(match); 329 | data.pathParams = match ? match.namedParams : {}; 330 | } 331 | if ((pattern = this.queryStringPattern)) { 332 | match = pattern.match(loc.queryString); 333 | if (match) addMatch(match); 334 | data.namedQueryParams = match ? match.namedQueryParams : {}; 335 | } 336 | if ((pattern = this.hashPattern)) { 337 | match = pattern.match(loc.hash); 338 | if (match) addMatch(match); 339 | data.hashParams = match ? match.namedParams : {}; 340 | } 341 | return data; 342 | }; 343 | 344 | // This compiles a route string into a set of options which a new RoutePattern is created with 345 | RoutePattern.fromString = function(routeString) { 346 | var parts = splitLocation(routeString); 347 | 348 | var matchPath = parts.path; 349 | var matchQueryString = parts.queryString || routeString.indexOf('?') > -1; 350 | var matchHash = parts.hash || routeString.indexOf('#') > -1; 351 | 352 | // Options object are created, now instantiate the RoutePattern 353 | return new RoutePattern({ 354 | pathPattern: matchPath && PathPattern.fromString(parts.path), 355 | queryStringPattern: 356 | matchQueryString && QueryStringPattern.fromString(parts.queryString), 357 | hashPattern: matchHash && PathPattern.fromString(parts.hash), 358 | routeString: routeString 359 | }); 360 | }; 361 | 362 | return RoutePattern; 363 | })(); 364 | 365 | // CommonJS export 366 | module.exports = RoutePattern; 367 | 368 | // Also export the individual pattern classes 369 | RoutePattern.QueryStringPattern = QueryStringPattern; 370 | RoutePattern.PathPattern = PathPattern; 371 | RoutePattern.RegExpPattern = RegExpPattern; 372 | --------------------------------------------------------------------------------