diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue
index 170d73919e..c5ddd79567 100644
--- a/packages/playground/src/App.vue
+++ b/packages/playground/src/App.vue
@@ -158,6 +158,16 @@
/p_1/absolute-a
+
+ /entity/section:aaabbbccc
+
+
+ /entity/sectionaaabbbccc
+
diff --git a/packages/playground/src/router.ts b/packages/playground/src/router.ts
index 981b4192f6..3238884f48 100644
--- a/packages/playground/src/router.ts
+++ b/packages/playground/src/router.ts
@@ -84,6 +84,11 @@ export const router = createRouter({
},
{ path: '/with-data', component: ComponentWithData, name: 'WithData' },
{ path: '/rep/:a*', component: RepeatedParams, name: 'repeat' },
+ {
+ path: '/entity/:entityType([^:]+)\\::entityID',
+ name: 'entity',
+ component,
+ },
{ path: '/:data(.*)', component: NotFound, name: 'NotFound' },
{
path: '/nested',
diff --git a/packages/router/__tests__/matcher/pathParser.spec.ts b/packages/router/__tests__/matcher/pathParser.spec.ts
index b7b9969b1b..dae0f4c043 100644
--- a/packages/router/__tests__/matcher/pathParser.spec.ts
+++ b/packages/router/__tests__/matcher/pathParser.spec.ts
@@ -30,6 +30,58 @@ describe('Path parser', () => {
])
})
+ it('escapes : after param', () => {
+ expect(tokenizePath('/:foo\\:abc')).toEqual([
+ [
+ {
+ type: TokenType.Param,
+ value: 'foo',
+ regexp: '',
+ repeatable: false,
+ optional: false,
+ },
+ { type: TokenType.Static, value: ':abc' },
+ ],
+ ])
+ })
+
+ it('escapes : after param with custom re', () => {
+ expect(tokenizePath('/:foo([^:]+)\\:abc')).toEqual([
+ [
+ {
+ type: TokenType.Param,
+ value: 'foo',
+ regexp: '[^:]+',
+ repeatable: false,
+ optional: false,
+ },
+ { type: TokenType.Static, value: ':abc' },
+ ],
+ ])
+ })
+
+ it('escapes : between two params', () => {
+ expect(tokenizePath('/:foo([^:]+)\\::bar')).toEqual([
+ [
+ {
+ type: TokenType.Param,
+ value: 'foo',
+ regexp: '[^:]+',
+ repeatable: false,
+ optional: false,
+ },
+ { type: TokenType.Static, value: ':' },
+ {
+ type: TokenType.Param,
+ value: 'bar',
+ regexp: '',
+ repeatable: false,
+ optional: false,
+ },
+ ],
+ ])
+ })
+
// not sure how useful this is and if it's worth supporting because of the
// cost to support the ranking as well
it.skip('groups', () => {
@@ -808,6 +860,33 @@ describe('Path parser', () => {
})
})
+ it('param followed by escaped colon and static', () => {
+ matchParams('/:foo\\:abc', '/section:abc', { foo: 'section' })
+ matchParams('/:foo\\:abc', '/sectionabc', null)
+ })
+
+ it('optional param followed by escaped colon and static', () => {
+ matchParams('/:foo?\\:abc', '/:abc', { foo: '' })
+ matchParams('/:foo?\\:abc', '/section:abc', { foo: 'section' })
+ })
+
+ it('repeatable param followed by escaped colon and static', () => {
+ matchParams('/:foo+\\:abc', '/section:abc', { foo: ['section'] })
+ matchParams('/:foo+\\:abc', '/a/b:abc', { foo: ['a', 'b'] })
+ })
+
+ it('param with custom re followed by escaped colon and static', () => {
+ matchParams('/:foo([^:]+)\\:abc', '/section:abc', { foo: 'section' })
+ matchParams('/:foo([^:]+)\\:abc', '/sectionabc', null)
+ })
+
+ it('param with custom re followed by escaped colon and another param', () => {
+ matchParams('/:foo([^:]+)\\::bar', '/section:aaabbbccc', {
+ foo: 'section',
+ bar: 'aaabbbccc',
+ })
+ })
+
// end of parsing urls
})
diff --git a/packages/router/src/matcher/pathTokenizer.ts b/packages/router/src/matcher/pathTokenizer.ts
index 65795d18d1..cb901782db 100644
--- a/packages/router/src/matcher/pathTokenizer.ts
+++ b/packages/router/src/matcher/pathTokenizer.ts
@@ -119,6 +119,14 @@ export function tokenizePath(path: string): Array {
char = path[i++]
if (char === '\\' && state !== TokenizerState.ParamRegExp) {
+ if (
+ state === TokenizerState.Param ||
+ state === TokenizerState.ParamRegExpEnd
+ ) {
+ consumeBuffer()
+ customRe = ''
+ state = TokenizerState.Static
+ }
previousState = state
state = TokenizerState.EscapeNext
continue