Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8d45009
extract Markdown config types to separate file + add config.markdown.…
slorber Jun 20, 2025
36a22cc
add onBrokenMarkdownLinks hook
slorber Jun 20, 2025
6eebe37
refactor tests + test for onBrokenMarkdownLinks
slorber Jun 20, 2025
3a445fe
fix types
slorber Jun 20, 2025
3c863df
fix tests
slorber Jun 20, 2025
717cca8
add deprecation warning for siteConfig.onBrokenMarkdownLinks + use it…
slorber Jun 20, 2025
84299a1
fix types
slorber Jun 20, 2025
f117114
remove deprecated options from init templates + website
slorber Jun 20, 2025
04c87e5
upgade test message
slorber Jun 20, 2025
b7f2062
upgrade snapshots
slorber Jun 20, 2025
fa4e79c
Add tests for resolveMarkdownLinks
slorber Jun 20, 2025
df0c965
Support onBrokenMarkdownLinks function form and broken url recovery
slorber Jun 20, 2025
abebb13
add onBrokenMarkdownImages config + validation
slorber Jun 23, 2025
d0fe043
refactor bad TS imports
slorber Jun 23, 2025
84eef70
Validation should allow function form
slorber Jun 23, 2025
d7671f0
fix TS errors
slorber Jun 23, 2025
182f63e
use "throw" by default, same as current behavior
slorber Jun 23, 2025
ecb4380
wire onBrokenMarkdownImages in transformImage plugin
slorber Jun 23, 2025
70b4e6e
use inline snapshots for error messages
slorber Jun 23, 2025
8a08766
refactor plugin tests
slorber Jun 23, 2025
5a0ff72
refactor plugin tests
slorber Jun 23, 2025
1b453c1
add basic test case
slorber Jun 23, 2025
da38b0a
add proper implementation and unit tests for onBrokenMarkdownImages
slorber Jun 23, 2025
9ee6060
better resolveMarkdownLinks
slorber Jun 23, 2025
e3462f8
improve error message
slorber Jun 23, 2025
3e61fab
improve onBrokenMarkdownLinks error message
slorber Jun 23, 2025
e7bfd55
refactor transformLinks tests
slorber Jun 23, 2025
fff0d96
improve resolveMarkdownLink error
slorber Jun 23, 2025
adccddd
improve resolveMarkdownLink tests
slorber Jun 23, 2025
52dff04
error message typo
slorber Jun 23, 2025
402ff1c
proper onBrokenMarkdownLinks tests
slorber Jun 23, 2025
745cd46
update snapshots
slorber Jun 23, 2025
e6efe41
fix wiring of onBrokenMarkdownLinks option
slorber Jun 23, 2025
5b0240d
Add AST node to API + line numbering in error message
slorber Jun 24, 2025
c73998b
Add AST node to API + line numbering in error message
slorber Jun 24, 2025
f0e9ade
Add AST node to API + line numbering in error message
slorber Jun 24, 2025
4496b59
Add docs
slorber Jun 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const config: Config = {
projectName: 'docusaurus', // Usually your repo name.

onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',

// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const config = {
projectName: 'docusaurus', // Usually your repo name.

onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',

// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
Expand Down
17 changes: 14 additions & 3 deletions packages/docusaurus-mdx-loader/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import type {WebpackCompilerName} from '@docusaurus/utils';
import type {MDXFrontMatter} from './frontMatter';
import type {Options} from './options';
import type {AdmonitionOptions} from './remark/admonitions';
import type {PluginOptions as ResolveMarkdownLinksOptions} from './remark/resolveMarkdownLinks';
import type {PluginOptions as TransformLinksOptions} from './remark/transformLinks';
import type {PluginOptions as TransformImageOptions} from './remark/transformImage';
import type {ProcessorOptions} from '@mdx-js/mdx';

// TODO as of April 2023, no way to import/re-export this ESM type easily :/
Expand Down Expand Up @@ -121,21 +124,29 @@ async function createProcessorFactory() {
{
staticDirs: options.staticDirs,
siteDir: options.siteDir,
},
onBrokenMarkdownImages:
options.markdownConfig.hooks.onBrokenMarkdownImages,
} satisfies TransformImageOptions,
],
// TODO merge this with transformLinks?
options.resolveMarkdownLink
? [
resolveMarkdownLinks,
{resolveMarkdownLink: options.resolveMarkdownLink},
{
resolveMarkdownLink: options.resolveMarkdownLink,
onBrokenMarkdownLinks:
options.markdownConfig.hooks.onBrokenMarkdownLinks,
} satisfies ResolveMarkdownLinksOptions,
]
: undefined,
[
transformLinks,
{
staticDirs: options.staticDirs,
siteDir: options.siteDir,
},
onBrokenMarkdownLinks:
options.markdownConfig.hooks.onBrokenMarkdownLinks,
} satisfies TransformLinksOptions,
],
gfm,
options.markdownConfig.mdx1Compat.comments ? comment : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,47 @@
* LICENSE file in the root directory of this source tree.
*/

