Skip to content

Commit c443a95

Browse files
Mister-Hopemeteorlxy
andauthoredMay 13, 2024··
feat(shared): support relative links in normalizeRoutePath (#1544)
Co-authored-by: Xinyu Liu <meteor.lxy@foxmail.com>
1 parent bab6ae9 commit c443a95

File tree

5 files changed

+324
-109
lines changed

5 files changed

+324
-109
lines changed
 

‎packages/shared/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './dedupeHead.js'
22
export * from './ensureLeadingSlash.js'
33
export * from './ensureEndingSlash.js'
44
export * from './formatDateString.js'
5+
export * from './inferRoutePath.js'
56
export * from './isLinkExternal.js'
67
export * from './isLinkHttp.js'
78
export * from './isLinkWithProtocol.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Infer route path according to the given (markdown file) path
3+
*/
4+
export const inferRoutePath = (path: string): string => {
5+
// if the pathname is empty or ends with `/`, return as is
6+
if (!path || path.endsWith('/')) return path
7+
8+
// convert README.md to index.html
9+
let routePath = path.replace(/(^|\/)README.md$/i, '$1index.html')
10+
11+
// convert /foo/bar.md to /foo/bar.html
12+
if (routePath.endsWith('.md')) {
13+
routePath = routePath.substring(0, routePath.length - 3) + '.html'
14+
}
15+
// convert /foo/bar to /foo/bar.html
16+
else if (!routePath.endsWith('.html')) {
17+
routePath = routePath + '.html'
18+
}
19+
20+
// convert /foo/index.html to /foo/
21+
if (routePath.endsWith('/index.html')) {
22+
routePath = routePath.substring(0, routePath.length - 10)
23+
}
24+
25+
return routePath
26+
}
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
1+
import { inferRoutePath } from './inferRoutePath.js'
2+
3+
const FAKE_HOST = 'http://.'
4+
15
/**
26
* Normalize the given path to the final route path
37
*/
4-
export const normalizeRoutePath = (path: string): string => {
5-
// split pathname and query/hash
6-
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
7-
8-
// if the pathname is empty or ends with `/`, return as is
9-
if (!pathname || pathname.endsWith('/')) return path
8+
export const normalizeRoutePath = (path: string, current?: string): string => {
9+
if (!path.startsWith('/') && current) {
10+
// the relative path should be resolved against the current path
11+
const loc = current.slice(0, current.lastIndexOf('/'))
1012

11-
// convert README.md to index.html
12-
let routePath = pathname.replace(/(^|\/)README.md$/i, '$1index.html')
13+
const { pathname, search, hash } = new URL(`${loc}/${path}`, FAKE_HOST)
1314

14-
// convert /foo/bar.md to /foo/bar.html
15-
if (routePath.endsWith('.md')) {
16-
routePath = routePath.substring(0, routePath.length - 3) + '.html'
17-
}
18-
// convert /foo/bar to /foo/bar.html
19-
else if (!routePath.endsWith('.html')) {
20-
routePath = routePath + '.html'
15+
return inferRoutePath(pathname) + search + hash
2116
}
2217

23-
// convert /foo/index.html to /foo/
24-
if (routePath.endsWith('/index.html')) {
25-
routePath = routePath.substring(0, routePath.length - 10)
26-
}
18+
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
2719

28-
// add query and hash back
29-
return routePath + queryAndHash.join('')
20+
return inferRoutePath(pathname) + queryAndHash.join('')
3021
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { inferRoutePath } from '../src/index.js'
3+
4+
const testCases = [
5+
// absolute index
6+
['/', '/'],
7+
['/README.md', '/'],
8+
['/readme.md', '/'],
9+
['/index.md', '/'],
10+
['/index.html', '/'],
11+
['/index', '/'],
12+
['/foo/', '/foo/'],
13+
['/foo/README.md', '/foo/'],
14+
['/foo/readme.md', '/foo/'],
15+
['/foo/index.md', '/foo/'],
16+
['/foo/index.html', '/foo/'],
17+
['/foo/index', '/foo/'],
18+
['README.md', 'index.html'],
19+
['readme.md', 'index.html'],
20+
['index.md', 'index.html'],
21+
['index.html', 'index.html'],
22+
['index', 'index.html'],
23+
24+
// absolute non-index
25+
['/foo', '/foo.html'],
26+
['/foo.md', '/foo.html'],
27+
['/foo.html', '/foo.html'],
28+
['/foo/bar', '/foo/bar.html'],
29+
['/foo/bar.md', '/foo/bar.html'],
30+
['/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
41+
['foo', 'foo.html'],
42+
['foo.md', 'foo.html'],
43+
['foo.html', 'foo.html'],
44+
['foo/bar', 'foo/bar.html'],
45+
['foo/bar.md', 'foo/bar.html'],
46+
['foo/bar.html', 'foo/bar.html'],
47+
48+
// unexpected corner cases
49+
['', ''],
50+
['.md', '.html'],
51+
['foo/.md', 'foo/.html'],
52+
['/.md', '/.html'],
53+
['/foo/.md', '/foo/.html'],
54+
]
55+
56+
describe('should normalize clean paths correctly', () => {
57+
testCases.forEach(([path, expected]) =>
58+
it(`"${path}" -> "${expected}"`, () => {
59+
expect(inferRoutePath(path)).toBe(expected)
60+
}),
61+
)
62+
})

‎packages/shared/tests/normalizeRoutePath.spec.ts

+223-88
Original file line numberDiff line numberDiff line change
@@ -2,109 +2,244 @@ import { describe, expect, it } from 'vitest'
22
import { normalizeRoutePath } from '../src/index.js'
33

44
const testCases = [
5-
// index
6-
['/', '/'],
7-
['/README.md', '/'],
8-
['/readme.md', '/'],
9-
['/index.md', '/'],
10-
['/index.html', '/'],
11-
['/index', '/'],
12-
['/foo/', '/foo/'],
13-
['/foo/README.md', '/foo/'],
14-
['/foo/readme.md', '/foo/'],
15-
['/foo/index.md', '/foo/'],
16-
['/foo/index.html', '/foo/'],
17-
['/foo/index', '/foo/'],
18-
['', ''],
19-
['README.md', 'index.html'],
20-
['readme.md', 'index.html'],
21-
['index.md', 'index.html'],
22-
['index.html', 'index.html'],
23-
['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/'],
5+
// absolute index
6+
[['/'], '/'],
7+
[['/README.md'], '/'],
8+
[['/readme.md'], '/'],
9+
[['/index.md'], '/'],
10+
[['/index.html'], '/'],
11+
[['/index'], '/'],
12+
[['/foo/'], '/foo/'],
13+
[['/foo/README.md'], '/foo/'],
14+
[['/foo/readme.md'], '/foo/'],
15+
[['/foo/index.md'], '/foo/'],
16+
[['/foo/index.html'], '/foo/'],
17+
[['/foo/index'], '/foo/'],
18+
[['README.md'], 'index.html'],
19+
[['readme.md'], 'index.html'],
20+
[['index.md'], 'index.html'],
21+
[['index.html'], 'index.html'],
22+
[['index'], 'index.html'],
3023

31-
// non-index
32-
['/foo', '/foo.html'],
33-
['/foo.md', '/foo.html'],
34-
['/foo.html', '/foo.html'],
35-
['/foo/bar', '/foo/bar.html'],
36-
['/foo/bar.md', '/foo/bar.html'],
37-
['/foo/bar.html', '/foo/bar.html'],
38-
['foo', 'foo.html'],
39-
['foo.md', 'foo.html'],
40-
['foo.html', 'foo.html'],
41-
['foo/bar', 'foo/bar.html'],
42-
['foo/bar.md', 'foo/bar.html'],
43-
['foo/bar.html', 'foo/bar.html'],
24+
// absolute non-index
25+
[['/foo'], '/foo.html'],
26+
[['/foo.md'], '/foo.html'],
27+
[['/foo.html'], '/foo.html'],
28+
[['/foo/bar'], '/foo/bar.html'],
29+
[['/foo/bar.md'], '/foo/bar.html'],
30+
[['/foo/bar.html'], '/foo/bar.html'],
4431

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

68181
// unexpected corner cases
69-
['.md', '.html'],
70-
['foo/.md', 'foo/.html'],
71-
['/.md', '/.html'],
72-
['/foo/.md', '/foo/.html'],
182+
[['.md'], '.html'],
183+
[['foo/.md'], 'foo/.html'],
184+
[['/.md'], '/.html'],
185+
[['/foo/.md'], '/foo/.html'],
186+
[['.md', '/a/'], '/a/.html'],
187+
[['foo/.md', '/a/'], '/a/foo/.html'],
188+
[['/.md', '/a/'], '/.html'],
189+
[['/foo/.md', '/a/'], '/foo/.html'],
190+
[['.md', '/a/index.html'], '/a/.html'],
191+
[['foo/.md', '/a/index.html'], '/a/foo/.html'],
192+
[['/.md', '/a/index.html'], '/.html'],
193+
[['/foo/.md', '/a/index.html'], '/foo/.html'],
194+
[['.md', '/a/b.html'], '/a/.html'],
195+
[['foo/.md', '/a/b.html'], '/a/foo/.html'],
196+
[['/.md', '/a/b.html'], '/.html'],
197+
[['/foo/.md', '/a/b.html'], '/foo/.html'],
73198
]
74199

75-
describe('should normalize clean paths correctly', () =>
76-
testCases.forEach(([path, expected]) =>
77-
it(`"${path}" -> "${expected}"`, () => {
78-
expect(normalizeRoutePath(path)).toBe(expected)
200+
describe('should normalize clean paths correctly', () => {
201+
testCases.forEach(([[path, current], expected]) =>
202+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
203+
expect(normalizeRoutePath(path, current)).toBe(expected)
79204
}),
80-
))
205+
)
206+
})
81207

82-
describe('should normalize paths with query correctly', () =>
208+
describe('should normalize paths with query correctly', () => {
83209
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)
210+
.map(([[path, current], expected]) => [
211+
[`${path}?foo=bar`, current],
212+
`${expected}?foo=bar`,
213+
])
214+
.forEach(([[path, current], expected]) =>
215+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
216+
expect(normalizeRoutePath(path, current)).toBe(expected)
88217
}),
89-
))
218+
)
219+
})
90220

91-
describe('should normalize paths with hash correctly', () =>
221+
describe('should normalize paths with hash correctly', () => {
92222
testCases
93-
.map(([path, expected]) => [`${path}#foobar`, `${expected}#foobar`])
94-
.forEach(([path, expected]) =>
95-
it(`"${path}" -> "${expected}"`, () => {
96-
expect(normalizeRoutePath(path)).toBe(expected)
223+
.map(([[path, current], expected]) => [
224+
[`${path}#foobar`, current],
225+
`${expected}#foobar`,
226+
])
227+
.map(([[path, current], expected]) =>
228+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
229+
expect(normalizeRoutePath(path, current)).toBe(expected)
97230
}),
98-
))
231+
)
232+
})
99233

100-
describe('should normalize paths with query and hash correctly', () =>
234+
describe('should normalize paths with query and hash correctly', () => {
101235
testCases
102-
.map(([path, expected]) => [
103-
`${path}?foo=1&bar=2#foobar`,
236+
.map(([[path, current], expected]) => [
237+
[`${path}?foo=1&bar=2#foobar`, current],
104238
`${expected}?foo=1&bar=2#foobar`,
105239
])
106-
.forEach(([path, expected]) =>
107-
it(`"${path}" -> "${expected}"`, () => {
108-
expect(normalizeRoutePath(path)).toBe(expected)
240+
.map(([[path, current], expected]) =>
241+
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
242+
expect(normalizeRoutePath(path, current)).toBe(expected)
109243
}),
110-
))
244+
)
245+
})

0 commit comments

Comments
 (0)
Please sign in to comment.