Skip to content
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

Closed
wants to merge 7 commits into from
Closed

Change to ranking and sorting #136

wants to merge 7 commits into from

Conversation

ashsearle
Copy link
Contributor

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 as 543 which (using string comparison) is higher ranked than /a (5), but lower than /a/b (55)

@developit
Copy link
Member

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 [].some() don't exist there. They also transpile to a fair bit more code. Any chance we could split the Array stuff into a separate PR from the ranking?

@@ -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;
Copy link
Member

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.

Copy link
Contributor Author

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 "/"

Copy link
Member

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);
Copy link
Member

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

Copy link
Contributor Author

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 }));
};

Copy link
Member

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('='));
Copy link
Member

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?

Copy link
Contributor Author

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.)

Copy link
Member

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?

Copy link
Contributor Author

@ashsearle ashsearle Feb 1, 2017

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...

Copy link
Member

@developit developit Feb 2, 2017

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 :
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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) => {
Copy link
Member

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;

Copy link
Member

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.

Copy link
Contributor Author

@ashsearle ashsearle Feb 1, 2017

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.

Copy link
Member

@developit developit Feb 1, 2017

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() 👍

@ashsearle
Copy link
Contributor Author

ashsearle commented Feb 1, 2017

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 some, and reduce (and every) are supported in IE9, along with the old stalwarts of sort, map and filter.

Just checked MSDN too: some and reduce are supported in IE9.

I did think it was odd that Object.assign was being used though: I'm happy to remove that if you want...?

@ashsearle
Copy link
Contributor Author

I've made rank() more compact (closer to your spitball approach), and use a named function for callback to avoid recreating on each invocation. I think it's a bit more readable now too.

I tried pulling out a separate $query property on matches for query-parameters as suggested, but this throws a DOMException on this line:

Object.assign(attributes { url, matches }, matches);

This is basic DOM restriction that you can't have a property called $query hanging on a DOM element (try reproducing with document.body.setAttribute('$query', 'anything')) I don't have a good suggestion for how else to handle this at the moment, so have left code as is: preserving existing query-parameter handling behaviour.

@ashsearle ashsearle mentioned this pull request Feb 24, 2017
@developit
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants