Collected · regexparam · MIT
Temporary exhibition · Hall of the Commons
A Route, Compiled
JavaScript·2018·40 lines·1201 bytes
/**
* @param {string|RegExp} input The route pattern
* @param {boolean} [loose] Allow open-ended matching. Ignored with `RegExp` input.
*/
export function parse(input, loose) {
if (input instanceof RegExp) return { keys:false, pattern:input };
var c, o, tmp, ext, keys=[], pattern='', arr = input.split('/');
arr[0] || arr.shift();
while (tmp = arr.shift()) {
c = tmp[0];
if (c === '*') {
keys.push(c);
pattern += tmp[1] === '?' ? '(?:/(.*))?' : '/(.*)';
} else if (c === ':') {
o = tmp.indexOf('?', 1);
ext = tmp.indexOf('.', 1);
keys.push( tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length) );
pattern += !!~o && !~ext ? '(?:/([^/]+?))?' : '/([^/]+?)';
if (!!~ext) pattern += (!!~o ? '?' : '') + '\\' + tmp.substring(ext);
} else {
pattern += '/' + tmp;
}
}
return {
keys: keys,
pattern: new RegExp('^' + pattern + (loose ? '(?=$|\/)' : '\/?$'), 'i')
};
}
var RGX = /(\/|^)([:*][^/]*?)(\?)?(?=[/.]|$)/g;
// error if key missing?
export function inject(route, values) {
return route.replace(RGX, (x, lead, key, optional) => {
x = values[key=='*' ? key : key.substring(1)];
return x ? '/'+x : (optional || key=='*') ? '' : '/' + key;
});
}Curator’s note
Forty lines that turn /users/:id into a regular expression, and they are underneath more routers than anyone has counted.
Every framework with client-side routing needs this, and almost none of them write it themselves. The pattern language is small — a literal segment, a named parameter with :, an optional one with ?, a wildcard with *, an extension with . — and the whole translation is one while loop that shifts segments off the front and appends to a string.
The idiom to look at is !!~o. indexOf returns -1 when it finds nothing; ~ is bitwise NOT, and ~-1 is 0, so ~o is falsy exactly when the search failed and truthy otherwise. The double negation turns it into a boolean. Three characters to say "was it found", and it was ordinary style in JavaScript for years before includes existed. It reads as line noise now, which is what happens to idioms when the language catches up with them.
There is a nice inversion in the pair of functions. parse goes from a pattern to a regular expression; inject goes the other way, from a pattern and some values back to a URL, using a regular expression of its own to find the placeholders. The same shape, run in both directions, in forty lines total.
Worth noticing what it does not attempt. The output is a flat regular expression, which works because a URL path is flat — segments separated by slashes, no nesting, ever. Ask a router to match something that can contain itself and no amount of regular expression would help, which is a boundary the theatre has a whole lecture about.