Skip to content

Commit 0897e0f

Browse files
committed
hashrouter: refactor braces impl
1 parent e7c427f commit 0897e0f

File tree

1 file changed

+35
-29
lines changed

1 file changed

+35
-29
lines changed

s/hashrouter/plumbing/braces.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,47 @@ export function hasher<S extends string>(spec: S): Hasher<[ParamsOf<S>]> {
1111
if (!spec.startsWith("#/"))
1212
throw new Error(`hash route spec must start with "#/"`)
1313

14-
// capture names in order
15-
const names: string[] = []
16-
spec.replace(/\{([^}]+)\}/g, (_m, name: string) => {
17-
if (names.includes(name)) throw new Error(`route hasher spec "${spec}" has forbidden duplicate name "${name}"`)
18-
names.push(name)
19-
return ""
20-
})
21-
22-
// build a regex that matches the full hash
23-
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
24-
const pattern = "^" + escape(spec).replace(/\\\{[^}]+\\\}/g, "([^/]+)") + "$"
25-
const regex = new RegExp(pattern)
14+
const specparts = spec.split("/")
15+
const braceregex = /\{([^\}\/]+)\}/
2616

2717
function parse(hash: string): [ParamsOf<S>] | null {
28-
const m = regex.exec(hash)
29-
if (!m) return null
30-
const obj: Record<string, string> = {}
31-
for (const [index, name] of names.entries()) {
32-
try { obj[name] = decodeURIComponent(m[index + 1]) }
33-
catch { return null }
18+
if (!hash.startsWith("#/"))
19+
throw new Error(`hash must start with "#/"`)
20+
21+
const hashparts = hash.split("/")
22+
const params: Record<string, string> = {}
23+
24+
if (hashparts.length !== specparts.length)
25+
return null
26+
27+
for (const [index, specpart] of specparts.entries()) {
28+
const hashpart = hashparts.at(index)
29+
if (hashpart === undefined) return null
30+
const bracematch = specpart.match(braceregex)
31+
try {
32+
if (bracematch) params[bracematch[1]] = decodeURIComponent(hashpart)
33+
else if (hashpart !== specpart) return null
34+
}
35+
catch {
36+
return null
37+
}
3438
}
35-
return [obj as ParamsOf<S>]
39+
40+
return [params as ParamsOf<S>]
3641
}
3742

38-
function make(...[obj]: [ParamsOf<S>]): string {
39-
let out = spec
40-
for (const name of names) {
41-
const v = (obj as Record<string, string>)[name]
42-
if (v == null) throw new Error(`missing param "${name}"`)
43-
out = out.replace(
44-
new RegExp(`\\{${name.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\}`),
45-
encodeURIComponent(v),
46-
) as any
43+
function make(params: ParamsOf<S>): string {
44+
const get = (param: string) => {
45+
const p = param as keyof typeof params
46+
if (p in params) return params[p]
47+
else throw new Error(`missing param "${p}"`)
4748
}
48-
return out
49+
return specparts.map(specpart => {
50+
const bracematch = specpart.match(braceregex)
51+
return bracematch
52+
? encodeURIComponent(get(bracematch[1]))
53+
: specpart
54+
}).join("/")
4955
}
5056

5157
return {parse, make}

0 commit comments

Comments
 (0)