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
1 change: 1 addition & 0 deletions packages/shared/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './dedupeHead.js'
export * from './ensureLeadingSlash.js'
export * from './ensureEndingSlash.js'
export * from './formatDateString.js'
export * from './inferRoutePath.js'
export * from './isLinkExternal.js'
export * from './isLinkHttp.js'
export * from './isLinkWithProtocol.js'
Expand Down
26 changes: 26 additions & 0 deletions packages/shared/src/utils/inferRoutePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Infer route path according to the given (markdown file) path
*/
export const inferRoutePath = (path: string): string => {
// if the pathname is empty or ends with `/`, return as is
if (!path || path.endsWith('/')) return path

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

// convert /foo/bar.md to /foo/bar.html
if (routePath.endsWith('.md')) {
routePath = routePath.substring(0, routePath.length - 3) + '.html'
}
// convert /foo/bar to /foo/bar.html
else if (!routePath.endsWith('.html')) {
routePath = routePath + '.html'
}

// convert /foo/index.html to /foo/
if (routePath.endsWith('/index.html')) {
routePath = routePath.substring(0, routePath.length - 10)
}

return routePath
}
33 changes: 12 additions & 21 deletions packages/shared/src/utils/normalizeRoutePath.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { inferRoutePath } from './inferRoutePath.js'

const FAKE_HOST = 'http://.'

/**
* 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(/(\?|#)/)

// if the pathname is empty or ends with `/`, return as is
if (!pathname || pathname.endsWith('/')) return 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('/'))

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

// convert /foo/bar.md to /foo/bar.html
if (routePath.endsWith('.md')) {
routePath = routePath.substring(0, routePath.length - 3) + '.html'
}
// convert /foo/bar to /foo/bar.html
else if (!routePath.endsWith('.html')) {
routePath = routePath + '.html'
return inferRoutePath(pathname) + search + hash
}

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

// add query and hash back
return routePath + queryAndHash.join('')
return inferRoutePath(pathname) + queryAndHash.join('')
}
62 changes: 62 additions & 0 deletions packages/shared/tests/inferRoutePath.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import { inferRoutePath } from '../src/index.js'

const testCases = [
// absolute index
['/', '/'],
['/README.md', '/'],
['/readme.md', '/'],
['/index.md', '/'],
['/index.html', '/'],
['/index', '/'],
['/foo/', '/foo/'],
['/foo/README.md', '/foo/'],
['/foo/readme.md', '/foo/'],
['/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'],

// 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'],

// unexpected corner cases
['', ''],
['.md', '.html'],
['foo/.md', 'foo/.html'],
['/.md', '/.html'],
['/foo/.md', '/foo/.html'],
]

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