Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Commit c0c717d

Browse files
authored
Merge pull request #283 from elcobvg/feature/route-regexp
Feature: use regexp in routes
2 parents 09b4dc1 + 4f011bf commit c0c717d

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

src/core/create_routes.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
6868
(a_sub_part.content < b_sub_part.content ? -1 : 1)
6969
);
7070
}
71+
72+
// If both parts dynamic, check for regexp patterns
73+
if (a_sub_part.dynamic && b_sub_part.dynamic) {
74+
const regexp_pattern = /\((.*?)\)/;
75+
const a_match = regexp_pattern.exec(a_sub_part.content);
76+
const b_match = regexp_pattern.exec(b_sub_part.content);
77+
78+
if (!a_match && b_match) {
79+
return 1; // No regexp, so less specific than b
80+
}
81+
if (!b_match && a_match) {
82+
return -1;
83+
}
84+
if (a_match && b_match && a_match[1] !== b_match[1]) {
85+
return b_match[1].length - a_match[1].length;
86+
}
87+
}
7188
}
7289
}
7390

@@ -79,10 +96,18 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
7996
) || '_';
8097

8198
const params: string[] = [];
82-
const param_pattern = /\[([^\]]+)\]/g;
99+
const match_patterns: object = {};
100+
const param_pattern = /\[([^\(\]]+)(?:\((.+?)\))?\]/g;
83101
let match;
84102
while (match = param_pattern.exec(base)) {
85103
params.push(match[1]);
104+
if (typeof match[2] !== 'undefined') {
105+
if (/[\(\)\?\:]/.exec(match[2])) {
106+
throw new Error('Sapper does not allow (, ), ? or : in RegExp routes yet');
107+
}
108+
// Make a map of the regexp patterns
109+
match_patterns[match[1]] = `(${match[2]}?)`;
110+
}
86111
}
87112

88113
// TODO can we do all this with sub-parts? or does
@@ -95,7 +120,13 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
95120
const dynamic = ~part.indexOf('[');
96121

97122
if (dynamic) {
98-
const matcher = part.replace(param_pattern, `([^\/]+?)`);
123+
// Get keys from part and replace with stored match patterns
124+
const keys = part.replace(/\(.*?\)/, '').split(/[\[\]]/).filter((x, i) => { if (i % 2) return x });
125+
let matcher = part;
126+
keys.forEach(k => {
127+
const key_pattern = new RegExp('\\[' + k + '(?:\\((.+?)\\))?\\]');
128+
matcher = matcher.replace(key_pattern, match_patterns[k] || `([^/]+?)`);
129+
})
99130
pattern_string = nested ? `(?:\\/${matcher}${pattern_string})?` : `\\/${matcher}${pattern_string}`;
100131
} else {
101132
nested = false;
@@ -147,7 +178,7 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
147178
}
148179

149180
function get_sub_parts(part: string) {
150-
return part.split(/[\[\]]/)
181+
return part.split(/\[(.+)\]/)
151182
.map((content, i) => {
152183
if (!content) return null;
153184
return {

test/unit/create_routes.test.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ describe('create_routes', () => {
4545

4646
it('sorts routes correctly', () => {
4747
const routes = create_routes({
48-
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
48+
files: [
49+
'index.html',
50+
'about.html',
51+
'post/f[xx].html',
52+
'[wildcard].html',
53+
'post/foo.html',
54+
'post/[id].html',
55+
'post/bar.html',
56+
'post/[id].json.js',
57+
'post/[id([0-9-a-z]{3,})].html',
58+
]
4959
});
5060

5161
assert.deepEqual(
@@ -56,13 +66,33 @@ describe('create_routes', () => {
5666
'post/bar.html',
5767
'post/foo.html',
5868
'post/f[xx].html',
69+
'post/[id([0-9-a-z]{3,})].html', // RegExp is more specific
5970
'post/[id].json.js',
6071
'post/[id].html',
6172
'[wildcard].html'
6273
]
6374
);
6475
});
6576

77+
it('distinguishes and sorts regexp routes correctly', () => {
78+
const routes = create_routes({
79+
files: [
80+
'[slug].html',
81+
'[slug([a-z]{2})].html',
82+
'[slug([0-9-a-z]{3,})].html',
83+
]
84+
});
85+
86+
assert.deepEqual(
87+
routes.map(r => r.handlers[0].file),
88+
[
89+
'[slug([0-9-a-z]{3,})].html',
90+
'[slug([a-z]{2})].html',
91+
'[slug].html',
92+
]
93+
);
94+
});
95+
6696
it('prefers index page to nested route', () => {
6797
let routes = create_routes({
6898
files: [
@@ -131,6 +161,24 @@ describe('create_routes', () => {
131161
'api/blog/[slug].js',
132162
]
133163
);
164+
165+
// RegExp routes
166+
routes = create_routes({
167+
files: [
168+
'blog/[slug].html',
169+
'blog/index.html',
170+
'blog/[slug([^0-9]+)].html',
171+
]
172+
});
173+
174+
assert.deepEqual(
175+
routes.map(r => r.handlers[0].file),
176+
[
177+
'blog/index.html',
178+
'blog/[slug([^0-9]+)].html',
179+
'blog/[slug].html',
180+
]
181+
);
134182
});
135183

136184
it('generates params', () => {
@@ -204,8 +252,15 @@ describe('create_routes', () => {
204252
files: ['[foo].html', '[bar]/index.html']
205253
});
206254
}, /The \[foo\] and \[bar\]\/index routes clash/);
255+
256+
assert.throws(() => {
257+
create_routes({
258+
files: ['[foo([0-9-a-z]+)].html', '[bar([0-9-a-z]+)]/index.html']
259+
});
260+
}, /The \[foo\(\[0-9-a-z\]\+\)\] and \[bar\(\[0-9-a-z\]\+\)\]\/index routes clash/);
207261
});
208262

263+
209264
it('matches nested routes', () => {
210265
const route = create_routes({
211266
files: ['settings/[submenu].html']
@@ -281,6 +336,14 @@ describe('create_routes', () => {
281336
}, /Invalid route \[foo\]\[bar\]\.js parameters must be separated/);
282337
});
283338

339+
it('errors when trying to use reserved characters in route regexp', () => {
340+
assert.throws(() => {
341+
create_routes({
342+
files: ['[lang([a-z]{2}(?:-[a-z]{2,4})?)]']
343+
});
344+
}, /Sapper does not allow \(, \), \? or \: in RegExp routes yet/);
345+
});
346+
284347
it('errors on 4xx.html', () => {
285348
assert.throws(() => {
286349
create_routes({

0 commit comments

Comments
 (0)