2022-07-02 06:09:45 +00:00
|
|
|
const LABEL_REG_EXP_STR = '[^/]+'
|
|
|
|
const ONLY_WILDCARD_REG_EXP_STR = '.*'
|
|
|
|
const TAIL_WILDCARD_REG_EXP_STR = '(?:|/.*)'
|
2022-09-13 23:01:14 +00:00
|
|
|
export const PATH_ERROR = Symbol()
|
2022-07-02 06:09:45 +00:00
|
|
|
|
2023-10-15 23:39:37 +00:00
|
|
|
export type ParamAssocArray = [string, number][]
|
2022-07-02 06:09:45 +00:00
|
|
|
export interface Context {
|
|
|
|
varIndex: number
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sort order:
|
|
|
|
* 1. literal
|
|
|
|
* 2. special pattern (e.g. :label{[0-9]+})
|
|
|
|
* 3. common label pattern (e.g. :label)
|
|
|
|
* 4. wildcard
|
|
|
|
*/
|
|
|
|
function compareKey(a: string, b: string): number {
|
|
|
|
if (a.length === 1) {
|
|
|
|
return b.length === 1 ? (a < b ? -1 : 1) : -1
|
|
|
|
}
|
|
|
|
if (b.length === 1) {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// wildcard
|
|
|
|
if (a === ONLY_WILDCARD_REG_EXP_STR || a === TAIL_WILDCARD_REG_EXP_STR) {
|
|
|
|
return 1
|
|
|
|
} else if (b === ONLY_WILDCARD_REG_EXP_STR || b === TAIL_WILDCARD_REG_EXP_STR) {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
// label
|
|
|
|
if (a === LABEL_REG_EXP_STR) {
|
|
|
|
return 1
|
|
|
|
} else if (b === LABEL_REG_EXP_STR) {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
return a.length === b.length ? (a < b ? -1 : 1) : b.length - a.length
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Node {
|
|
|
|
index?: number
|
|
|
|
varIndex?: number
|
|
|
|
children: Record<string, Node> = {}
|
|
|
|
|
2023-01-29 04:55:27 +00:00
|
|
|
insert(
|
|
|
|
tokens: readonly string[],
|
|
|
|
index: number,
|
2023-10-15 23:39:37 +00:00
|
|
|
paramMap: ParamAssocArray,
|
2023-01-29 04:55:27 +00:00
|
|
|
context: Context,
|
|
|
|
pathErrorCheckOnly: boolean
|
|
|
|
): void {
|
2022-07-02 06:09:45 +00:00
|
|
|
if (tokens.length === 0) {
|
2022-09-13 23:01:14 +00:00
|
|
|
if (this.index !== undefined) {
|
|
|
|
throw PATH_ERROR
|
|
|
|
}
|
2023-01-29 04:55:27 +00:00
|
|
|
if (pathErrorCheckOnly) {
|
|
|
|
return
|
|
|
|
}
|
2022-09-13 23:01:14 +00:00
|
|
|
|
2022-07-02 06:09:45 +00:00
|
|
|
this.index = index
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const [token, ...restTokens] = tokens
|
|
|
|
const pattern =
|
|
|
|
token === '*'
|
|
|
|
? restTokens.length === 0
|
|
|
|
? ['', '', ONLY_WILDCARD_REG_EXP_STR] // '*' matches to all the trailing paths
|
|
|
|
: ['', '', LABEL_REG_EXP_STR]
|
|
|
|
: token === '/*'
|
|
|
|
? ['', '', TAIL_WILDCARD_REG_EXP_STR] // '/path/to/*' is /\/path\/to(?:|/.*)$
|
|
|
|
: token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/)
|
|
|
|
|
|
|
|
let node
|
|
|
|
if (pattern) {
|
|
|
|
const name = pattern[1]
|
2023-10-09 22:49:59 +00:00
|
|
|
let regexpStr = pattern[2] || LABEL_REG_EXP_STR
|
|
|
|
if (name && pattern[2]) {
|
|
|
|
regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, '(?:') // (a|b) => (?:a|b)
|
|
|
|
if (/\((?!\?:)/.test(regexpStr)) {
|
|
|
|
// prefix(?:a|b) is allowed, but prefix(a|b) is not
|
|
|
|
throw PATH_ERROR
|
|
|
|
}
|
|
|
|
}
|
2022-07-02 06:09:45 +00:00
|
|
|
|
|
|
|
node = this.children[regexpStr]
|
|
|
|
if (!node) {
|
2022-09-13 23:01:14 +00:00
|
|
|
if (
|
|
|
|
Object.keys(this.children).some(
|
|
|
|
(k) => k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw PATH_ERROR
|
|
|
|
}
|
2023-01-29 04:55:27 +00:00
|
|
|
if (pathErrorCheckOnly) {
|
|
|
|
return
|
|
|
|
}
|
2022-09-13 23:01:14 +00:00
|
|
|
node = this.children[regexpStr] = new Node()
|
2022-07-02 06:09:45 +00:00
|
|
|
if (name !== '') {
|
|
|
|
node.varIndex = context.varIndex++
|
|
|
|
}
|
|
|
|
}
|
2023-01-29 04:55:27 +00:00
|
|
|
if (!pathErrorCheckOnly && name !== '') {
|
2022-07-02 06:09:45 +00:00
|
|
|
paramMap.push([name, node.varIndex as number])
|
|
|
|
}
|
|
|
|
} else {
|
2022-09-13 23:01:14 +00:00
|
|
|
node = this.children[token]
|
|
|
|
if (!node) {
|
|
|
|
if (
|
|
|
|
Object.keys(this.children).some(
|
|
|
|
(k) =>
|
|
|
|
k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw PATH_ERROR
|
|
|
|
}
|
2023-01-29 04:55:27 +00:00
|
|
|
if (pathErrorCheckOnly) {
|
|
|
|
return
|
|
|
|
}
|
2022-09-13 23:01:14 +00:00
|
|
|
node = this.children[token] = new Node()
|
|
|
|
}
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
|
|
|
|
2023-01-29 04:55:27 +00:00
|
|
|
node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly)
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
buildRegExpStr(): string {
|
2022-09-13 23:01:14 +00:00
|
|
|
const childKeys = Object.keys(this.children).sort(compareKey)
|
2022-07-02 06:09:45 +00:00
|
|
|
|
|
|
|
const strList = childKeys.map((k) => {
|
|
|
|
const c = this.children[k]
|
|
|
|
return (typeof c.varIndex === 'number' ? `(${k})@${c.varIndex}` : k) + c.buildRegExpStr()
|
|
|
|
})
|
|
|
|
|
|
|
|
if (typeof this.index === 'number') {
|
|
|
|
strList.unshift(`#${this.index}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (strList.length === 0) {
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
if (strList.length === 1) {
|
|
|
|
return strList[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
return '(?:' + strList.join('|') + ')'
|
|
|
|
}
|
|
|
|
}
|