-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(mdx-loader): resolve Markdown/MDX links with Remark instead of Re…
…gExp (#10168)
- Loading branch information
Showing
36 changed files
with
905 additions
and
1,623 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
" | ||
`); | ||
}); | ||
}); |
96 changes: 96 additions & 0 deletions
96
packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
23 changes: 0 additions & 23 deletions
23
packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/blogUtils.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.