Skip to content

Commit

Permalink
feat(shared): support relative links in normalizeRoutePath
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Apr 16, 2024
1 parent 1825636 commit 62cec2e
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 64 deletions.
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
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', '/'],
['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)
}),
))
)
})

0 comments on commit 62cec2e

Please sign in to comment.