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