mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 07:37:19 +02:00
feat(core): make broken link checker detect broken anchors - add onBrokenAnchors
config (#9528)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
332a466893
commit
fd49301a45
52 changed files with 1220 additions and 519 deletions
|
@ -18,6 +18,8 @@ import {
|
|||
buildSshUrl,
|
||||
buildHttpsUrl,
|
||||
hasSSHProtocol,
|
||||
parseURLPath,
|
||||
serializeURLPath,
|
||||
} from '../urlUtils';
|
||||
|
||||
describe('normalizeUrl', () => {
|
||||
|
@ -232,6 +234,137 @@ describe('removeTrailingSlash', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parseURLPath', () => {
|
||||
it('parse and resolve pathname', () => {
|
||||
expect(parseURLPath('')).toEqual({
|
||||
pathname: '/',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/')).toEqual({
|
||||
pathname: '/',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/page')).toEqual({
|
||||
pathname: '/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/dir1/page')).toEqual({
|
||||
pathname: '/dir1/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/dir1/dir2/./../page')).toEqual({
|
||||
pathname: '/dir1/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/dir1/dir2/../..')).toEqual({
|
||||
pathname: '/',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/dir1/dir2/../../..')).toEqual({
|
||||
pathname: '/',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('./dir1/dir2./../page', '/dir3/dir4/page2')).toEqual({
|
||||
pathname: '/dir3/dir4/dir1/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('parse query string', () => {
|
||||
expect(parseURLPath('/page')).toEqual({
|
||||
pathname: '/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/page?')).toEqual({
|
||||
pathname: '/page',
|
||||
search: '',
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/page?test')).toEqual({
|
||||
pathname: '/page',
|
||||
search: 'test',
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/page?age=42&great=true')).toEqual({
|
||||
pathname: '/page',
|
||||
search: 'age=42&great=true',
|
||||
hash: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('parse hash', () => {
|
||||
expect(parseURLPath('/page')).toEqual({
|
||||
pathname: '/page',
|
||||
search: undefined,
|
||||
hash: undefined,
|
||||
});
|
||||
expect(parseURLPath('/page#')).toEqual({
|
||||
pathname: '/page',
|
||||
search: undefined,
|
||||
hash: '',
|
||||
});
|
||||
expect(parseURLPath('/page#anchor')).toEqual({
|
||||
pathname: '/page',
|
||||
search: undefined,
|
||||
hash: 'anchor',
|
||||
});
|
||||
});
|
||||
|
||||
it('parse fancy real-world edge cases', () => {
|
||||
expect(parseURLPath('/page?#')).toEqual({
|
||||
pathname: '/page',
|
||||
search: '',
|
||||
hash: '',
|
||||
});
|
||||
expect(
|
||||
parseURLPath('dir1/dir2/../page?age=42#anchor', '/dir3/page2'),
|
||||
).toEqual({
|
||||
pathname: '/dir3/dir1/page',
|
||||
search: 'age=42',
|
||||
hash: 'anchor',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeURLPath', () => {
|
||||
function test(input: string, base?: string, expectedOutput?: string) {
|
||||
expect(serializeURLPath(parseURLPath(input, base))).toEqual(
|
||||
expectedOutput ?? input,
|
||||
);
|
||||
}
|
||||
|
||||
it('works for already resolved paths', () => {
|
||||
test('/');
|
||||
test('/dir1/page');
|
||||
test('/dir1/page?');
|
||||
test('/dir1/page#');
|
||||
test('/dir1/page?#');
|
||||
test('/dir1/page?age=42#anchor');
|
||||
});
|
||||
|
||||
it('works for relative paths', () => {
|
||||
test('', undefined, '/');
|
||||
test('', '/dir1/dir2/page2', '/dir1/dir2/page2');
|
||||
test('page', '/dir1/dir2/page2', '/dir1/dir2/page');
|
||||
test('../page', '/dir1/dir2/page2', '/dir1/page');
|
||||
test('/dir1/dir2/../page', undefined, '/dir1/page');
|
||||
test(
|
||||
'/dir1/dir2/../page?age=42#anchor',
|
||||
undefined,
|
||||
'/dir1/page?age=42#anchor',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePathname', () => {
|
||||
it('works', () => {
|
||||
// These tests are directly copied from https://github.com/mjackson/resolve-pathname/blob/master/modules/__tests__/resolvePathname-test.js
|
||||
|
|
|
@ -48,6 +48,8 @@ export {
|
|||
encodePath,
|
||||
isValidPathname,
|
||||
resolvePathname,
|
||||
parseURLPath,
|
||||
serializeURLPath,
|
||||
addLeadingSlash,
|
||||
addTrailingSlash,
|
||||
removeTrailingSlash,
|
||||
|
@ -55,6 +57,7 @@ export {
|
|||
buildHttpsUrl,
|
||||
buildSshUrl,
|
||||
} from './urlUtils';
|
||||
export type {URLPath} from './urlUtils';
|
||||
export {
|
||||
type Tag,
|
||||
type TagsListItem,
|
||||
|
|
|
@ -165,14 +165,73 @@ export function isValidPathname(str: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export type URLPath = {pathname: string; search?: string; hash?: string};
|
||||
|
||||
// Let's name the concept of (pathname + search + hash) as URLPath
|
||||
// See also https://twitter.com/kettanaito/status/1741768992866308120
|
||||
// Note: this function also resolves relative pathnames while parsing!
|
||||
export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
|
||||
function parseURL(url: string, base?: string | URL): URL {
|
||||
try {
|
||||
// A possible alternative? https://github.com/unjs/ufo#url
|
||||
return new URL(url, base ?? 'https://example.com');
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Can't parse URL ${url}${base ? ` with base ${base}` : ''}`,
|
||||
{cause: e},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const base = fromPath ? parseURL(fromPath) : undefined;
|
||||
const url = parseURL(urlPath, base);
|
||||
|
||||
const {pathname} = url;
|
||||
|
||||
// Fixes annoying url.search behavior
|
||||
// "" => undefined
|
||||
// "?" => ""
|
||||
// "?param => "param"
|
||||
const search = url.search
|
||||
? url.search.slice(1)
|
||||
: urlPath.includes('?')
|
||||
? ''
|
||||
: undefined;
|
||||
|
||||
// Fixes annoying url.hash behavior
|
||||
// "" => undefined
|
||||
// "#" => ""
|
||||
// "?param => "param"
|
||||
const hash = url.hash
|
||||
? url.hash.slice(1)
|
||||
: urlPath.includes('#')
|
||||
? ''
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeURLPath(urlPath: URLPath): string {
|
||||
const search = urlPath.search === undefined ? '' : `?${urlPath.search}`;
|
||||
const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`;
|
||||
return `${urlPath.pathname}${search}${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve pathnames and fail-fast if resolution fails. Uses standard URL
|
||||
* semantics (provided by `resolve-pathname` which is used internally by React
|
||||
* router)
|
||||
*/
|
||||
export function resolvePathname(to: string, from?: string): string {
|
||||
// TODO do we really need resolve-pathname lib anymore?
|
||||
// possible alternative: decodeURI(parseURLPath(to, from).pathname);
|
||||
return resolvePathnameUnsafe(to, from);
|
||||
}
|
||||
|
||||
/** Appends a leading slash to `str`, if one doesn't exist. */
|
||||
export function addLeadingSlash(str: string): string {
|
||||
return addPrefix(str, '/');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue