Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shared): support relative links in normalizeRoutePath #1544

Merged
merged 10 commits into from
May 13, 2024
33 changes: 23 additions & 10 deletions packages/shared/src/utils/normalizeRoutePath.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/**
* Normalize the given path to the final route path
*/
export const normalizeRoutePath = (path: string): string => {
// split pathname and query/hash
const [pathname, ...queryAndHash] = path.split(/(\?|#)/)
const FAKE_HOST = 'http://.'

export const inferRoutePath = (path: string): string => {
// if the pathname is empty or ends with `/`, return as is
Mister-Hope marked this conversation as resolved.
Show resolved Hide resolved
if (!pathname || pathname.endsWith('/')) return path
if (!path || path.endsWith('/')) return path

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

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

// add query and hash back
return routePath + queryAndHash.join('')
return routePath
}

/**
* Normalize the given path to the final route path
*/
export const normalizeRoutePath = (path: string, current?: string): string => {
if (!path.startsWith('/') && current) {
// the relative path should be resolved against the current path
const loc = current.slice(0, current.lastIndexOf('/'))

const { pathname, search, hash } = new URL(`${loc}/${path}`, FAKE_HOST)

return inferRoutePath(pathname) + search + hash
}

const [pathname, ...queryAndHash] = path.split(/(\?|#)/)

return inferRoutePath(pathname) + queryAndHash.join('')
}
234 changes: 180 additions & 54 deletions packages/shared/tests/normalizeRoutePath.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { normalizeRoutePath } from '../src/index.js'

const testCases = [
// index
// absolute index
['/', '/'],
['/README.md', '/'],
['/readme.md', '/'],
Expand All @@ -15,55 +15,168 @@ const testCases = [
['/foo/index.md', '/foo/'],
['/foo/index.html', '/foo/'],
['/foo/index', '/foo/'],
['', ''],
['README.md', 'index.html'],
['readme.md', 'index.html'],
['index.md', 'index.html'],
['index.html', 'index.html'],
['index', 'index.html'],
['foo/', 'foo/'],
['foo/README.md', 'foo/'],
['foo/readme.md', 'foo/'],
['foo/index.md', 'foo/'],
['foo/index.html', 'foo/'],
['foo/index', 'foo/'],

// non-index
// absolute non-index
['/foo', '/foo.html'],
['/foo.md', '/foo.html'],
['/foo.html', '/foo.html'],
['/foo/bar', '/foo/bar.html'],
['/foo/bar.md', '/foo/bar.html'],
['/foo/bar.html', '/foo/bar.html'],

// relative index without current
['foo/', 'foo/'],
['foo/README.md', 'foo/'],
['foo/readme.md', 'foo/'],
['foo/index.md', 'foo/'],
['foo/index.html', 'foo/'],
['foo/index', 'foo/'],

// relative non index without current
['foo', 'foo.html'],
['foo.md', 'foo.html'],
['foo.html', 'foo.html'],
['foo/bar', 'foo/bar.html'],
['foo/bar.md', 'foo/bar.html'],
['foo/bar.html', 'foo/bar.html'],

// hash and query
['/foo#bar', '/foo.html#bar'],
['/foo.md#bar', '/foo.html#bar'],
['/foo.html#bar', '/foo.html#bar'],
['/foo?bar=baz', '/foo.html?bar=baz'],
['/foo.md?bar=baz', '/foo.html?bar=baz'],
['/foo.html?bar=baz', '/foo.html?bar=baz'],
['/foo?bar=baz#qux', '/foo.html?bar=baz#qux'],
['/foo.md?bar=baz#qux', '/foo.html?bar=baz#qux'],
['/foo.html?bar=baz#qux', '/foo.html?bar=baz#qux'],
['foo#bar', 'foo.html#bar'],
['foo.md#bar', 'foo.html#bar'],
['foo.html#bar', 'foo.html#bar'],
['foo?bar=baz', 'foo.html?bar=baz'],
['foo.md?bar=baz', 'foo.html?bar=baz'],
['foo.html?bar=baz', 'foo.html?bar=baz'],
['foo?bar=baz#qux', 'foo.html?bar=baz#qux'],
['foo.md?bar=baz#qux', 'foo.html?bar=baz#qux'],
['foo.html?bar=baz#qux', 'foo.html?bar=baz#qux'],
['#bar', '#bar'],
['?bar=baz', '?bar=baz'],
['?bar=baz#qux', '?bar=baz#qux'],
// relative non index with current
['foo', '/foo.html', '/'],
Mister-Hope marked this conversation as resolved.
Show resolved Hide resolved
['foo', '/foo.html', '/a.html'],
['foo', '/foo.html', '/index.html'],
['foo', '/a/foo.html', '/a/'],
['foo', '/a/foo.html', '/a/index.html'],
['foo', '/a/foo.html', '/a/b.html'],
['foo.md', '/foo.html', '/'],
['foo.md', '/foo.html', '/a.html'],
['foo.md', '/foo.html', '/index.html'],
['foo.md', '/a/foo.html', '/a/'],
['foo.md', '/a/foo.html', '/a/index.html'],
['foo.md', '/a/foo.html', '/a/b.html'],
['foo.html', '/foo.html', '/'],
['foo.html', '/foo.html', '/a.html'],
['foo.html', '/foo.html', '/index.html'],
['foo.html', '/a/foo.html', '/a/'],
['foo.html', '/a/foo.html', '/a/index.html'],
['foo.html', '/a/foo.html', '/a/b.html'],
['foo/bar', '/foo/bar.html', '/'],
['foo/bar', '/foo/bar.html', '/a.html'],
['foo/bar', '/foo/bar.html', '/index.html'],
['foo/bar', '/a/foo/bar.html', '/a/'],
['foo/bar', '/a/foo/bar.html', '/a/index.html'],
['foo/bar', '/a/foo/bar.html', '/a/b.html'],
['foo/bar.md', '/foo/bar.html', '/'],
['foo/bar.md', '/foo/bar.html', '/a.html'],
['foo/bar.md', '/foo/bar.html', '/index.html'],
['foo/bar.md', '/a/foo/bar.html', '/a/'],
['foo/bar.md', '/a/foo/bar.html', '/a/index.html'],
['foo/bar.md', '/a/foo/bar.html', '/a/b.html'],
['foo/bar.html', '/foo/bar.html', '/'],
['foo/bar.html', '/foo/bar.html', '/a.html'],
['foo/bar.html', '/foo/bar.html', '/index.html'],
['foo/bar.html', '/a/foo/bar.html', '/a/'],
['foo/bar.html', '/a/foo/bar.html', '/a/index.html'],
['foo/bar.html', '/a/foo/bar.html', '/a/b.html'],
['./foo', '/foo.html', '/'],
['./foo', '/foo.html', '/a.html'],
['./foo', '/foo.html', '/index.html'],
['./foo', '/a/foo.html', '/a/'],
['./foo', '/a/foo.html', '/a/index.html'],
['./foo', '/a/foo.html', '/a/b.html'],
['./foo.md', '/foo.html', '/'],
['./foo.md', '/foo.html', '/a.html'],
['./foo.md', '/foo.html', '/index.html'],
['./foo.md', '/a/foo.html', '/a/'],
['./foo.md', '/a/foo.html', '/a/index.html'],
['./foo.md', '/a/foo.html', '/a/b.html'],
['./foo.html', '/foo.html', '/'],
['./foo.html', '/foo.html', '/a.html'],
['./foo.html', '/foo.html', '/index.html'],
['./foo.html', '/a/foo.html', '/a/'],
['./foo.html', '/a/foo.html', '/a/index.html'],
['./foo.html', '/a/foo.html', '/a/b.html'],
['./foo/bar', '/foo/bar.html', '/'],
['./foo/bar', '/foo/bar.html', '/a.html'],
['./foo/bar', '/foo/bar.html', '/index.html'],
['./foo/bar', '/a/foo/bar.html', '/a/'],
['./foo/bar', '/a/foo/bar.html', '/a/index.html'],
['./foo/bar', '/a/foo/bar.html', '/a/b.html'],
['./foo/bar.md', '/foo/bar.html', '/'],
['./foo/bar.md', '/foo/bar.html', '/a.html'],
['./foo/bar.md', '/foo/bar.html', '/index.html'],
['./foo/bar.md', '/a/foo/bar.html', '/a/'],
['./foo/bar.md', '/a/foo/bar.html', '/a/index.html'],
['./foo/bar.md', '/a/foo/bar.html', '/a/b.html'],
['./foo/bar.html', '/foo/bar.html', '/'],
['./foo/bar.html', '/foo/bar.html', '/a.html'],
['./foo/bar.html', '/foo/bar.html', '/index.html'],
['./foo/bar.html', '/a/foo/bar.html', '/a/'],
['./foo/bar.html', '/a/foo/bar.html', '/a/index.html'],
['./foo/bar.html', '/a/foo/bar.html', '/a/b.html'],
['../foo', '/foo.html', '/a/'],
['../foo', '/foo.html', '/a/index.html'],
['../foo', '/foo.html', '/a/b.html'],
['../foo.md', '/foo.html', '/a/'],
['../foo.md', '/foo.html', '/a/index.html'],
['../foo.md', '/foo.html', '/a/b.html'],
['../foo.html', '/foo.html', '/a/'],
['../foo.html', '/foo.html', '/a/index.html'],
['../foo.html', '/foo.html', '/a/b.html'],
['../foo/bar', '/foo/bar.html', '/a/'],
['../foo/bar', '/foo/bar.html', '/a/index.html'],
['../foo/bar', '/foo/bar.html', '/a/b.html'],
['../foo/bar.md', '/foo/bar.html', '/a/'],
['../foo/bar.md', '/foo/bar.html', '/a/index.html'],
['../foo/bar.md', '/foo/bar.html', '/a/b.html'],
['../foo/bar.html', '/foo/bar.html', '/a/'],
['../foo/bar.html', '/foo/bar.html', '/a/index.html'],
['../foo/bar.html', '/foo/bar.html', '/a/b.html'],

// absolute non index with current
['/foo', '/foo.html', '/'],
['/foo', '/foo.html', '/a.html'],
['/foo', '/foo.html', '/index.html'],
['/foo', '/foo.html', '/a/'],
['/foo', '/foo.html', '/a/index.html'],
['/foo', '/foo.html', '/a/b.html'],
['/foo.md', '/foo.html', '/'],
['/foo.md', '/foo.html', '/a.html'],
['/foo.md', '/foo.html', '/index.html'],
['/foo.md', '/foo.html', '/a/'],
['/foo.md', '/foo.html', '/a/index.html'],
['/foo.md', '/foo.html', '/a/b.html'],
['/foo.html', '/foo.html', '/'],
['/foo.html', '/foo.html', '/a.html'],
['/foo.html', '/foo.html', '/index.html'],
['/foo.html', '/foo.html', '/a/'],
['/foo.html', '/foo.html', '/a/index.html'],
['/foo.html', '/foo.html', '/a/b.html'],
['/foo/bar', '/foo/bar.html', '/'],
['/foo/bar', '/foo/bar.html', '/a.html'],
['/foo/bar', '/foo/bar.html', '/index.html'],
['/foo/bar', '/foo/bar.html', '/a/'],
['/foo/bar', '/foo/bar.html', '/a/index.html'],
['/foo/bar', '/foo/bar.html', '/a/b.html'],
['/foo/bar.md', '/foo/bar.html', '/'],
['/foo/bar.md', '/foo/bar.html', '/a.html'],
['/foo/bar.md', '/foo/bar.html', '/index.html'],
['/foo/bar.md', '/foo/bar.html', '/a/'],
['/foo/bar.md', '/foo/bar.html', '/a/index.html'],
['/foo/bar.md', '/foo/bar.html', '/a/b.html'],
['/foo/bar.html', '/foo/bar.html', '/'],
['/foo/bar.html', '/foo/bar.html', '/a.html'],
['/foo/bar.html', '/foo/bar.html', '/index.html'],
['/foo/bar.html', '/foo/bar.html', '/a/'],
['/foo/bar.html', '/foo/bar.html', '/a/index.html'],
['/foo/bar.html', '/foo/bar.html', '/a/b.html'],

// only hash and query
['', ''],

// unexpected corner cases
['.md', '.html'],
Expand All @@ -72,39 +185,52 @@ const testCases = [
['/foo/.md', '/foo/.html'],
]

describe('should normalize clean paths correctly', () =>
testCases.forEach(([path, expected]) =>
it(`"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path)).toBe(expected)
describe('should normalize clean paths correctly', () => {
testCases.forEach(([path, expected, current]) =>
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path, current)).toBe(expected)
}),
))
)
})

describe('should normalize paths with query correctly', () =>
describe('should normalize paths with query correctly', () => {
testCases
.map(([path, expected]) => [`${path}?foo=bar`, `${expected}?foo=bar`])
.forEach(([path, expected]) =>
it(`"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path)).toBe(expected)
.map(([path, expected, current]) => [
`${path}?foo=bar`,
`${expected}?foo=bar`,
current,
])
.forEach(([path, expected, current]) =>
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path, current)).toBe(expected)
}),
))
)
})

describe('should normalize paths with hash correctly', () =>
describe('should normalize paths with hash correctly', () => {
testCases
.map(([path, expected]) => [`${path}#foobar`, `${expected}#foobar`])
.forEach(([path, expected]) =>
it(`"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path)).toBe(expected)
.map(([path, expected, current]) => [
`${path}#foobar`,
`${expected}#foobar`,
current,
])
.forEach(([path, expected, current]) =>
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path, current)).toBe(expected)
}),
))
)
})

describe('should normalize paths with query and hash correctly', () =>
describe('should normalize paths with query and hash correctly', () => {
testCases
.map(([path, expected]) => [
.map(([path, expected, current]) => [
`${path}?foo=1&bar=2#foobar`,
`${expected}?foo=1&bar=2#foobar`,
current,
])
.forEach(([path, expected]) =>
it(`"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path)).toBe(expected)
.forEach(([path, expected, current]) =>
it(`${current ? `"${current}"-` : ''}"${path}" -> "${expected}"`, () => {
expect(normalizeRoutePath(path, current)).toBe(expected)
}),
))
)
})
Loading