uri-template-router
Version:
Match a URI to a pattern in a collection of URI Templates
721 lines (661 loc) • 26.2 kB
JavaScript
"use strict";
module.exports.Router = Router;
const { Node, reduce, parallel, union, concat, optional, star, fromString, compare } = require('./lib/fsm.js');
// Export a function that docs/demo.js uses
// FIXME this might be relocated later
module.exports.compare = require('./lib/fsm.js').compare;
const RANGE = {};
RANGE.UNRES = ['-', '.', '0-9', 'A-Z', '_', 'a-z', '~'].join('');
RANGE.GEN_D = [':', '/', '?', '#', '[', ']', '@'].join('');
RANGE.SUB_D = ['!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '='].join('');
RANGE.RESER = [RANGE.GEN_D, RANGE.SUB_D].join('');
RANGE.URI = [RANGE.UNRES, RANGE.RESER].join('');
const regex_sc = /[.*+?^${}()|[\]\\]/g;
function regex_escape(str){
return str.replace(regex_sc, '\\$&');
}
const regex_rangesc = /[\\\]]/g;
function range_regex(str){
return '(?:['+str.replace(regex_rangesc, '\\$&')+']|%[0-9A-Fa-f]{2})';
}
function range_fsm(str, uriTemplate, offset){
return optional([
new Node({[str]: 0}, {[uriTemplate]: new PartialMatch(offset, Value)}, true),
]);
}
function encodeURIComponent_v(v){
return encodeURIComponent(v).replace(/!/g, '%21');
}
function Operator(prefix, separator, delimiter, range, named, form){
this.prefix = prefix;
this.separator = separator;
this.delimiter = delimiter;
this.range = range;
this.named = named;
this.form = form;
this.encode = (range===RANGE.URI) ? encodeURI_literal : encodeURIComponent_v;
}
const operators = {
'': new Operator( '', ',', null, RANGE.UNRES, false),
'+': new Operator('', ',', null, RANGE.URI, false),
'#': new Operator('#', ',', null, RANGE.URI, false),
'.': new Operator('.', '.', '.', RANGE.UNRES, false),
'/': new Operator('/', '/', '/', RANGE.UNRES, false),
';': new Operator(';', ';', ';', RANGE.UNRES, true, false),
'?': new Operator('?', '&', '&', RANGE.UNRES, true, true),
'&': new Operator('&', '&', '&', RANGE.UNRES, true, true),
};
// This technique works only because the 2-3rd characters in pct-encoding are also legal characters by themselves
encodeURI_literal.pattern = new RegExp('[^'+RANGE.URI.replace(regex_rangesc, '\\$&')+'%'+']|%(?![0-9A-Fa-f]{2})', 'ug');
function encodeURI_literal(v){
return v.replace(encodeURI_literal.pattern, function(a){
return encodeURIComponent(a);
});
}
function Router(){
this.clear();
}
Router.prototype.clear = function clear(){
this.nid = 0;
this.fsm = [];
this.routeSet = new Set;
this.templateRouteMap = new Map;
this.valueRouteMap = new Map;
this.hierarchy = {children: []};
};
Router.prototype.hasRoute = function hasRoute(route){
return this.routeSet.has(route);
};
Object.defineProperty(Router.prototype, "size", {
get: function sizeGet(){ return this.routeSet.size; },
});
Object.defineProperty(Router.prototype, "routes", {
get: function routesGet(){ return Array.from(this.routeSet); },
});
Router.prototype.getTemplate = function getTemplate(uriTemplate){
if(typeof uriTemplate !== 'string') throw new Error('Expected string `uriTemplate`');
return this.templateRouteMap.get(uriTemplate);
};
Router.prototype.hasTemplate = function hasTemplate(uriTemplate){
if(typeof uriTemplate !== 'string') throw new Error('Expected string `uriTemplate`');
return this.templateRouteMap.has(uriTemplate);
};
Router.prototype.getValue = function getValue(matchValue){
return this.valueRouteMap.get(matchValue);
};
Router.prototype.hasValue = function hasValue(matchValue){
return this.valueRouteMap.has(matchValue);
};
const Literal = ('Literal');
// const Prefix = ('Prefix');
const Value = ('Value');
module.exports.PartialMatch = PartialMatch;
function PartialMatch(position, type, open, close){
if(!type) throw new Error('Expected a Type');
this.position = position;
this.type = type;
this.open = open || []; // list of groups that open
this.close = close || []; // list of groups that open
}
module.exports.FinalMatch = FinalMatch;
function FinalMatch(route, close){
this.route = route;
this.close = close;
}
FinalMatch.prototype.toString = function toString(){
return '<'+this.route.uriTemplate+'>';
};
var rule_literals = /([\x21\x23-\x24\x26\x28-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E\xA0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|%[0-9A-Fa-f][0-9A-Fa-f])/;
var rule_varspec = /^([0-9A-Za-z_]|%[0-9A-Fa-f]{2})(\.?([0-9A-Za-z_]|%[0-9A-Fa-f]{2}))*(:[0-9]{0,3}|\*)?$/;
module.exports.Route = Route;
function Route(uriTemplate, options, matchValue){
if(typeof uriTemplate!=='string') throw new Error('Expected `uriTemplate` to be a string');
this.uriTemplate = uriTemplate;
this.options = options || {};
this.matchValue = this.options.matchValue!==undefined ? this.options.matchValue : matchValue;
// Parse the URI template
var varnames = this.varnames = {};
var variables = this.variables = [];
var tokens = this.tokens = [];
var expressionList = [];
for(var uri_i=0; uri_i<uriTemplate.length; uri_i++){
var chr = uriTemplate[uri_i];
if(chr==='%'){
// A pct-encoded sequence is treated as a single character for efficiency
// (this more than halves the size of the tree)
if(uriTemplate.substring(uri_i, uri_i+3).match(/^%[0-9A-F]{2}$/)){
chr += uriTemplate[uri_i+1] + uriTemplate[uri_i+2];
uri_i += 2;
}else{
throw new Error('Invalid pct-encoded sequence '+JSON.stringify(uriTemplate.substring(uri_i, uri_i+3)));
}
}
if(chr=='{'){
var endpos = uriTemplate.indexOf('}', uri_i+2);
if(endpos<0) throw new Error('Unclosed expression: Expected "}" but found end of template');
var patternBody = uriTemplate.substring(uri_i+1, endpos);
uri_i = endpos;
// If the first character is part of a valid variable name, assume the default operator
// Else, assume the first character is a operator
var operatorChar = patternBody[0].match(/[a-zA-Z0-9_%]/) ? '' : patternBody[0] ;
var operator = operators[operatorChar];
if(!operator){
throw new Error('Unknown expression operator: '+JSON.stringify(operatorChar));
}
const expression = Expression.from(patternBody);
expression.variableList.forEach(function(varspec){
varspec.index = Object.keys(varnames).length;
varnames[varspec.varname] = varspec;
variables[varspec.index] = varspec;
});
expressionList.push(expression);
tokens.push(expression);
}else if(chr.match(rule_literals)){
if(typeof tokens[tokens.length-1]=='string') tokens[tokens.length-1] += chr;
else tokens.push(chr);
}else{
throw new Error('Unexpected character '+JSON.stringify(chr));
}
}
this.finalMatch = new FinalMatch(this);
this.fsm = reduce(this.toFSM());
function partial_intersect(states){
if(states[0]) return states[0].partials;
}
function final_intersect(states){
if(states.every(final => final && (Array.isArray(final) ? final.length : final))){
const items = states.flatMap(final => (final && Array.isArray(final)) ? final : []);
return items.length ? items : true;
}else{
return false;
}
}
if(options && options.parent){
var parent = options.parent;
if(typeof options.parent=='object'){
parent = options.parent;
}else if(typeof options.parent==='string'){
parent = new Route(options.parent);
}else{
throw new Error('Unknown type for parent');
}
this.fsm = parallel([this.fsm, parent.fsm], partial_intersect, final_intersect);
}
}
Route.prototype.gen = function Route_gen(params){
if(typeof params!='object') throw new Error('Expected arguments[0] `params` to be an object');
return this.tokens.map( (v)=>v.toString(params) ).join('');
};
Route.prototype.toString = function toString(params){
return this.tokens.map( (v)=>v.toString(params) ).join('');
};
Route.prototype.compare = function routecompare(other){
return compare([this.fsm || this.toFSM(), other.fsm || other.toFSM()]);
}
Route.prototype.toJSON = function toJSON(){
return this.uriTemplate;
};
Route.prototype.toFSM = function toFSM(){
const route = this;
var template_i = 0;
// Get the FSM of each of the tokens, and concatenate them together
const fsms = route.tokens.map(function addExpression(expression){
// If a string, treat as literal characters
if(typeof expression=='string'){
const offset = template_i;
template_i += expression.length;
return fromString(expression, v=>({[route.uriTemplate]:new PartialMatch((offset+v), Literal)}));
}
return expression.toFSM(route.uriTemplate, template_i);
});
return concat(fsms);
// return reduce(concat(fsms));
};
Route.prototype.toRegex = function toRegex(){
const regex_str = this.tokens.map(function(segment){
if(typeof segment==='string'){
return regex_escape(segment);
}else{
return segment.toRegex().source;
}
}).join('');
return new RegExp('^'+regex_str+'$', 'u');
};
Route.prototype.decode = function decode(uri){
const regex = this.toRegex();
const match = uri.match(regex);
if(!match) return;
var offset = 1;
const result = {};
for(var i=0; i<this.tokens.length; i++){
const segment = this.tokens[i];
// This segment is not an expression, there's nothing to parse here
if(typeof segment === 'string') continue;
for(var j=0; j<segment.variableList.length; j++){
const varname = segment.variableList[j].varname;
const value = match[offset++];
if(typeof value === 'string'){
if(segment.variableList[j].explode){
// If the variable is exploded, split it apart by the separator since toRegex matched it as a single string
if(segment.variableList[j].named){
// Also if this is a named variable, the string includes "varname=" in each segment
result[varname] = value.split(segment.separator).map(decodeURIComponent).map( v => v.replace(new RegExp('^'+regex_escape(segment.variableList[j].varname)+'=', 'ug'), '') );
}else{
result[varname] = value.split(segment.separator).map(decodeURIComponent);
}
}else if(value){
result[varname] = decodeURI(value);
}
}
}
}
return result;
}
// This is slightly different than Router#resolveURI
// Route does not store detailed final state data, only a boolean
Route.prototype.resolveURI = function resolveString(uri, flags){
if(typeof uri!=='string') throw new Error('Expected arguments[0] `uri` to be a string');
const self = this;
// 0 is the initial state
var state = this.fsm[0];
if(!state) return;
const history = [{state}];
const pctenc = /^%[0-9A-F]{2}$/;
for(var offset = 0; state && offset < uri.length; offset++){
const symbol = uri[offset]==='%' ? uri.slice(offset, offset+3) : uri[offset];
// Double-check that pct-encoded sequences are valid (in addition to what the FSM should prohibit)
if(symbol.length===3){
if(!pctenc.test(symbol)){
return;
}
offset += 2;
}
const nextStateId = state.get(symbol);
if(nextStateId === undefined) return;
state = this.fsm[nextStateId];
if(!state) return;
history.push({symbol, nextStateId, state});
}
// With all of the characters parsed, the current "state" contains the solution
const solution = state.final;
if(!solution) return;
return new Result(self, uri, flags, history, [self.finalMatch]);
};
module.exports.Expression = Expression;
function Expression(operatorChar, variableList){
if(typeof operatorChar !== 'string') throw new Error('Expected `operatorChar` to be a string');
if(!operators[operatorChar]) throw new Error('Unknown operator: '+JSON.stringify(operatorChar));
variableList.forEach(function(v){
if(!(v instanceof Variable)) throw new Error('Expected `variableList` to be array of Variable instances');
});
this.operatorChar = operatorChar;
this.prefix = operators[operatorChar].prefix;
this.separator = operators[operatorChar].separator;
this.range = operators[operatorChar].range;
this.variableList = variableList;
}
Expression.from = function(patternBody){
// If the first character is part of a valid variable name, assume the default operator
// Else, assume the first character is a operator
var operatorChar = patternBody[0].match(/[a-zA-Z0-9_%]/) ? '' : patternBody[0] ;
var operator = operators[operatorChar];
if(!operator){
throw new Error('Unknown expression operator: '+JSON.stringify(operator));
}
const variableList = patternBody
.substring(operatorChar.length)
.split(/,/g)
.map( Variable.from.bind(null, operatorChar) );
return new Expression(operatorChar, variableList);
};
Expression.prototype.toString = function toString(params){
const operator = operators[this.operatorChar];
if(params){
const values = this.variableList.map( (v)=>v.expand(params) ).filter( (v)=>(typeof v==='string') );
if(values.length){
return operator.prefix + values.join(operator.separator);
}else{
return '';
}
}else{
// toString will join the Variable#toString() values with commas
return '{' + this.operatorChar + this.variableList.toString() + '}';
}
};
Expression.prototype.toFSM = function toFSM(uriTemplate, offset){
var offset_i = offset;
const fsm_0 = [];
for(var i=0; i<this.variableList.length; i++){
if(i==0 && this.prefix){
fsm_0.push(concat([ fromString(this.prefix), this.variableList[i].toFSM(uriTemplate, offset_i) ]));
offset_i += 1;
}else if(i>0 && this.separator){
fsm_0.push(concat([ fromString(this.separator), this.variableList[i].toFSM(uriTemplate, offset_i) ]));
offset_i += 1;
}else{
fsm_0.push(this.variableList[i].toFSM(uriTemplate, offset_i));
}
offset_i += this.variableList[i].varname.length;
}
return optional(concat(fsm_0));
};
Expression.prototype.toRegex = function toRegex(){
var fsm_0 = '';
if(this.prefix){
fsm_0 += regex_escape(this.prefix);
}
for(var i=0; i<this.variableList.length; i++){
if(i>0 && this.separator){
fsm_0 += '(?:' + regex_escape(this.separator) + this.variableList[i].toRegex().source + ')?';
}else{
fsm_0 += this.variableList[i].toRegex().source;
}
}
// The entire expression is optional
return new RegExp('(?:'+fsm_0+')?', 'u');
}
module.exports.Variable = Variable;
function Variable(operatorChar, varname, explode, maxLength){
if(typeof varname !== 'string') throw new Error('Expected `varname` to be a string');
if(typeof operatorChar !== 'string') throw new Error('Expected `operatorChar` to be a string');
const operator = operators[operatorChar];
if(!operators[operatorChar]) throw new Error('Expected `operator` to be a valid operator');
if(typeof explode !== 'boolean') throw new Error('Expected `explode` to be a boolean');
if(maxLength!==null && typeof maxLength !== 'number') throw new Error('Expected `maxLength` to be a number');
this.operatorChar = operatorChar;
this.varname = varname;
this.explode = explode;
this.maxLength = maxLength;
this.optional = true;
this.prefix = operator.prefix;
this.separator = operator.separator;
this.delimiter = operator.delimiter;
this.range = operator.range;
this.named = operator.named;
}
Variable.from = function(operatorChar, varspec){
if(!varspec.match(rule_varspec)){
throw new Error('Malformed variable '+JSON.stringify(varspec));
}
const separator = operators[operatorChar];
// Test for explode operator
const explode = !!varspec.match(/\*$/);
const varnameMaxLength = explode ? varspec.substring(0, varspec.length-1) : varspec;
if(explode && !separator){
throw new Error('Variable operator '+JSON.stringify(operatorChar)+' does not work with explode modifier');
}
// Test for substring modifier
const varnameMaxLength_i = varnameMaxLength.indexOf(':');
const varname = varnameMaxLength_i<0 ? varnameMaxLength : varnameMaxLength.substring(0, varnameMaxLength_i);
const maxLengthStr = varnameMaxLength_i<0 ? null : varnameMaxLength.substring(varnameMaxLength_i+1);
const maxLength = maxLengthStr ? parseInt(maxLengthStr, 10) : null;
return new Variable(
operatorChar,
varname,
explode,
maxLength,
);
};
Variable.prototype.toString = function(params){
if(params) return this.expand(params);
return this.varname +
(this.explode ? '*' : '') +
(typeof this.maxLength==='number' ? ':'+this.maxLength : '');
};
Variable.prototype.expand = function(params){
const t = this;
const op = operators[t.operatorChar];
const varvalue = params[t.varname];
const encode = op.encode;
if(typeof varvalue=='string' || typeof varvalue=='number'){
let value = varvalue;
if(t.maxLength) value = value.substring(0, t.maxLength);
if(op.named){
if(op.form || value) return t.varname + '=' + encode(value);
else return t.varname;
}else{
return encode(value);
}
}else if(Array.isArray(varvalue) && varvalue.length>0){
if(t.explode){
const items = varvalue.map(function(value){
if(t.maxLength) value = value.toString().substring(0, t.maxLength);
if(op.named){
if(op.form || value) return t.varname + '=' + encode(value);
else return t.varname;
}else{
return encode(value);
}
});
return items.length ? items.join(t.separator) : null;
}else{
let value = varvalue;
if(t.maxLength) value = value.substring(0, t.maxLength);
if(value.length===0) return null;
if(op.named){
return t.varname + '=' + value.map(function(v){ return encode(v); }).join(',');
}else{
return value.map(function(v){ return encode(v); }).join(',');
}
}
}else if(typeof varvalue == 'object' && varvalue){
if(t.maxLength){
throw new Error('Cannot substring object');
}
if(t.explode){
// Apparently op.named doesn't matter in this case
const items = Object.keys(varvalue).map(function(key){
if(op.form || varvalue[key]) return encode(key) + '=' + encode(varvalue[key]);
else return key;
});
return items.length ? items.join(t.separator) : null;
}else{
if(op.named){
const items = Object.keys(varvalue).map(function(key){
return encode(key) + ',' + encode(varvalue[key]);
});
return items.length ? t.varname + '=' + items.join(',') : null;
}else{
const items = Object.keys(varvalue).map(function(key){
return encode(key) + ',' + encode(varvalue[key]);
});
return items.length ? items.join(',') : null;
}
}
}
return null;
};
Variable.prototype.toFSM = function toFSM(uriTemplate, offset){
const op = operators[this.operatorChar];
const fsm = range_fsm(this.range, uriTemplate, offset);
if(this.explode){
if(op.named){
return optional(concat([concat([fromString(this.varname), optional(concat([fromString('='), fsm]))]), star(concat([fromString(this.separator), fromString(this.varname), optional(concat([fromString('='), fsm]))]))]));
}else{
return optional(concat([fsm, star(concat([fromString(this.separator), fsm]))]));
}
}else if(op.named){
return optional(concat([fromString(this.varname), optional(concat([fromString('='), fsm]))]));
}else{
return fsm;
}
}
Variable.prototype.toRegex = function toRegex(){
const op = operators[this.operatorChar];
if(this.explode){
if(op.named){
return new RegExp('((?:'+regex_escape(this.varname)+'(?:=('+range_regex(this.range)+'*)))(?:'+this.separator+'(?:'+regex_escape(this.varname)+'(='+range_regex(this.range)+'*)))*)?', 'u');
}else{
// Include the separator in the range, we will split() it later
return new RegExp('('+range_regex(this.range+this.separator)+'*)', 'u');
}
}else if(op.named){
return new RegExp('(?:'+regex_escape(this.varname)+'(?:=('+range_regex(this.range)+'*))?)?', 'u');
}else{
return new RegExp('('+range_regex(this.range)+'*)', 'u');
}
}
module.exports.Result = Result;
function Result(router, uri, options, history, final_states){
const final_match = final_states[0];
if(!final_match) throw new Error();
const route = final_match.route;
this.router = router;
this.uri = uri;
this.options = options;
this.route = route;
this.uriTemplate = route.uriTemplate;
this.matchValue = route.matchValue;
this.params = route.decode(this.uri);
this.history = history;
this.final_states = final_states;
}
Result.prototype.rewrite = function rewrite(uriTemplate, options){
if(!(uriTemplate instanceof Route)){
throw new Error('Expected argument `uriTemplate` to be a Route');
}
var uri = uriTemplate.gen(this.params);
return new Result(this.router, uri, options, [], [ { route: uriTemplate } ]);
};
Result.prototype.next = function next(){
// return this.router.resolveURI(this.uri, this.options, this.remaining_state);
// With all of the characters parsed, the current "state" contains the solution
const remaining_states = this.final_states.slice(1);
// ... If it lists one
if(!remaining_states.length) return;
return new Result(this.router, this.uri, this.options, this.history, remaining_states);
};
Object.defineProperty(Result.prototype, "template", {
get: function templateGet(){ return this.uriTemplate; },
set: function templateSet(v){ this.uriTemplate = v; },
});
Object.defineProperty(Result.prototype, "name", {
get: function templateGet(){ return this.matchValue; },
});
Router.prototype.addTemplate = function addTemplate(uriTemplate, options, matchValue){
if(typeof uriTemplate=='object' && options===undefined && matchValue===undefined){
var route = uriTemplate;
uriTemplate = route.uriTemplate;
options = route.options;
matchValue = route.matchValue;
}else{
route = new Route(uriTemplate, options, matchValue);
}
// Verify the template doesn't re-use a variable name
const varnames = new Set;
route.tokens.forEach(function(token){
if(typeof token === 'string') return;
token.variableList.forEach(function(varspec){
if(varnames.has(varspec.varname)){
throw new Error('Duplicate variable name '+varspec.varname);
}
varnames.add(varspec.varname);
});
});
const fsm = route.fsm;
fsm.forEach(function(state){
if(!state.partials[uriTemplate]){
// state.partials[uriTemplate] = new PartialMatch(0, 9);
}
if(state.final){
state.final = [new FinalMatch(route)];
}
});
this.routeSet.add(route);
this.templateRouteMap.set(uriTemplate, route);
if(!this.valueRouteMap.has(matchValue)){
this.valueRouteMap.set(matchValue, route);
}
// Update the route tree that maintains ordering
// For every node, all of its children must be disjoint
// Scan through each of the children:
children: for(var current=this.hierarchy; current;){
const compares = current.children.map(v => compare([fsm, v.node.fsm]));
// 1. if new route is a subset of exactly one of them, then descend into that child.
for(var i=0; i<compares.length; i++){
if(compares[i][0]===false && compares[i][1]===true){
current = current.children[i];
continue children;
}
}
// 2. if the new route is a superset of any number of them (and disjoint with all others), then insert new route as a child and move all matching children underneath it.
const route_siblings = [], route_children = [];
for(var i=0; i<compares.length; i++){
if(compares[i][0]===true){
if(compares[i][1]===true){
// This is the same as an existing route
throw new Error('Inserted route '+uriTemplate+' is the same as other route '+current.children[i].uriTemplate);
}else{
// Move subsets into this route
route_children.push(current.children[i]);
}
}else if(compares[i][2]===true){
// Record disjoint nodes and make them siblings
route_siblings.push(current.children[i]);
}else{
throw new Error('Inserted route '+uriTemplate+' partially overlaps with other routes '+current.children[i].uriTemplate);
}
}
route_siblings.push({node: route, uriTemplate, children: route_children});
current.children = route_siblings;
break;
}
this.reindex();
return route;
};
Router.prototype.reindex = function reindex(){
// Update the sort index based on the hierarchy
const order_map = new Map;
var order = 0;
function visit(hierarchy){
hierarchy.children.forEach(visit);
if(hierarchy.node) order_map.set(hierarchy.node, order++);
}
visit(this.hierarchy);
this.fsm = union(this.routes.map(r => r.fsm), order_map);
};
// like resolveString, but additionally verify that the URI matches the legal HTTP form
// userinfo and fragment components are not allowed
// Router.prototype.resolveRequest = function resolveRequest(scheme, host, target, flags, initial_state){
// };
// like resolveString, but additionally verify that the URI matches the legal HTTP form
// userinfo and fragment components are not allowed
Router.prototype.resolveRequestURI = function resolveRequestURI(uri, flags, initial_state){
if(typeof uri!=='string') throw new Error('Expected arguments[0] `uri` to be a string');
// First verify the URI looks OK, save the components, then parse it normally
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
const scheme_m = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
if(!scheme_m) throw new Error('parseURI: `uri` missing valid scheme');
// const hierpart_m = uri.substring(scheme_m[0].length).match(/^\/\/(?:\x5b(?:[\x2e0-:a-f]*|v[0-9a-f]+\x2e[!\x24&-\x2e0-;=_a-z~]+)\x5d|(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\x2e(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\x2e(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\x2e(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])|(?:[\x2d\x2e0-9_a-z~]|%[0-9a-f][0-9a-f]|[!\x24&-,;=])*)(?::\d*)?/);
// URI appears to be valid, now resolve it normally
return this.resolveURI(uri, flags, initial_state);
};
// TODO rename this to `resolveString`
Router.prototype.resolveURI = function resolveString(uri, flags){
if(typeof uri!=='string') throw new Error('Expected arguments[0] `uri` to be a string');
const self = this;
// 0 is the initial state
var state = this.fsm[0];
if(!state) return;
const history = [{state}];
const pctenc = /^%[0-9A-F]{2}$/;
for(var offset = 0; state && offset < uri.length; offset++){
const symbol = uri[offset]==='%' ? uri.slice(offset, offset+3) : uri[offset];
// Double-check that pct-encoded sequences are valid (in addition to what the FSM should prohibit)
if(symbol.length===3){
if(!pctenc.test(symbol)){
return;
}
offset += 2;
}
const nextStateId = state.get(symbol);
if(nextStateId === undefined) return;
state = this.fsm[nextStateId];
if(!state) return;
history.push({symbol, nextStateId, state});
}
// With all of the characters parsed, the current "state" contains the solution
const solution = state.final[0];
if(!solution) return;
return new Result(self, uri, flags, history, state.final);
};