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

Commit

Permalink
Merge pull request #283 from elcobvg/feature/route-regexp
Browse files Browse the repository at this point in the history
Feature: use regexp in routes
  • Loading branch information
Rich-Harris authored Jul 2, 2018
2 parents 09b4dc1 + 4f011bf commit c0c717d
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 4 deletions.
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

0 comments on commit c0c717d

Please sign in to comment.