├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── docs
├── GUIDE.md
├── README.md
├── router-dom.md
└── router.md
├── examples
├── dom
│ ├── about
│ │ └── index.html
│ ├── contact
│ │ └── index.html
│ ├── index.html
│ ├── index.js
│ └── style.css
└── simple
│ └── index.html
├── index.js
├── lib
├── lemonade-router-dom.umd.js
└── lemonade-router.umd.js
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── DefaultTransition.js
├── DefaultTransitionDOM.js
├── Router.js
├── RouterDOM.js
└── helpers.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /examples
2 | /docs
3 | package-lock.json
4 | rollup.config.js
5 | node_modules
6 | .npmignore
7 | .gitignore
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Raphaël Améaume
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lemonade-router
2 |
3 | `lemonade-router` is a minimal non-opiniated routing library to create websites and interactive experiences with custom transitions. It is heavily inspired by [Barba.js](https://github.com/barbajs/barba) and [React Router](https://github.com/ReactTraining/react-router) and uses [history](https://npmjs.com/package/history) under the hood.
4 |
5 | It is built using ES6 features.
6 |
7 | - [Documentation](https://github.com/raphaelameaume/lemonade-router/tree/master/docs/README.md)
8 | - [Examples](https://github.com/raphaelameaume/lemonade-router/tree/master/demo)
9 |
10 | ## Installation
11 |
12 | `npm install lemonade-router`
13 |
14 | ### Usage
15 |
16 | ```js
17 | import { Router } from "lemonade-router";
18 |
19 | let router = Router();
20 |
21 | // views
22 | router.view('/', () => Home());
23 | router.view('/news', () => News());
24 |
25 | // matches
26 | router.match('/news/:id', async ({ params }) => {
27 | let News = await import('./News.js');
28 |
29 | router.view(`/news/${params.id}`, () => News(params.id));
30 | });
31 |
32 | router.match('*', async () => {
33 | let NotFound = await import('./NotFound.js');
34 |
35 | router.view('*', () => NotFound());
36 | });
37 |
38 | // transitions
39 | router.transition('/', '/news', () => FromHomeToNews());
40 |
41 | router.listen();
42 |
43 | ```
44 |
45 | ## Motivation
46 | This routing library attempts to solve different problems I had the past few years working on websites and interactives experiences:
47 | - Change URL without fetching an existing page, useful for WebGL experiences or when all the DOM is already here
48 | - Create custom transitions where I have total control over DOM changes
49 | - Define complex loading sequences
50 | - Allow multiple pages to work on different URLs (multilingual websites)
51 | - Split code to avoid loading big bundles
52 |
53 | ## Credits
54 | - [Barba.js](https://github.com/barbajs/barba)
55 | - [React Router](https://github.com/ReactTraining/react-router)
56 | - [Canvas-sketch](https://github.com/mattdesl/canvas-sketch)
57 |
58 | ## License
59 |
60 | MIT License, see [LICENSE](https://github.com/raphaelameaume/lemonade-router/tree/master/LICENSE) for details
--------------------------------------------------------------------------------
/docs/GUIDE.md:
--------------------------------------------------------------------------------
1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → Guide
2 |
3 | ---
4 |
5 | - [Register a view](#register-a-view)
6 | - [Register a transition](#register-a-transition)
7 | - [Handle URL parameters](#handle-url-parameters)
8 | - [Code splitting](#code-splitting)
9 |
10 | ## Register a view
11 | ```js
12 | /* with functions */
13 | function Home() {
14 | function enter() {
15 | console.log('Home :: enter');
16 | }
17 |
18 | function leave() {
19 | console.log('Home :: enter');
20 | }
21 |
22 | return { enter, leave };
23 | }
24 |
25 | router.view('/', () => Home());
26 |
27 | /* with classes */
28 | class Home {
29 | constructor() {}
30 | enter() {}
31 | leave() {}
32 | }
33 |
34 | router.view('/' , () => return new Home());
35 | ```
36 |
37 | ## Register a transition
38 | ```js
39 | /* with functions*/
40 | function CustomTransition() {
41 | return {
42 | play: (prevView, nextView) {
43 | // do sync or async stuff
44 | if (prevView) { // prevView is null on start
45 | prevView.leave(nextView)
46 | }
47 |
48 | nextView.enter(prevView);
49 | }
50 | }
51 | }
52 |
53 | router.transition('/about', '/news', () => CustomTransition(), false);
54 |
55 | /* with classes */
56 | class CustomTransition() {
57 | constructor() {}
58 | play(prevView, nextView) {}
59 | }
60 |
61 | router.transition('/news', '/', () => new CustomTransition());
62 | ```
63 |
64 | ## Code Splitting
65 | ```js
66 | router.match('/about', async ({ url }) => {
67 | let About = await import('./About.js'); // dynamic import
68 |
69 | router.view(url, () => About());
70 | })
71 | ```
72 |
73 | ## Handle URL parameters
74 | `router.match` supports async functions and return URL parameters as an object
75 | ```js
76 | router.match('/news/:slug', async ({ url, params }) => {
77 | let News = await import('./News.js');
78 |
79 | router.view(url, () => News(params.slug));
80 | })
81 | ```
82 |
83 | ## [RouterDOM] Use a custom fetch function
84 | By default, RouterDOM fetch the next page using the `fetch` API. In some cases you'll want to provide your own fetch function. To do so, you can overwrite the fetch function on RouterDOM. Be careful as Router.fetch expect a function returning a `Promise` in order to work.
85 |
86 | ```js
87 | import { RouterDOM } from "lemonade-router";
88 |
89 | RouterDOM.fetch = (url) => {
90 | return new Promise((resolve, reject) => {
91 | let xhr = new XMLHttpRequest();
92 | xhr.addEventListener("load", () => {
93 | resolve(xhr.responseText);
94 | });
95 | xhr.open("GET", url);
96 | xhr.send();
97 | });
98 | };
99 | ```
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | #### [lemonade-router](../README.md) → [Documentation](./README.md)
2 |
3 | ---
4 |
5 | API docs
6 | - [`Router` documentation](./router.md)
7 | - [`RouterDOM` documentation](./router-dom.md)
8 |
9 | Guide
10 | - [Register a view](./GUIDE.md#register-a-view)
11 | - [Register a transition](./GUIDE.md#register-a-transition)
12 | - [Handle URL parameters](./GUIDE.md#handle-url-parameters)
13 | - [Code splitting](./GUIDE.md#code-splitting)
14 | - [Use a custom fetch function](./GUIDE.md#use-a-custom-fetch-function)
--------------------------------------------------------------------------------
/docs/router-dom.md:
--------------------------------------------------------------------------------
1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → RouterDOM
2 |
3 | ---
4 |
5 | ## RouterDOM
6 |
7 | ### router = RouterDOM([options])
8 | - `options.wrapperQuery`: A function used to query the main wrapper. Default to `() => querySelector('.lemonade-wrapper`)`.
9 | - `options.containerQuery`: A function used to query the containers. Default to `(wrapper) => wrapper.querySelector('.lemonade-container`)`.
10 | - `options.cacheEnabled`: A boolean defining if fetched pages should be cached. Default to `true`.
11 | Check [`Router` documentation](./router.md) for other available options.
12 |
13 | ### router.listen([options])
14 | Check [`Router` documentation](./router.md) for available options.
15 |
16 | ### router.view(url, fn)
17 | See [`Router` documentation](./router.md#routerviewurl-fn))
18 |
19 | ### router.transition(from, to, fn, backAndForth)
20 | See [`Router` documentation](./router.md#routertransitionfrom-to-fn-backandforth)
21 |
22 | ### router.match(pattern, fn)
23 | See [`Router` documentation](./router.md#routermatchpattern-fn)
--------------------------------------------------------------------------------
/docs/router.md:
--------------------------------------------------------------------------------
1 | #### [lemonade-router](../README.md) → [Documentation](./README.md) → Router
2 |
3 | ---
4 |
5 | ## Router
6 |
7 | ### router = Router([options])
8 | - `options.defaultTransition`: An object or a class implementing a `play(prevView, nextView, transitionParams)` method. Default to [DefaultTransition]
9 | - `options.scrollRestoration`: A string to change `history.scrollRestoration`. Check the [documentation](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration) to see available options. Default to `'auto'`.
10 | - `options.basename`: Set a prefix to URL before navigating to an other url. Useful for subdomain with relative links.
11 | - `options.transitionParams`: An object that will be passed to `transition.play`
12 |
13 | ### router.listen([options])
14 | Search for current view from current URL and start listening to URL changes.
15 | - `options.clickEvents`: If set to `true`, will prevent links across the pages to trigger a page refresh
16 | - `options.clickIgnoreClass`: Disable previous behaviour for links with specified className
17 |
18 | ### router.view(url, fn)
19 | Register a view to the Router. If the current URL matches the `url` param, it will trigger `fn`.
20 | - `url`: A string or an array of string reflecting exact URLs
21 | - `fn`: Must return an instance of a view
22 |
23 | Guide:
24 | - [Register a view](./GUIDE.md#register-a-view)
25 |
26 | ### router.transition(from, to, fn, backAndForth)
27 | Register a transition to the router defined by source and destination URLs
28 | - `from`: A string or an array of URLs
29 | - `to`: A string or an array of URLs
30 | - `fn`: A function that returns a class or an object implementing a `play(prevView, nextView)` method (can be async) like in [DefaultTransition]
31 | - `backAndForth`: A boolean defining if the transition should be played in reverse too. Default to `true`.
32 |
33 | Guide:
34 | - [Register a transition](./GUIDE.md#register-a-transition)
35 |
36 |
37 | ### router.match(pattern, fn)
38 | Register a pattern and execute `fn` on match. Useful for URL with parameters like `/news/:slug` or code splitting.
39 | The router will resolve views after executing matching callbacks so you can add view in `fn`.
40 | - `options.pattern`: A string or an array of string of patterns. This uses [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) under the hood.
41 | - `fn`: A sync or async function to be executed on match.
42 |
43 | Guides:
44 | - [Handle URL parameters](./GUIDE.md#handle-URL-parameters)
45 | - [Code splitting](./GUIDE.md#code-splitting)
--------------------------------------------------------------------------------
/examples/dom/about/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | About
9 |
10 |
11 |
12 |
13 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/dom/contact/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Contact
9 |
10 |
11 |
12 |
13 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/dom/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Home
9 |
10 |
11 |
12 |
13 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/dom/index.js:
--------------------------------------------------------------------------------
1 | function enterView(view) {
2 | view.style.display = 'flex';
3 | }
4 |
5 | function leaveView(view) {
6 | view.style.display = 'none';
7 | }
8 |
9 | function Home() {
10 | let view;
11 |
12 | function enter() {
13 | console.log('Home :: enter');
14 | view = document.querySelector('.view--home');
15 | enterView(view);
16 | }
17 |
18 | function leave() {
19 | console.log('Home :: leave');
20 | leaveView(view);
21 | }
22 |
23 | return { enter, leave };
24 | }
25 |
26 | function About() {
27 | let view;
28 |
29 | function enter() {
30 | console.log('About :: enter');
31 | view = document.querySelector('.view--about');
32 | enterView(view);
33 | }
34 |
35 | function leave() {
36 | console.log('About :: leave');
37 | leaveView(view);
38 | }
39 |
40 | return { enter, leave };
41 | }
42 |
43 | function Contact() {
44 | let view;
45 |
46 | function enter() {
47 | console.log('Contact :: enter');
48 | view = document.querySelector('.view--contact');
49 | enterView(view);
50 | }
51 |
52 | function leave() {
53 | console.log('Contact :: leave');
54 | leaveView(view);
55 | }
56 |
57 | return { enter, leave };
58 | }
59 |
60 | let router = Lemonade.RouterDOM({
61 | basename: '/examples/dom',
62 | });
63 |
64 | router.view('', Home);
65 |
66 | router.view('/contact/', Contact);
67 |
68 | router.match('/about', async () => {
69 | router.view('/about', About);
70 | });
71 |
72 | router.listen({
73 | clickEvents: true,
74 | });
--------------------------------------------------------------------------------
/examples/dom/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Arial, sans-serif;
5 | }
6 | .header {
7 | background: #f0f0f0;
8 | border-radius: 2px;
9 | display: flex;
10 | justify-content: center;
11 | padding: 20px 0;
12 | }
13 |
14 | .link {
15 | margin: 0 20px;
16 | text-transform: uppercase;
17 | }
18 |
19 | .view {
20 | display: none;
21 | justify-content: center;
22 | }
23 |
24 | .view__title {
25 | font-size: 48px;
26 | margin-top: 100px;
27 | }
--------------------------------------------------------------------------------
/examples/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | lemonade-router
8 |
25 |
26 |
27 |
28 |
33 |
36 |
39 |
42 |
43 |
44 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { Router } from "./src/Router.js";
2 | export { RouterDOM } from "./src/RouterDOM.js";
--------------------------------------------------------------------------------
/lib/lemonade-router-dom.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).Lemonade={})}(this,(function(e){"use strict";function n(){return(n=Object.assign||function(e){for(var n=1;n=0;h--){var d=a[h];"."===d?r(a,h):".."===d?(r(a,h),l++):l&&(r(a,h),l--)}if(!f)for(;l--;l)a.unshift("..");!f||""===a[0]||a[0]&&t(a[0])||a.unshift("");var p=a.join("/");return i&&"/"!==p.substr(-1)&&(p+="/"),p}(s.pathname,c.pathname)):s.pathname=c.pathname:s.pathname||(s.pathname="/"),s}var a=!("undefined"==typeof window||!window.document||!window.document.createElement);function c(e,n){n(window.confirm(e))}var s="popstate",f="hashchange";function u(){try{return window.history.state||{}}catch(e){return{}}}function l(e){void 0===e&&(e={}),a||function(e,n){if(!e)throw new Error("Invariant failed")}(!1);var t,r,i=window.history,l=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,h=!(-1===window.navigator.userAgent.indexOf("Trident")),d=e,p=d.forceRefresh,v=void 0!==p&&p,w=d.getUserConfirmation,m=void 0===w?c:w,g=d.keyLength,y=void 0===g?6:g,x=e.basename?function(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}("/"===(r=e.basename).charAt(0)?r:"/"+r):"";function E(e){var n=e||{},t=n.key,r=n.state,i=window.location,a=i.pathname+i.search+i.hash;return x&&(a=function(e,n){return function(e,n){return 0===e.toLowerCase().indexOf(n.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(n.length))}(e,n)?e.substr(n.length):e}(a,x)),o(a,r,t)}function A(){return Math.random().toString(36).substr(2,y)}var k,b,O=(k=null,b=[],{setPrompt:function(e){return k=e,function(){k===e&&(k=null)}},confirmTransitionTo:function(e,n,t,r){if(null!=k){var i="function"==typeof k?k(e,n):k;"string"==typeof i?"function"==typeof t?t(i,r):r(!0):r(!1!==i)}else r(!0)},appendListener:function(e){var n=!0;function t(){n&&e.apply(void 0,arguments)}return b.push(t),function(){n=!1,b=b.filter((function(e){return e!==t}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),t=0;t=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122||95===s))break;a+=e[c++]}if(!a)throw new TypeError("Missing parameter name at "+t);n.push({type:"NAME",index:t,value:a}),t=c}else n.push({type:"CLOSE",index:t,value:e[t++]});else n.push({type:"OPEN",index:t,value:e[t++]});else n.push({type:"ESCAPED_CHAR",index:t++,value:e[t++]});else n.push({type:"MODIFIER",index:t,value:e[t++]})}return n.push({type:"END",index:t,value:""}),n}(e),r=n.prefixes,i=void 0===r?"./":r,o="[^"+d(n.delimiter||"/#?")+"]+?",a=[],c=0,s=0,f="",u=function(e){if(s-1:void 0===A;i||(v+="(?:"+h+"(?="+l+"))?"),k||(v+="(?="+h+"|"+l+")")}return new RegExp(v,p(t))}(h(e,t),n,t)}function w(e,n,t){return e instanceof RegExp?function(e,n){if(!n)return e;var t=e.source.match(/\((?!\?)/g);if(t)for(var r=0;r0){const[e,...o]=a,s=r.reduce(((e,n,t)=>(e[n.name]=o[t],e)),{}),f=[...n,i].filter((e=>!e.includes(":")));await t({urls:f,url:i,params:s}),c.has(i)&&(u=c.get(i));break}}if(u)break}if(u)if(d.nextLocation=s.createHref(n),a.length>0)for(let n=0;n{h(e,u),u=g(e.pathname,n)})),h(s.location,null),e&&document.addEventListener("click",(e=>{let n=e.target;for(;n&&!x(n);)n=n.parentNode;if(n&&function(e,n){const t=x(n),r=e.which>1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey,i=n.target&&"_blank"===n.target,o=window.location.protocol!==n.protocol||window.location.hostname!==n.hostname,a="string"===n.getAttribute("download"),c=t&&t.includes("mailto:");return!(r||i||o||a||c)}(e,n)&&!n.classList.contains(d.clickIgnoreClass)){e.preventDefault(),e.stopPropagation();const t=m(x(n));d.goTo(t)}}))},goTo:function(e){const{pathname:t,search:r,hash:o}=i(m(window.location.href)),a=m(e),{pathname:c,search:f,hash:u}=i(a);t===c&&r===f&&o===u||s.push(g(a,n))},history:s,getPath:e=>g(m(e),n)};return d}function A({wrapperQuery:e=(()=>document.querySelector(".lemonade-wrapper")),containerQuery:n=(e=>e.querySelector(".lemonade-container")),cacheEnabled:t=!0,defaultTransition:r={play:async function(e,n,{loadView:t,appendView:r,removeView:i}){e&&(await e.leave(n),await t(),r(),i()),await n.enter(e)}},transitionParams:i={},basename:o=""}={}){let a,c,s,f=E({defaultTransition:r,basename:o,transitionParams:{...i,loadView:async function(){let e,{nextLocation:r}=f,i=f.getPath(r);t&&l.cache.get(i)?e=u.get(i):(e=await A.fetch(r),t&&u.set(i,e));const o=document.createElement("html");o.innerHTML=e;const h=o.querySelector("title");h&&(document.title=h.textContent);return c=n(a),s=n(o),{prevContainer:c,nextContainer:s,temp:o}},appendView:function(){a.appendChild(s)},removeView:function(){a.removeChild(c)},wrapperQuery:e,containerQuery:n}}),u=new Map;const l={listen:function({clickEvents:r=!1,clickIgnoreClass:i="no-router"}={}){a=e(),c=n(a),f.listen({clickEvents:r,clickIgnoreClass:i});let o=f.getPath(window.location.href);t&&!u.has(o)&&u.set(o,document.documentElement.innerHTML)},match:f.match,view:f.view,transition:f.transition,goTo:f.goTo,getPath:f.getPath,cache:u};return l}A.fetch=async e=>{let n=await fetch(e);return await n.text()},e.RouterDOM=A,Object.defineProperty(e,"__esModule",{value:!0})}));
2 |
--------------------------------------------------------------------------------
/lib/lemonade-router.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e=e||self).Lemonade={})}(this,(function(e){"use strict";function n(){return(n=Object.assign||function(e){for(var n=1;n=0;h--){var d=a[h];"."===d?r(a,h):".."===d?(r(a,h),l++):l&&(r(a,h),l--)}if(!c)for(;l--;l)a.unshift("..");!c||""===a[0]||a[0]&&t(a[0])||a.unshift("");var p=a.join("/");return i&&"/"!==p.substr(-1)&&(p+="/"),p}(f.pathname,s.pathname)):f.pathname=s.pathname:f.pathname||(f.pathname="/"),f}var a=!("undefined"==typeof window||!window.document||!window.document.createElement);function s(e,n){n(window.confirm(e))}var f="popstate",c="hashchange";function u(){try{return window.history.state||{}}catch(e){return{}}}function l(e){void 0===e&&(e={}),a||function(e,n){if(!e)throw new Error("Invariant failed")}(!1);var t,r,i=window.history,l=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,h=!(-1===window.navigator.userAgent.indexOf("Trident")),d=e,p=d.forceRefresh,v=void 0!==p&&p,w=d.getUserConfirmation,g=void 0===w?s:w,y=d.keyLength,m=void 0===y?6:y,x=e.basename?function(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}("/"===(r=e.basename).charAt(0)?r:"/"+r):"";function A(e){var n=e||{},t=n.key,r=n.state,i=window.location,a=i.pathname+i.search+i.hash;return x&&(a=function(e,n){return function(e,n){return 0===e.toLowerCase().indexOf(n.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(n.length))}(e,n)?e.substr(n.length):e}(a,x)),o(a,r,t)}function E(){return Math.random().toString(36).substr(2,m)}var O,b,k=(O=null,b=[],{setPrompt:function(e){return O=e,function(){O===e&&(O=null)}},confirmTransitionTo:function(e,n,t,r){if(null!=O){var i="function"==typeof O?O(e,n):O;"string"==typeof i?"function"==typeof t?t(i,r):r(!0):r(!1!==i)}else r(!0)},appendListener:function(e){var n=!0;function t(){n&&e.apply(void 0,arguments)}return b.push(t),function(){n=!1,b=b.filter((function(e){return e!==t}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),t=0;t=48&&f<=57||f>=65&&f<=90||f>=97&&f<=122||95===f))break;a+=e[s++]}if(!a)throw new TypeError("Missing parameter name at "+t);n.push({type:"NAME",index:t,value:a}),t=s}else n.push({type:"CLOSE",index:t,value:e[t++]});else n.push({type:"OPEN",index:t,value:e[t++]});else n.push({type:"ESCAPED_CHAR",index:t++,value:e[t++]});else n.push({type:"MODIFIER",index:t,value:e[t++]})}return n.push({type:"END",index:t,value:""}),n}(e),r=n.prefixes,i=void 0===r?"./":r,o="[^"+d(n.delimiter||"/#?")+"]+?",a=[],s=0,f=0,c="",u=function(e){if(f-1:void 0===E;i||(v+="(?:"+h+"(?="+l+"))?"),O||(v+="(?="+h+"|"+l+")")}return new RegExp(v,p(t))}(h(e,t),n,t)}function w(e,n,t){return e instanceof RegExp?function(e,n){if(!n)return e;var t=e.source.match(/\((?!\?)/g);if(t)for(var r=0;r0){const[e,...o]=a,f=r.reduce(((e,n,t)=>(e[n.name]=o[t],e)),{}),c=[...n,i].filter((e=>!e.includes(":")));await t({urls:c,url:i,params:f}),s.has(i)&&(u=s.get(i));break}}if(u)break}if(u)if(d.nextLocation=f.createHref(n),a.length>0)for(let n=0;n{h(e,u),u=y(e.pathname,n)})),h(f.location,null),e&&document.addEventListener("click",(e=>{let n=e.target;for(;n&&!x(n);)n=n.parentNode;if(n&&function(e,n){const t=x(n),r=e.which>1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey,i=n.target&&"_blank"===n.target,o=window.location.protocol!==n.protocol||window.location.hostname!==n.hostname,a="string"===n.getAttribute("download"),s=t&&t.includes("mailto:");return!(r||i||o||a||s)}(e,n)&&!n.classList.contains(d.clickIgnoreClass)){e.preventDefault(),e.stopPropagation();const t=g(x(n));d.goTo(t)}}))},goTo:function(e){const{pathname:t,search:r,hash:o}=i(g(window.location.href)),a=g(e),{pathname:s,search:c,hash:u}=i(a);t===s&&r===c&&o===u||f.push(y(a,n))},history:f,getPath:e=>y(g(e),n)};return d},Object.defineProperty(e,"__esModule",{value:!0})}));
2 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lemonade-router",
3 | "version": "0.2.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@babel/code-frame": {
8 | "version": "7.10.4",
9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
10 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
11 | "dev": true,
12 | "requires": {
13 | "@babel/highlight": "^7.10.4"
14 | }
15 | },
16 | "@babel/helper-validator-identifier": {
17 | "version": "7.10.4",
18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
19 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
20 | "dev": true
21 | },
22 | "@babel/highlight": {
23 | "version": "7.10.4",
24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
26 | "dev": true,
27 | "requires": {
28 | "@babel/helper-validator-identifier": "^7.10.4",
29 | "chalk": "^2.0.0",
30 | "js-tokens": "^4.0.0"
31 | }
32 | },
33 | "@babel/runtime": {
34 | "version": "7.8.4",
35 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz",
36 | "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==",
37 | "requires": {
38 | "regenerator-runtime": "^0.13.2"
39 | }
40 | },
41 | "@rollup/plugin-commonjs": {
42 | "version": "11.0.2",
43 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz",
44 | "integrity": "sha512-MPYGZr0qdbV5zZj8/2AuomVpnRVXRU5XKXb3HVniwRoRCreGlf5kOE081isNWeiLIi6IYkwTX9zE0/c7V8g81g==",
45 | "dev": true,
46 | "requires": {
47 | "@rollup/pluginutils": "^3.0.0",
48 | "estree-walker": "^1.0.1",
49 | "is-reference": "^1.1.2",
50 | "magic-string": "^0.25.2",
51 | "resolve": "^1.11.0"
52 | },
53 | "dependencies": {
54 | "estree-walker": {
55 | "version": "1.0.1",
56 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
57 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
58 | "dev": true
59 | }
60 | }
61 | },
62 | "@rollup/plugin-node-resolve": {
63 | "version": "7.1.1",
64 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz",
65 | "integrity": "sha512-14ddhD7TnemeHE97a4rLOhobfYvUVcaYuqTnL8Ti7Jxi9V9Jr5LY7Gko4HZ5k4h4vqQM0gBQt6tsp9xXW94WPA==",
66 | "dev": true,
67 | "requires": {
68 | "@rollup/pluginutils": "^3.0.6",
69 | "@types/resolve": "0.0.8",
70 | "builtin-modules": "^3.1.0",
71 | "is-module": "^1.0.0",
72 | "resolve": "^1.14.2"
73 | }
74 | },
75 | "@rollup/plugin-replace": {
76 | "version": "2.3.1",
77 | "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.3.1.tgz",
78 | "integrity": "sha512-qDcXj2VOa5+j0iudjb+LiwZHvBRRgWbHPhRmo1qde2KItTjuxDVQO21rp9/jOlzKR5YO0EsgRQoyox7fnL7y/A==",
79 | "dev": true,
80 | "requires": {
81 | "@rollup/pluginutils": "^3.0.4",
82 | "magic-string": "^0.25.5"
83 | }
84 | },
85 | "@rollup/pluginutils": {
86 | "version": "3.0.8",
87 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.8.tgz",
88 | "integrity": "sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==",
89 | "dev": true,
90 | "requires": {
91 | "estree-walker": "^1.0.1"
92 | },
93 | "dependencies": {
94 | "estree-walker": {
95 | "version": "1.0.1",
96 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
97 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
98 | "dev": true
99 | }
100 | }
101 | },
102 | "@types/estree": {
103 | "version": "0.0.39",
104 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
105 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
106 | "dev": true
107 | },
108 | "@types/node": {
109 | "version": "13.7.0",
110 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz",
111 | "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==",
112 | "dev": true
113 | },
114 | "@types/resolve": {
115 | "version": "0.0.8",
116 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
117 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
118 | "dev": true,
119 | "requires": {
120 | "@types/node": "*"
121 | }
122 | },
123 | "acorn": {
124 | "version": "7.1.1",
125 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
126 | "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
127 | "dev": true
128 | },
129 | "ansi-styles": {
130 | "version": "3.2.1",
131 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
132 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
133 | "dev": true,
134 | "requires": {
135 | "color-convert": "^1.9.0"
136 | }
137 | },
138 | "buffer-from": {
139 | "version": "1.1.1",
140 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
141 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
142 | "dev": true
143 | },
144 | "builtin-modules": {
145 | "version": "3.1.0",
146 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
147 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
148 | "dev": true
149 | },
150 | "chalk": {
151 | "version": "2.4.2",
152 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
153 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
154 | "dev": true,
155 | "requires": {
156 | "ansi-styles": "^3.2.1",
157 | "escape-string-regexp": "^1.0.5",
158 | "supports-color": "^5.3.0"
159 | }
160 | },
161 | "color-convert": {
162 | "version": "1.9.3",
163 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
164 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
165 | "dev": true,
166 | "requires": {
167 | "color-name": "1.1.3"
168 | }
169 | },
170 | "color-name": {
171 | "version": "1.1.3",
172 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
173 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
174 | "dev": true
175 | },
176 | "commander": {
177 | "version": "2.20.3",
178 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
179 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
180 | "dev": true
181 | },
182 | "escape-string-regexp": {
183 | "version": "1.0.5",
184 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
185 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
186 | "dev": true
187 | },
188 | "has-flag": {
189 | "version": "3.0.0",
190 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
191 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
192 | "dev": true
193 | },
194 | "history": {
195 | "version": "4.10.1",
196 | "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
197 | "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
198 | "requires": {
199 | "@babel/runtime": "^7.1.2",
200 | "loose-envify": "^1.2.0",
201 | "resolve-pathname": "^3.0.0",
202 | "tiny-invariant": "^1.0.2",
203 | "tiny-warning": "^1.0.0",
204 | "value-equal": "^1.0.1"
205 | }
206 | },
207 | "is-module": {
208 | "version": "1.0.0",
209 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
210 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
211 | "dev": true
212 | },
213 | "is-reference": {
214 | "version": "1.1.4",
215 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz",
216 | "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==",
217 | "dev": true,
218 | "requires": {
219 | "@types/estree": "0.0.39"
220 | }
221 | },
222 | "jest-worker": {
223 | "version": "26.5.0",
224 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz",
225 | "integrity": "sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug==",
226 | "dev": true,
227 | "requires": {
228 | "@types/node": "*",
229 | "merge-stream": "^2.0.0",
230 | "supports-color": "^7.0.0"
231 | },
232 | "dependencies": {
233 | "has-flag": {
234 | "version": "4.0.0",
235 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
236 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
237 | "dev": true
238 | },
239 | "supports-color": {
240 | "version": "7.2.0",
241 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
242 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
243 | "dev": true,
244 | "requires": {
245 | "has-flag": "^4.0.0"
246 | }
247 | }
248 | }
249 | },
250 | "js-tokens": {
251 | "version": "4.0.0",
252 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
253 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
254 | },
255 | "loose-envify": {
256 | "version": "1.4.0",
257 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
258 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
259 | "requires": {
260 | "js-tokens": "^3.0.0 || ^4.0.0"
261 | }
262 | },
263 | "magic-string": {
264 | "version": "0.25.6",
265 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.6.tgz",
266 | "integrity": "sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g==",
267 | "dev": true,
268 | "requires": {
269 | "sourcemap-codec": "^1.4.4"
270 | }
271 | },
272 | "merge-stream": {
273 | "version": "2.0.0",
274 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
275 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
276 | "dev": true
277 | },
278 | "path-parse": {
279 | "version": "1.0.7",
280 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
281 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
282 | "dev": true
283 | },
284 | "path-to-regexp": {
285 | "version": "6.1.0",
286 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz",
287 | "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="
288 | },
289 | "randombytes": {
290 | "version": "2.1.0",
291 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
292 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
293 | "dev": true,
294 | "requires": {
295 | "safe-buffer": "^5.1.0"
296 | }
297 | },
298 | "regenerator-runtime": {
299 | "version": "0.13.3",
300 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
301 | "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
302 | },
303 | "resolve": {
304 | "version": "1.15.1",
305 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
306 | "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
307 | "dev": true,
308 | "requires": {
309 | "path-parse": "^1.0.6"
310 | }
311 | },
312 | "resolve-pathname": {
313 | "version": "3.0.0",
314 | "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
315 | "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
316 | },
317 | "rollup": {
318 | "version": "1.31.0",
319 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.31.0.tgz",
320 | "integrity": "sha512-9C6ovSyNeEwvuRuUUmsTpJcXac1AwSL1a3x+O5lpmQKZqi5mmrjauLeqIjvREC+yNRR8fPdzByojDng+af3nVw==",
321 | "dev": true,
322 | "requires": {
323 | "@types/estree": "*",
324 | "@types/node": "*",
325 | "acorn": "^7.1.0"
326 | }
327 | },
328 | "rollup-plugin-terser": {
329 | "version": "7.0.2",
330 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
331 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
332 | "dev": true,
333 | "requires": {
334 | "@babel/code-frame": "^7.10.4",
335 | "jest-worker": "^26.2.1",
336 | "serialize-javascript": "^4.0.0",
337 | "terser": "^5.0.0"
338 | }
339 | },
340 | "safe-buffer": {
341 | "version": "5.2.1",
342 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
343 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
344 | "dev": true
345 | },
346 | "serialize-javascript": {
347 | "version": "4.0.0",
348 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
349 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
350 | "dev": true,
351 | "requires": {
352 | "randombytes": "^2.1.0"
353 | }
354 | },
355 | "source-map": {
356 | "version": "0.7.3",
357 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
358 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
359 | "dev": true
360 | },
361 | "source-map-support": {
362 | "version": "0.5.19",
363 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
364 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
365 | "dev": true,
366 | "requires": {
367 | "buffer-from": "^1.0.0",
368 | "source-map": "^0.6.0"
369 | },
370 | "dependencies": {
371 | "source-map": {
372 | "version": "0.6.1",
373 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
374 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
375 | "dev": true
376 | }
377 | }
378 | },
379 | "sourcemap-codec": {
380 | "version": "1.4.8",
381 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
382 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
383 | "dev": true
384 | },
385 | "supports-color": {
386 | "version": "5.5.0",
387 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
388 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
389 | "dev": true,
390 | "requires": {
391 | "has-flag": "^3.0.0"
392 | }
393 | },
394 | "terser": {
395 | "version": "5.3.5",
396 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.5.tgz",
397 | "integrity": "sha512-Qw3CZAMmmfU824AoGKalx+riwocSI5Cs0PoGp9RdSLfmxkmJgyBxqLBP/isDNtFyhHnitikvRMZzyVgeq+U+Tg==",
398 | "dev": true,
399 | "requires": {
400 | "commander": "^2.20.0",
401 | "source-map": "~0.7.2",
402 | "source-map-support": "~0.5.19"
403 | }
404 | },
405 | "tiny-invariant": {
406 | "version": "1.1.0",
407 | "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
408 | "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
409 | },
410 | "tiny-warning": {
411 | "version": "1.0.3",
412 | "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
413 | "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
414 | },
415 | "value-equal": {
416 | "version": "1.0.1",
417 | "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
418 | "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
419 | }
420 | }
421 | }
422 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lemonade-router",
3 | "version": "0.2.1",
4 | "description": "Minimal routing library",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "NODE_ENV='development' rollup -c -w",
8 | "build": "NODE_ENV='production' rollup -c"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/raphaelameaume/lemonade-router.git"
13 | },
14 | "keywords": [
15 | "routing",
16 | "client-side",
17 | "fetch"
18 | ],
19 | "author": "Raphaël Améaume",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/raphaelameaume/lemonade-router/issues"
23 | },
24 | "homepage": "https://github.com/raphaelameaume/lemonade-router#readme",
25 | "dependencies": {
26 | "history": "4.10.1",
27 | "path-to-regexp": "6.1.0"
28 | },
29 | "devDependencies": {
30 | "@rollup/plugin-commonjs": "^11.0.2",
31 | "@rollup/plugin-node-resolve": "^7.1.1",
32 | "@rollup/plugin-replace": "^2.3.1",
33 | "rollup": "^1.31.0",
34 | "rollup-plugin-terser": "^7.0.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import replace from '@rollup/plugin-replace';
4 | import { terser } from 'rollup-plugin-terser';
5 |
6 | const production = process.env.NODE_ENV === 'production';
7 |
8 | const plugins = [
9 | resolve(),
10 | commonjs(),
11 | replace({
12 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
13 | }),
14 | production && terser()
15 | ];
16 |
17 | export default [
18 | // browser-friendly UMD build
19 | {
20 | input: 'src/Router.js',
21 | output: {
22 | name: 'Lemonade',
23 | file: 'lib/lemonade-router.umd.js',
24 | format: 'umd'
25 | },
26 | plugins
27 | },
28 | {
29 | input: 'src/RouterDOM.js',
30 | output: {
31 | name: 'Lemonade',
32 | file: 'lib/lemonade-router-dom.umd.js',
33 | format: 'umd'
34 | },
35 | plugins
36 | }
37 | ];
--------------------------------------------------------------------------------
/src/DefaultTransition.js:
--------------------------------------------------------------------------------
1 | export function DefaultTransition() {
2 | function play(prevView, nextView) {
3 | if (prevView) {
4 | prevView.leave(nextView);
5 | }
6 |
7 | nextView.enter(prevView);
8 | }
9 |
10 | return {
11 | play,
12 | }
13 | }
--------------------------------------------------------------------------------
/src/DefaultTransitionDOM.js:
--------------------------------------------------------------------------------
1 | export function DefaultTransitionDOM() {
2 | async function play(prevView, nextView, { loadView, appendView, removeView }) {
3 | if (prevView) {
4 | await prevView.leave(nextView);
5 | await loadView(); // must be called before appendView
6 | appendView();
7 | removeView();
8 | }
9 |
10 | await nextView.enter(prevView);
11 | }
12 |
13 | return {
14 | play,
15 | };
16 | };
--------------------------------------------------------------------------------
/src/Router.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory, parsePath } from "history";
2 | import { pathToRegexp } from "path-to-regexp";
3 | import { getPath, retrieveHref, preventClick, stripBasename, stripTrailingSlash } from "./helpers.js";
4 | import { DefaultTransition } from "./DefaultTransition.js";
5 |
6 | function Router({
7 | defaultTransition = DefaultTransition(),
8 | basename = '',
9 | transitionParams = {},
10 | } = {}) {
11 | const matches = [];
12 | const transitions = [];
13 | const views = new Map();
14 |
15 | let history = createBrowserHistory({ basename });
16 | let prevView = null;
17 | let prevPathname = null;
18 |
19 | /*
20 | * Register URL pattern
21 | * @param {string|array} urls
22 | * @param {function} fn
23 | */
24 | function match(urls, fn) {
25 | if (!Array.isArray(urls)) {
26 | urls = [urls];
27 | }
28 |
29 | matches.push({ urls, fn });
30 | }
31 |
32 | /*
33 | * Register transition from one or multiple URLs to other(s)
34 | * @param {string|array} fromURLs -
35 | * @param {string|array} toURLs -
36 | * @param {function} fn -
37 | * @param {boolean} backAndForth -
38 | */
39 | function transition(fromURLs, toURLs, fn, backAndForth = true) {
40 | fromURLs = !Array.isArray(fromURLs) ? [fromURLs] : fromURLs;
41 | toURLs = !Array.isArray(toURLs) ? [toURLs] : toURLs;
42 |
43 | transitions.push({ fromURLs, toURLs, fn, backAndForth });
44 | }
45 |
46 | /*
47 | * Register view for one or multiple URLs
48 | * @param {string|array} urls -
49 | * @param {function} fn -
50 | */
51 | function view(urls, fn) {
52 | const view = fn();
53 |
54 | if (!Array.isArray(urls)) {
55 | urls = [urls];
56 | }
57 |
58 | for (let i = 0; i < urls.length; i++) {
59 | let url = stripTrailingSlash(urls[i]);
60 | views.set(url, view);
61 | }
62 | }
63 |
64 | function goTo(href) {
65 | const { pathname, search, hash } = parsePath(getPath(window.location.href));
66 |
67 | const nextPath = getPath(href);
68 | const { pathname: nextPathname, search: nextSearch, hash: nextHash } = parsePath(nextPath);
69 |
70 | if (pathname === nextPathname && search === nextSearch && hash === nextHash) return;
71 |
72 | history.push(stripBasename(nextPath, basename));
73 | }
74 |
75 | async function apply(location, prevPathname) {
76 | try {
77 | const pathname = stripTrailingSlash(location.pathname);
78 |
79 | let nextView;
80 |
81 | if (views.has(pathname)) {
82 | nextView = views.get(pathname);
83 | } else {
84 | for (let i = 0; i < matches.length; i++) {
85 | const { urls, fn } = matches[i];
86 |
87 | for (let j = 0; j < urls.length; j++) {
88 | const keys = [];
89 | const regex = urls[j] === '*' ? /^\/.*(?:\/)?$/i : pathToRegexp(urls[j], keys);
90 | const match = pathname !== "" ? regex.exec(pathname) : regex.exec('/');
91 |
92 | if (match && match.length > 0) {
93 | const [url, ...values] = match;
94 | const params = keys.reduce((acc, key, index) => {
95 | acc[key.name] = values[index];
96 | return acc;
97 | }, {});
98 |
99 | // remove urls with params
100 | const all = [...urls, pathname].filter(u => !u.includes(':'));
101 |
102 | await fn({ urls: all, url: pathname, params });
103 |
104 | if (views.has(pathname)) {
105 | nextView = views.get(pathname);
106 | }
107 |
108 | break;
109 | }
110 | }
111 |
112 | if (nextView) break;
113 | }
114 | }
115 |
116 | if (nextView) {
117 | router.nextLocation = history.createHref(location);
118 |
119 | if (transitions.length > 0) {
120 | for (let i = 0; i < transitions.length; i++) {
121 | const { fromURLs, toURLs, backAndForth, fn } = transitions[i];
122 |
123 | const matchFrom = fromURLs.includes('*') || fromURLs.includes(prevPathname);
124 | const reverseMatchFrom = backAndForth && fromURLs.includes(pathname);
125 | const matchTo = toURLs.includes('*') || toURLs.includes(pathname);
126 | const reverseMatchTo = backAndForth && toURLs.includes(prevPathname);
127 |
128 | if ((matchFrom && matchTo) || (reverseMatchFrom && reverseMatchTo)) {
129 | const transition = await fn();
130 |
131 | let currentPrevView = prevView;
132 | let currentNextView = nextView;
133 | prevView = nextView;
134 |
135 | await transition.play(currentPrevView, currentNextView, transitionParams);
136 | } else {
137 | let currentPrevView = prevView;
138 | let currentNextView = nextView;
139 | prevView = nextView;
140 |
141 | await defaultTransition.play(currentPrevView, currentNextView, transitionParams);
142 | }
143 | }
144 | } else {
145 | let currentPrevView = prevView;
146 | let currentNextView = nextView;
147 | prevView = nextView;
148 |
149 | await defaultTransition.play(currentPrevView, currentNextView, transitionParams);
150 | }
151 | } else {
152 | console.error('Router :: View not found', views);
153 | }
154 | } catch (error) {
155 | console.error(error);
156 | }
157 | }
158 |
159 | /*
160 | * Start listening to URL changes
161 | * @param {object} options -
162 | * @param {boolean} options.clickEvents -
163 | * @param {string} options.clickIgnoreClass -
164 | */
165 | function listen({ clickEvents = false, clickIgnoreClass = 'no-router' } = {}) {
166 | router.clickIgnoreClass = clickIgnoreClass;
167 |
168 | prevPathname = stripBasename(parsePath(getPath(window.location.href)).pathname, basename);
169 |
170 | history.listen((location) => {
171 | apply(location, prevPathname);
172 |
173 | prevPathname = stripBasename(location.pathname, basename);
174 | });
175 |
176 | apply(history.location, null);
177 |
178 | if (clickEvents) {
179 | document.addEventListener('click', (event) => {
180 | let target = event.target;
181 |
182 | while (target && !retrieveHref(target)) {
183 | target = target.parentNode;
184 | }
185 |
186 | if (target && preventClick(event, target) && !target.classList.contains(router.clickIgnoreClass)) {
187 | event.preventDefault();
188 | event.stopPropagation();
189 |
190 | const href = retrieveHref(target);
191 | const url = getPath(href);
192 | router.goTo(url);
193 | }
194 | });
195 | }
196 | }
197 |
198 | const router = {
199 | nextLocation: null,
200 | clickIgnoreClass: 'no-router',
201 | match,
202 | transition,
203 | view,
204 | listen,
205 | goTo,
206 | history,
207 | getPath: (url) => stripBasename(getPath(url), basename),
208 | };
209 |
210 | return router;
211 | }
212 |
213 | export { Router };
--------------------------------------------------------------------------------
/src/RouterDOM.js:
--------------------------------------------------------------------------------
1 | import { Router } from "./Router.js";
2 | import { DefaultTransitionDOM } from "./DefaultTransitionDOM.js";
3 |
4 | function RouterDOM({
5 | wrapperQuery = () => document.querySelector('.lemonade-wrapper'),
6 | containerQuery = ($wrapper) => $wrapper.querySelector('.lemonade-container'),
7 | cacheEnabled = true,
8 | defaultTransition = DefaultTransitionDOM(),
9 | transitionParams = {},
10 | basename = ''
11 | } = {}) {
12 | let router = Router({
13 | defaultTransition,
14 | basename,
15 | transitionParams: {
16 | ...transitionParams,
17 | loadView,
18 | appendView,
19 | removeView,
20 | wrapperQuery,
21 | containerQuery,
22 | }
23 | });
24 | let cache = new Map();
25 | let $wrapper, $prevContainer, $nextContainer;
26 |
27 | function listen({ clickEvents = false, clickIgnoreClass = 'no-router' } = {}) {
28 | $wrapper = wrapperQuery();
29 | $prevContainer = containerQuery($wrapper);
30 |
31 | router.listen({ clickEvents, clickIgnoreClass });
32 |
33 | let path = router.getPath(window.location.href);
34 | if (cacheEnabled && !cache.has(path)) {
35 | cache.set(path, document.documentElement.innerHTML);
36 | }
37 | }
38 |
39 | async function loadView() {
40 | let html;
41 | let { nextLocation } = router;
42 | let nextPath = router.getPath(nextLocation);
43 |
44 | if (cacheEnabled && routerDOM.cache.get(nextPath)) {
45 | html = cache.get(nextPath);
46 | } else {
47 | html = await RouterDOM.fetch(nextLocation);
48 |
49 | if (cacheEnabled) {
50 | cache.set(nextPath, html);
51 | }
52 | }
53 |
54 | const temp = document.createElement('html');
55 | temp.innerHTML = html;
56 |
57 | const title = temp.querySelector('title');
58 |
59 | if (title) {
60 | document.title = title.textContent;
61 | }
62 |
63 | $prevContainer = containerQuery($wrapper);
64 | $nextContainer = containerQuery(temp);
65 |
66 | return {
67 | prevContainer: $prevContainer,
68 | nextContainer: $nextContainer,
69 | temp,
70 | };
71 | }
72 |
73 | function appendView() {
74 | $wrapper.appendChild($nextContainer);
75 | }
76 |
77 | function removeView() {
78 | $wrapper.removeChild($prevContainer);
79 | }
80 |
81 | const routerDOM = {
82 | listen: listen,
83 | history: router.history,
84 | match: router.match,
85 | view: router.view,
86 | transition: router.transition,
87 | goTo: router.goTo,
88 | getPath: router.getPath,
89 | cache,
90 | };
91 |
92 | return routerDOM;
93 | }
94 |
95 | RouterDOM.fetch = async (url) => {
96 | let response = await fetch(url);
97 | let html = await response.text();
98 |
99 | return html;
100 | };
101 |
102 | export { RouterDOM };
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | export function getPath(url) {
2 | return parse(url).path;
3 | }
4 |
5 | export function parse(url) {
6 | let path = url.replace(window.location.origin, '');
7 |
8 | return { path };
9 | }
10 |
11 | /*
12 | * https://github.com/ReactTraining/history/blob/3f69f9e07b0a739419704cffc3b3563133281548/modules/PathUtils.js
13 | */
14 | export function hasBasename(path, prefix) {
15 | return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path);
16 | }
17 |
18 | export function stripBasename(path, prefix) {
19 | return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
20 | }
21 |
22 | export function stripTrailingSlash(path) {
23 | return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
24 | }
25 |
26 | /*
27 | * https://github.com/barbajs/barba/blob/1.x/src/Pjax/Pjax.js#L179
28 | */
29 | export function retrieveHref(element) {
30 | if (element) {
31 | const xlink = element.getAttribute && element.getAttribute('xlink:href');
32 |
33 | if (typeof xlink === 'string') {
34 | return xlink;
35 | }
36 |
37 | if (element.href) {
38 | return element.href;
39 | }
40 | }
41 |
42 | return false;
43 | }
44 |
45 | /*
46 | * https://github.com/barbajs/barba/blob/1.x/src/Pjax/Pjax.js#L239
47 | */
48 | export function preventClick(event, element) {
49 | const href = retrieveHref(element);
50 | const withKey = event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
51 | const blankTarget = element.target && element.target === '_blank';
52 | const differentDomain = window.location.protocol !== element.protocol || window.location.hostname !== element.hostname;
53 | const isDownload = element.getAttribute('download') === 'string';
54 | const isMailto = href && href.includes('mailto:');
55 | const shouldPrevent = !withKey && !blankTarget && !differentDomain && !isDownload && !isMailto;
56 |
57 | return shouldPrevent;
58 | }
--------------------------------------------------------------------------------