mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-11 07:12:29 +02:00
fix(mdx-loader): resolve Markdown/MDX links with Remark instead of RegExp (#10168)
This commit is contained in:
parent
aab332c2ae
commit
e34614963e
36 changed files with 902 additions and 1620 deletions
|
@ -17,6 +17,7 @@ import stringifyObject from 'stringify-object';
|
|||
import preprocessor from './preprocessor';
|
||||
import {validateMDXFrontMatter} from './frontMatter';
|
||||
import {createProcessorCached} from './processor';
|
||||
import type {ResolveMarkdownLink} from './remark/resolveMarkdownLinks';
|
||||
import type {MDXOptions} from './processor';
|
||||
|
||||
import type {MarkdownConfig} from '@docusaurus/types';
|
||||
|
@ -45,6 +46,7 @@ export type Options = Partial<MDXOptions> & {
|
|||
frontMatter: {[key: string]: unknown};
|
||||
metadata: {[key: string]: unknown};
|
||||
}) => {[key: string]: unknown};
|
||||
resolveMarkdownLink?: ResolveMarkdownLink;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ import contentTitle from './remark/contentTitle';
|
|||
import toc from './remark/toc';
|
||||
import transformImage from './remark/transformImage';
|
||||
import transformLinks from './remark/transformLinks';
|
||||
import resolveMarkdownLinks from './remark/resolveMarkdownLinks';
|
||||
import details from './remark/details';
|
||||
import head from './remark/head';
|
||||
import mermaid from './remark/mermaid';
|
||||
|
@ -120,6 +121,13 @@ async function createProcessorFactory() {
|
|||
siteDir: options.siteDir,
|
||||
},
|
||||
],
|
||||
// TODO merge this with transformLinks?
|
||||
options.resolveMarkdownLink
|
||||
? [
|
||||
resolveMarkdownLinks,
|
||||
{resolveMarkdownLink: options.resolveMarkdownLink},
|
||||
]
|
||||
: undefined,
|
||||
[
|
||||
transformLinks,
|
||||
{
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import plugin from '..';
|
||||
import type {PluginOptions} from '../index';
|
||||
|
||||
async function process(content: string) {
|
||||
const {remark} = await import('remark');
|
||||
|
||||
const options: PluginOptions = {
|
||||
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
|
||||
};
|
||||
|
||||
const result = await remark().use(plugin, options).process(content);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
describe('resolveMarkdownLinks remark plugin', () => {
|
||||
it('resolves Markdown and MDX links', async () => {
|
||||
/* language=markdown */
|
||||
const content = `[link1](link1.mdx)
|
||||
|
||||
[link2](../myLink2.md) [link3](myLink3.md)
|
||||
|
||||
[link4](../myLink4.mdx?qs#hash) [link5](./../my/great/link5.md?#)
|
||||
|
||||
[link6](../myLink6.mdx?qs#hash)
|
||||
|
||||
[link7](<link with spaces 7.md?qs#hash>)
|
||||
|
||||
<b>[link8](/link8.md)</b>
|
||||
|
||||
[**link** \`9\`](/link9.md)
|
||||
`;
|
||||
|
||||
const result = await process(content);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"[link1](/RESOLVED---link1.mdx)
|
||||
|
||||
[link2](/RESOLVED---../myLink2.md) [link3](/RESOLVED---myLink3.md)
|
||||
|
||||
[link4](/RESOLVED---../myLink4.mdx?qs#hash) [link5](/RESOLVED---./../my/great/link5.md?#)
|
||||
|
||||
[link6](/RESOLVED---../myLink6.mdx?qs#hash)
|
||||
|
||||
[link7](</RESOLVED---link with spaces 7.md?qs#hash>)
|
||||
|
||||
<b>[link8](/RESOLVED---/link8.md)</b>
|
||||
|
||||
[**link** \`9\`](/RESOLVED---/link9.md)
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('skips non-Markdown links', async () => {
|
||||
/* language=markdown */
|
||||
const content = `[link1](./myLink1.m)
|
||||
|
||||
[link2](../myLink2mdx)
|
||||
|
||||
[link3](https://github.com/facebook/docusaurus/blob/main/README.md)
|
||||
|
||||
[link4](ftp:///README.mdx)
|
||||
|
||||
[link5](../link5.js)
|
||||
|
||||
[link6](../link6.jsx)
|
||||
|
||||
[link7](../link7.tsx)
|
||||
|
||||
<!--
|
||||
[link8](link8.mdx)
|
||||
-->
|
||||
|
||||
\`\`\`md
|
||||
[link9](link9.md)
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const result = await process(content);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"[link1](./myLink1.m)
|
||||
|
||||
[link2](../myLink2mdx)
|
||||
|
||||
[link3](https://github.com/facebook/docusaurus/blob/main/README.md)
|
||||
|
||||
[link4](ftp:///README.mdx)
|
||||
|
||||
[link5](../link5.js)
|
||||
|
||||
[link6](../link6.jsx)
|
||||
|
||||
[link7](../link7.tsx)
|
||||
|
||||
<!--
|
||||
[link8](link8.mdx)
|
||||
-->
|
||||
|
||||
\`\`\`md
|
||||
[link9](link9.md)
|
||||
\`\`\`
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('keeps regular Markdown unmodified', async () => {
|
||||
/* language=markdown */
|
||||
const content = `# Title
|
||||
|
||||
Simple link
|
||||
|
||||
\`\`\`js
|
||||
this is a code block
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const result = await process(content);
|
||||
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
|
||||
it('supports link references', async () => {
|
||||
/* language=markdown */
|
||||
const content = `Testing some link refs:
|
||||
|
||||
* [link-ref1]
|
||||
* [link-ref2]
|
||||
* [link-ref3]
|
||||
|
||||
[link-ref1]: target.mdx
|
||||
[link-ref2]: ./target.mdx
|
||||
[link-ref3]: ../links/target.mdx?qs#target-heading
|
||||
`;
|
||||
|
||||
const result = await process(content);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"Testing some link refs:
|
||||
|
||||
* [link-ref1]
|
||||
* [link-ref2]
|
||||
* [link-ref3]
|
||||
|
||||
[link-ref1]: /RESOLVED---target.mdx
|
||||
|
||||
[link-ref2]: /RESOLVED---./target.mdx
|
||||
|
||||
[link-ref3]: /RESOLVED---../links/target.mdx?qs#target-heading
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
parseLocalURLPath,
|
||||
serializeURLPath,
|
||||
type URLPath,
|
||||
} from '@docusaurus/utils';
|
||||
|
||||
// @ts-expect-error: TODO see https://github.com/microsoft/TypeScript/issues/49721
|
||||
import type {Transformer} from 'unified';
|
||||
import type {Definition, Link} from 'mdast';
|
||||
|
||||
type ResolveMarkdownLinkParams = {
|
||||
/**
|
||||
* Absolute path to the source file containing this Markdown link.
|
||||
*/
|
||||
sourceFilePath: string;
|
||||
/**
|
||||
* The Markdown link pathname to resolve, as found in the source file.
|
||||
* If the link is "./myFile.mdx?qs#hash", this will be "./myFile.mdx"
|
||||
*/
|
||||
linkPathname: string;
|
||||
};
|
||||
|
||||
export type ResolveMarkdownLink = (
|
||||
params: ResolveMarkdownLinkParams,
|
||||
) => string | null;
|
||||
|
||||
export interface PluginOptions {
|
||||
resolveMarkdownLink: ResolveMarkdownLink;
|
||||
}
|
||||
|
||||
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
|
||||
// TODO upgrade to TS 5.3
|
||||
// See https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1517839391
|
||||
// import type {Plugin} from 'unified';
|
||||
type Plugin = any; // TODO fix this asap
|
||||
|
||||
const HAS_MARKDOWN_EXTENSION = /\.mdx?$/i;
|
||||
|
||||
function parseMarkdownLinkURLPath(link: string): URLPath | null {
|
||||
const urlPath = parseLocalURLPath(link);
|
||||
|
||||
// If it's not local, we don't resolve it even if it's a Markdown file
|
||||
// Example, we don't resolve https://github.com/project/README.md
|
||||
if (!urlPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore links without a Markdown file extension (ignoring qs/hash)
|
||||
if (!HAS_MARKDOWN_EXTENSION.test(urlPath.pathname)) {
|
||||
return null;
|
||||
}
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* A remark plugin to extract the h1 heading found in Markdown files
|
||||
* This is exposed as "data.contentTitle" to the processed vfile
|
||||
* Also gives the ability to strip that content title (used for the blog plugin)
|
||||
*/
|
||||
const plugin: Plugin = function plugin(options: PluginOptions): Transformer {
|
||||
const {resolveMarkdownLink} = options;
|
||||
return async (root, file) => {
|
||||
const {visit} = await import('unist-util-visit');
|
||||
|
||||
visit(root, ['link', 'definition'], (node) => {
|
||||
const link = node as unknown as Link | Definition;
|
||||
const linkURLPath = parseMarkdownLinkURLPath(link.url);
|
||||
if (!linkURLPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const permalink = resolveMarkdownLink({
|
||||
sourceFilePath: file.path,
|
||||
linkPathname: linkURLPath.pathname,
|
||||
});
|
||||
|
||||
if (permalink) {
|
||||
// This reapplies the link ?qs#hash part to the resolved pathname
|
||||
const resolvedUrl = serializeURLPath({
|
||||
...linkURLPath,
|
||||
pathname: permalink,
|
||||
});
|
||||
link.url = resolvedUrl;
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default plugin;
|
Loading…
Add table
Add a link
Reference in a new issue