From: Eduardo San Martin Morote Date: Tue, 17 Dec 2019 11:59:58 +0000 (+0100) Subject: feat(parser): handle sub segments scoring X-Git-Tag: v4.0.0-alpha.0~137 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8fd197b16a9f4ce8636920ba4e5dd2af250733f5;p=thirdparty%2Fvuejs%2Frouter.git feat(parser): handle sub segments scoring --- diff --git a/__tests__/matcher/path-ranking.spec.ts b/__tests__/matcher/path-ranking.spec.ts index 2b6cd33b..0e0cf5e3 100644 --- a/__tests__/matcher/path-ranking.spec.ts +++ b/__tests__/matcher/path-ranking.spec.ts @@ -1,24 +1,26 @@ -import { tokensToParser, tokenizePath } from '../../src/matcher/tokenizer' -import { comparePathParserScore } from '../../src/matcher/path-ranker' +import { + tokensToParser, + tokenizePath, + comparePathParserScore, +} from '../../src/matcher/tokenizer' 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) - }) + describe.skip('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) + // }) }) const possibleOptions: PathParserOptions[] = [ @@ -28,6 +30,10 @@ describe('Path ranking', () => { { strict: true, sensitive: true }, ] + function joinScore(score: number[][]): string { + return score.map(s => `[${s.join(', ')}]`).join(' ') + } + function checkPathOrder(paths: Array) { const normalizedPaths = paths.map(pathOrArray => { let path: string @@ -55,7 +61,7 @@ describe('Path ranking', () => { id, })) - parsers.sort((a, b) => comparePathParserScore(a.score, b.score)) + parsers.sort((a, b) => comparePathParserScore(a, b)) for (let i = 0; i < parsers.length - 1; i++) { const a = parsers[i] @@ -66,9 +72,7 @@ describe('Path ranking', () => { } catch (err) { console.warn( 'Different routes should not have the same score:\n' + - `${a.id} -> [${a.score.join(', ')}]\n${b.id} -> [${b.score.join( - ', ' - )}]` + `${a.id} -> ${joinScore(a.score)}\n${b.id} -> ${joinScore(b.score)}` ) throw err @@ -82,7 +86,7 @@ describe('Path ranking', () => { } catch (err) { console.warn( parsers - .map(parser => `${parser.id} -> [${parser.score.join(', ')}]`) + .map(parser => `${parser.id} -> ${joinScore(parser.score)}`) .join('\n') ) throw err @@ -197,4 +201,30 @@ describe('Path ranking', () => { checkPathOrder([['/:a(\\d+)*', options], '/:rest(.*)']) }) }) + + it('handles sub segments', () => { + checkPathOrder([ + '/a/_2_', + // something like /a/_23_ + '/a/_:b(\\d)other', + '/a/_:b(\\d)?other', + '/a/_:b-other', // the _ is escaped but b can be also letters + '/a/a_:b', + ]) + }) + + it('handles repeatable and optional in sub segments', () => { + checkPathOrder([ + '/a/_:b-other', + '/a/_:b?-other', + '/a/_:b+-other', + '/a/_:b*-other', + ]) + checkPathOrder([ + '/a/_:b(\\d)-other', + '/a/_:b(\\d)?-other', + '/a/_:b(\\d)+-other', + '/a/_:b(\\d)*-other', + ]) + }) }) diff --git a/src/matcher/tokenizer.ts b/src/matcher/tokenizer.ts index 4988c5c6..91461db9 100644 --- a/src/matcher/tokenizer.ts +++ b/src/matcher/tokenizer.ts @@ -194,7 +194,7 @@ interface ParamKey { export interface PathParser { re: RegExp - score: number[] + score: Array keys: ParamKey[] parse(path: string): Params | null stringify(params: Params): string @@ -239,35 +239,17 @@ 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 / + Root = 9 * _multiplier, // just / Segment = 4 * _multiplier, // /a-segment - SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment - Static = 3 * _multiplier, // /static + SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment + Static = 4 * _multiplier, // /static Dynamic = 2 * _multiplier, // /:someId BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+) BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp BonusRepeatable = -2 * _multiplier, // /:w+ or /:w* - BonusOptional = -1 * _multiplier, // /:w? or /:w* + BonusOptional = -0.8 * _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 \/? @@ -289,7 +271,7 @@ export function tokensToParser( } // the amount of scores is the same as the length of segments - let score: number[] = [] + let score: Array = [] let pattern = options.start ? '^' : '' const keys: ParamKey[] = [] @@ -297,22 +279,20 @@ export function tokensToParser( // allow an empty path to be different from slash // if (!segment.length) pattern += '/' - let segmentScore = segment.length - ? segment.length > 1 - ? PathScore.SubSegment - : PathScore.Segment - : PathScore.Root - - if (options.sensitive) segmentScore += PathScore.BonusCaseSensitive + const segmentScores: number[] = segment.length ? [] : [PathScore.Root] for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) { const token = segment[tokenIndex] + // resets the score if we are inside a sub segment /:a-other-:b + let subSegmentScore: number = + PathScore.Segment + + (options.sensitive ? PathScore.BonusCaseSensitive : 0) + if (token.type === TokenType.Static) { // prepend the slash if we are starting a new segment if (!tokenIndex) pattern += '/' pattern += token.value - - segmentScore += PathScore.Static + subSegmentScore += PathScore.Static } else if (token.type === TokenType.Param) { const { value, repeatable, optional, regexp } = token keys.push({ @@ -322,7 +302,7 @@ export function tokensToParser( }) const re = regexp ? regexp : BASE_PARAM_PATTERN if (re !== BASE_PARAM_PATTERN) { - segmentScore += PathScore.BonusCustomRegExp + subSegmentScore += PathScore.BonusCustomRegExp try { new RegExp(`(${re})`) } catch (err) { @@ -340,18 +320,23 @@ export function tokensToParser( pattern += subPattern - segmentScore += PathScore.Dynamic - if (optional) segmentScore += PathScore.BonusOptional - if (repeatable) segmentScore += PathScore.BonusRepeatable - if (re === '.*') segmentScore += PathScore.BonusWildcard + subSegmentScore += PathScore.Dynamic + if (optional) subSegmentScore += PathScore.BonusOptional + if (repeatable) subSegmentScore += PathScore.BonusRepeatable + if (re === '.*') subSegmentScore += PathScore.BonusWildcard } + + segmentScores.push(subSegmentScore) } - score.push(segmentScore) + score.push(segmentScores) } // only apply the strict bonus to the last score - if (options.strict) score[score.length - 1] += PathScore.BonusStrict + if (options.strict) { + const i = score.length - 1 + score[i][score[i].length - 1] += PathScore.BonusStrict + } // TODO: warn double trailing slash if (!options.strict) pattern += '/?' @@ -414,3 +399,49 @@ export function tokensToParser( stringify, } } + +export function compareScoreArray(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++ + } + + // if the last subsegment was Static, the shorter + if (a.length < b.length) { + return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment + ? -1 + : 1 + } else if (a.length > b.length) { + return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment + ? 1 + : -1 + } + + return 0 +} + +export function comparePathParserScore(a: PathParser, b: PathParser): number { + let i = 0 + const aScore = a.score + const bScore = b.score + while (i < aScore.length && i < bScore.length) { + const comp = compareScoreArray(aScore[i], bScore[i]) + // do not return if both are equal + if (comp) return comp + + i++ + } + + // TODO: one is this way the other the opposite it's more complicated than + // that because with subsegments the length matters while with segment it + // doesnt (1 vs 1+). So I need to treat the first entry of each array + // differently + return aScore.length < bScore.length + ? 1 + : aScore.length > bScore.length + ? -1 + : 0 +}