fix(mdx-loader): resolve Markdown/MDX links with Remark instead of RegExp (#10168)

This commit is contained in:
Sébastien Lorber 2024-05-24 19:03:23 +02:00 committed by GitHub
parent aab332c2ae
commit e34614963e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 902 additions and 1620 deletions

View file

@ -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;
};
/**

View file

@ -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,
{

View file

@ -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
"
`);
});
});

View file

@ -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;