]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: add wip path parser to replace path-to-regexp
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 10 Dec 2019 20:54:12 +0000 (21:54 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 18 Dec 2019 09:26:15 +0000 (10:26 +0100)
__tests__/matcher/path-parser.spec.ts [new file with mode: 0644]
src/matcher/tokenizer.ts [new file with mode: 0644]

diff --git a/__tests__/matcher/path-parser.spec.ts b/__tests__/matcher/path-parser.spec.ts
new file mode 100644 (file)
index 0000000..18d35ab
--- /dev/null
@@ -0,0 +1,186 @@
+import {
+  tokenizePath,
+  TokenType,
+  tokensToRegExp,
+} from '../../src/matcher/tokenizer'
+
+describe('Path parser', () => {
+  describe('tokenizer', () => {
+    it('root', () => {
+      expect(tokenizePath('/')).toEqual([[]])
+    })
+
+    it('static single', () => {
+      expect(tokenizePath('/home')).toEqual([
+        [{ type: TokenType.Static, value: 'home' }],
+      ])
+    })
+
+    it('static multiple', () => {
+      expect(tokenizePath('/one/two/three')).toEqual([
+        [{ type: TokenType.Static, value: 'one' }],
+        [{ type: TokenType.Static, value: 'two' }],
+        [{ type: TokenType.Static, value: 'three' }],
+      ])
+    })
+
+    it('param single', () => {
+      expect(tokenizePath('/:id')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+      ])
+    })
+
+    it('param single?', () => {
+      expect(tokenizePath('/:id?')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: true,
+          },
+        ],
+      ])
+    })
+
+    it('param single+', () => {
+      expect(tokenizePath('/:id+')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: true,
+            optional: false,
+          },
+        ],
+      ])
+    })
+
+    it('param single*', () => {
+      expect(tokenizePath('/:id*')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: true,
+            optional: true,
+          },
+        ],
+      ])
+    })
+
+    it('param multiple', () => {
+      expect(tokenizePath('/:id/:other')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+        [
+          {
+            type: TokenType.Param,
+            value: 'other',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+      ])
+    })
+
+    it('param with static in between', () => {
+      expect(tokenizePath('/:id-:other')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+          {
+            type: TokenType.Static,
+            value: '-',
+          },
+          {
+            type: TokenType.Param,
+            value: 'other',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+      ])
+    })
+
+    it('param with static beginning', () => {
+      expect(tokenizePath('/hey-:id')).toEqual([
+        [
+          {
+            type: TokenType.Static,
+            value: 'hey-',
+          },
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+      ])
+    })
+
+    it('param with static end', () => {
+      expect(tokenizePath('/:id-end')).toEqual([
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+          {
+            type: TokenType.Static,
+            value: '-end',
+          },
+        ],
+      ])
+    })
+  })
+
+  describe('tokensToRegexp', () => {
+    function matchRegExp(
+      expectedRe: string,
+      ...args: Parameters<typeof tokensToRegExp>
+    ) {
+      const pathParser = tokensToRegExp(...args)
+      expect(expectedRe).toBe(
+        pathParser.re.toString().replace(/(:?^\/|\\|\/$)/g, '')
+      )
+    }
+
+    it('static', () => {
+      matchRegExp('^/home$', [[{ type: TokenType.Static, value: 'home' }]])
+    })
+
+    it('param simple', () => {
+      matchRegExp('^/([^/]+)$', [
+        [
+          {
+            type: TokenType.Param,
+            value: 'id',
+            repeatable: false,
+            optional: false,
+          },
+        ],
+      ])
+    })
+  })
+})
diff --git a/src/matcher/tokenizer.ts b/src/matcher/tokenizer.ts
new file mode 100644 (file)
index 0000000..08f40f0
--- /dev/null
@@ -0,0 +1,181 @@
+export const enum TokenType {
+  Static,
+  Param,
+}
+
+const enum TokenizerState {
+  Static,
+  Param,
+  EscapeNext,
+}
+
+interface TokenStatic {
+  type: TokenType.Static
+  value: string
+}
+
+interface TokenParam {
+  type: TokenType.Param
+  regex?: string
+  value: string
+}
+
+interface TokenParam {
+  type: TokenType.Param
+  regex?: string
+  value: string
+  optional: boolean
+  repeatable: boolean
+}
+
+type Token = TokenStatic | TokenParam
+
+// const ROOT_TOKEN: Token = {
+//   type: TokenType.Static,
+//   value: '/',
+// }
+
+const VALID_PARAM_RE = /[a-zA-Z0-9_]/
+
+export function tokenizePath(path: string): Array<Token[]> {
+  if (path === '/') return [[]]
+  // remove the leading slash
+  if (!path) throw new Error('An empty path cannot be tokenized')
+
+  function crash(message: string) {
+    throw new Error(`ERR (${state})/"${buffer}": ${message}`)
+  }
+
+  let state: TokenizerState = TokenizerState.Static
+  let previousState: TokenizerState = state
+  const tokens: Array<Token[]> = []
+  // the segment will always be valid because we get into the initial state
+  // with the leading /
+  let segment!: Token[]
+
+  function finalizeSegment() {
+    if (segment) tokens.push(segment)
+    segment = []
+  }
+
+  // index on the path
+  let i = 0
+  // char at index
+  let char: string
+  // buffer of the value read
+  let buffer: string = ''
+
+  function consumeBuffer() {
+    if (!buffer) return
+
+    if (state === TokenizerState.Static) {
+      segment.push({
+        type: TokenType.Static,
+        value: buffer,
+      })
+    } else if (state === TokenizerState.Param) {
+      segment.push({
+        type: TokenType.Param,
+        value: buffer,
+        repeatable: char === '*' || char === '+',
+        optional: char === '*' || char === '?',
+      })
+    } else {
+      crash('Invalid state to consume buffer')
+    }
+    buffer = ''
+  }
+
+  function addCharToBuffer() {
+    buffer += char
+  }
+
+  while (i < path.length) {
+    char = path[i++]
+
+    if (char === '\\') {
+      previousState = state
+      state = TokenizerState.EscapeNext
+      continue
+    }
+
+    switch (state) {
+      case TokenizerState.Static:
+        if (char === '/') {
+          if (buffer) {
+            consumeBuffer()
+          }
+          finalizeSegment()
+        } else if (char === ':') {
+          consumeBuffer()
+          state = TokenizerState.Param
+        } else if (char === '{') {
+          // TODO: handle group
+        } else {
+          addCharToBuffer()
+        }
+        break
+
+      case TokenizerState.EscapeNext:
+        addCharToBuffer()
+        state = previousState
+        break
+
+      case TokenizerState.Param:
+        if (char === '(') {
+          // TODO: start custom regex
+        } else if (VALID_PARAM_RE.test(char)) {
+          addCharToBuffer()
+        } else {
+          consumeBuffer()
+          state = TokenizerState.Static
+          // go back one character if we were not modifying
+          if (char !== '*' && char !== '?' && char !== '+') i--
+        }
+        break
+
+      default:
+        crash('Unkwnonw state')
+        break
+    }
+  }
+
+  consumeBuffer()
+  finalizeSegment()
+
+  return tokens
+}
+
+interface PathParser {
+  re: RegExp
+  score: number
+  keys: string[]
+}
+
+export function tokensToRegExp(segments: Array<Token[]>): PathParser {
+  let score = 0
+  let pattern = '^'
+  const keys: string[] = []
+
+  for (const segment of segments) {
+    pattern += '/'
+
+    for (const token of segment) {
+      if (token.type === TokenType.Static) {
+        pattern += token.value
+      } else if (token.type === TokenType.Param) {
+        keys.push(token.value)
+        pattern += `([^/]+)`
+        // TODO: repeatable and others
+      }
+    }
+  }
+
+  pattern += '$'
+
+  return {
+    re: new RegExp(pattern),
+    score,
+    keys,
+  }
+}