Skip to content

Commit

Permalink
fix(mdx-loader): resolve Markdown/MDX links with Remark instead of Re…
Browse files Browse the repository at this point in the history
…gExp (#10168)
  • Loading branch information
slorber authored May 24, 2024
1 parent aab332c commit e346149
Show file tree
Hide file tree
Showing 36 changed files with 905 additions and 1,623 deletions.
2 changes: 2 additions & 0 deletions packages/docusaurus-mdx-loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,6 +46,7 @@ export type Options = Partial<MDXOptions> & {
frontMatter: {[key: string]: unknown};
metadata: {[key: string]: unknown};
}) => {[key: string]: unknown};
resolveMarkdownLink?: ResolveMarkdownLink;
};

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/docusaurus-mdx-loader/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -120,6 +121,13 @@ async function createProcessorFactory() {
siteDir: options.siteDir,
},
],
// TODO merge this with transformLinks?
options.resolveMarkdownLink
? [
resolveMarkdownLinks,
{resolveMarkdownLink: options.resolveMarkdownLink},
]
: undefined,
[
transformLinks,
{
Expand Down
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
"
`);
});
});
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;
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`linkify reports broken markdown links 1`] = `
"---
title: This post links to another one!
---
[Good link 1](/blog/2018/12/14/Happy-First-Birthday-Slash)
[Good link 2](/blog/2018/12/14/Happy-First-Birthday-Slash)
[Bad link 1](postNotExist1.md)
[Bad link 1](./postNotExist2.mdx)
"
`;

exports[`linkify transforms to correct link 1`] = `
"---
title: This post links to another one!
---
[Linked post](/blog/2018/12/14/Happy-First-Birthday-Slash)"
`;

exports[`paginateBlogPosts generates a single page 1`] = `
[
{
Expand Down
Loading

0 comments on commit e346149

Please sign in to comment.