-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change to ranking and sorting #136
Conversation
Hi @ashsearle - very happy to see action on this! Only thing I have to ask - the Array changes are actually problematic. This library supports IE9+ (all Preact libs do) and things like |
@@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. While we're here, might be worth extending this check to also ignore javascript:
prefixed href
values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That should already be handled as "javascript:" doesn't start with "/"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Man I need more coffee today eh
if (matches) { | ||
if (invoke!==false) { | ||
// copy matches onto props | ||
Object.assign(attributes, { url, matches }, matches); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think Object.assign
is transpiled in this repo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was already in use in the Link function (but think that might have been a recent change: in the last few months or so...)
const Link = (props) => {
return h('a', Object.assign({}, props, { onClick: handleLinkClick }));
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you're right, and it is being transpiled. My bad!
const queryParams = {}; | ||
search.split('&').forEach(parameter => { | ||
let [name, ...value] = parameter.split('='); | ||
queryParams[decodeURIComponent(name)] = decodeURIComponent(value.join('=')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought: instead of the object rest spreads, maybe assign directly into matches
here and skip queryParams
entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid wasted effort parsing query parameters on routes that didn't match, but didn't want to change behaviour.
e.g. with a route of /:a
and url of /foo?a=bar
, you either end up with matches being {a: 'foo'}
(path takes priority - current behaviour in 2.4.1) or {a: 'bar'}
(query param takes priority, which would happen if I assigned direct to matches
after moving code down here.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ashsearle makes sense. What about just collecting up the query params under $query
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if you're just suggesting renaming queryParams
or something else(?)
Thinking about this a bit more: you could do a bit of refactoring and extract the url parsing from exec. You could go as far as splitting to pathname
and url
, and running segmentize(pathname)
before calling exec
. Then, in terms of $query
parsing / handling, that could be done in the if (invoke!==false)
block in getMatchingChildren
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had meant instead of merging queryParams
into matches
, do matches.$query = queryParams
to separate the two. Not sure which is better.
Agreed about pulling out the query param parsing, exec should never need to look at the querystring anyway, so that seems like a good call. I'm impartial to whether we call segmentize()
before exec()
or within it - probably nice to do the split once and re-use, but only if its not annoying.
return diff || (aAttr.path.length - bAttr.path.length); | ||
let aRank = rank(aAttr.path), | ||
bRank = rank(bAttr.path); | ||
return (aRank < bRank) ? 1 : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like subtraction could work here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't work with subtraction with string ranks: I need '5' < '543' < '55'
.
Using <
and >
will work for both numeric and string ranks - so gives that little extra flexibility if you need to try out / plug-in different ranking methods later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry I commented here before I re-read your technique and understood the idea. It works nicely!
src/util.js
Outdated
return (strip(url).match(/\/+/g) || '').length; | ||
export function rank(path) { | ||
return strip(path). | ||
replace(/(:)?([^\/]*?)([*+?])?(?:\/+|$)/g, (match, isParam, segment, flag) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this scoring. Would prefer an exec-based tokenizer though to keep the math numeric.
Just spitballing, but here's what I came up with:
path = strip(path);
let rank=0, token, reg = /(:)?([^\/]*?)([*+?])?(?:\/+|$)/g;
let flags = ['*', '+', '?'];
while (token=reg.exec(path)) {
let [, isParam, segment, flag] = token;
rank += 100 + (segment ? 5 : isParam ? (flags.indexOf(flag)+1 || 4) : 0);
}
return rank;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just re-read your comment about the ranking and take back the numeric bit. Still nice to have the exec()-based rather than replace() based parse just for perf.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the exec hard a little gnarly to work with, and went through a few different sort implementations. The thing I find weird about exec is it's doing double-duty: doing the equivalent of 'exec' and 'test' at the same time (stealing terminology from regex.)
In my defence: the ranking is done via a Schwartzian transform before sorting, so it's only done once per route, not multiple times as you might expect in a dumb sort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mostly thinking of avoiding the function invocation and replace since it's mutative. That said, since the output is actually being used I'm more amenable to it (now that I properly understand the sort ordering approach).
Only thing I'd change, then, would be to pull out the replace callback and make it a named function. It's pure, so there's no need to recreate it on each call to rank()
👍
Hi - it's nice to have such a quick response. The array methods used should all be fine in IE9. I checked your browser targets against info at MDN which says that Just checked MSDN too: some and reduce are supported in IE9. I did think it was odd that |
I've made I tried pulling out a separate Object.assign(attributes { url, matches }, matches); This is basic DOM restriction that you can't have a property called |
Not 100% I can safely resolve conflicts here. Maybe we switch back to the single sort? I can't remember if that was something related to this PR. |
These changes address issues #123 and go some way to help with #63, and update bits of code to use more built-in array methods.
The ranking proposed here works on a super-set principle, with each segment of the pathname ranked in turn. From highest rank to lowest:
/foo
,/:foo
,/:foo?
,/:foo+
,/:foo*
The rank function now produces a string, and uses string sorting to rank. This allows comparison at each level of a path:
/a/:b/:c?
would be ranked as543
which (using string comparison) is higher ranked than/a
(5
), but lower than/a/b
(55
)