@shuvi/router 0.0.1-pre.1 → 0.0.1-pre.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/createRoutesFromArray.d.ts +2 -0
- package/esm/createRoutesFromArray.js +9 -0
- package/esm/getRedirectFromRoutes.d.ts +2 -0
- package/esm/getRedirectFromRoutes.js +8 -0
- package/esm/history/base.d.ts +53 -0
- package/esm/history/base.js +80 -0
- package/esm/history/browser.d.ts +12 -0
- package/esm/history/browser.js +104 -0
- package/esm/history/hash.d.ts +13 -0
- package/esm/history/hash.js +135 -0
- package/esm/history/index.d.ts +7 -0
- package/esm/history/index.js +13 -0
- package/esm/history/memory.d.ts +22 -0
- package/esm/history/memory.js +75 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.js +7 -0
- package/esm/matchPathname.d.ts +15 -0
- package/esm/matchPathname.js +54 -0
- package/esm/matchRoutes.d.ts +6 -0
- package/esm/matchRoutes.js +92 -0
- package/esm/pathParserRanker.d.ts +65 -0
- package/esm/pathParserRanker.js +223 -0
- package/esm/pathTokenizer.d.ts +23 -0
- package/esm/pathTokenizer.js +149 -0
- package/esm/router.d.ts +10 -0
- package/esm/router.js +207 -0
- package/esm/types/history.d.ts +176 -0
- package/esm/types/history.js +0 -0
- package/esm/types/index.d.ts +2 -0
- package/esm/types/index.js +1 -0
- package/esm/types/router.d.ts +78 -0
- package/esm/types/router.js +0 -0
- package/esm/utils/async.d.ts +2 -0
- package/esm/utils/async.js +18 -0
- package/esm/utils/createRedirector.d.ts +8 -0
- package/esm/utils/createRedirector.js +30 -0
- package/esm/utils/dom.d.ts +1 -0
- package/esm/utils/dom.js +1 -0
- package/esm/utils/error.d.ts +1 -0
- package/esm/utils/error.js +3 -0
- package/esm/utils/extract-hooks.d.ts +3 -0
- package/esm/utils/extract-hooks.js +16 -0
- package/esm/utils/history.d.ts +12 -0
- package/esm/utils/history.js +51 -0
- package/esm/utils/index.d.ts +4 -0
- package/esm/utils/index.js +4 -0
- package/esm/utils/misc.d.ts +11 -0
- package/esm/utils/misc.js +41 -0
- package/esm/utils/path.d.ts +13 -0
- package/esm/utils/path.js +101 -0
- package/package.json +5 -4
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { matchPathname } from './matchPathname';
|
|
2
|
+
import { joinPaths, resolvePath } from './utils';
|
|
3
|
+
import { tokensToParser, comparePathParserScore } from './pathParserRanker';
|
|
4
|
+
import { tokenizePath } from './pathTokenizer';
|
|
5
|
+
function matchRouteBranch(branch, pathname) {
|
|
6
|
+
let routes = branch[1];
|
|
7
|
+
let matchedPathname = '/';
|
|
8
|
+
let matchedParams = {};
|
|
9
|
+
let matches = [];
|
|
10
|
+
for (let i = 0; i < routes.length; ++i) {
|
|
11
|
+
let route = routes[i];
|
|
12
|
+
let remainingPathname = matchedPathname === '/'
|
|
13
|
+
? pathname
|
|
14
|
+
: pathname.slice(matchedPathname.length) || '/';
|
|
15
|
+
let routeMatch = matchPathname({
|
|
16
|
+
path: route.path,
|
|
17
|
+
caseSensitive: route.caseSensitive,
|
|
18
|
+
end: i === routes.length - 1
|
|
19
|
+
}, remainingPathname);
|
|
20
|
+
if (!routeMatch)
|
|
21
|
+
return null;
|
|
22
|
+
matchedPathname = joinPaths([matchedPathname, routeMatch.pathname]);
|
|
23
|
+
matchedParams = Object.assign(Object.assign({}, matchedParams), routeMatch.params);
|
|
24
|
+
matches.push({
|
|
25
|
+
route,
|
|
26
|
+
pathname: matchedPathname,
|
|
27
|
+
params: Object.freeze(matchedParams)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return matches;
|
|
31
|
+
}
|
|
32
|
+
export function rankRouteBranches(branches) {
|
|
33
|
+
if (branches.length <= 1) {
|
|
34
|
+
return branches;
|
|
35
|
+
}
|
|
36
|
+
const normalizedPaths = branches.map((branch, index) => {
|
|
37
|
+
const [path] = branch;
|
|
38
|
+
return Object.assign(Object.assign({}, tokensToParser(tokenizePath(path))), { path,
|
|
39
|
+
index });
|
|
40
|
+
});
|
|
41
|
+
normalizedPaths.sort((a, b) => comparePathParserScore(a, b));
|
|
42
|
+
const newBranches = [];
|
|
43
|
+
// console.log(
|
|
44
|
+
// normalizedPaths
|
|
45
|
+
// .map(parser => `${parser.path} -> ${JSON.stringify(parser.score)}`)
|
|
46
|
+
// .join('\n')
|
|
47
|
+
// )
|
|
48
|
+
normalizedPaths.forEach((branch, newBranchesIndex) => {
|
|
49
|
+
const { index } = branch;
|
|
50
|
+
newBranches[newBranchesIndex] = branches[index];
|
|
51
|
+
});
|
|
52
|
+
return newBranches;
|
|
53
|
+
}
|
|
54
|
+
function flattenRoutes(routes, branches = [], parentPath = '', parentRoutes = [], parentIndexes = []) {
|
|
55
|
+
routes.forEach((route, index) => {
|
|
56
|
+
let path = joinPaths([parentPath, route.path]);
|
|
57
|
+
let routes = parentRoutes.concat(route);
|
|
58
|
+
let indexes = parentIndexes.concat(index);
|
|
59
|
+
// Add the children before adding this route to the array so we traverse the
|
|
60
|
+
// route tree depth-first and child routes appear before their parents in
|
|
61
|
+
// the "flattened" version.
|
|
62
|
+
if (route.children) {
|
|
63
|
+
flattenRoutes(route.children, branches, path, routes, indexes);
|
|
64
|
+
}
|
|
65
|
+
branches.push([path, routes, indexes]);
|
|
66
|
+
});
|
|
67
|
+
return branches;
|
|
68
|
+
}
|
|
69
|
+
export function matchRoutes(routes, location, basename = '') {
|
|
70
|
+
if (typeof location === 'string') {
|
|
71
|
+
location = resolvePath(location);
|
|
72
|
+
}
|
|
73
|
+
let pathname = location.pathname || '/';
|
|
74
|
+
if (basename) {
|
|
75
|
+
let base = basename.replace(/^\/*/, '/').replace(/\/+$/, '');
|
|
76
|
+
if (pathname.startsWith(base)) {
|
|
77
|
+
pathname = pathname === base ? '/' : pathname.slice(base.length);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Pathname does not start with the basename, no match.
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let branches = flattenRoutes(routes);
|
|
85
|
+
branches = rankRouteBranches(branches);
|
|
86
|
+
let matches = null;
|
|
87
|
+
for (let i = 0; matches == null && i < branches.length; ++i) {
|
|
88
|
+
// TODO: Match on search, state too?
|
|
89
|
+
matches = matchRouteBranch(branches[i], pathname);
|
|
90
|
+
}
|
|
91
|
+
return matches;
|
|
92
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Token } from './pathTokenizer';
|
|
2
|
+
declare type PathParams = Record<string, string | string[]>;
|
|
3
|
+
export declare type MatchPathParams = {
|
|
4
|
+
match: string;
|
|
5
|
+
params: PathParams;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A param in a url like `/users/:id`
|
|
9
|
+
*/
|
|
10
|
+
export interface PathParserParamKey {
|
|
11
|
+
name: string;
|
|
12
|
+
repeatable: boolean;
|
|
13
|
+
optional: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PathParser {
|
|
16
|
+
/**
|
|
17
|
+
* The regexp used to match a url
|
|
18
|
+
*/
|
|
19
|
+
re: RegExp;
|
|
20
|
+
/**
|
|
21
|
+
* The score of the parser
|
|
22
|
+
*/
|
|
23
|
+
score: Array<number[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Keys that appeared in the path
|
|
26
|
+
*/
|
|
27
|
+
keys: PathParserParamKey[];
|
|
28
|
+
/**
|
|
29
|
+
* Parses a url and returns the matched params or nul if it doesn't match. An
|
|
30
|
+
* optional param that isn't preset will be an empty string. A repeatable
|
|
31
|
+
* param will be an array if there is at least one value.
|
|
32
|
+
*
|
|
33
|
+
* @param path - url to parse
|
|
34
|
+
* @returns a Params object, empty if there are no params. `null` if there is
|
|
35
|
+
* no match
|
|
36
|
+
*/
|
|
37
|
+
parse(path: string): MatchPathParams | null;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a string version of the url
|
|
40
|
+
*
|
|
41
|
+
* @param params - object of params
|
|
42
|
+
* @returns a url
|
|
43
|
+
*/
|
|
44
|
+
stringify(params: PathParams): string;
|
|
45
|
+
}
|
|
46
|
+
export declare type PathParserOptions = Pick<_PathParserOptions, 'end' | 'sensitive' | 'strict'>;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a path parser from an array of Segments (a segment is an array of Tokens)
|
|
49
|
+
*
|
|
50
|
+
* @param segments - array of segments returned by tokenizePath
|
|
51
|
+
* @param extraOptions - optional options for the regexp
|
|
52
|
+
* @returns a PathParser
|
|
53
|
+
*/
|
|
54
|
+
export declare function tokensToParser(segments: Array<Token[]>, extraOptions?: _PathParserOptions): PathParser;
|
|
55
|
+
/**
|
|
56
|
+
* Compare function that can be used with `sort` to sort an array of PathParser
|
|
57
|
+
* @param a - first PathParser
|
|
58
|
+
* @param b - second PathParser
|
|
59
|
+
* @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b, last sort by index
|
|
60
|
+
*/
|
|
61
|
+
declare type PathParserIndex = PathParser & {
|
|
62
|
+
index: number;
|
|
63
|
+
};
|
|
64
|
+
export declare function comparePathParserScore(a: PathParserIndex, b: PathParserIndex): number;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// default pattern for a param: non greedy everything but /
|
|
2
|
+
const BASE_PARAM_PATTERN = '[^/]+?';
|
|
3
|
+
const BASE_PATH_PARSER_OPTIONS = {
|
|
4
|
+
sensitive: false,
|
|
5
|
+
strict: false,
|
|
6
|
+
start: true,
|
|
7
|
+
end: true,
|
|
8
|
+
};
|
|
9
|
+
// Special Regex characters that must be escaped in static tokens
|
|
10
|
+
const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a path parser from an array of Segments (a segment is an array of Tokens)
|
|
13
|
+
*
|
|
14
|
+
* @param segments - array of segments returned by tokenizePath
|
|
15
|
+
* @param extraOptions - optional options for the regexp
|
|
16
|
+
* @returns a PathParser
|
|
17
|
+
*/
|
|
18
|
+
export function tokensToParser(segments, extraOptions) {
|
|
19
|
+
const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
|
|
20
|
+
// the amount of scores is the same as the length of segments except for the root segment "/"
|
|
21
|
+
let score = [];
|
|
22
|
+
// the regexp as a string
|
|
23
|
+
let pattern = options.start ? '^' : '';
|
|
24
|
+
// extracted keys
|
|
25
|
+
const keys = [];
|
|
26
|
+
for (const segment of segments) {
|
|
27
|
+
// the root segment needs special treatment
|
|
28
|
+
const segmentScores = segment.length ? [] : [90 /* Root */];
|
|
29
|
+
// allow trailing slash
|
|
30
|
+
if (options.strict && !segment.length)
|
|
31
|
+
pattern += '/';
|
|
32
|
+
for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
|
|
33
|
+
const token = segment[tokenIndex];
|
|
34
|
+
// resets the score if we are inside a sub segment /:a-other-:b
|
|
35
|
+
let subSegmentScore = 40 /* Segment */ +
|
|
36
|
+
(options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
|
|
37
|
+
if (token.type === 0 /* Static */) {
|
|
38
|
+
// prepend the slash if we are starting a new segment
|
|
39
|
+
if (!tokenIndex)
|
|
40
|
+
pattern += '/';
|
|
41
|
+
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
|
|
42
|
+
subSegmentScore += 40 /* Static */;
|
|
43
|
+
}
|
|
44
|
+
else if (token.type === 1 /* Param */) {
|
|
45
|
+
const { value, repeatable, optional, regexp } = token;
|
|
46
|
+
keys.push({
|
|
47
|
+
name: value,
|
|
48
|
+
repeatable,
|
|
49
|
+
optional,
|
|
50
|
+
});
|
|
51
|
+
const re = regexp ? regexp : BASE_PARAM_PATTERN;
|
|
52
|
+
// the user provided a custom regexp /:id(\\d+)
|
|
53
|
+
if (re !== BASE_PARAM_PATTERN) {
|
|
54
|
+
subSegmentScore += 10 /* BonusCustomRegExp */;
|
|
55
|
+
// make sure the regexp is valid before using it
|
|
56
|
+
try {
|
|
57
|
+
new RegExp(`(${re})`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
|
|
61
|
+
err.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// when we repeat we must take care of the repeating leading slash
|
|
65
|
+
let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
|
|
66
|
+
// prepend the slash if we are starting a new segment
|
|
67
|
+
if (!tokenIndex)
|
|
68
|
+
subPattern =
|
|
69
|
+
// avoid an optional / if there are more segments e.g. /:p?-static
|
|
70
|
+
// or /:p?-:p2
|
|
71
|
+
optional && segment.length < 2
|
|
72
|
+
? `(?:/${subPattern})`
|
|
73
|
+
: '/' + subPattern;
|
|
74
|
+
if (optional)
|
|
75
|
+
subPattern += '?';
|
|
76
|
+
pattern += subPattern;
|
|
77
|
+
if (!options.end)
|
|
78
|
+
pattern += '(?=\/|$)';
|
|
79
|
+
subSegmentScore += 20 /* Dynamic */;
|
|
80
|
+
if (optional)
|
|
81
|
+
subSegmentScore += -8 /* BonusOptional */;
|
|
82
|
+
if (repeatable)
|
|
83
|
+
subSegmentScore += -20 /* BonusRepeatable */;
|
|
84
|
+
if (re === '.*')
|
|
85
|
+
subSegmentScore += -50 /* BonusWildcard */;
|
|
86
|
+
}
|
|
87
|
+
segmentScores.push(subSegmentScore);
|
|
88
|
+
}
|
|
89
|
+
// an empty array like /home/ -> [[{home}], []]
|
|
90
|
+
// if (!segment.length) pattern += '/'
|
|
91
|
+
score.push(segmentScores);
|
|
92
|
+
}
|
|
93
|
+
// only apply the strict bonus to the last score
|
|
94
|
+
if (options.strict && options.end) {
|
|
95
|
+
const i = score.length - 1;
|
|
96
|
+
score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
|
|
97
|
+
}
|
|
98
|
+
// TODO: dev only warn double trailing slash
|
|
99
|
+
if (!options.strict)
|
|
100
|
+
pattern += '/*?';
|
|
101
|
+
if (options.end)
|
|
102
|
+
pattern += '$';
|
|
103
|
+
// allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
|
|
104
|
+
else if (options.strict)
|
|
105
|
+
pattern += '(?:/*|$)';
|
|
106
|
+
const re = new RegExp(pattern, options.sensitive ? '' : 'i');
|
|
107
|
+
function parse(path) {
|
|
108
|
+
const match = path.match(re);
|
|
109
|
+
const params = {};
|
|
110
|
+
if (!match)
|
|
111
|
+
return null;
|
|
112
|
+
for (let i = 1; i < match.length; i++) {
|
|
113
|
+
const value = match[i] || '';
|
|
114
|
+
const key = keys[i - 1];
|
|
115
|
+
params[key.name] = value && key.repeatable ? value.split('/') : value;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
match: match[0],
|
|
119
|
+
params,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function stringify(params) {
|
|
123
|
+
let path = '';
|
|
124
|
+
// for optional parameters to allow to be empty
|
|
125
|
+
let avoidDuplicatedSlash = false;
|
|
126
|
+
for (const segment of segments) {
|
|
127
|
+
if (!avoidDuplicatedSlash || !path.endsWith('/'))
|
|
128
|
+
path += '/';
|
|
129
|
+
avoidDuplicatedSlash = false;
|
|
130
|
+
for (const token of segment) {
|
|
131
|
+
if (token.type === 0 /* Static */) {
|
|
132
|
+
path += token.value;
|
|
133
|
+
}
|
|
134
|
+
else if (token.type === 1 /* Param */) {
|
|
135
|
+
const { value, repeatable, optional } = token;
|
|
136
|
+
const param = params[value];
|
|
137
|
+
if (Array.isArray(param) && !repeatable)
|
|
138
|
+
throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
|
|
139
|
+
if (param === undefined && !optional) {
|
|
140
|
+
throw new Error(`Missing required param "${value}"`);
|
|
141
|
+
}
|
|
142
|
+
const text = Array.isArray(param) ? param.join('/') : (param || '');
|
|
143
|
+
if (!text && optional) {
|
|
144
|
+
// if we have more than one optional param like /:a?-static we
|
|
145
|
+
// don't need to care about the optional param
|
|
146
|
+
if (segment.length < 2) {
|
|
147
|
+
// remove the last slash as we could be at the end
|
|
148
|
+
if (path.endsWith('/'))
|
|
149
|
+
path = path.slice(0, -1);
|
|
150
|
+
// do not append a slash on the next iteration
|
|
151
|
+
else
|
|
152
|
+
avoidDuplicatedSlash = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
path += text;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return path;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
re,
|
|
163
|
+
score,
|
|
164
|
+
keys,
|
|
165
|
+
parse,
|
|
166
|
+
stringify,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Compares an array of numbers as used in PathParser.score and returns a
|
|
171
|
+
* number. This function can be used to `sort` an array
|
|
172
|
+
* @param a - first array of numbers
|
|
173
|
+
* @param b - second array of numbers
|
|
174
|
+
* @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
|
|
175
|
+
* should be sorted first
|
|
176
|
+
*/
|
|
177
|
+
function compareScoreArray(a, b) {
|
|
178
|
+
let i = 0;
|
|
179
|
+
while (i < a.length && i < b.length) {
|
|
180
|
+
const diff = b[i] - a[i];
|
|
181
|
+
// only keep going if diff === 0
|
|
182
|
+
if (diff)
|
|
183
|
+
return diff;
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
// if the last subsegment was Static, the shorter segments should be sorted first
|
|
187
|
+
// otherwise sort the longest segment first
|
|
188
|
+
if (a.length < b.length) {
|
|
189
|
+
return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
|
|
190
|
+
? -1
|
|
191
|
+
: 1;
|
|
192
|
+
}
|
|
193
|
+
else if (a.length > b.length) {
|
|
194
|
+
return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
|
|
195
|
+
? 1
|
|
196
|
+
: -1;
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
export function comparePathParserScore(a, b) {
|
|
201
|
+
let i = 0;
|
|
202
|
+
const aScore = a.score;
|
|
203
|
+
const bScore = b.score;
|
|
204
|
+
while (i < aScore.length && i < bScore.length) {
|
|
205
|
+
const comp = compareScoreArray(aScore[i], bScore[i]);
|
|
206
|
+
// do not return if both are equal
|
|
207
|
+
if (comp)
|
|
208
|
+
return comp;
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
// if a and b share the same score entries but b has more, sort b first
|
|
212
|
+
const lengthDiff = bScore.length - aScore.length;
|
|
213
|
+
if (lengthDiff)
|
|
214
|
+
return lengthDiff;
|
|
215
|
+
// this is the ternary version
|
|
216
|
+
// return aScore.length < bScore.length
|
|
217
|
+
// ? 1
|
|
218
|
+
// : aScore.length > bScore.length
|
|
219
|
+
// ? -1
|
|
220
|
+
// : 0
|
|
221
|
+
//
|
|
222
|
+
return a.index - b.index;
|
|
223
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const enum TokenType {
|
|
2
|
+
Static = 0,
|
|
3
|
+
Param = 1,
|
|
4
|
+
Group = 2
|
|
5
|
+
}
|
|
6
|
+
interface TokenStatic {
|
|
7
|
+
type: TokenType.Static;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
interface TokenParam {
|
|
11
|
+
type: TokenType.Param;
|
|
12
|
+
regexp?: string;
|
|
13
|
+
value: string;
|
|
14
|
+
optional: boolean;
|
|
15
|
+
repeatable: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface TokenGroup {
|
|
18
|
+
type: TokenType.Group;
|
|
19
|
+
value: Exclude<Token, TokenGroup>[];
|
|
20
|
+
}
|
|
21
|
+
export declare type Token = TokenStatic | TokenParam | TokenGroup;
|
|
22
|
+
export declare function tokenizePath(path: string): Array<Token[]>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const ROOT_TOKEN = {
|
|
2
|
+
type: 0 /* Static */,
|
|
3
|
+
value: ''
|
|
4
|
+
};
|
|
5
|
+
const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
|
|
6
|
+
// After some profiling, the cache seems to be unnecessary because tokenizePath
|
|
7
|
+
// (the slowest part of adding a route) is very fast
|
|
8
|
+
// const tokenCache = new Map<string, Token[][]>()
|
|
9
|
+
export function tokenizePath(path) {
|
|
10
|
+
if (!path)
|
|
11
|
+
return [[]];
|
|
12
|
+
if (path === '/')
|
|
13
|
+
return [[ROOT_TOKEN]];
|
|
14
|
+
if (!path.startsWith('/')) {
|
|
15
|
+
path = path.replace(/^\/*/, '/'); // Make sure it has a leading /
|
|
16
|
+
}
|
|
17
|
+
// if (tokenCache.has(path)) return tokenCache.get(path)!
|
|
18
|
+
function crash(message) {
|
|
19
|
+
throw new Error(`ERR (${state})/"${buffer}": ${message}`);
|
|
20
|
+
}
|
|
21
|
+
let state = 0 /* Static */;
|
|
22
|
+
let previousState = state;
|
|
23
|
+
const tokens = [];
|
|
24
|
+
// the segment will always be valid because we get into the initial state
|
|
25
|
+
// with the leading /
|
|
26
|
+
let segment;
|
|
27
|
+
function finalizeSegment() {
|
|
28
|
+
if (segment)
|
|
29
|
+
tokens.push(segment);
|
|
30
|
+
segment = [];
|
|
31
|
+
}
|
|
32
|
+
// index on the path
|
|
33
|
+
let i = 0;
|
|
34
|
+
// char at index
|
|
35
|
+
let char;
|
|
36
|
+
// buffer of the value read
|
|
37
|
+
let buffer = '';
|
|
38
|
+
// custom regexp for a param
|
|
39
|
+
let customRe = '';
|
|
40
|
+
function consumeBuffer() {
|
|
41
|
+
if (!buffer)
|
|
42
|
+
return;
|
|
43
|
+
if (state === 0 /* Static */) {
|
|
44
|
+
segment.push({
|
|
45
|
+
type: 0 /* Static */,
|
|
46
|
+
value: buffer
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else if (state === 1 /* Param */ ||
|
|
50
|
+
state === 2 /* ParamRegExp */ ||
|
|
51
|
+
state === 3 /* ParamRegExpEnd */) {
|
|
52
|
+
if (segment.length > 1 && (char === '*' || char === '+'))
|
|
53
|
+
crash(`A repeatable param (${buffer}) must be alone in its segment. `);
|
|
54
|
+
segment.push({
|
|
55
|
+
type: 1 /* Param */,
|
|
56
|
+
value: buffer,
|
|
57
|
+
regexp: customRe,
|
|
58
|
+
repeatable: char === '*' || char === '+',
|
|
59
|
+
optional: char === '*' || char === '?'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
crash('Invalid state to consume buffer');
|
|
64
|
+
}
|
|
65
|
+
buffer = '';
|
|
66
|
+
}
|
|
67
|
+
function addCharToBuffer() {
|
|
68
|
+
buffer += char;
|
|
69
|
+
}
|
|
70
|
+
while (i < path.length) {
|
|
71
|
+
char = path[i++];
|
|
72
|
+
if (char === '\\' && state !== 2 /* ParamRegExp */) {
|
|
73
|
+
previousState = state;
|
|
74
|
+
state = 4 /* EscapeNext */;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
switch (state) {
|
|
78
|
+
case 0 /* Static */:
|
|
79
|
+
if (char === '/') {
|
|
80
|
+
if (buffer) {
|
|
81
|
+
consumeBuffer();
|
|
82
|
+
}
|
|
83
|
+
finalizeSegment();
|
|
84
|
+
}
|
|
85
|
+
else if (char === ':') {
|
|
86
|
+
consumeBuffer();
|
|
87
|
+
state = 1 /* Param */;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
addCharToBuffer();
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case 4 /* EscapeNext */:
|
|
94
|
+
addCharToBuffer();
|
|
95
|
+
state = previousState;
|
|
96
|
+
break;
|
|
97
|
+
case 1 /* Param */:
|
|
98
|
+
if (char === '(') {
|
|
99
|
+
state = 2 /* ParamRegExp */;
|
|
100
|
+
}
|
|
101
|
+
else if (VALID_PARAM_RE.test(char)) {
|
|
102
|
+
addCharToBuffer();
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
consumeBuffer();
|
|
106
|
+
state = 0 /* Static */;
|
|
107
|
+
// go back one character if we were not modifying
|
|
108
|
+
if (char !== '*' && char !== '?' && char !== '+')
|
|
109
|
+
i--;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case 2 /* ParamRegExp */:
|
|
113
|
+
// TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
|
|
114
|
+
// it already works by escaping the closing )
|
|
115
|
+
// https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
|
|
116
|
+
// is this really something people need since you can also write
|
|
117
|
+
// /prefix_:p()_suffix
|
|
118
|
+
if (char === ')') {
|
|
119
|
+
// handle the escaped )
|
|
120
|
+
if (customRe[customRe.length - 1] == '\\')
|
|
121
|
+
customRe = customRe.slice(0, -1) + char;
|
|
122
|
+
else
|
|
123
|
+
state = 3 /* ParamRegExpEnd */;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
customRe += char;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case 3 /* ParamRegExpEnd */:
|
|
130
|
+
// same as finalizing a param
|
|
131
|
+
consumeBuffer();
|
|
132
|
+
state = 0 /* Static */;
|
|
133
|
+
// go back one character if we were not modifying
|
|
134
|
+
if (char !== '*' && char !== '?' && char !== '+')
|
|
135
|
+
i--;
|
|
136
|
+
customRe = '';
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
crash('Unknown state');
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (state === 2 /* ParamRegExp */)
|
|
144
|
+
crash(`Unfinished custom RegExp for param "${buffer}"`);
|
|
145
|
+
consumeBuffer();
|
|
146
|
+
finalizeSegment();
|
|
147
|
+
// tokenCache.set(path, tokens)
|
|
148
|
+
return tokens;
|
|
149
|
+
}
|
package/esm/router.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IRouter, IRouteRecord, IPartialRouteRecord } from './types';
|
|
2
|
+
import History from './history/base';
|
|
3
|
+
interface IRouterOptions<RouteRecord extends IPartialRouteRecord> {
|
|
4
|
+
history: History;
|
|
5
|
+
routes: RouteRecord[];
|
|
6
|
+
caseSensitive?: boolean;
|
|
7
|
+
basename?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const createRouter: <RouteRecord extends IRouteRecord<any>>(options: IRouterOptions<RouteRecord>) => IRouter<RouteRecord>;
|
|
10
|
+
export {};
|