From: Eduardo San Martin Morote Date: Sat, 14 Dec 2019 18:23:10 +0000 (+0100) Subject: feat(parser): start scoring parsers X-Git-Tag: v4.0.0-alpha.0~144 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=302db8550ab3d6547301d89b39a528086ae6abd0;p=thirdparty%2Fvuejs%2Frouter.git feat(parser): start scoring parsers --- diff --git a/__tests__/matcher/path-ranking.spec.ts b/__tests__/matcher/path-ranking.spec.ts index 59ea5185..8627ea98 100644 --- a/__tests__/matcher/path-ranking.spec.ts +++ b/__tests__/matcher/path-ranking.spec.ts @@ -1,15 +1,29 @@ -import { - tokensToParser, - tokenizePath, - comparePathParser, -} from '../../src/matcher/tokenizer' +import { tokensToParser, tokenizePath } from '../../src/matcher/tokenizer' +import { comparePathParserScore } from '../../src/matcher/path-ranker' type PathParserOptions = Parameters[1] describe('Path ranking', () => { + describe('comparePathParser', () => { + it('same length', () => { + expect(comparePathParserScore([2], [3])).toEqual(1) + expect(comparePathParserScore([2], [2])).toEqual(0) + expect(comparePathParserScore([4], [3])).toEqual(-1) + }) + + it('longer', () => { + expect(comparePathParserScore([2], [3, 1])).toEqual(1) + // TODO: we are assuming we never pass end: false + expect(comparePathParserScore([3], [3, 1])).toEqual(1) + expect(comparePathParserScore([1, 3], [2])).toEqual(1) + expect(comparePathParserScore([4], [3])).toEqual(-1) + expect(comparePathParserScore([], [3])).toEqual(1) + }) + }) + function checkPathOrder(paths: Array) { const pathsAsStrings = paths.map(path => - typeof path === 'string' ? path : path[0] + typeof path === 'string' ? path : path[0] + JSON.stringify(path[1]) ) // reverse the array to force some reordering const parsers = paths.reverse().map(path => { @@ -18,11 +32,12 @@ describe('Path ranking', () => { ? tokensToParser(tokenizePath(path)) : tokensToParser(tokenizePath(path[0]), path[1]) // @ts-ignore - parser.path = typeof path === 'string' ? path : path[0] + parser.path = + typeof path === 'string' ? path : path[0] + JSON.stringify(path[1]) return parser }) - parsers.sort(comparePathParser) + parsers.sort((a, b) => comparePathParserScore(a.score, b.score)) try { expect( @@ -38,7 +53,7 @@ describe('Path ranking', () => { .map( parser => // @ts-ignore - `${parser.path}: [${parser.score.join(', ')}]` + `${parser.path} -> [${parser.score.join(', ')}]` ) .join('\n') ) @@ -49,4 +64,40 @@ describe('Path ranking', () => { it('orders static before params', () => { checkPathOrder(['/a', '/:id']) }) + + it('empty path before slash', () => { + checkPathOrder(['', '/']) + }) + + it('works with long paths', () => { + checkPathOrder(['/a/b/c/d/e', '/:k/b/c/d/e', '/:k/b/c/d/:j']) + }) + + it('puts the wildcard at the end', () => { + checkPathOrder(['/', '/:rest(.*)']) + checkPathOrder(['/static', '/:rest(.*)']) + checkPathOrder(['/:other', '/:rest(.*)']) + }) + + it('prioritises custom regex', () => { + checkPathOrder(['/:a(\\d+)', '/:a', '/:a(.*)']) + checkPathOrder(['/b-:a(\\d+)', '/b-:a', '/b-:a(.*)']) + }) + + it('prioritizes ending slashes', () => { + checkPathOrder([ + // no strict + '/a/', + '/a', + ]) + checkPathOrder([ + // no strict + '/a/b/', + '/a/b', + ]) + + // does this really make sense? + // checkPathOrder([['/a/', { strict: true }], '/a/']) + // checkPathOrder([['/a', { strict: true }], '/a']) + }) }) diff --git a/src/matcher/path-ranker.ts b/src/matcher/path-ranker.ts new file mode 100644 index 00000000..ca6ccf04 --- /dev/null +++ b/src/matcher/path-ranker.ts @@ -0,0 +1,11 @@ +export function comparePathParserScore(a: number[], b: number[]): number { + let i = 0 + while (i < a.length && i < b.length) { + if (a[i] < b[i]) return 1 + if (a[i] > b[i]) return -1 + + i++ + } + + return a.length < b.length ? 1 : a.length > b.length ? -1 : 0 +} diff --git a/src/matcher/tokenizer.ts b/src/matcher/tokenizer.ts index 8dc34e72..ccc3164f 100644 --- a/src/matcher/tokenizer.ts +++ b/src/matcher/tokenizer.ts @@ -185,7 +185,7 @@ interface ParamKey { optional: boolean } -interface PathParser { +export interface PathParser { re: RegExp score: number[] keys: ParamKey[] @@ -232,10 +232,46 @@ const BASE_PATH_PARSER_OPTIONS: Required = { decode: v => v, } +// const enum PathScore { +// _multiplier = 10, +// Segment = 4 * _multiplier, // /a-segment +// SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment +// Static = 3 * _multiplier, // /static +// Dynamic = 2 * _multiplier, // /:someId +// DynamicCustomRegexp = 2.5 * _multiplier, // /:someId(\\d+) +// Wildcard = -1 * _multiplier, // /:namedWildcard(.*) +// SubWildcard = 1 * _multiplier, // Wildcard as a subsegment +// Repeatable = -0.5 * _multiplier, // /:w+ or /:w* +// // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b +// Strict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/? +// CaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/? +// Optional = -4 * _multiplier, // /:w? or /:w* +// SubOptional = -0.1 * _multiplier, // optional inside a subsegment /a-:w? or /a-:w* +// Root = 1 * _multiplier, // just / +// } + +const enum PathScore { + _multiplier = 10, + Root = 8 * _multiplier, // just / + Segment = 4 * _multiplier, // /a-segment + SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment + Static = 3 * _multiplier, // /static + Dynamic = 2 * _multiplier, // /:someId + BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+) + BonusWildcard = -3 * _multiplier, // /:namedWildcard(.*) + BonusRepeatable = -0.5 * _multiplier, // /:w+ or /:w* + BonusOptional = -4 * _multiplier, // /:w? or /:w* + // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b + BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/? + BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/? +} + /** - * TODO: add options strict, sensitive, encode, decode + * Creates a path parser from an array of Segments (a segment is an array of Tokens) + * + * @param segments array of segments returned by tokenizePath + * @param extraOptions optional options for the regexp */ - export function tokensToParser( segments: Array, extraOptions?: PathParserOptions @@ -245,6 +281,7 @@ export function tokensToParser( ...extraOptions, } + // the amount of scores is the same as the length of segments let score: number[] = [] let pattern = options.start ? '^' : '' const keys: ParamKey[] = [] @@ -253,12 +290,21 @@ export function tokensToParser( // allow an empty path to be different from slash // if (!segment.length) pattern += '/' + // TODO: add strict and sensitive + let segmentScore = segment.length + ? segment.length > 1 + ? PathScore.SubSegment + : PathScore.Segment + : PathScore.Root + for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) { const token = segment[tokenIndex] if (token.type === TokenType.Static) { // prepend the slash if we are starting a new segment if (!tokenIndex) pattern += '/' pattern += token.value + + segmentScore += PathScore.Static } else if (token.type === TokenType.Param) { keys.push({ name: token.value, @@ -267,6 +313,7 @@ export function tokensToParser( }) const re = token.regexp ? token.regexp : BASE_PARAM_PATTERN if (re !== BASE_PARAM_PATTERN) { + segmentScore += PathScore.BonusCustomRegExp try { new RegExp(`(${re})`) } catch (err) { @@ -285,8 +332,13 @@ export function tokensToParser( else subPattern += token.optional ? '?' : '' pattern += subPattern + + segmentScore += PathScore.Dynamic + if (re === '.*') segmentScore += PathScore.BonusWildcard } } + + score.push(segmentScore) } // TODO: warn double trailing slash @@ -351,15 +403,3 @@ export function tokensToParser( stringify, } } - -export function comparePathParser(a: PathParser, b: PathParser): number { - let i = 0 - while (i < a.score.length && i < b.score.length) { - if (a.score[i] < b.score[i]) return -1 - else if (a.score[i] > b.score[i]) 1 - - i++ - } - - return 0 -}