├── test ├── index.js └── index.html ├── cjs ├── package.json └── index.js ├── .gitignore ├── .npmignore ├── rollup ├── new.config.js └── babel.config.js ├── LICENSE ├── package.json ├── README.md ├── esm └── index.js ├── new.js ├── min.js └── index.js /test/index.js: -------------------------------------------------------------------------------- 1 | require('../cjs'); -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /rollup/new.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import {terser} from 'rollup-plugin-terser'; 4 | 5 | export default { 6 | input: './esm/index.js', 7 | plugins: [ 8 | resolve(), 9 | commonjs(), 10 | terser() 11 | ], 12 | output: { 13 | exports: 'named', 14 | file: './new.js', 15 | format: 'iife', 16 | name: 'ARoute' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | 5 | export default { 6 | input: './esm/index.js', 7 | plugins: [ 8 | resolve(), 9 | commonjs(), 10 | babel({presets: ['@babel/preset-env']}) 11 | ], 12 | output: { 13 | exports: 'named', 14 | file: './index.js', 15 | format: 'iife', 16 | name: 'ARoute' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-route", 3 | "version": "1.1.1", 4 | "description": "Express like routing as Custom Element or standalone", 5 | "main": "cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:new && npm run rollup:babel && npm run min && npm run size", 8 | "cjs": "ascjs esm cjs", 9 | "rollup:new": "rollup --config rollup/new.config.js", 10 | "rollup:babel": "rollup --config rollup/babel.config.js && drop-babel-typeof index.js", 11 | "min": "terser index.js --comments=/^!/ -c -m -o min.js", 12 | "size": "cat index.js | wc -c;cat min.js | wc -c;gzip -c9 min.js | wc -c;gzip -c9 new.js | wc -c" 13 | }, 14 | "keywords": [ 15 | "routing", 16 | "route", 17 | "client" 18 | ], 19 | "author": "Andrea Giammarchi", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "@babel/core": "^7.11.6", 23 | "@babel/preset-env": "^7.11.5", 24 | "ascjs": "^4.0.1", 25 | "drop-babel-typeof": "^1.0.3", 26 | "rollup": "^2.27.0", 27 | "rollup-plugin-babel": "^4.4.0", 28 | "rollup-plugin-commonjs": "^10.1.0", 29 | "rollup-plugin-node-resolve": "^5.2.0", 30 | "rollup-plugin-terser": "^7.0.2", 31 | "terser": "^5.3.1" 32 | }, 33 | "module": "esm/index.js", 34 | "unpkg": "min.js", 35 | "dependencies": { 36 | "path-to-regexp": "^3.1.0" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/WebReflection/a-route.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/WebReflection/a-route/issues" 44 | }, 45 | "homepage": "https://github.com/WebReflection/a-route#readme" 46 | } 47 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 39 | 40 | 41 | test query 42 | test OK 43 | test 404 44 |

45 | 
46 | 
47 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # a-route
 2 | 
 3 | **Social Media Photo by [Jakub Gorajek](https://unsplash.com/@cinegeek) on [Unsplash](https://unsplash.com/)**
 4 | 
 5 | Express like routing, as Custom Element or standalone, inspired by [page.js](https://visionmedia.github.io/page.js/).
 6 | 
 7 | 
 8 | ### app API
 9 | 
10 |   * `app.get(path:string|RegExp, cb:Function[, cb2, ...]):app` to subscribe one or more callbacks for the specified route
11 |   * `app.delete(path:string|RegExp, cb:Function[, cb2, ...]):app` to unsubscribe one or more callbacks for the specified route
12 |   * `app.navigate(path:string[, operation:string = 'push']):void` to navigate to the first matching route for the given path. By default, it pushes to the history but it could `replace`, if the second parameter is the _replace_ string, or `ignore`.
13 |   * `app.param(path:string|RegExp):app` to subscribe to a specific parameter regardless of the route
14 |   * `app.use(path:string|RegExp):app` to subscribe a callback for a specific mount point or all of them
15 | 
16 | 
17 | ### Example
18 | 
19 | The following is a basic example, also [available live](https://webreflection.github.io/a-route/test/?).
20 | 
21 | ```html
22 | 
23 | 
24 | 
25 | test query
26 | 
27 | 
29 | test OK
30 | 
31 | 
32 | test 404
33 | ```
34 | 
35 | ```js
36 | // import {app} from 'a-route';
37 | // const {app} = require('a-route');
38 | const {app} = ARoute;
39 | 
40 | // define routes
41 | app
42 |   .get('/test/?query=:query', function (ctx) {
43 |     console.log(ctx);
44 |     /*
45 |     {
46 |       "path": "/test/?query=value",
47 |       "params": {
48 |         "query": "value"
49 |       }
50 |     }
51 |     */
52 |   })
53 |   .get('/test/:status', function (ctx) {
54 |     console.log(ctx);
55 |     /*
56 |     {
57 |       "path": "/test/OK",
58 |       "params": {
59 |         "status": "OK"
60 |       }
61 |     }
62 |     */
63 |   });
64 | 
65 | // intercept all unregistered calls
66 | app.get('*',
67 |   function (ctx, next) {
68 |     console.log(ctx);
69 |     /*
70 |     {
71 |       "path": "/whatever"
72 |     }
73 |     */
74 |     next();
75 |   },
76 |   // will receive the ctx object too
77 |   console.error
78 | );
79 | ```
80 | 


--------------------------------------------------------------------------------
/esm/index.js:
--------------------------------------------------------------------------------
  1 | import pathToRegexp from 'path-to-regexp';
  2 | 
  3 | const {create, freeze, keys} = Object;
  4 | 
  5 | const Class = customElements.get('a-route');
  6 | 
  7 | const app = Class ? Class.app : freeze({
  8 | 
  9 |   _: freeze({
 10 |     params: create(null),
 11 |     paths: create(null)
 12 |   }),
 13 | 
 14 |   get(path) {
 15 |     for (let
 16 |       {paths} = app._,
 17 |       keys = [],
 18 |       all = path === '*',
 19 |       re = all ? '*' : asPath2RegExp(path, keys),
 20 |       info = paths[re] || (paths[re] = {
 21 |         keys,
 22 |         re,
 23 |         cb: []
 24 |       }),
 25 |       i = 1, {length} = arguments;
 26 |       i < length; i++
 27 |     ) {
 28 |       info.cb.push(arguments[i]);
 29 |     }
 30 |     return app;
 31 |   },
 32 | 
 33 |   delete(path) {
 34 |     for (let
 35 |       all = path === '*',
 36 |       re = all ? '*' : asPath2RegExp(path, []),
 37 |       info = app._.paths[re],
 38 |       i = 1, {length} = arguments;
 39 |       i < length; i++
 40 |     ) {
 41 |       const cb = arguments[i];
 42 |       const index = info ? info.cb.lastIndexOf(cb) : -1;
 43 |       if (-1 < index)
 44 |         info.cb.splice(index, 1);
 45 |     }
 46 |     return app;
 47 |   },
 48 | 
 49 |   navigate(path, operation) {
 50 |     navigate(
 51 |       path,
 52 |       !operation || operation === 'push' ? 1 : (
 53 |         operation === 'replace' ? -1 : 0
 54 |       )
 55 |     );
 56 |   },
 57 | 
 58 |   param(name, cb) {
 59 |     for (let
 60 |       {params} = app._,
 61 |       names = [].concat(name),
 62 |       i = 0, {length} = names;
 63 |       i < length; i++
 64 |     ) {
 65 |       params[names[i]] = cb;
 66 |     }
 67 |     return app;
 68 |   },
 69 | 
 70 |   use(mount, cb) {
 71 |     if (typeof mount === 'function') {
 72 |       cb = mount;
 73 |       mount = '(.*)';
 74 |     }
 75 |     for (let
 76 |       paths = [].concat(mount),
 77 |       i = 0, {length} = paths;
 78 |       i < length; i++
 79 |     ) {
 80 |       app.get(paths[i], cb);
 81 |     }
 82 |     return app;
 83 |   }
 84 | });
 85 | 
 86 | const isModifiedEvent = ({metaKey, altKey, ctrlKey, shiftKey}) =>
 87 |   !!(metaKey || altKey || ctrlKey || shiftKey);
 88 | 
 89 | const ARoute = Class || class ARoute extends HTMLAnchorElement {
 90 |   static get app() { return app; }
 91 |   connectedCallback() { this.addEventListener('click', this); }
 92 |   disconnectedCallback() { this.removeEventListener('click', this); }
 93 |   handleEvent(event) {
 94 |     // Let the browser handle modified click events (ctrl-click etc.)
 95 |     if (isModifiedEvent(event))
 96 |       return;
 97 | 
 98 |     event.preventDefault();
 99 |     if (this.hasAttribute('no-propagation'))
100 |       event.stopPropagation();
101 |     const {pathname, search, hash} = new URL(this.href);
102 |     const path = pathname + search + hash;
103 |     navigate(path, this.hasAttribute('replace') ? -1 : 1);
104 |   }
105 | }
106 | 
107 | if (!Class) {
108 |   customElements.define('a-route', ARoute, {extends: 'a'});
109 |   addEventListener('popstate', function () {
110 |     const {pathname, search, hash} = location;
111 |     navigate(pathname + search + hash, 0);
112 |   });
113 | }
114 | 
115 | export {app, ARoute};
116 | 
117 | function asPath2RegExp(path, keys) {
118 |   if (typeof path !== 'string') {
119 |     path = path.toString();
120 |     path = path.slice(1, path.lastIndexOf('/'));
121 |   }
122 |   return pathToRegexp(path, keys);
123 | }
124 | 
125 | function byKey(key) {
126 |   return key in this;
127 | }
128 | 
129 | function callNext(ctx, cbs) {
130 |   const invoked = [];
131 |   (function next() {
132 |     const cb = cbs.shift();
133 |     if (cb) {
134 |       if (invoked.lastIndexOf(cb) < 0) {
135 |         invoked.push(cb);
136 |         cb(ctx, next);
137 |       } else {
138 |         next();
139 |       }
140 |     }
141 |   }());
142 | }
143 | 
144 | function createParams(match, keys) {
145 |   const params = create(null);
146 |   for (let i = 1, {length} = match; i < length; i++) {
147 |     if (match[i] != null)
148 |       params[keys[i - 1].name] = match[i];
149 |   }
150 |   return params;
151 | }
152 | 
153 | function navigate(path, operation) {
154 |   const {params, paths} = app._;
155 |   if (operation < 0)
156 |     history.replaceState(location.href, document.title, path);
157 |   else if (operation)
158 |     history.pushState(location.href, document.title, path);
159 |   for (let key in paths) {
160 |     if (key === '*')
161 |       continue;
162 |     const info = paths[key];
163 |     const match = info.re.exec(path);
164 |     if (match) {
165 |       const ctx = {
166 |         path,
167 |         params: createParams(match, info.keys)
168 |       };
169 |       const all = keys(ctx.params).filter(byKey, params);
170 |       return (function param() {
171 |         if (all.length) {
172 |           const key = all.shift();
173 |           params[key](ctx, param, ctx.params[key]);
174 |         }
175 |         else
176 |           callNext(ctx, info.cb.slice(0));
177 |       }());
178 |     }
179 |   }
180 |   if ('*' in paths)
181 |     callNext({path}, paths['*'].cb.slice(0));
182 | }
183 | 


--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const pathToRegexp = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('path-to-regexp'));
  3 | 
  4 | const {create, freeze, keys} = Object;
  5 | 
  6 | const Class = customElements.get('a-route');
  7 | 
  8 | const app = Class ? Class.app : freeze({
  9 | 
 10 |   _: freeze({
 11 |     params: create(null),
 12 |     paths: create(null)
 13 |   }),
 14 | 
 15 |   get(path) {
 16 |     for (let
 17 |       {paths} = app._,
 18 |       keys = [],
 19 |       all = path === '*',
 20 |       re = all ? '*' : asPath2RegExp(path, keys),
 21 |       info = paths[re] || (paths[re] = {
 22 |         keys,
 23 |         re,
 24 |         cb: []
 25 |       }),
 26 |       i = 1, {length} = arguments;
 27 |       i < length; i++
 28 |     ) {
 29 |       info.cb.push(arguments[i]);
 30 |     }
 31 |     return app;
 32 |   },
 33 | 
 34 |   delete(path) {
 35 |     for (let
 36 |       all = path === '*',
 37 |       re = all ? '*' : asPath2RegExp(path, []),
 38 |       info = app._.paths[re],
 39 |       i = 1, {length} = arguments;
 40 |       i < length; i++
 41 |     ) {
 42 |       const cb = arguments[i];
 43 |       const index = info ? info.cb.lastIndexOf(cb) : -1;
 44 |       if (-1 < index)
 45 |         info.cb.splice(index, 1);
 46 |     }
 47 |     return app;
 48 |   },
 49 | 
 50 |   navigate(path, operation) {
 51 |     navigate(
 52 |       path,
 53 |       !operation || operation === 'push' ? 1 : (
 54 |         operation === 'replace' ? -1 : 0
 55 |       )
 56 |     );
 57 |   },
 58 | 
 59 |   param(name, cb) {
 60 |     for (let
 61 |       {params} = app._,
 62 |       names = [].concat(name),
 63 |       i = 0, {length} = names;
 64 |       i < length; i++
 65 |     ) {
 66 |       params[names[i]] = cb;
 67 |     }
 68 |     return app;
 69 |   },
 70 | 
 71 |   use(mount, cb) {
 72 |     if (typeof mount === 'function') {
 73 |       cb = mount;
 74 |       mount = '(.*)';
 75 |     }
 76 |     for (let
 77 |       paths = [].concat(mount),
 78 |       i = 0, {length} = paths;
 79 |       i < length; i++
 80 |     ) {
 81 |       app.get(paths[i], cb);
 82 |     }
 83 |     return app;
 84 |   }
 85 | });
 86 | 
 87 | const isModifiedEvent = ({metaKey, altKey, ctrlKey, shiftKey}) =>
 88 |   !!(metaKey || altKey || ctrlKey || shiftKey);
 89 | 
 90 | const ARoute = Class || class ARoute extends HTMLAnchorElement {
 91 |   static get app() { return app; }
 92 |   connectedCallback() { this.addEventListener('click', this); }
 93 |   disconnectedCallback() { this.removeEventListener('click', this); }
 94 |   handleEvent(event) {
 95 |     // Let the browser handle modified click events (ctrl-click etc.)
 96 |     if (isModifiedEvent(event))
 97 |       return;
 98 | 
 99 |     event.preventDefault();
100 |     if (this.hasAttribute('no-propagation'))
101 |       event.stopPropagation();
102 |     const {pathname, search, hash} = new URL(this.href);
103 |     const path = pathname + search + hash;
104 |     navigate(path, this.hasAttribute('replace') ? -1 : 1);
105 |   }
106 | }
107 | 
108 | if (!Class) {
109 |   customElements.define('a-route', ARoute, {extends: 'a'});
110 |   addEventListener('popstate', function () {
111 |     const {pathname, search, hash} = location;
112 |     navigate(pathname + search + hash, 0);
113 |   });
114 | }
115 | 
116 | exports.app = app;
117 | exports.ARoute = ARoute;
118 | 
119 | function asPath2RegExp(path, keys) {
120 |   if (typeof path !== 'string') {
121 |     path = path.toString();
122 |     path = path.slice(1, path.lastIndexOf('/'));
123 |   }
124 |   return pathToRegexp(path, keys);
125 | }
126 | 
127 | function byKey(key) {
128 |   return key in this;
129 | }
130 | 
131 | function callNext(ctx, cbs) {
132 |   const invoked = [];
133 |   (function next() {
134 |     const cb = cbs.shift();
135 |     if (cb) {
136 |       if (invoked.lastIndexOf(cb) < 0) {
137 |         invoked.push(cb);
138 |         cb(ctx, next);
139 |       } else {
140 |         next();
141 |       }
142 |     }
143 |   }());
144 | }
145 | 
146 | function createParams(match, keys) {
147 |   const params = create(null);
148 |   for (let i = 1, {length} = match; i < length; i++) {
149 |     if (match[i] != null)
150 |       params[keys[i - 1].name] = match[i];
151 |   }
152 |   return params;
153 | }
154 | 
155 | function navigate(path, operation) {
156 |   const {params, paths} = app._;
157 |   if (operation < 0)
158 |     history.replaceState(location.href, document.title, path);
159 |   else if (operation)
160 |     history.pushState(location.href, document.title, path);
161 |   for (let key in paths) {
162 |     if (key === '*')
163 |       continue;
164 |     const info = paths[key];
165 |     const match = info.re.exec(path);
166 |     if (match) {
167 |       const ctx = {
168 |         path,
169 |         params: createParams(match, info.keys)
170 |       };
171 |       const all = keys(ctx.params).filter(byKey, params);
172 |       return (function param() {
173 |         if (all.length) {
174 |           const key = all.shift();
175 |           params[key](ctx, param, ctx.params[key]);
176 |         }
177 |         else
178 |           callNext(ctx, info.cb.slice(0));
179 |       }());
180 |     }
181 |   }
182 |   if ('*' in paths)
183 |     callNext({path}, paths['*'].cb.slice(0));
184 | }
185 | 


--------------------------------------------------------------------------------
/new.js:
--------------------------------------------------------------------------------
1 | var ARoute=function(e){"use strict";var t=d,n=function(e,t){var n=[];return l(d(e,n,t),n)},r=l,a=p,o=function(e,t){return u(p(e,t),t)},i=u,c=m,s=new RegExp(["(\\\\.)","(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?"].join("|"),"g");function p(e,t){for(var n,r=[],a=0,o=0,i="",c=t&&t.delimiter||"/",p=t&&t.whitelist||void 0,l=!1;null!==(n=s.exec(e));){var u=n[0],g=n[1],m=n.index;if(i+=e.slice(o,m),o=m+u.length,g)i+=g[1],l=!0;else{var d="",v=n[2],x=n[3],y=n[4],E=n[5];if(!l&&i.length){var b=i.length-1,w=i[b];(!p||p.indexOf(w)>-1)&&(d=w,i=i.slice(0,b))}i&&(r.push(i),i="",l=!1);var R="+"===E||"*"===E,A="?"===E||"*"===E,k=x||y,T=d||c;r.push({name:v||a++,prefix:d,delimiter:T,optional:A,repeat:R,pattern:k?h(k):"[^"+f(T===c?T:T+c)+"]+?"})}}return(i||o!!(e||t||n||r))(e))return;e.preventDefault(),this.hasAttribute("no-propagation")&&e.stopPropagation();const{pathname:t,search:n,hash:r}=new URL(this.href);$(t+n+r,this.hasAttribute("replace")?-1:1)}};function R(e,n){return"string"!=typeof e&&(e=(e=e.toString()).slice(1,e.lastIndexOf("/"))),t(e,n)}function A(e){return e in this}function k(e,t){const n=[];!function r(){const a=t.shift();a&&(n.lastIndexOf(a)<0?(n.push(a),a(e,r)):r())}()}function T(e,t){const n=v(null);for(let r=1,{length:a}=e;r-1)&&(h=E,a=a.slice(0,b))}a&&(r.push(a),a="",f=!1);var O="+"===m||"*"===m,R="?"===m||"*"===m,j=y||d,k=h||u;r.push({name:v||o++,prefix:h,delimiter:k,optional:R,repeat:O,pattern:j?w(j):"[^"+x(k===u?k:k+u)+"]+?"})}}return(a||i ["test", "\d+", undefined, "?"]
189 |   // "(\\d+)"  => [undefined, undefined, "\d+", undefined]
190 |   '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'].join('|'), 'g');
191 |   /**
192 |    * Parse a string for the raw tokens.
193 |    *
194 |    * @param  {string}  str
195 |    * @param  {Object=} options
196 |    * @return {!Array}
197 |    */
198 | 
199 |   function parse(str, options) {
200 |     var tokens = [];
201 |     var key = 0;
202 |     var index = 0;
203 |     var path = '';
204 |     var defaultDelimiter = options && options.delimiter || DEFAULT_DELIMITER;
205 |     var whitelist = options && options.whitelist || undefined;
206 |     var pathEscaped = false;
207 |     var res;
208 | 
209 |     while ((res = PATH_REGEXP.exec(str)) !== null) {
210 |       var m = res[0];
211 |       var escaped = res[1];
212 |       var offset = res.index;
213 |       path += str.slice(index, offset);
214 |       index = offset + m.length; // Ignore already escaped sequences.
215 | 
216 |       if (escaped) {
217 |         path += escaped[1];
218 |         pathEscaped = true;
219 |         continue;
220 |       }
221 | 
222 |       var prev = '';
223 |       var name = res[2];
224 |       var capture = res[3];
225 |       var group = res[4];
226 |       var modifier = res[5];
227 | 
228 |       if (!pathEscaped && path.length) {
229 |         var k = path.length - 1;
230 |         var c = path[k];
231 |         var matches = whitelist ? whitelist.indexOf(c) > -1 : true;
232 | 
233 |         if (matches) {
234 |           prev = c;
235 |           path = path.slice(0, k);
236 |         }
237 |       } // Push the current path onto the tokens.
238 | 
239 | 
240 |       if (path) {
241 |         tokens.push(path);
242 |         path = '';
243 |         pathEscaped = false;
244 |       }
245 | 
246 |       var repeat = modifier === '+' || modifier === '*';
247 |       var optional = modifier === '?' || modifier === '*';
248 |       var pattern = capture || group;
249 |       var delimiter = prev || defaultDelimiter;
250 |       tokens.push({
251 |         name: name || key++,
252 |         prefix: prev,
253 |         delimiter: delimiter,
254 |         optional: optional,
255 |         repeat: repeat,
256 |         pattern: pattern ? escapeGroup(pattern) : '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : delimiter + defaultDelimiter) + ']+?'
257 |       });
258 |     } // Push any remaining characters.
259 | 
260 | 
261 |     if (path || index < str.length) {
262 |       tokens.push(path + str.substr(index));
263 |     }
264 | 
265 |     return tokens;
266 |   }
267 |   /**
268 |    * Compile a string to a template function for the path.
269 |    *
270 |    * @param  {string}             str
271 |    * @param  {Object=}            options
272 |    * @return {!function(Object=, Object=)}
273 |    */
274 | 
275 | 
276 |   function compile(str, options) {
277 |     return tokensToFunction(parse(str, options), options);
278 |   }
279 |   /**
280 |    * Create path match function from `path-to-regexp` spec.
281 |    */
282 | 
283 | 
284 |   function match(str, options) {
285 |     var keys = [];
286 |     var re = pathToRegexp(str, keys, options);
287 |     return regexpToFunction(re, keys);
288 |   }
289 |   /**
290 |    * Create a path match function from `path-to-regexp` output.
291 |    */
292 | 
293 | 
294 |   function regexpToFunction(re, keys) {
295 |     return function (pathname, options) {
296 |       var m = re.exec(pathname);
297 |       if (!m) return false;
298 |       var path = m[0];
299 |       var index = m.index;
300 |       var params = {};
301 |       var decode = options && options.decode || decodeURIComponent;
302 | 
303 |       for (var i = 1; i < m.length; i++) {
304 |         if (m[i] === undefined) continue;
305 |         var key = keys[i - 1];
306 | 
307 |         if (key.repeat) {
308 |           params[key.name] = m[i].split(key.delimiter).map(function (value) {
309 |             return decode(value, key);
310 |           });
311 |         } else {
312 |           params[key.name] = decode(m[i], key);
313 |         }
314 |       }
315 | 
316 |       return {
317 |         path: path,
318 |         index: index,
319 |         params: params
320 |       };
321 |     };
322 |   }
323 |   /**
324 |    * Expose a method for transforming tokens into the path function.
325 |    */
326 | 
327 | 
328 |   function tokensToFunction(tokens, options) {
329 |     // Compile all the tokens into regexps.
330 |     var matches = new Array(tokens.length); // Compile all the patterns before compilation.
331 | 
332 |     for (var i = 0; i < tokens.length; i++) {
333 |       if (typeof(tokens[i]) === 'object') {
334 |         matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options));
335 |       }
336 |     }
337 | 
338 |     return function (data, options) {
339 |       var path = '';
340 |       var encode = options && options.encode || encodeURIComponent;
341 |       var validate = options ? options.validate !== false : true;
342 | 
343 |       for (var i = 0; i < tokens.length; i++) {
344 |         var token = tokens[i];
345 | 
346 |         if (typeof token === 'string') {
347 |           path += token;
348 |           continue;
349 |         }
350 | 
351 |         var value = data ? data[token.name] : undefined;
352 |         var segment;
353 | 
354 |         if (Array.isArray(value)) {
355 |           if (!token.repeat) {
356 |             throw new TypeError('Expected "' + token.name + '" to not repeat, but got array');
357 |           }
358 | 
359 |           if (value.length === 0) {
360 |             if (token.optional) continue;
361 |             throw new TypeError('Expected "' + token.name + '" to not be empty');
362 |           }
363 | 
364 |           for (var j = 0; j < value.length; j++) {
365 |             segment = encode(value[j], token);
366 | 
367 |             if (validate && !matches[i].test(segment)) {
368 |               throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"');
369 |             }
370 | 
371 |             path += (j === 0 ? token.prefix : token.delimiter) + segment;
372 |           }
373 | 
374 |           continue;
375 |         }
376 | 
377 |         if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
378 |           segment = encode(String(value), token);
379 | 
380 |           if (validate && !matches[i].test(segment)) {
381 |             throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"');
382 |           }
383 | 
384 |           path += token.prefix + segment;
385 |           continue;
386 |         }
387 | 
388 |         if (token.optional) continue;
389 |         throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string'));
390 |       }
391 | 
392 |       return path;
393 |     };
394 |   }
395 |   /**
396 |    * Escape a regular expression string.
397 |    *
398 |    * @param  {string} str
399 |    * @return {string}
400 |    */
401 | 
402 | 
403 |   function escapeString(str) {
404 |     return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
405 |   }
406 |   /**
407 |    * Escape the capturing group by escaping special characters and meaning.
408 |    *
409 |    * @param  {string} group
410 |    * @return {string}
411 |    */
412 | 
413 | 
414 |   function escapeGroup(group) {
415 |     return group.replace(/([=!:$/()])/g, '\\$1');
416 |   }
417 |   /**
418 |    * Get the flags for a regexp from the options.
419 |    *
420 |    * @param  {Object} options
421 |    * @return {string}
422 |    */
423 | 
424 | 
425 |   function flags(options) {
426 |     return options && options.sensitive ? '' : 'i';
427 |   }
428 |   /**
429 |    * Pull out keys from a regexp.
430 |    *
431 |    * @param  {!RegExp} path
432 |    * @param  {Array=}  keys
433 |    * @return {!RegExp}
434 |    */
435 | 
436 | 
437 |   function regexpToRegexp(path, keys) {
438 |     if (!keys) return path; // Use a negative lookahead to match only capturing groups.
439 | 
440 |     var groups = path.source.match(/\((?!\?)/g);
441 | 
442 |     if (groups) {
443 |       for (var i = 0; i < groups.length; i++) {
444 |         keys.push({
445 |           name: i,
446 |           prefix: null,
447 |           delimiter: null,
448 |           optional: false,
449 |           repeat: false,
450 |           pattern: null
451 |         });
452 |       }
453 |     }
454 | 
455 |     return path;
456 |   }
457 |   /**
458 |    * Transform an array into a regexp.
459 |    *
460 |    * @param  {!Array}  path
461 |    * @param  {Array=}  keys
462 |    * @param  {Object=} options
463 |    * @return {!RegExp}
464 |    */
465 | 
466 | 
467 |   function arrayToRegexp(path, keys, options) {
468 |     var parts = [];
469 | 
470 |     for (var i = 0; i < path.length; i++) {
471 |       parts.push(pathToRegexp(path[i], keys, options).source);
472 |     }
473 | 
474 |     return new RegExp('(?:' + parts.join('|') + ')', flags(options));
475 |   }
476 |   /**
477 |    * Create a path regexp from string input.
478 |    *
479 |    * @param  {string}  path
480 |    * @param  {Array=}  keys
481 |    * @param  {Object=} options
482 |    * @return {!RegExp}
483 |    */
484 | 
485 | 
486 |   function stringToRegexp(path, keys, options) {
487 |     return tokensToRegExp(parse(path, options), keys, options);
488 |   }
489 |   /**
490 |    * Expose a function for taking tokens and returning a RegExp.
491 |    *
492 |    * @param  {!Array}  tokens
493 |    * @param  {Array=}  keys
494 |    * @param  {Object=} options
495 |    * @return {!RegExp}
496 |    */
497 | 
498 | 
499 |   function tokensToRegExp(tokens, keys, options) {
500 |     options = options || {};
501 |     var strict = options.strict;
502 |     var start = options.start !== false;
503 |     var end = options.end !== false;
504 |     var delimiter = options.delimiter || DEFAULT_DELIMITER;
505 |     var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|');
506 |     var route = start ? '^' : ''; // Iterate over the tokens and create our regexp string.
507 | 
508 |     for (var i = 0; i < tokens.length; i++) {
509 |       var token = tokens[i];
510 | 
511 |       if (typeof token === 'string') {
512 |         route += escapeString(token);
513 |       } else {
514 |         var capture = token.repeat ? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*' : token.pattern;
515 |         if (keys) keys.push(token);
516 | 
517 |         if (token.optional) {
518 |           if (!token.prefix) {
519 |             route += '(' + capture + ')?';
520 |           } else {
521 |             route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?';
522 |           }
523 |         } else {
524 |           route += escapeString(token.prefix) + '(' + capture + ')';
525 |         }
526 |       }
527 |     }
528 | 
529 |     if (end) {
530 |       if (!strict) route += '(?:' + escapeString(delimiter) + ')?';
531 |       route += endsWith === '$' ? '$' : '(?=' + endsWith + ')';
532 |     } else {
533 |       var endToken = tokens[tokens.length - 1];
534 |       var isEndDelimited = typeof endToken === 'string' ? endToken[endToken.length - 1] === delimiter : endToken === undefined;
535 |       if (!strict) route += '(?:' + escapeString(delimiter) + '(?=' + endsWith + '))?';
536 |       if (!isEndDelimited) route += '(?=' + escapeString(delimiter) + '|' + endsWith + ')';
537 |     }
538 | 
539 |     return new RegExp(route, flags(options));
540 |   }
541 |   /**
542 |    * Normalize the given path string, returning a regular expression.
543 |    *
544 |    * An empty array can be passed in for the keys, which will hold the
545 |    * placeholder key descriptions. For example, using `/user/:id`, `keys` will
546 |    * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
547 |    *
548 |    * @param  {(string|RegExp|Array)} path
549 |    * @param  {Array=}                keys
550 |    * @param  {Object=}               options
551 |    * @return {!RegExp}
552 |    */
553 | 
554 | 
555 |   function pathToRegexp(path, keys, options) {
556 |     if (path instanceof RegExp) {
557 |       return regexpToRegexp(path, keys);
558 |     }
559 | 
560 |     if (Array.isArray(path)) {
561 |       return arrayToRegexp(
562 |       /** @type {!Array} */
563 |       path, keys, options);
564 |     }
565 | 
566 |     return stringToRegexp(
567 |     /** @type {string} */
568 |     path, keys, options);
569 |   }
570 |   pathToRegexp_1.match = match_1;
571 |   pathToRegexp_1.regexpToFunction = regexpToFunction_1;
572 |   pathToRegexp_1.parse = parse_1;
573 |   pathToRegexp_1.compile = compile_1;
574 |   pathToRegexp_1.tokensToFunction = tokensToFunction_1;
575 |   pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;
576 | 
577 |   var create = Object.create,
578 |       freeze = Object.freeze,
579 |       keys = Object.keys;
580 |   var Class = customElements.get('a-route');
581 |   var app = Class ? Class.app : freeze({
582 |     _: freeze({
583 |       params: create(null),
584 |       paths: create(null)
585 |     }),
586 |     get: function get(path) {
587 |       for (var paths = app._.paths, _keys = [], all = path === '*', re = all ? '*' : asPath2RegExp(path, _keys), info = paths[re] || (paths[re] = {
588 |         keys: _keys,
589 |         re: re,
590 |         cb: []
591 |       }), i = 1, length = arguments.length; i < length; i++) {
592 |         info.cb.push(arguments[i]);
593 |       }
594 | 
595 |       return app;
596 |     },
597 |     "delete": function _delete(path) {
598 |       for (var all = path === '*', re = all ? '*' : asPath2RegExp(path, []), info = app._.paths[re], i = 1, length = arguments.length; i < length; i++) {
599 |         var cb = arguments[i];
600 |         var index = info ? info.cb.lastIndexOf(cb) : -1;
601 |         if (-1 < index) info.cb.splice(index, 1);
602 |       }
603 | 
604 |       return app;
605 |     },
606 |     navigate: function navigate(path, operation) {
607 |       _navigate(path, !operation || operation === 'push' ? 1 : operation === 'replace' ? -1 : 0);
608 |     },
609 |     param: function param(name, cb) {
610 |       for (var params = app._.params, names = [].concat(name), i = 0, length = names.length; i < length; i++) {
611 |         params[names[i]] = cb;
612 |       }
613 | 
614 |       return app;
615 |     },
616 |     use: function use(mount, cb) {
617 |       if (typeof mount === 'function') {
618 |         cb = mount;
619 |         mount = '(.*)';
620 |       }
621 | 
622 |       for (var paths = [].concat(mount), i = 0, length = paths.length; i < length; i++) {
623 |         app.get(paths[i], cb);
624 |       }
625 | 
626 |       return app;
627 |     }
628 |   });
629 | 
630 |   var isModifiedEvent = function isModifiedEvent(_ref) {
631 |     var metaKey = _ref.metaKey,
632 |         altKey = _ref.altKey,
633 |         ctrlKey = _ref.ctrlKey,
634 |         shiftKey = _ref.shiftKey;
635 |     return !!(metaKey || altKey || ctrlKey || shiftKey);
636 |   };
637 | 
638 |   var ARoute = Class || /*#__PURE__*/function (_HTMLAnchorElement) {
639 |     _inherits(ARoute, _HTMLAnchorElement);
640 | 
641 |     var _super = _createSuper(ARoute);
642 | 
643 |     function ARoute() {
644 |       _classCallCheck(this, ARoute);
645 | 
646 |       return _super.apply(this, arguments);
647 |     }
648 | 
649 |     _createClass(ARoute, [{
650 |       key: "connectedCallback",
651 |       value: function connectedCallback() {
652 |         this.addEventListener('click', this);
653 |       }
654 |     }, {
655 |       key: "disconnectedCallback",
656 |       value: function disconnectedCallback() {
657 |         this.removeEventListener('click', this);
658 |       }
659 |     }, {
660 |       key: "handleEvent",
661 |       value: function handleEvent(event) {
662 |         // Let the browser handle modified click events (ctrl-click etc.)
663 |         if (isModifiedEvent(event)) return;
664 |         event.preventDefault();
665 |         if (this.hasAttribute('no-propagation')) event.stopPropagation();
666 | 
667 |         var _URL = new URL(this.href),
668 |             pathname = _URL.pathname,
669 |             search = _URL.search,
670 |             hash = _URL.hash;
671 | 
672 |         var path = pathname + search + hash;
673 | 
674 |         _navigate(path, this.hasAttribute('replace') ? -1 : 1);
675 |       }
676 |     }], [{
677 |       key: "app",
678 |       get: function get() {
679 |         return app;
680 |       }
681 |     }]);
682 | 
683 |     return ARoute;
684 |   }( /*#__PURE__*/_wrapNativeSuper(HTMLAnchorElement));
685 | 
686 |   if (!Class) {
687 |     customElements.define('a-route', ARoute, {
688 |       "extends": 'a'
689 |     });
690 |     addEventListener('popstate', function () {
691 |       var _location = location,
692 |           pathname = _location.pathname,
693 |           search = _location.search,
694 |           hash = _location.hash;
695 | 
696 |       _navigate(pathname + search + hash, 0);
697 |     });
698 |   }
699 | 
700 |   function asPath2RegExp(path, keys) {
701 |     if (typeof path !== 'string') {
702 |       path = path.toString();
703 |       path = path.slice(1, path.lastIndexOf('/'));
704 |     }
705 | 
706 |     return pathToRegexp_1(path, keys);
707 |   }
708 | 
709 |   function byKey(key) {
710 |     return key in this;
711 |   }
712 | 
713 |   function callNext(ctx, cbs) {
714 |     var invoked = [];
715 | 
716 |     (function next() {
717 |       var cb = cbs.shift();
718 | 
719 |       if (cb) {
720 |         if (invoked.lastIndexOf(cb) < 0) {
721 |           invoked.push(cb);
722 |           cb(ctx, next);
723 |         } else {
724 |           next();
725 |         }
726 |       }
727 |     })();
728 |   }
729 | 
730 |   function createParams(match, keys) {
731 |     var params = create(null);
732 | 
733 |     for (var i = 1, length = match.length; i < length; i++) {
734 |       if (match[i] != null) params[keys[i - 1].name] = match[i];
735 |     }
736 | 
737 |     return params;
738 |   }
739 | 
740 |   function _navigate(path, operation) {
741 |     var _app$_ = app._,
742 |         params = _app$_.params,
743 |         paths = _app$_.paths;
744 |     if (operation < 0) history.replaceState(location.href, document.title, path);else if (operation) history.pushState(location.href, document.title, path);
745 | 
746 |     var _loop = function _loop(key) {
747 |       if (key === '*') return "continue";
748 |       var info = paths[key];
749 |       var match = info.re.exec(path);
750 | 
751 |       if (match) {
752 |         var ctx = {
753 |           path: path,
754 |           params: createParams(match, info.keys)
755 |         };
756 |         var all = keys(ctx.params).filter(byKey, params);
757 |         return {
758 |           v: function param() {
759 |             if (all.length) {
760 |               var _key = all.shift();
761 | 
762 |               params[_key](ctx, param, ctx.params[_key]);
763 |             } else callNext(ctx, info.cb.slice(0));
764 |           }()
765 |         };
766 |       }
767 |     };
768 | 
769 |     for (var key in paths) {
770 |       var _ret = _loop(key);
771 | 
772 |       if (_ret === "continue") continue;
773 |       if (typeof(_ret) === "object") return _ret.v;
774 |     }
775 | 
776 |     if ('*' in paths) callNext({
777 |       path: path
778 |     }, paths['*'].cb.slice(0));
779 |   }
780 | 
781 |   exports.ARoute = ARoute;
782 |   exports.app = app;
783 | 
784 |   return exports;
785 | 
786 | }({}));
787 | 


--------------------------------------------------------------------------------