From 854cc4857c02a719e4d67aba6175d4cf9feea907 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Sat, 28 Jan 2017 16:12:26 +0000 Subject: [PATCH 1/6] Use classic array methods, and more es2015 --- src/index.js | 29 ++++++----------------------- src/util.js | 26 ++++++++++++-------------- test/util.js | 5 +++++ 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/index.js b/src/index.js index f1c385b0..619e134d 100644 --- a/src/index.js +++ b/src/index.js @@ -57,22 +57,13 @@ function route(url, replace=false) { /** Check if the given URL can be handled by any router instances. */ function canRoute(url) { - for (let i=ROUTERS.length; i--; ) { - if (ROUTERS[i].canRoute(url)) return true; - } - return false; + return ROUTERS.some(router => router.canRoute(url)); } /** Tell all router instances to handle the given URL. */ function routeTo(url) { - let didRoute = false; - for (let i=0; i (router.routeTo(url) === true || didRoute), false); } @@ -84,7 +75,7 @@ function routeFromLink(node) { target = node.getAttribute('target'); // ignore links with targets and non-path URLs - if (!href || !href.match(/^\//g) || (target && !target.match(/^_?self$/i))) return; + if (!/^\//.test(href) || (target && !target.match(/^_?self$/i))) return; // attempt to route, if no match simply cede control to browser return route(href); @@ -110,12 +101,11 @@ function prevent(e) { function delegateLinkHandler(e) { // ignore events the browser takes care of already: - if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return; + if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) return; let t = e.target; do { if (String(t.nodeName).toUpperCase()==='A' && t.getAttribute('href') && isPreactElement(t)) { - if (e.button !== 0) return; // if link is handled by the router, prevent browser defaults if (routeFromLink(t)) { return prevent(e); @@ -149,8 +139,7 @@ class Router extends Component { } shouldComponentUpdate(props) { - if (props.static!==true) return true; - return props.url!==this.props.url || props.onChange!==this.props.onChange; + return props.static!==true || props.url!==this.props.url || props.onChange!==this.props.onChange; } /** Check if the given URL can be matched against any children */ @@ -197,14 +186,8 @@ class Router extends Component { matches = exec(url, path, attributes); if (matches) { if (invoke!==false) { - attributes.url = url; - attributes.matches = matches; // copy matches onto props - for (let i in matches) { - if (matches.hasOwnProperty(i)) { - attributes[i] = matches[i]; - } - } + Object.assign(attributes, { url, matches }, matches); } return true; } diff --git a/src/util.js b/src/util.js index 7e433f1e..8502082d 100644 --- a/src/util.js +++ b/src/util.js @@ -2,24 +2,22 @@ const EMPTY = {}; export function exec(url, route, opts=EMPTY) { - let reg = /(?:\?([^#]*))?(#.*)?$/, - c = url.match(reg), + let reg = /^([^?]*)(?:\?([^#]*))?(#.*)?$/, + [, pathname, searcn] = (url.match(reg) || []), matches = {}, ret; - if (c && c[1]) { - let p = c[1].split('&'); - for (let i=0; i { + let [name, ...value] = parameter.split('='); + matches[decodeURIComponent(name)] = decodeURIComponent(value.join('=')); + }); } - url = segmentize(url.replace(reg, '')); + url = segmentize(pathname); route = segmentize(route || ''); let max = Math.max(url.length, route.length); for (let i=0; i { expect(exec('/a/b', '/:foo+')).to.eql({ foo:'a/b' }); expect(exec('/a/b/c', '/:foo+')).to.eql({ foo:'a/b/c' }); }); + + it('should handle query-string', () => { + expect(exec('/?foo=bar', '/')).to.eql({ foo: 'bar' }); + expect(exec('/a?foo=bar', '/:foo')).to.eql({ foo: 'a' }); + }); }); }); From 982249558f8d9d5c77b1c5435d958c47a552375a Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Wed, 1 Feb 2017 08:57:37 +0000 Subject: [PATCH 2/6] Implement stable sort to fix #123 --- src/index.js | 32 ++++++++++++++++++----------- src/util.js | 57 ++++++++++++++++++++++++++++++++++++++++++---------- test/util.js | 21 ++++++++++--------- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/index.js b/src/index.js index 67c18322..ba6e62c7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import { h, Component } from 'preact'; -import { exec, pathRankSort } from './util'; +import { exec, rankChild } from './util'; let customHistory = null; @@ -157,7 +157,7 @@ class Router extends Component { /** Check if the given URL can be matched against any children */ canRoute(url) { - return this.getMatchingChildren(this.props.children, url, false).length > 0; + return this.props.children.some(({ attributes=EMPTY }) => !!exec(url, attributes.path, attributes)); } /** Re-render children with a new URL to match against. */ @@ -200,17 +200,25 @@ class Router extends Component { } getMatchingChildren(children, url, invoke) { - return children.slice().sort(pathRankSort).filter( ({ attributes }) => { - let path = attributes.path, - matches = exec(url, path, attributes); - if (matches) { - if (invoke!==false) { - // copy matches onto props - Object.assign(attributes, { url, matches }, matches); + return children + .map((child, index) => ({ child, index, rank: rankChild(child) })) + .sort((a, b) => ( + (a.rank < b.rank) ? 1 : + (a.rank > b.rank) ? -1 : + (a.index - b.index) + )) + .map(({ child }) => child) + .filter(({ attributes=EMPTY }) => { + let path = attributes.path, + matches = exec(url, path, attributes); + if (matches) { + if (invoke!==false) { + // copy matches onto props + Object.assign(attributes, { url, matches }, matches); + } + return true; } - return true; - } - }); + }); } render({ children, onChange }, { url }) { diff --git a/src/util.js b/src/util.js index 8502082d..8449ecc7 100644 --- a/src/util.js +++ b/src/util.js @@ -3,15 +3,9 @@ const EMPTY = {}; export function exec(url, route, opts=EMPTY) { let reg = /^([^?]*)(?:\?([^#]*))?(#.*)?$/, - [, pathname, searcn] = (url.match(reg) || []), + [, pathname, search] = (url.match(reg) || []), matches = {}, ret; - if (searcn) { - searcn.split('&').forEach(parameter => { - let [name, ...value] = parameter.split('='); - matches[decodeURIComponent(name)] = decodeURIComponent(value.join('=')); - }); - } url = segmentize(pathname); route = segmentize(route || ''); let max = Math.max(url.length, route.length); @@ -37,6 +31,18 @@ export function exec(url, route, opts=EMPTY) { } } if (opts.default!==true && ret===false) return false; + + if (search) { + const queryParams = {}; + search.split('&').forEach(parameter => { + let [name, ...value] = parameter.split('='); + queryParams[decodeURIComponent(name)] = decodeURIComponent(value.join('=')); + }); + matches = { + ...queryParams, + ...matches + }; + } return matches; } @@ -45,16 +51,45 @@ export function pathRankSort(a, b) { bAttr = b.attributes || EMPTY; if (aAttr.default) return 1; if (bAttr.default) return -1; - let diff = rank(aAttr.path) - rank(bAttr.path); - return diff || (aAttr.path.length - bAttr.path.length); + let aRank = rank(aAttr.path), + bRank = rank(bAttr.path); + return (aRank < bRank) ? 1 : + (aRank == bRank) ? 0 : + -1; } export function segmentize(url) { return strip(url).split('/'); } -export function rank(url) { - return (strip(url).match(/\/+/g) || '').length; +export function rank(path) { + return strip(path). + replace(/(:)?([^\/]*?)([*+?])?(?:\/+|$)/g, (match, isParam, segment, flag) => { + if (isParam) { + if (flag === '*') { + return '1'; + } else if (flag === '+') { + return '2'; + } else if (flag === '?') { + return '3'; + } + return '4'; + } else if (segment) { + return '5'; + } + return ''; + }) || '5'; +} + +// export const rank = (path) => ( +// strip(path). +// replace(/(:)?([^\/]*?)([*+?]?)(?:\/+|$)/g, (match, isParam, segment, flag) => ( +// isParam ? ('0*+?'.indexOf(flag) || 4) : (segment ? 5 : '') +// )) || '5' +// ); + +export function rankChild({ attributes=EMPTY }) { + return attributes.default ? '0' : rank(attributes.path); } export function strip(url) { diff --git a/test/util.js b/test/util.js index a4dd631e..22802c5b 100644 --- a/test/util.js +++ b/test/util.js @@ -19,12 +19,13 @@ describe('util', () => { }); describe('rank', () => { - it('should return number of path segments', () => { - expect(rank('')).to.equal(0); - expect(rank('/')).to.equal(0); - expect(rank('//')).to.equal(0); - expect(rank('a/b/c')).to.equal(2); - expect(rank('/a/b/c/')).to.equal(2); + it('should return rank of path segments', () => { + expect(rank('')).to.eql('5'); + expect(rank('/')).to.eql('5'); + expect(rank('//')).to.eql('5'); + expect(rank('a/b/c')).to.eql('555'); + expect(rank('/a/b/c/')).to.eql('555'); + expect(rank('/:a/b?/:c?/:d*/:e+')).to.eql('45312'); }); }); @@ -39,13 +40,13 @@ describe('util', () => { }); describe('pathRankSort', () => { - it('should sort by segment count', () => { + it('should sort by highest rank first', () => { let paths = arr => arr.map( path => ({attributes:{path}}) ); expect( - paths(['/a/b/','/a/b','/','b']).sort(pathRankSort) + paths(['/:a*','/a','/:a+','/:a?','/a/:b*']).sort(pathRankSort) ).to.eql( - paths(['/','b','/a/b','/a/b/']) + paths(['/a/:b*','/a','/:a?','/:a+','/:a*']) ); }); @@ -59,7 +60,7 @@ describe('util', () => { expect( p.sort(pathRankSort) ).to.eql( - paths(['/','b','/a/b','/a/b/']).concat(defaultPath) + paths(['/a/b/','/a/b','/','b']).concat(defaultPath) ); }); }); From ffd8cc53db3b9ef096e6951c8fb268d9a40ef833 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Sun, 5 Feb 2017 10:59:17 +0000 Subject: [PATCH 3/6] Use named function for rank callback --- src/util.js | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/util.js b/src/util.js index 8449ecc7..4b5e79ad 100644 --- a/src/util.js +++ b/src/util.js @@ -62,31 +62,14 @@ export function segmentize(url) { return strip(url).split('/'); } -export function rank(path) { - return strip(path). - replace(/(:)?([^\/]*?)([*+?])?(?:\/+|$)/g, (match, isParam, segment, flag) => { - if (isParam) { - if (flag === '*') { - return '1'; - } else if (flag === '+') { - return '2'; - } else if (flag === '?') { - return '3'; - } - return '4'; - } else if (segment) { - return '5'; - } - return ''; - }) || '5'; -} +export const rankSegment = (segment) => { + let [, isParam, , flag] = /^(:?)(.*?)([*+?]?)$/.exec(segment); + return isParam ? ('0*+?'.indexOf(flag) || 4) : 5; +}; -// export const rank = (path) => ( -// strip(path). -// replace(/(:)?([^\/]*?)([*+?]?)(?:\/+|$)/g, (match, isParam, segment, flag) => ( -// isParam ? ('0*+?'.indexOf(flag) || 4) : (segment ? 5 : '') -// )) || '5' -// ); +export const rank = (path) => ( + segmentize(path).map(rankSegment).join('') +); export function rankChild({ attributes=EMPTY }) { return attributes.default ? '0' : rank(attributes.path); From 3cf318c6ce964808b40f6696fe83e7ceabb5675f Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Sun, 5 Feb 2017 11:16:25 +0000 Subject: [PATCH 4/6] Ignore text children of Router --- src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index ba6e62c7..debddb83 100644 --- a/src/index.js +++ b/src/index.js @@ -201,6 +201,7 @@ class Router extends Component { getMatchingChildren(children, url, invoke) { return children + .filter(({ attributes }) => !!attributes) .map((child, index) => ({ child, index, rank: rankChild(child) })) .sort((a, b) => ( (a.rank < b.rank) ? 1 : @@ -208,7 +209,7 @@ class Router extends Component { (a.index - b.index) )) .map(({ child }) => child) - .filter(({ attributes=EMPTY }) => { + .filter(({ attributes }) => { let path = attributes.path, matches = exec(url, path, attributes); if (matches) { From 3dc6fa588d3eae4e9753fb53b6b04ad80686914b Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Sun, 5 Feb 2017 14:29:24 +0000 Subject: [PATCH 5/6] #129: Handle relative URLs --- src/index.js | 58 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index 437ceeee..b761b0e6 100644 --- a/src/index.js +++ b/src/index.js @@ -25,27 +25,61 @@ function setUrl(url, type='push') { } +function getCurrentLocation() { + return (customHistory && customHistory.location) || + (customHistory && customHistory.getCurrentLocation && customHistory.getCurrentLocation()) || + (typeof location!=='undefined' ? location : EMPTY); +} + function getCurrentUrl() { - let url; - if (customHistory && customHistory.location) { - url = customHistory.location; - } - else if (customHistory && customHistory.getCurrentLocation) { - url = customHistory.getCurrentLocation(); - } - else { - url = typeof location!=='undefined' ? location : EMPTY; - } + let url = getCurrentLocation(); return `${url.pathname || ''}${url.search || ''}`; } +// Lifted from https://tools.ietf.org/html/rfc3986#appendix-B +const uriRegex = new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?'); + +/* Resolve URL relative to current location */ +function resolve(url) { + let current = getCurrentLocation(); + let [, + protocol,, + ,hostname, + pathname, + search,, + // hash,, + ] = uriRegex.exec(url); + if ( + (protocol && protocol !== current.protocol) || + (hostname && hostname !== current.hostname) + ) { + return; + } + if (pathname.charAt(0) !== '/') { + let stack = (current.pathname||'/').split("/"), + segments = pathname.split("/"); + stack.pop(); + for (let i=0; i Date: Fri, 24 Feb 2017 16:56:18 +0000 Subject: [PATCH 6/6] Use for URL resolution --- src/index.js | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 5da28156..9a13c447 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,7 @@ function setUrl(url, type='push') { function getCurrentLocation() { return (customHistory && customHistory.location) || (customHistory && customHistory.getCurrentLocation && customHistory.getCurrentLocation()) || - (typeof location!=='undefined' ? location : EMPTY); + (typeof location!=='undefined' ? location : EMPTY); } function getCurrentUrl() { @@ -36,39 +36,26 @@ function getCurrentUrl() { return `${url.pathname || ''}${url.search || ''}`; } +const a = typeof document!=='undefined' && document.createElement('a'); -// Lifted from https://tools.ietf.org/html/rfc3986#appendix-B -const uriRegex = new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?'); +// Based on https://tools.ietf.org/html/rfc3986#appendix-B +const uriRegex = new RegExp('^([^:/?#]+:)?(?://([^/?#]*))?([^?#]*)((?:\\?[^#]*)?)((?:#.*)?)'); /* Resolve URL relative to current location */ function resolve(url) { let current = getCurrentLocation(); - let [, - protocol,, - ,hostname, - pathname, - search,, - // hash,, - ] = uriRegex.exec(url); + if (a) { + a.setAttribute('href', url); + url = a.href; + } + let [,protocol,host,pathname,search] = uriRegex.exec(url); if ( - (protocol && protocol !== current.protocol) || - (hostname && hostname !== current.hostname) + (current.protocol && protocol !== current.protocol) || + (current.host && host !== current.host) ) { return; } - if (pathname.charAt(0) !== '/') { - let stack = (current.pathname||'/').split("/"), - segments = pathname.split("/"); - stack.pop(); - for (let i=0; i