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

Feature: use regexp in routes #283

Merged
merged 5 commits into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/core/create_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
(a_sub_part.content < b_sub_part.content ? -1 : 1)
);
}

// If both parts dynamic, check for regexp patterns
if (a_sub_part.dynamic && b_sub_part.dynamic) {
const regexp_pattern = /\((.*?)\)/;
const a_match = regexp_pattern.exec(a_sub_part.content);
const b_match = regexp_pattern.exec(b_sub_part.content);

if (!a_match && b_match) {
return 1; // No regexp, so less specific than b
}
if (!b_match && a_match) {
return -1;
}
if (a_match && b_match && a_match[1] !== b_match[1]) {
return b_match[1].length - a_match[1].length;
}
}
}
}

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

const params: string[] = [];
const param_pattern = /\[([^\]]+)\]/g;
const match_patterns: object = {};
const param_pattern = /\[([^\(\]]+)(?:\((.+?)\))?\]/g;
let match;
while (match = param_pattern.exec(base)) {
params.push(match[1]);
if (typeof match[2] !== 'undefined') {
if (/[\(\)\?\:]/.exec(match[2])) {
throw new Error('Sapper does not allow (, ), ? or : in RegExp routes yet');
}
// Make a map of the regexp patterns
match_patterns[match[1]] = `(${match[2]}?)`;
}
}

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

if (dynamic) {
const matcher = part.replace(param_pattern, `([^\/]+?)`);
// Get keys from part and replace with stored match patterns
const keys = part.replace(/\(.*?\)/, '').split(/[\[\]]/).filter((x, i) => { if (i % 2) return x });
let matcher = part;
keys.forEach(k => {
const key_pattern = new RegExp('\\[' + k + '(?:\\((.+?)\\))?\\]');
matcher = matcher.replace(key_pattern, match_patterns[k] || `([^/]+?)`);
})
pattern_string = nested ? `(?:\\/${matcher}${pattern_string})?` : `\\/${matcher}${pattern_string}`;
} else {
nested = false;
Expand Down Expand Up @@ -147,7 +178,7 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
}

function get_sub_parts(part: string) {
return part.split(/[\[\]]/)
return part.split(/\[(.+)\]/)
.map((content, i) => {
if (!content) return null;
return {
Expand Down
65 changes: 64 additions & 1 deletion test/unit/create_routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ describe('create_routes', () => {

it('sorts routes correctly', () => {
const routes = create_routes({
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
files: [
'index.html',
'about.html',
'post/f[xx].html',
'[wildcard].html',
'post/foo.html',
'post/[id].html',
'post/bar.html',
'post/[id].json.js',
'post/[id([0-9-a-z]{3,})].html',
]
});

assert.deepEqual(
Expand All @@ -56,13 +66,33 @@ describe('create_routes', () => {
'post/bar.html',
'post/foo.html',
'post/f[xx].html',
'post/[id([0-9-a-z]{3,})].html', // RegExp is more specific
'post/[id].json.js',
'post/[id].html',
'[wildcard].html'
]
);
});

it('distinguishes and sorts regexp routes correctly', () => {
const routes = create_routes({
files: [
'[slug].html',
'[slug([a-z]{2})].html',
'[slug([0-9-a-z]{3,})].html',
]
});

assert.deepEqual(
routes.map(r => r.handlers[0].file),
[
'[slug([0-9-a-z]{3,})].html',
'[slug([a-z]{2})].html',
'[slug].html',
]
);
});

it('prefers index page to nested route', () => {
let routes = create_routes({
files: [
Expand Down Expand Up @@ -131,6 +161,24 @@ describe('create_routes', () => {
'api/blog/[slug].js',
]
);

// RegExp routes
routes = create_routes({
files: [
'blog/[slug].html',
'blog/index.html',
'blog/[slug([^0-9]+)].html',
]
});

assert.deepEqual(
routes.map(r => r.handlers[0].file),
[
'blog/index.html',
'blog/[slug([^0-9]+)].html',
'blog/[slug].html',
]
);
});

it('generates params', () => {
Expand Down Expand Up @@ -204,8 +252,15 @@ describe('create_routes', () => {
files: ['[foo].html', '[bar]/index.html']
});
}, /The \[foo\] and \[bar\]\/index routes clash/);

assert.throws(() => {
create_routes({
files: ['[foo([0-9-a-z]+)].html', '[bar([0-9-a-z]+)]/index.html']
});
}, /The \[foo\(\[0-9-a-z\]\+\)\] and \[bar\(\[0-9-a-z\]\+\)\]\/index routes clash/);
});


it('matches nested routes', () => {
const route = create_routes({
files: ['settings/[submenu].html']
Expand Down Expand Up @@ -281,6 +336,14 @@ describe('create_routes', () => {
}, /Invalid route \[foo\]\[bar\]\.js — parameters must be separated/);
});

it('errors when trying to use reserved characters in route regexp', () => {
assert.throws(() => {
create_routes({
files: ['[lang([a-z]{2}(?:-[a-z]{2,4})?)]']
});
}, /Sapper does not allow \(, \), \? or \: in RegExp routes yet/);
});

it('errors on 4xx.html', () => {
assert.throws(() => {
create_routes({
Expand Down