]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(parser): start scoring parsers
authorEduardo San Martin Morote <posva13@gmail.com>
Sat, 14 Dec 2019 18:23:10 +0000 (19:23 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 18 Dec 2019 09:26:15 +0000 (10:26 +0100)
__tests__/matcher/path-ranking.spec.ts
src/matcher/path-ranker.ts [new file with mode: 0644]
src/matcher/tokenizer.ts

index 59ea51855186e88326015f1406b6e691fed50420..8627ea98648f31c9157d6660eb6f11f1075ebc6a 100644 (file)
@@ -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<typeof tokensToParser>[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<string | [string, PathParserOptions]>) {
     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 (file)
index 0000000..ca6ccf0
--- /dev/null
@@ -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
+}
index 8dc34e726bd261244dd7a8c9106c45f022ad12b3..ccc3164fc807fa3654874a5b751896af102fc553 100644 (file)
@@ -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<PathParserOptions> = {
   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<Token[]>,
   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
-}