Skip to content

Commit 62cec2e

Browse files
committed
feat(shared): support relative links in normalizeRoutePath
1 parent 1825636 commit 62cec2e

File tree

2 files changed

+203
-64
lines changed

2 files changed

+203
-64
lines changed
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
/**
2-
* Normalize the given path to the final route path
3-
*/
4-
export const normalizeRoutePath = (path: string): string => {
5-
// split pathname and query/hash
6-
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
1+
const FAKE_HOST = 'http://.'
72

3+
export const inferRoutePath = (path: string): string => {
84
// if the pathname is empty or ends with `/`, return as is
9-
if (!pathname || pathname.endsWith('/')) return path
5+
if (!path || path.endsWith('/')) return path
106

117
// convert README.md to index.html
12-
let routePath = pathname.replace(/(^|\/)README.md$/i, '$1index.html')
8+
let routePath = path.replace(/(^|\/)README.md$/i, '$1index.html')
139

1410
// convert /foo/bar.md to /foo/bar.html
1511
if (routePath.endsWith('.md')) {
@@ -25,6 +21,23 @@ export const normalizeRoutePath = (path: string): string => {
2521
routePath = routePath.substring(0, routePath.length - 10)
2622
}
2723

28-
// add query and hash back
29-
return routePath + queryAndHash.join('')
24+
return routePath
25+
}
26+
27+
/**
28+
* Normalize the given path to the final route path
29+
*/
30+
export const normalizeRoutePath = (path: string, current?: string): string => {
31+
if (!path.startsWith('/') && current) {
32+
// the relative path should be resolved against the current path
33+
const loc = current.slice(0, current.lastIndexOf('/'))
34+
35+
const { pathname, search, hash } = new URL(`${loc}/${path}`, FAKE_HOST)
36+
37+
return inferRoutePath(pathname) + search + hash
38+
}
39+
40+
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
41+
42+
return inferRoutePath(pathname) + queryAndHash.join('')
3043
}

packages/shared/tests/normalizeRoutePath.spec.ts

Lines changed: 180 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import { normalizeRoutePath } from '../src/index.js'
33

44
const testCases = [
5-
// index
5+
// absolute index
66
['/', '/'],
77
['/README.md', '/'],
88
['/readme.md', '/'],
@@ -15,55 +15,168 @@ const testCases = [
1515
['/foo/index.md', '/foo/'],
1616
['/foo/index.html', '/foo/'],
1717
['/foo/index', '/foo/'],
18-
['', ''],
1918
['README.md', 'index.html'],
2019
['readme.md', 'index.html'],
2120
['index.md', 'index.html'],
2221
['index.html', 'index.html'],
2322
['index', 'index.html'],
24-
['foo/', 'foo/'],
25-
['foo/README.md', 'foo/'],
26-
['foo/readme.md', 'foo/'],
27-
['foo/index.md', 'foo/'],
28-
['foo/index.html', 'foo/'],
29-
['foo/index', 'foo/'],
3023

31-
// non-index
24+
// absolute non-index
3225
['/foo', '/foo.html'],
3326
['/foo.md', '/foo.html'],
3427
['/foo.html', '/foo.html'],
3528
['/foo/bar', '/foo/bar.html'],
3629
['/foo/bar.md', '/foo/bar.html'],
3730
['/foo/bar.html', '/foo/bar.html'],
31+
32+
// relative index without current
33+
['foo/', 'foo/'],
34+
['foo/README.md', 'foo/'],
35+
['foo/readme.md', 'foo/'],
36+
['foo/index.md', 'foo/'],
37+
['foo/index.html', 'foo/'],
38+
['foo/index', 'foo/'],
39+
40+
// relative non index without current
3841
['foo', 'foo.html'],
3942
['foo.md', 'foo.html'],
4043
['foo.html', 'foo.html'],
4144
['foo/bar', 'foo/bar.html'],
4245
['foo/bar.md', 'foo/bar.html'],
4346
['foo/bar.html', 'foo/bar.html'],
4447

45-
// hash and query
46-
['/foo#bar', '/foo.html#bar'],
47-
['/foo.md#bar', '/foo.html#bar'],
48-
['/foo.html#bar', '/foo.html#bar'],
49-
['/foo?bar=baz', '/foo.html?bar=baz'],
50-
['/foo.md?bar=baz', '/foo.html?bar=baz'],
51-
['/foo.html?bar=baz', '/foo.html?bar=baz'],
52-
['/foo?bar=baz#qux', '/foo.html?bar=baz#qux'],
53-
['/foo.md?bar=baz#qux', '/foo.html?bar=baz#qux'],
54-
['/foo.html?bar=baz#qux', '/foo.html?bar=baz#qux'],
55-
['foo#bar', 'foo.html#bar'],
56-
['foo.md#bar', 'foo.html#bar'],
57-
['foo.html#bar', 'foo.html#bar'],
58-
['foo?bar=baz', 'foo.html?bar=baz'],
59-
['foo.md?bar=baz', 'foo.html?bar=baz'],
60-
['foo.html?bar=baz', 'foo.html?bar=baz'],
61-
['foo?bar=baz#qux', 'foo.html?bar=baz#qux'],
62-
['foo.md?bar=baz#qux', 'foo.html?bar=baz#qux'],
63-
['foo.html?bar=baz#qux', 'foo.html?bar=baz#qux'],
64-
['#bar', '#bar'],
65-
['?bar=baz', '?bar=baz'],
66-
['?bar=baz#qux', '?bar=baz#qux'],
48+
// relative non index with current
49+
['foo', '/foo.html', '/'],
50+
['foo', '/foo.html', '/a.html'],
51+
['foo', '/foo.html', '/index.html'],
52+
['foo', '/a/foo.html', '/a/'],
53+
['foo', '/a/foo.html', '/a/index.html'],
54+
['foo', '/a/foo.html', '/a/b.html'],
55+
['foo.md', '/foo.html', '/'],
56+
['foo.md', '/foo.html', '/a.html'],
57+
['foo.md', '/foo.html', '/index.html'],
58+
['foo.md', '/a/foo.html', '/a/'],
59+
['foo.md', '/a/foo.html', '/a/index.html'],
60+
['foo.md', '/a/foo.html', '/a/b.html'],
61+
['foo.html', '/foo.html', '/'],
62+
['foo.html', '/foo.html', '/a.html'],
63+
['foo.html', '/foo.html', '/index.html'],
64+
['foo.html', '/a/foo.html', '/a/'],
65+
['foo.html', '/a/foo.html', '/a/index.html'],
66+
['foo.html', '/a/foo.html', '/a/b.html'],
67+
['foo/bar', '/foo/bar.html', '/'],
68+
['foo/bar', '/foo/bar.html', '/a.html'],
69+
['foo/bar', '/foo/bar.html', '/index.html'],
70+
['foo/bar', '/a/foo/bar.html', '/a/'],
71+
['foo/bar', '/a/foo/bar.html', '/a/index.html'],
72+
['foo/bar', '/a/foo/bar.html', '/a/b.html'],
73+
['foo/bar.md', '/foo/bar.html', '/'],
74+
['foo/bar.md', '/foo/bar.html', '/a.html'],
75+
['foo/bar.md', '/foo/bar.html', '/index.html'],
76+
['foo/bar.md', '/a/foo/bar.html', '/a/'],
77+
['foo/bar.md', '/a/foo/bar.html', '/a/index.html'],
78+
['foo/bar.md', '/a/foo/bar.html', '/a/b.html'],
79+
['foo/bar.html', '/foo/bar.html', '/'],
80+
['foo/bar.html', '/foo/bar.html', '/a.html'],
81+
['foo/bar.html', '/foo/bar.html', '/index.html'],
82+
['foo/bar.html', '/a/foo/bar.html', '/a/'],
83+
['foo/bar.html', '/a/foo/bar.html', '/a/index.html'],
84+
['foo/bar.html', '/a/foo/bar.html', '/a/b.html'],
85+
['./foo', '/foo.html', '/'],
86+
['./foo', '/foo.html', '/a.html'],
87+
['./foo', '/foo.html', '/index.html'],
88+
['./foo', '/a/foo.html', '/a/'],
89+
['./foo', '/a/foo.html', '/a/index.html'],
90+
['./foo', '/a/foo.html', '/a/b.html'],
91+
['./foo.md', '/foo.html', '/'],
92+
['./foo.md', '/foo.html', '/a.html'],
93+
['./foo.md', '/foo.html', '/index.html'],
94+
['./foo.md', '/a/foo.html', '/a/'],
95+
['./foo.md', '/a/foo.html', '/a/index.html'],
96+
['./foo.md', '/a/foo.html', '/a/b.html'],
97+
['./foo.html', '/foo.html', '/'],
98+
['./foo.html', '/foo.html', '/a.html'],
99+
['./foo.html', '/foo.html', '/index.html'],
100+
['./foo.html', '/a/foo.html', '/a/'],
101+
['./foo.html', '/a/foo.html', '/a/index.html'],
102+
['./foo.html', '/a/foo.html', '/a/b.html'],
103+
['./foo/bar', '/foo/bar.html', '/'],
104+
['./foo/bar', '/foo/bar.html', '/a.html'],
105+
['./foo/bar', '/foo/bar.html', '/index.html'],
106+
['./foo/bar', '/a/foo/bar.html', '/a/'],
107+
['./foo/bar', '/a/foo/bar.html', '/a/index.html'],
108+
['./foo/bar', '/a/foo/bar.html', '/a/b.html'],
109+
['./foo/bar.md', '/foo/bar.html', '/'],
110+
['./foo/bar.md', '/foo/bar.html', '/a.html'],
111+
['./foo/bar.md', '/foo/bar.html', '/index.html'],
112+
['./foo/bar.md', '/a/foo/bar.html', '/a/'],
113+
['./foo/bar.md', '/a/foo/bar.html', '/a/index.html'],
114+
['./foo/bar.md', '/a/foo/bar.html', '/a/b.html'],
115+
['./foo/bar.html', '/foo/bar.html', '/'],
116+
['./foo/bar.html', '/foo/bar.html', '/a.html'],
117+
['./foo/bar.html', '/foo/bar.html', '/index.html'],
118+
['./foo/bar.html', '/a/foo/bar.html', '/a/'],
119+
['./foo/bar.html', '/a/foo/bar.html', '/a/index.html'],
120+
['./foo/bar.html', '/a/foo/bar.html', '/a/b.html'],
121+
['../foo', '/foo.html', '/a/'],
122+
['../foo', '/foo.html', '/a/index.html'],
123+
['../foo', '/foo.html', '/a/b.html'],
124+
['../foo.md', '/foo.html', '/a/'],
125+
['../foo.md', '/foo.html', '/a/index.html'],
126+
['../foo.md', '/foo.html', '/a/b.html'],
127+
['../foo.html', '/foo.html', '/a/'],
128+
['../foo.html', '/foo.html', '/a/index.html'],
129+
['../foo.html', '/foo.html', '/a/b.html'],
130+
['../foo/bar', '/foo/bar.html', '/a/'],
131+
['../foo/bar', '/foo/bar.html', '/a/index.html'],
132+
['../foo/bar', '/foo/bar.html', '/a/b.html'],
133+
['../foo/bar.md', '/foo/bar.html', '/a/'],
134+
['../foo/bar.md', '/foo/bar.html', '/a/index.html'],
135+
['../foo/bar.md', '/foo/bar.html', '/a/b.html'],
136+
['../foo/bar.html', '/foo/bar.html', '/a/'],
137+
['../foo/bar.html', '/foo/bar.html', '/a/index.html'],
138+
['../foo/bar.html', '/foo/bar.html', '/a/b.html'],
139+
140+
// absolute non index with current
141+
['/foo', '/foo.html', '/'],
142+
['/foo', '/foo.html', '/a.html'],
143+
['/foo', '/foo.html', '/index.html'],
144+
['/foo', '/foo.html', '/a/'],
145+
['/foo', '/foo.html', '/a/index.html'],
146+
['/foo', '/foo.html', '/a/b.html'],
147+
['/foo.md', '/foo.html', '/'],
148+
['/foo.md', '/foo.html', '/a.html'],
149+
['/foo.md', '/foo.html', '/index.html'],
150+
['/foo.md', '/foo.html', '/a/'],
151+
['/foo.md', '/foo.html', '/a/index.html'],
152+
['/foo.md', '/foo.html', '/a/b.html'],
153+
['/foo.html', '/foo.html', '/'],
154+
['/foo.html', '/foo.html', '/a.html'],
155+
['/foo.html', '/foo.html', '/index.html'],
156+
['/foo.html', '/foo.html', '/a/'],
157+
['/foo.html', '/foo.html', '/a/index.html'],
158+
['/foo.html', '/foo.html', '/a/b.html'],
159+
['/foo/bar', '/foo/bar.html', '/'],
160+
['/foo/bar', '/foo/bar.html', '/a.html'],
161+
['/foo/bar', '/foo/bar.html', '/index.html'],
162+
['/foo/bar', '/foo/bar.html', '/a/'],
163+
['/foo/bar', '/foo/bar.html', '/a/index.html'],
164+
['/foo/bar', '/foo/bar.html', '/a/b.html'],
165+
['/foo/bar.md', '/foo/bar.html', '/'],
166+
['/foo/bar.md', '/foo/bar.html', '/a.html'],
167+
['/foo/bar.md', '/foo/bar.html', '/index.html'],
168+
['/foo/bar.md', '/foo/bar.html', '/a/'],
169+
['/foo/bar.md', '/foo/bar.html', '/a/index.html'],
170+
['/foo/bar.md', '/foo/bar.html', '/a/b.html'],
171+
['/foo/bar.html', '/foo/bar.html', '/'],
172+
['/foo/bar.html', '/foo/bar.html', '/a.html'],
173+
['/foo/bar.html', '/foo/bar.html', '/index.html'],
174+
['/foo/bar.html', '/foo/bar.html', '/a/'],
175+
['/foo/bar.html', '/foo/bar.html', '/a/index.html'],
176+
['/foo/bar.html', '/foo/bar.html', '/a/b.html'],
177+
178+
// only hash and query
179+
['', ''],
67180

68181
// unexpected corner cases
69182
['.md', '.html'],
@@ -72,39 +185,52 @@ const testCases = [
72185
['/foo/.md', '/foo/.html'],
73186
]
74187

75-
describe('should normalize clean paths correctly', () =>
76-
testCases.forEach(([path, expected]) =>
77-
it(`"${path}" -> "${expected}"`, () => {
78-
expect(normalizeRoutePath(path)).toBe(expected)
188+
describe('should normalize clean paths correctly', () => {
189+
testCases.forEach(([path, expected, current]) =>
190+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
191+
expect(normalizeRoutePath(path, current)).toBe(expected)
79192
}),
80-
))
193+
)
194+
})
81195

82-
describe('should normalize paths with query correctly', () =>
196+
describe('should normalize paths with query correctly', () => {
83197
testCases
84-
.map(([path, expected]) => [`${path}?foo=bar`, `${expected}?foo=bar`])
85-
.forEach(([path, expected]) =>
86-
it(`"${path}" -> "${expected}"`, () => {
87-
expect(normalizeRoutePath(path)).toBe(expected)
198+
.map(([path, expected, current]) => [
199+
`${path}?foo=bar`,
200+
`${expected}?foo=bar`,
201+
current,
202+
])
203+
.forEach(([path, expected, current]) =>
204+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
205+
expect(normalizeRoutePath(path, current)).toBe(expected)
88206
}),
89-
))
207+
)
208+
})
90209

91-
describe('should normalize paths with hash correctly', () =>
210+
describe('should normalize paths with hash correctly', () => {
92211
testCases
93-
.map(([path, expected]) => [`${path}#foobar`, `${expected}#foobar`])
94-
.forEach(([path, expected]) =>
95-
it(`"${path}" -> "${expected}"`, () => {
96-
expect(normalizeRoutePath(path)).toBe(expected)
212+
.map(([path, expected, current]) => [
213+
`${path}#foobar`,
214+
`${expected}#foobar`,
215+
current,
216+
])
217+
.forEach(([path, expected, current]) =>
218+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
219+
expect(normalizeRoutePath(path, current)).toBe(expected)
97220
}),
98-
))
221+
)
222+
})
99223

100-
describe('should normalize paths with query and hash correctly', () =>
224+
describe('should normalize paths with query and hash correctly', () => {
101225
testCases
102-
.map(([path, expected]) => [
226+
.map(([path, expected, current]) => [
103227
`${path}?foo=1&bar=2#foobar`,
104228
`${expected}?foo=1&bar=2#foobar`,
229+
current,
105230
])
106-
.forEach(([path, expected]) =>
107-
it(`"${path}" -> "${expected}"`, () => {
108-
expect(normalizeRoutePath(path)).toBe(expected)
231+
.forEach(([path, expected, current]) =>
232+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
233+
expect(normalizeRoutePath(path, current)).toBe(expected)
109234
}),
110-
))
235+
)
236+
})

0 commit comments

Comments
 (0)