import {jest} from '@jest/globals';
import * as path from 'path';
import plugin from '..';
import type {PluginOptions} from '../index';

async function process(content: string) {
const {remark} = await import('remark');
const siteDir = __dirname;

const DefaultTestOptions: PluginOptions = {
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
onBrokenMarkdownLinks: 'throw',
};

const options: PluginOptions = {
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
async function process(content: string, optionsInput?: Partial<PluginOptions>) {
const options = {
...DefaultTestOptions,
...optionsInput,
};

const result = await remark().use(plugin, options).process(content);
const {remark} = await import('remark');

const result = await remark()
.use(plugin, options)
.process({
value: content,
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
});

return result.value;
}

describe('resolveMarkdownLinks remark plugin', () => {
it('accepts non-md link', async () => {
/* language=markdown */
const content = `[link1](link1)`;
const result = await process(content);
expect(result).toMatchInlineSnapshot(`
"[link1](link1)
"
`);
});

it('resolves Markdown and MDX links', async () => {
/* language=markdown */
const content = `[link1](link1.mdx)
Expand Down Expand Up @@ -157,4 +182,212 @@ this is a code block
"
`);
});

describe('onBrokenMarkdownLinks', () => {
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
beforeEach(() => {
warnMock.mockClear();
});

async function processResolutionErrors(
content: string,
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'] = 'throw',
) {
return process(content, {
resolveMarkdownLink: () => null,
onBrokenMarkdownLinks,
});
}

describe('throws', () => {
it('for unresolvable mdx link', async () => {
/* language=markdown */
const content = `[link1](link1.mdx)`;

await expect(() => processResolutionErrors(content)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with URL \`link1.mdx\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});

it('for unresolvable md link', async () => {
/* language=markdown */
const content = `[link1](link1.md)`;

await expect(() => processResolutionErrors(content)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with URL \`link1.md\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});
});

describe('warns', () => {
it('for unresolvable md and mdx link', async () => {
/* language=markdown */
const content = `
[link1](link1.mdx)

[link2](link2)

[link3](dir/link3.md)

[link 4](/link/4)
`;

const result = await processResolutionErrors(content, 'warn');

expect(result).toMatchInlineSnapshot(`
"[link1](link1.mdx)

[link2](link2)

[link3](dir/link3.md)

[link 4](/link/4)
"
`);

expect(warnMock).toHaveBeenCalledTimes(2);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown link with URL \`link1.mdx\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (2:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.",
],
[
"[WARNING] Markdown link with URL \`dir/link3.md\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (6:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.",
],
]
`);
});

it('for unresolvable md and mdx link - with recovery', async () => {
/* language=markdown */
const content = `
[link1](link1.mdx)

[link2](link2)

[link3](dir/link3.md?query#hash)

[link 4](/link/4)
`;

const result = await processResolutionErrors(content, (params) => {
console.warn(`onBrokenMarkdownLinks called with`, params);
// We can alter the AST Node
params.node.title = 'fixed link title';
params.node.url = 'ignored, less important than returned value';
// Or return a new URL
return `/recovered-link`;
});

expect(result).toMatchInlineSnapshot(`
"[link1](/recovered-link "fixed link title")

[link2](link2)

[link3](/recovered-link "fixed link title")

[link 4](/link/4)
"
`);

expect(warnMock).toHaveBeenCalledTimes(2);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 7,
"line": 2,
"offset": 7,
},
"start": {
"column": 2,
"line": 2,
"offset": 2,
},
},
"type": "text",
"value": "link1",
},
],
"position": {
"end": {
"column": 19,
"line": 2,
"offset": 19,
},
"start": {
"column": 1,
"line": 2,
"offset": 1,
},
},
"title": "fixed link title",
"type": "link",
"url": "/recovered-link",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx",
"url": "link1.mdx",
},
],
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 7,
"line": 6,
"offset": 43,
},
"start": {
"column": 2,
"line": 6,
"offset": 38,
},
},
"type": "text",
"value": "link3",
},
],
"position": {
"end": {
"column": 33,
"line": 6,
"offset": 69,
},
"start": {
"column": 1,
"line": 6,
"offset": 37,
},
},
"title": "fixed link title",
"type": "link",
"url": "/recovered-link",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx",
"url": "dir/link3.md?query#hash",
},
],
]
`);
});
});
});
});
Loading
Loading