mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-14 15:28:08 +02:00
feat(core): Add siteConfig.markdown.hooks
, deprecate siteConfig.onBrokenMarkdownLinks
(#11283)
This commit is contained in:
parent
ef71ddf937
commit
96c38d5fdd
35 changed files with 1580 additions and 381 deletions
|
@ -26,7 +26,6 @@ const config: Config = {
|
||||||
projectName: 'docusaurus', // Usually your repo name.
|
projectName: 'docusaurus', // Usually your repo name.
|
||||||
|
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
|
|
||||||
// Even if you don't use internationalization, you can use this field to set
|
// 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
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
|
|
@ -31,7 +31,6 @@ const config = {
|
||||||
projectName: 'docusaurus', // Usually your repo name.
|
projectName: 'docusaurus', // Usually your repo name.
|
||||||
|
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
|
|
||||||
// Even if you don't use internationalization, you can use this field to set
|
// 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
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
|
|
@ -22,6 +22,9 @@ import type {WebpackCompilerName} from '@docusaurus/utils';
|
||||||
import type {MDXFrontMatter} from './frontMatter';
|
import type {MDXFrontMatter} from './frontMatter';
|
||||||
import type {Options} from './options';
|
import type {Options} from './options';
|
||||||
import type {AdmonitionOptions} from './remark/admonitions';
|
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';
|
import type {ProcessorOptions} from '@mdx-js/mdx';
|
||||||
|
|
||||||
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
|
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
|
||||||
|
@ -121,13 +124,19 @@ async function createProcessorFactory() {
|
||||||
{
|
{
|
||||||
staticDirs: options.staticDirs,
|
staticDirs: options.staticDirs,
|
||||||
siteDir: options.siteDir,
|
siteDir: options.siteDir,
|
||||||
},
|
onBrokenMarkdownImages:
|
||||||
|
options.markdownConfig.hooks.onBrokenMarkdownImages,
|
||||||
|
} satisfies TransformImageOptions,
|
||||||
],
|
],
|
||||||
// TODO merge this with transformLinks?
|
// TODO merge this with transformLinks?
|
||||||
options.resolveMarkdownLink
|
options.resolveMarkdownLink
|
||||||
? [
|
? [
|
||||||
resolveMarkdownLinks,
|
resolveMarkdownLinks,
|
||||||
{resolveMarkdownLink: options.resolveMarkdownLink},
|
{
|
||||||
|
resolveMarkdownLink: options.resolveMarkdownLink,
|
||||||
|
onBrokenMarkdownLinks:
|
||||||
|
options.markdownConfig.hooks.onBrokenMarkdownLinks,
|
||||||
|
} satisfies ResolveMarkdownLinksOptions,
|
||||||
]
|
]
|
||||||
: undefined,
|
: undefined,
|
||||||
[
|
[
|
||||||
|
@ -135,7 +144,9 @@ async function createProcessorFactory() {
|
||||||
{
|
{
|
||||||
staticDirs: options.staticDirs,
|
staticDirs: options.staticDirs,
|
||||||
siteDir: options.siteDir,
|
siteDir: options.siteDir,
|
||||||
},
|
onBrokenMarkdownLinks:
|
||||||
|
options.markdownConfig.hooks.onBrokenMarkdownLinks,
|
||||||
|
} satisfies TransformLinksOptions,
|
||||||
],
|
],
|
||||||
gfm,
|
gfm,
|
||||||
options.markdownConfig.mdx1Compat.comments ? comment : null,
|
options.markdownConfig.mdx1Compat.comments ? comment : null,
|
||||||
|
|
|
@ -5,22 +5,47 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {jest} from '@jest/globals';
|
||||||
|
import * as path from 'path';
|
||||||
import plugin from '..';
|
import plugin from '..';
|
||||||
import type {PluginOptions} from '../index';
|
import type {PluginOptions} from '../index';
|
||||||
|
|
||||||
async function process(content: string) {
|
const siteDir = __dirname;
|
||||||
const {remark} = await import('remark');
|
|
||||||
|
|
||||||
const options: PluginOptions = {
|
const DefaultTestOptions: PluginOptions = {
|
||||||
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
|
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
|
||||||
|
onBrokenMarkdownLinks: 'throw',
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
return result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('resolveMarkdownLinks remark plugin', () => {
|
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 () => {
|
it('resolves Markdown and MDX links', async () => {
|
||||||
/* language=markdown */
|
/* language=markdown */
|
||||||
const content = `[link1](link1.mdx)
|
const content = `[link1](link1.mdx)
|
||||||
|
@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,11 +8,18 @@
|
||||||
import {
|
import {
|
||||||
parseLocalURLPath,
|
parseLocalURLPath,
|
||||||
serializeURLPath,
|
serializeURLPath,
|
||||||
|
toMessageRelativeFilePath,
|
||||||
type URLPath,
|
type URLPath,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
|
||||||
|
import {formatNodePositionExtraMessage} from '../utils';
|
||||||
import type {Plugin, Transformer} from 'unified';
|
import type {Plugin, Transformer} from 'unified';
|
||||||
import type {Definition, Link, Root} from 'mdast';
|
import type {Definition, Link, Root} from 'mdast';
|
||||||
|
import type {
|
||||||
|
MarkdownConfig,
|
||||||
|
OnBrokenMarkdownLinksFunction,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type ResolveMarkdownLinkParams = {
|
type ResolveMarkdownLinkParams = {
|
||||||
/**
|
/**
|
||||||
|
@ -32,6 +39,33 @@ export type ResolveMarkdownLink = (
|
||||||
|
|
||||||
export interface PluginOptions {
|
export interface PluginOptions {
|
||||||
resolveMarkdownLink: ResolveMarkdownLink;
|
resolveMarkdownLink: ResolveMarkdownLink;
|
||||||
|
onBrokenMarkdownLinks: MarkdownConfig['hooks']['onBrokenMarkdownLinks'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function asFunction(
|
||||||
|
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'],
|
||||||
|
): OnBrokenMarkdownLinksFunction {
|
||||||
|
if (typeof onBrokenMarkdownLinks === 'string') {
|
||||||
|
const extraHelp =
|
||||||
|
onBrokenMarkdownLinks === 'throw'
|
||||||
|
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} option, or apply the code=${'pathname://'} protocol to the broken link URLs.`
|
||||||
|
: '';
|
||||||
|
return ({sourceFilePath, url: linkUrl, node}) => {
|
||||||
|
const relativePath = toMessageRelativeFilePath(sourceFilePath);
|
||||||
|
logger.report(
|
||||||
|
onBrokenMarkdownLinks,
|
||||||
|
)`Markdown link with URL code=${linkUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
|
||||||
|
node,
|
||||||
|
)} couldn't be resolved.
|
||||||
|
Make sure it references a local Markdown file that exists within the current plugin.${extraHelp}`;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return (params) =>
|
||||||
|
onBrokenMarkdownLinks({
|
||||||
|
...params,
|
||||||
|
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const HAS_MARKDOWN_EXTENSION = /\.mdx?$/i;
|
const HAS_MARKDOWN_EXTENSION = /\.mdx?$/i;
|
||||||
|
@ -57,10 +91,15 @@ function parseMarkdownLinkURLPath(link: string): URLPath | null {
|
||||||
* This is exposed as "data.contentTitle" to the processed vfile
|
* This is exposed as "data.contentTitle" to the processed vfile
|
||||||
* Also gives the ability to strip that content title (used for the blog plugin)
|
* Also gives the ability to strip that content title (used for the blog plugin)
|
||||||
*/
|
*/
|
||||||
|
// TODO merge this plugin with "transformLinks"
|
||||||
|
// in general we'd want to avoid traversing multiple times the same AST
|
||||||
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
||||||
options,
|
options,
|
||||||
): Transformer<Root> {
|
): Transformer<Root> {
|
||||||
const {resolveMarkdownLink} = options;
|
const {resolveMarkdownLink} = options;
|
||||||
|
|
||||||
|
const onBrokenMarkdownLinks = asFunction(options.onBrokenMarkdownLinks);
|
||||||
|
|
||||||
return async (root, file) => {
|
return async (root, file) => {
|
||||||
const {visit} = await import('unist-util-visit');
|
const {visit} = await import('unist-util-visit');
|
||||||
|
|
||||||
|
@ -71,18 +110,26 @@ const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceFilePath = file.path;
|
||||||
|
|
||||||
const permalink = resolveMarkdownLink({
|
const permalink = resolveMarkdownLink({
|
||||||
sourceFilePath: file.path,
|
sourceFilePath,
|
||||||
linkPathname: linkURLPath.pathname,
|
linkPathname: linkURLPath.pathname,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (permalink) {
|
if (permalink) {
|
||||||
// This reapplies the link ?qs#hash part to the resolved pathname
|
// This reapplies the link ?qs#hash part to the resolved pathname
|
||||||
const resolvedUrl = serializeURLPath({
|
link.url = serializeURLPath({
|
||||||
...linkURLPath,
|
...linkURLPath,
|
||||||
pathname: permalink,
|
pathname: permalink,
|
||||||
});
|
});
|
||||||
link.url = resolvedUrl;
|
} else {
|
||||||
|
link.url =
|
||||||
|
onBrokenMarkdownLinks({
|
||||||
|
url: link.url,
|
||||||
|
sourceFilePath,
|
||||||
|
node: link,
|
||||||
|
}) ?? link.url;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||

|
|
|
@ -1 +0,0 @@
|
||||||

|
|
|
@ -1 +0,0 @@
|
||||||

|
|
|
@ -1 +0,0 @@
|
||||||
![img]()
|
|
|
@ -1 +0,0 @@
|
||||||

|
|
|
@ -1,16 +1,10 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`transformImage plugin does not choke on invalid image 1`] = `
|
exports[`transformImage plugin does not choke on invalid image 1`] = `
|
||||||
"<img alt="invalid image" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/invalid.png").default} />
|
"<img alt="invalid image" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./../static/invalid.png").default} />
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`transformImage plugin fail if image does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/img/doesNotExist.png or packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static2/img/doesNotExist.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/fail.md not found."`;
|
|
||||||
|
|
||||||
exports[`transformImage plugin fail if image relative path does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/notFound.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/fail2.md not found."`;
|
|
||||||
|
|
||||||
exports[`transformImage plugin fail if image url is absent 1`] = `"Markdown image URL is mandatory in "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/noUrl.md" file"`;
|
|
||||||
|
|
||||||
exports[`transformImage plugin pathname protocol 1`] = `
|
exports[`transformImage plugin pathname protocol 1`] = `
|
||||||
"
|
"
|
||||||
"
|
"
|
||||||
|
|
|
@ -6,65 +6,361 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import path from 'path';
|
import * as path from 'path';
|
||||||
import vfile from 'to-vfile';
|
import vfile from 'to-vfile';
|
||||||
import plugin, {type PluginOptions} from '../index';
|
import plugin, {type PluginOptions} from '../index';
|
||||||
|
|
||||||
const processFixture = async (
|
const siteDir = path.join(__dirname, '__fixtures__');
|
||||||
name: string,
|
|
||||||
options: Partial<PluginOptions>,
|
|
||||||
) => {
|
|
||||||
const {remark} = await import('remark');
|
|
||||||
const {default: mdx} = await import('remark-mdx');
|
|
||||||
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
|
|
||||||
const file = await vfile.read(filePath);
|
|
||||||
|
|
||||||
const result = await remark()
|
|
||||||
.use(mdx)
|
|
||||||
.use(plugin, {siteDir: __dirname, staticDirs: [], ...options})
|
|
||||||
.process(file);
|
|
||||||
|
|
||||||
return result.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const staticDirs = [
|
const staticDirs = [
|
||||||
path.join(__dirname, '__fixtures__/static'),
|
path.join(__dirname, '__fixtures__/static'),
|
||||||
path.join(__dirname, '__fixtures__/static2'),
|
path.join(__dirname, '__fixtures__/static2'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__');
|
const getProcessor = async (options?: Partial<PluginOptions>) => {
|
||||||
|
const {remark} = await import('remark');
|
||||||
|
const {default: mdx} = await import('remark-mdx');
|
||||||
|
return remark()
|
||||||
|
.use(mdx)
|
||||||
|
.use(plugin, {
|
||||||
|
siteDir,
|
||||||
|
staticDirs,
|
||||||
|
onBrokenMarkdownImages: 'throw',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFixture = async (
|
||||||
|
name: string,
|
||||||
|
options?: Partial<PluginOptions>,
|
||||||
|
) => {
|
||||||
|
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
|
||||||
|
const file = await vfile.read(filePath);
|
||||||
|
const processor = await getProcessor(options);
|
||||||
|
const result = await processor.process(file);
|
||||||
|
return result.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processContent = async (
|
||||||
|
content: string,
|
||||||
|
options?: Partial<PluginOptions>,
|
||||||
|
) => {
|
||||||
|
const processor = await getProcessor(options);
|
||||||
|
const result = await processor.process({
|
||||||
|
value: content,
|
||||||
|
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
|
||||||
|
});
|
||||||
|
return result.value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
describe('transformImage plugin', () => {
|
describe('transformImage plugin', () => {
|
||||||
it('fail if image does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
processFixture('fail', {staticDirs}),
|
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
|
||||||
});
|
|
||||||
it('fail if image relative path does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
processFixture('fail2', {staticDirs}),
|
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
|
||||||
});
|
|
||||||
it('fail if image url is absent', async () => {
|
|
||||||
await expect(
|
|
||||||
processFixture('noUrl', {staticDirs}),
|
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('transform md images to <img />', async () => {
|
it('transform md images to <img />', async () => {
|
||||||
const result = await processFixture('img', {staticDirs, siteDir});
|
// TODO split that large fixture into many smaller test cases?
|
||||||
|
const result = await processFixture('img');
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pathname protocol', async () => {
|
it('pathname protocol', async () => {
|
||||||
const result = await processFixture('pathname', {staticDirs});
|
const result = await processContent(
|
||||||
|
``,
|
||||||
|
);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not choke on invalid image', async () => {
|
it('does not choke on invalid image', async () => {
|
||||||
const errorMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
const errorMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
const result = await processFixture('invalid-img', {staticDirs});
|
const result = await processContent(``);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
expect(errorMock).toHaveBeenCalledTimes(1);
|
expect(errorMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onBrokenMarkdownImages', () => {
|
||||||
|
const fixtures = {
|
||||||
|
doesNotExistAbsolute: ``,
|
||||||
|
doesNotExistRelative: ``,
|
||||||
|
doesNotExistSiteAlias: ``,
|
||||||
|
urlEmpty: `![img]()`,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('throws', () => {
|
||||||
|
it('if image absolute path does not exist', async () => {
|
||||||
|
await expect(processContent(fixtures.doesNotExistAbsolute)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown image with URL \`/img/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
|
||||||
|
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image relative path does not exist', async () => {
|
||||||
|
await expect(processContent(fixtures.doesNotExistRelative)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown image with URL \`./doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
|
||||||
|
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image @site path does not exist', async () => {
|
||||||
|
await expect(processContent(fixtures.doesNotExistSiteAlias)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown image with URL \`@site/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
|
||||||
|
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image url empty', async () => {
|
||||||
|
await expect(processContent(fixtures.urlEmpty)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown image with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1).
|
||||||
|
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warns', () => {
|
||||||
|
function processWarn(content: string) {
|
||||||
|
return processContent(content, {onBrokenMarkdownImages: 'warn'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
warnMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image absolute path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistAbsolute);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown image with URL \`/img/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image relative path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistRelative);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown image with URL \`./doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image @site path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistSiteAlias);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown image with URL \`@site/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image url empty', async () => {
|
||||||
|
const result = await processWarn(fixtures.urlEmpty);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"![img]()
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown image with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1).",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('function form', () => {
|
||||||
|
function processWarn(content: string) {
|
||||||
|
return processContent(content, {
|
||||||
|
onBrokenMarkdownImages: (params) => {
|
||||||
|
console.log('onBrokenMarkdownImages called for ', params);
|
||||||
|
// We can alter the AST Node
|
||||||
|
params.node.alt = 'new 404 alt';
|
||||||
|
params.node.url = 'ignored, less important than returned value';
|
||||||
|
// Or return a new URL
|
||||||
|
return '/404.png';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
logMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image absolute path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistAbsolute);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownImages called for ",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"alt": "new 404 alt",
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 30,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 29,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": null,
|
||||||
|
"type": "image",
|
||||||
|
"url": "/404.png",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "/img/doesNotExist.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image relative path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistRelative);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownImages called for ",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"alt": "new 404 alt",
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 27,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 26,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": null,
|
||||||
|
"type": "image",
|
||||||
|
"url": "/404.png",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "./doesNotExist.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image @site path does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.doesNotExistSiteAlias);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownImages called for ",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"alt": "new 404 alt",
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 31,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 30,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": null,
|
||||||
|
"type": "image",
|
||||||
|
"url": "/404.png",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "@site/doesNotExist.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if image url empty', async () => {
|
||||||
|
const result = await processWarn(fixtures.urlEmpty);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
"
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownImages called for ",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"alt": "new 404 alt",
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 9,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 8,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": null,
|
||||||
|
"type": "image",
|
||||||
|
"url": "/404.png",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,22 +19,67 @@ import {
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import {imageSizeFromFile} from 'image-size/fromFile';
|
import {imageSizeFromFile} from 'image-size/fromFile';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {assetRequireAttributeValue, transformNode} from '../utils';
|
import {
|
||||||
|
assetRequireAttributeValue,
|
||||||
|
formatNodePositionExtraMessage,
|
||||||
|
transformNode,
|
||||||
|
} from '../utils';
|
||||||
import type {Plugin, Transformer} from 'unified';
|
import type {Plugin, Transformer} from 'unified';
|
||||||
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
||||||
import type {Image, Root} from 'mdast';
|
import type {Image, Root} from 'mdast';
|
||||||
import type {Parent} from 'unist';
|
import type {Parent} from 'unist';
|
||||||
|
import type {
|
||||||
|
MarkdownConfig,
|
||||||
|
OnBrokenMarkdownImagesFunction,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type PluginOptions = {
|
export type PluginOptions = {
|
||||||
staticDirs: string[];
|
staticDirs: string[];
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
|
onBrokenMarkdownImages: MarkdownConfig['hooks']['onBrokenMarkdownImages'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Context = PluginOptions & {
|
type Context = {
|
||||||
|
staticDirs: PluginOptions['staticDirs'];
|
||||||
|
siteDir: PluginOptions['siteDir'];
|
||||||
|
onBrokenMarkdownImages: OnBrokenMarkdownImagesFunction;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
inlineMarkdownImageFileLoader: string;
|
inlineMarkdownImageFileLoader: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function asFunction(
|
||||||
|
onBrokenMarkdownImages: PluginOptions['onBrokenMarkdownImages'],
|
||||||
|
): OnBrokenMarkdownImagesFunction {
|
||||||
|
if (typeof onBrokenMarkdownImages === 'string') {
|
||||||
|
const extraHelp =
|
||||||
|
onBrokenMarkdownImages === 'throw'
|
||||||
|
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownImages'} option, or apply the code=${'pathname://'} protocol to the broken image URLs.`
|
||||||
|
: '';
|
||||||
|
return ({sourceFilePath, url: imageUrl, node}) => {
|
||||||
|
const relativePath = toMessageRelativeFilePath(sourceFilePath);
|
||||||
|
if (imageUrl) {
|
||||||
|
logger.report(
|
||||||
|
onBrokenMarkdownImages,
|
||||||
|
)`Markdown image with URL code=${imageUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
|
||||||
|
node,
|
||||||
|
)} couldn't be resolved to an existing local image file.${extraHelp}`;
|
||||||
|
} else {
|
||||||
|
logger.report(
|
||||||
|
onBrokenMarkdownImages,
|
||||||
|
)`Markdown image with empty URL found in source file path=${relativePath}${formatNodePositionExtraMessage(
|
||||||
|
node,
|
||||||
|
)}.${extraHelp}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return (params) =>
|
||||||
|
onBrokenMarkdownImages({
|
||||||
|
...params,
|
||||||
|
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Target = [node: Image, index: number, parent: Parent];
|
type Target = [node: Image, index: number, parent: Parent];
|
||||||
|
|
||||||
async function toImageRequireNode(
|
async function toImageRequireNode(
|
||||||
|
@ -51,7 +96,7 @@ async function toImageRequireNode(
|
||||||
);
|
);
|
||||||
relativeImagePath = `./${relativeImagePath}`;
|
relativeImagePath = `./${relativeImagePath}`;
|
||||||
|
|
||||||
const parsedUrl = parseURLOrPath(node.url, 'https://example.com');
|
const parsedUrl = parseURLOrPath(node.url);
|
||||||
const hash = parsedUrl.hash ?? '';
|
const hash = parsedUrl.hash ?? '';
|
||||||
const search = parsedUrl.search ?? '';
|
const search = parsedUrl.search ?? '';
|
||||||
const requireString = `${context.inlineMarkdownImageFileLoader}${
|
const requireString = `${context.inlineMarkdownImageFileLoader}${
|
||||||
|
@ -113,57 +158,53 @@ ${(err as Error).message}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
async function getLocalImageAbsolutePath(
|
||||||
const imageExists = await fs.pathExists(imagePath);
|
originalImagePath: string,
|
||||||
if (!imageExists) {
|
|
||||||
throw new Error(
|
|
||||||
`Image ${toMessageRelativeFilePath(
|
|
||||||
imagePath,
|
|
||||||
)} used in ${toMessageRelativeFilePath(sourceFilePath)} not found.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getImageAbsolutePath(
|
|
||||||
imagePath: string,
|
|
||||||
{siteDir, filePath, staticDirs}: Context,
|
{siteDir, filePath, staticDirs}: Context,
|
||||||
) {
|
) {
|
||||||
if (imagePath.startsWith('@site/')) {
|
if (originalImagePath.startsWith('@site/')) {
|
||||||
const imageFilePath = path.join(siteDir, imagePath.replace('@site/', ''));
|
const imageFilePath = path.join(
|
||||||
await ensureImageFileExist(imageFilePath, filePath);
|
siteDir,
|
||||||
|
originalImagePath.replace('@site/', ''),
|
||||||
|
);
|
||||||
|
if (!(await fs.pathExists(imageFilePath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return imageFilePath;
|
return imageFilePath;
|
||||||
} else if (path.isAbsolute(imagePath)) {
|
} else if (path.isAbsolute(originalImagePath)) {
|
||||||
// Absolute paths are expected to exist in the static folder.
|
// Absolute paths are expected to exist in the static folder.
|
||||||
const possiblePaths = staticDirs.map((dir) => path.join(dir, imagePath));
|
const possiblePaths = staticDirs.map((dir) =>
|
||||||
|
path.join(dir, originalImagePath),
|
||||||
|
);
|
||||||
const imageFilePath = await findAsyncSequential(
|
const imageFilePath = await findAsyncSequential(
|
||||||
possiblePaths,
|
possiblePaths,
|
||||||
fs.pathExists,
|
fs.pathExists,
|
||||||
);
|
);
|
||||||
if (!imageFilePath) {
|
if (!imageFilePath) {
|
||||||
throw new Error(
|
return null;
|
||||||
`Image ${possiblePaths
|
}
|
||||||
.map((p) => toMessageRelativeFilePath(p))
|
return imageFilePath;
|
||||||
.join(' or ')} used in ${toMessageRelativeFilePath(
|
} else {
|
||||||
filePath,
|
// relative paths are resolved against the source file's folder
|
||||||
)} not found.`,
|
const imageFilePath = path.join(path.dirname(filePath), originalImagePath);
|
||||||
);
|
if (!(await fs.pathExists(imageFilePath))) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return imageFilePath;
|
return imageFilePath;
|
||||||
}
|
}
|
||||||
// relative paths are resolved against the source file's folder
|
|
||||||
const imageFilePath = path.join(path.dirname(filePath), imagePath);
|
|
||||||
await ensureImageFileExist(imageFilePath, filePath);
|
|
||||||
return imageFilePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processImageNode(target: Target, context: Context) {
|
async function processImageNode(target: Target, context: Context) {
|
||||||
const [node] = target;
|
const [node] = target;
|
||||||
|
|
||||||
if (!node.url) {
|
if (!node.url) {
|
||||||
throw new Error(
|
node.url =
|
||||||
`Markdown image URL is mandatory in "${toMessageRelativeFilePath(
|
context.onBrokenMarkdownImages({
|
||||||
context.filePath,
|
url: node.url,
|
||||||
)}" file`,
|
sourceFilePath: context.filePath,
|
||||||
);
|
node,
|
||||||
|
}) ?? node.url;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUrl = url.parse(node.url);
|
const parsedUrl = url.parse(node.url);
|
||||||
|
@ -183,13 +224,27 @@ async function processImageNode(target: Target, context: Context) {
|
||||||
|
|
||||||
// We try to convert image urls without protocol to images with require calls
|
// We try to convert image urls without protocol to images with require calls
|
||||||
// going through webpack ensures that image assets exist at build time
|
// going through webpack ensures that image assets exist at build time
|
||||||
const imagePath = await getImageAbsolutePath(decodedPathname, context);
|
const localImagePath = await getLocalImageAbsolutePath(
|
||||||
await toImageRequireNode(target, imagePath, context);
|
decodedPathname,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (localImagePath === null) {
|
||||||
|
node.url =
|
||||||
|
context.onBrokenMarkdownImages({
|
||||||
|
url: node.url,
|
||||||
|
sourceFilePath: context.filePath,
|
||||||
|
node,
|
||||||
|
}) ?? node.url;
|
||||||
|
} else {
|
||||||
|
await toImageRequireNode(target, localImagePath, context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
||||||
options,
|
options,
|
||||||
): Transformer<Root> {
|
): Transformer<Root> {
|
||||||
|
const onBrokenMarkdownImages = asFunction(options.onBrokenMarkdownImages);
|
||||||
|
|
||||||
return async (root, vfile) => {
|
return async (root, vfile) => {
|
||||||
const {visit} = await import('unist-util-visit');
|
const {visit} = await import('unist-util-visit');
|
||||||
|
|
||||||
|
@ -201,6 +256,7 @@ const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
||||||
filePath: vfile.path!,
|
filePath: vfile.path!,
|
||||||
inlineMarkdownImageFileLoader:
|
inlineMarkdownImageFileLoader:
|
||||||
fileLoaderUtils.loaders.inlineMarkdownImageFileLoader,
|
fileLoaderUtils.loaders.inlineMarkdownImageFileLoader,
|
||||||
|
onBrokenMarkdownImages,
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
[asset]()
|
|
|
@ -1 +0,0 @@
|
||||||
[nonexistent](@site/foo.pdf)
|
|
|
@ -1 +0,0 @@
|
||||||
[asset](pathname:///asset/unchecked.pdf)
|
|
|
@ -1,15 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link URL is mandatory in "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/noUrl.md" file (title: asset, line: 1)."`;
|
exports[`transformLinks plugin transform md links to <a /> 1`] = `
|
||||||
|
|
||||||
exports[`transformAsset plugin fail if asset with site alias does not exist 1`] = `"Asset packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/foo.pdf used in packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/nonexistentSiteAlias.md not found."`;
|
|
||||||
|
|
||||||
exports[`transformAsset plugin pathname protocol 1`] = `
|
|
||||||
"[asset](pathname:///asset/unchecked.pdf)
|
|
||||||
"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`transformAsset plugin transform md links to <a /> 1`] = `
|
|
||||||
"[asset](https://example.com/asset.pdf)
|
"[asset](https://example.com/asset.pdf)
|
||||||
|
|
||||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
|
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
|
||||||
|
@ -54,6 +45,5 @@ in paragraph <a target="_blank" data-noBrokenLinkCheck={true} href={require("!<P
|
||||||
|
|
||||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
|
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
|
||||||
|
|
||||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>
|
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>"
|
||||||
"
|
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -5,53 +5,270 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import {jest} from '@jest/globals';
|
||||||
|
import * as path from 'path';
|
||||||
import vfile from 'to-vfile';
|
import vfile from 'to-vfile';
|
||||||
import plugin from '..';
|
import plugin, {type PluginOptions} from '..';
|
||||||
import transformImage, {type PluginOptions} from '../../transformImage';
|
import transformImage from '../../transformImage';
|
||||||
|
|
||||||
const processFixture = async (name: string, options?: PluginOptions) => {
|
const siteDir = path.join(__dirname, `__fixtures__`);
|
||||||
|
|
||||||
|
const staticDirs = [
|
||||||
|
path.join(siteDir, 'static'),
|
||||||
|
path.join(siteDir, 'static2'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const getProcessor = async (options?: Partial<PluginOptions>) => {
|
||||||
const {remark} = await import('remark');
|
const {remark} = await import('remark');
|
||||||
const {default: mdx} = await import('remark-mdx');
|
const {default: mdx} = await import('remark-mdx');
|
||||||
const siteDir = path.join(__dirname, `__fixtures__`);
|
return remark()
|
||||||
const staticDirs = [
|
|
||||||
path.join(siteDir, 'static'),
|
|
||||||
path.join(siteDir, 'static2'),
|
|
||||||
];
|
|
||||||
const file = await vfile.read(path.join(siteDir, `${name}.md`));
|
|
||||||
const result = await remark()
|
|
||||||
.use(mdx)
|
.use(mdx)
|
||||||
.use(transformImage, {...options, siteDir, staticDirs})
|
.use(transformImage, {
|
||||||
.use(plugin, {
|
siteDir,
|
||||||
...options,
|
|
||||||
staticDirs,
|
staticDirs,
|
||||||
siteDir: path.join(__dirname, '__fixtures__'),
|
onBrokenMarkdownImages: 'throw',
|
||||||
})
|
})
|
||||||
.process(file);
|
.use(plugin, {
|
||||||
|
staticDirs,
|
||||||
return result.value;
|
siteDir,
|
||||||
|
onBrokenMarkdownLinks: 'throw',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('transformAsset plugin', () => {
|
const processFixture = async (
|
||||||
it('fail if asset url is absent', async () => {
|
name: string,
|
||||||
await expect(
|
options?: Partial<PluginOptions>,
|
||||||
processFixture('noUrl'),
|
) => {
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
const processor = await getProcessor(options);
|
||||||
});
|
const file = await vfile.read(path.join(siteDir, `${name}.md`));
|
||||||
|
const result = await processor.process(file);
|
||||||
|
return result.value.toString().trim();
|
||||||
|
};
|
||||||
|
|
||||||
it('fail if asset with site alias does not exist', async () => {
|
const processContent = async (
|
||||||
await expect(
|
content: string,
|
||||||
processFixture('nonexistentSiteAlias'),
|
options?: Partial<PluginOptions>,
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
) => {
|
||||||
|
const processor = await getProcessor(options);
|
||||||
|
const result = await processor.process({
|
||||||
|
value: content,
|
||||||
|
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
|
||||||
});
|
});
|
||||||
|
return result.value.toString().trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('transformLinks plugin', () => {
|
||||||
it('transform md links to <a />', async () => {
|
it('transform md links to <a />', async () => {
|
||||||
|
// TODO split fixture in many smaller test cases
|
||||||
const result = await processFixture('asset');
|
const result = await processFixture('asset');
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pathname protocol', async () => {
|
it('pathname protocol', async () => {
|
||||||
const result = await processFixture('pathname');
|
const result = await processContent(`pathname:///unchecked.pdf)`);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchInlineSnapshot(`"pathname:///unchecked.pdf)"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts absolute file that does not exist', async () => {
|
||||||
|
const result = await processContent(`[file](/dir/file.zip)`);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"[file](/dir/file.zip)"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts relative file that does not exist', async () => {
|
||||||
|
const result = await processContent(`[file](dir/file.zip)`);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"[file](dir/file.zip)"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onBrokenMarkdownLinks', () => {
|
||||||
|
const fixtures = {
|
||||||
|
urlEmpty: `[empty]()`,
|
||||||
|
fileDoesNotExistSiteAlias: `[file](@site/file.zip)`,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('throws', () => {
|
||||||
|
it('if url is empty', async () => {
|
||||||
|
await expect(processContent(fixtures.urlEmpty)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown link with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1).
|
||||||
|
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if file with site alias does not exist', async () => {
|
||||||
|
await expect(processContent(fixtures.fileDoesNotExistSiteAlias)).rejects
|
||||||
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"Markdown link with URL \`@site/file.zip\` in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/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', () => {
|
||||||
|
function processWarn(content: string) {
|
||||||
|
return processContent(content, {onBrokenMarkdownLinks: 'warn'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
warnMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if url is empty', async () => {
|
||||||
|
const result = await processWarn(fixtures.urlEmpty);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"[empty]()"`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown link with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1).",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if file with site alias does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.fileDoesNotExistSiteAlias);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"[file](@site/file.zip)"`);
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"[WARNING] Markdown link with URL \`@site/file.zip\` in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved.
|
||||||
|
Make sure it references a local Markdown file that exists within the current plugin.",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('function form', () => {
|
||||||
|
function processWarn(content: string) {
|
||||||
|
return processContent(content, {
|
||||||
|
onBrokenMarkdownLinks: (params) => {
|
||||||
|
console.log('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 '/404';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
logMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if url is empty', async () => {
|
||||||
|
const result = await processWarn(fixtures.urlEmpty);
|
||||||
|
expect(result).toMatchInlineSnapshot(
|
||||||
|
`"[empty](/404 "fixed link title")"`,
|
||||||
|
);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownLinks called with",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 7,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 6,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 2,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "text",
|
||||||
|
"value": "empty",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 10,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 9,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": "fixed link title",
|
||||||
|
"type": "link",
|
||||||
|
"url": "/404",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if file with site alias does not exist', async () => {
|
||||||
|
const result = await processWarn(fixtures.fileDoesNotExistSiteAlias);
|
||||||
|
expect(result).toMatchInlineSnapshot(
|
||||||
|
`"[file](/404 "fixed link title")"`,
|
||||||
|
);
|
||||||
|
expect(logMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logMock.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"onBrokenMarkdownLinks called with",
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 6,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 5,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 2,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "text",
|
||||||
|
"value": "file",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"position": {
|
||||||
|
"end": {
|
||||||
|
"column": 23,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 22,
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"column": 1,
|
||||||
|
"line": 1,
|
||||||
|
"offset": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": "fixed link title",
|
||||||
|
"type": "link",
|
||||||
|
"url": "/404",
|
||||||
|
},
|
||||||
|
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx",
|
||||||
|
"url": "@site/file.zip",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,24 +17,72 @@ import {
|
||||||
parseURLOrPath,
|
parseURLOrPath,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import {assetRequireAttributeValue, transformNode} from '../utils';
|
import logger from '@docusaurus/logger';
|
||||||
|
import {
|
||||||
|
assetRequireAttributeValue,
|
||||||
|
formatNodePositionExtraMessage,
|
||||||
|
transformNode,
|
||||||
|
} from '../utils';
|
||||||
import type {Plugin, Transformer} from 'unified';
|
import type {Plugin, Transformer} from 'unified';
|
||||||
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
import type {MdxJsxTextElement} from 'mdast-util-mdx';
|
||||||
import type {Parent} from 'unist';
|
import type {Parent} from 'unist';
|
||||||
import type {Link, Literal, Root} from 'mdast';
|
import type {Link, Root} from 'mdast';
|
||||||
|
import type {
|
||||||
|
MarkdownConfig,
|
||||||
|
OnBrokenMarkdownLinksFunction,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type PluginOptions = {
|
export type PluginOptions = {
|
||||||
staticDirs: string[];
|
staticDirs: string[];
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
|
onBrokenMarkdownLinks: MarkdownConfig['hooks']['onBrokenMarkdownLinks'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Context = PluginOptions & {
|
type Context = PluginOptions & {
|
||||||
|
staticDirs: string[];
|
||||||
|
siteDir: string;
|
||||||
|
onBrokenMarkdownLinks: OnBrokenMarkdownLinksFunction;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
inlineMarkdownLinkFileLoader: string;
|
inlineMarkdownLinkFileLoader: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Target = [node: Link, index: number, parent: Parent];
|
type Target = [node: Link, index: number, parent: Parent];
|
||||||
|
|
||||||
|
function asFunction(
|
||||||
|
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'],
|
||||||
|
): OnBrokenMarkdownLinksFunction {
|
||||||
|
if (typeof onBrokenMarkdownLinks === 'string') {
|
||||||
|
const extraHelp =
|
||||||
|
onBrokenMarkdownLinks === 'throw'
|
||||||
|
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} option, or apply the code=${'pathname://'} protocol to the broken link URLs.`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return ({sourceFilePath, url: linkUrl, node}) => {
|
||||||
|
const relativePath = toMessageRelativeFilePath(sourceFilePath);
|
||||||
|
if (linkUrl) {
|
||||||
|
logger.report(
|
||||||
|
onBrokenMarkdownLinks,
|
||||||
|
)`Markdown link with URL code=${linkUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
|
||||||
|
node,
|
||||||
|
)} couldn't be resolved.
|
||||||
|
Make sure it references a local Markdown file that exists within the current plugin.${extraHelp}`;
|
||||||
|
} else {
|
||||||
|
logger.report(
|
||||||
|
onBrokenMarkdownLinks,
|
||||||
|
)`Markdown link with empty URL found in source file path=${relativePath}${formatNodePositionExtraMessage(
|
||||||
|
node,
|
||||||
|
)}.${extraHelp}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return (params) =>
|
||||||
|
onBrokenMarkdownLinks({
|
||||||
|
...params,
|
||||||
|
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the link node to a JSX `<a>` element with a `require()` call.
|
* Transforms the link node to a JSX `<a>` element with a `require()` call.
|
||||||
*/
|
*/
|
||||||
|
@ -123,27 +171,15 @@ async function toAssetRequireNode(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
|
async function getLocalFileAbsolutePath(
|
||||||
const assetExists = await fs.pathExists(assetPath);
|
|
||||||
if (!assetExists) {
|
|
||||||
throw new Error(
|
|
||||||
`Asset ${toMessageRelativeFilePath(
|
|
||||||
assetPath,
|
|
||||||
)} used in ${toMessageRelativeFilePath(sourceFilePath)} not found.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAssetAbsolutePath(
|
|
||||||
assetPath: string,
|
assetPath: string,
|
||||||
{siteDir, filePath, staticDirs}: Context,
|
{siteDir, filePath, staticDirs}: Context,
|
||||||
) {
|
) {
|
||||||
if (assetPath.startsWith('@site/')) {
|
if (assetPath.startsWith('@site/')) {
|
||||||
const assetFilePath = path.join(siteDir, assetPath.replace('@site/', ''));
|
const assetFilePath = path.join(siteDir, assetPath.replace('@site/', ''));
|
||||||
// The @site alias is the only way to believe that the user wants an asset.
|
if (await fs.pathExists(assetFilePath)) {
|
||||||
// Everything else can just be a link URL
|
return assetFilePath;
|
||||||
await ensureAssetFileExist(assetFilePath, filePath);
|
}
|
||||||
return assetFilePath;
|
|
||||||
} else if (path.isAbsolute(assetPath)) {
|
} else if (path.isAbsolute(assetPath)) {
|
||||||
const assetFilePath = await findAsyncSequential(
|
const assetFilePath = await findAsyncSequential(
|
||||||
staticDirs.map((dir) => path.join(dir, assetPath)),
|
staticDirs.map((dir) => path.join(dir, assetPath)),
|
||||||
|
@ -164,16 +200,13 @@ async function getAssetAbsolutePath(
|
||||||
async function processLinkNode(target: Target, context: Context) {
|
async function processLinkNode(target: Target, context: Context) {
|
||||||
const [node] = target;
|
const [node] = target;
|
||||||
if (!node.url) {
|
if (!node.url) {
|
||||||
// Try to improve error feedback
|
node.url =
|
||||||
// see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675
|
context.onBrokenMarkdownLinks({
|
||||||
const title =
|
url: node.url,
|
||||||
node.title ?? (node.children[0] as Literal | undefined)?.value ?? '?';
|
sourceFilePath: context.filePath,
|
||||||
const line = node.position?.start.line ?? '?';
|
node,
|
||||||
throw new Error(
|
}) ?? node.url;
|
||||||
`Markdown link URL is mandatory in "${toMessageRelativeFilePath(
|
return;
|
||||||
context.filePath,
|
|
||||||
)}" file (title: ${title}, line: ${line}).`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUrl = url.parse(node.url);
|
const parsedUrl = url.parse(node.url);
|
||||||
|
@ -189,29 +222,48 @@ async function processLinkNode(target: Target, context: Context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetPath = await getAssetAbsolutePath(
|
const localFilePath = await getLocalFileAbsolutePath(
|
||||||
decodeURIComponent(parsedUrl.pathname),
|
decodeURIComponent(parsedUrl.pathname),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
if (assetPath) {
|
|
||||||
await toAssetRequireNode(target, assetPath, context);
|
if (localFilePath) {
|
||||||
|
await toAssetRequireNode(target, localFilePath, context);
|
||||||
|
} else {
|
||||||
|
// The @site alias is the only way to believe that the user wants an asset.
|
||||||
|
if (hasSiteAlias) {
|
||||||
|
node.url =
|
||||||
|
context.onBrokenMarkdownLinks({
|
||||||
|
url: node.url,
|
||||||
|
sourceFilePath: context.filePath,
|
||||||
|
node,
|
||||||
|
}) ?? node.url;
|
||||||
|
} else {
|
||||||
|
// Even if the url has a dot, and it looks like a file extension
|
||||||
|
// it can be risky to throw and fail fast by default
|
||||||
|
// It's perfectly valid for a route path segment to look like a filename
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
const plugin: Plugin<PluginOptions[], Root> = function plugin(
|
||||||
options,
|
options,
|
||||||
): Transformer<Root> {
|
): Transformer<Root> {
|
||||||
|
const onBrokenMarkdownLinks = asFunction(options.onBrokenMarkdownLinks);
|
||||||
|
|
||||||
return async (root, vfile) => {
|
return async (root, vfile) => {
|
||||||
const {visit} = await import('unist-util-visit');
|
const {visit} = await import('unist-util-visit');
|
||||||
|
|
||||||
const fileLoaderUtils = getFileLoaderUtils(
|
const fileLoaderUtils = getFileLoaderUtils(
|
||||||
vfile.data.compilerName === 'server',
|
vfile.data.compilerName === 'server',
|
||||||
);
|
);
|
||||||
|
|
||||||
const context: Context = {
|
const context: Context = {
|
||||||
...options,
|
...options,
|
||||||
filePath: vfile.path!,
|
filePath: vfile.path!,
|
||||||
inlineMarkdownLinkFileLoader:
|
inlineMarkdownLinkFileLoader:
|
||||||
fileLoaderUtils.loaders.inlineMarkdownLinkFileLoader,
|
fileLoaderUtils.loaders.inlineMarkdownLinkFileLoader,
|
||||||
|
onBrokenMarkdownLinks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
|
@ -8,7 +8,7 @@ import path from 'path';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {posixPath} from '@docusaurus/utils';
|
import {posixPath} from '@docusaurus/utils';
|
||||||
import {transformNode} from '../utils';
|
import {formatNodePositionExtraMessage, transformNode} from '../utils';
|
||||||
import type {Root} from 'mdast';
|
import type {Root} from 'mdast';
|
||||||
import type {Parent} from 'unist';
|
import type {Parent} from 'unist';
|
||||||
import type {Transformer, Processor, Plugin} from 'unified';
|
import type {Transformer, Processor, Plugin} from 'unified';
|
||||||
|
@ -39,17 +39,9 @@ function formatDirectiveName(directive: Directives) {
|
||||||
return `${prefix}${directive.name}`;
|
return `${prefix}${directive.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDirectivePosition(directive: Directives): string | undefined {
|
|
||||||
return directive.position?.start
|
|
||||||
? logger.interpolate`number=${directive.position.start.line}:number=${directive.position.start.column}`
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnusedDirectiveMessage(directive: Directives) {
|
function formatUnusedDirectiveMessage(directive: Directives) {
|
||||||
const name = formatDirectiveName(directive);
|
const name = formatDirectiveName(directive);
|
||||||
const position = formatDirectivePosition(directive);
|
return `- ${name}${formatNodePositionExtraMessage(directive)}`;
|
||||||
|
|
||||||
return `- ${name} ${position ? `(${position})` : ''}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUnusedDirectivesMessage({
|
function formatUnusedDirectivesMessage({
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import type {Node} from 'unist';
|
import type {Node} from 'unist';
|
||||||
import type {MdxJsxAttributeValueExpression} from 'mdast-util-mdx';
|
import type {MdxJsxAttributeValueExpression} from 'mdast-util-mdx';
|
||||||
|
|
||||||
|
@ -83,3 +84,16 @@ export function assetRequireAttributeValue(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNodePosition(node: Node): string | undefined {
|
||||||
|
return node.position?.start
|
||||||
|
? logger.interpolate`number=${node.position.start.line}:number=${node.position.start.column}`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns " (line:column)" when position info is available
|
||||||
|
// The initial space is useful to append easily to any existing message
|
||||||
|
export function formatNodePositionExtraMessage(node: Node): string {
|
||||||
|
const position = formatNodePosition(node);
|
||||||
|
return `${position ? ` (${position})` : ''}`;
|
||||||
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default async function pluginContentBlog(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
|
const {baseUrl} = siteConfig;
|
||||||
|
|
||||||
const contentPaths: BlogContentPaths = {
|
const contentPaths: BlogContentPaths = {
|
||||||
contentPath: path.resolve(siteDir, options.path),
|
contentPath: path.resolve(siteDir, options.path),
|
||||||
|
@ -154,18 +154,12 @@ export default async function pluginContentBlog(
|
||||||
},
|
},
|
||||||
markdownConfig: siteConfig.markdown,
|
markdownConfig: siteConfig.markdown,
|
||||||
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
|
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
|
||||||
const permalink = resolveMarkdownLinkPathname(linkPathname, {
|
return resolveMarkdownLinkPathname(linkPathname, {
|
||||||
sourceFilePath,
|
sourceFilePath,
|
||||||
sourceToPermalink: contentHelpers.sourceToPermalink,
|
sourceToPermalink: contentHelpers.sourceToPermalink,
|
||||||
siteDir,
|
siteDir,
|
||||||
contentPaths,
|
contentPaths,
|
||||||
});
|
});
|
||||||
if (permalink === null) {
|
|
||||||
logger.report(
|
|
||||||
onBrokenMarkdownLinks,
|
|
||||||
)`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`;
|
|
||||||
}
|
|
||||||
return permalink;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import logger from '@docusaurus/logger';
|
|
||||||
import {
|
import {
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
docuHash,
|
docuHash,
|
||||||
|
@ -158,18 +157,12 @@ export default async function pluginContentDocs(
|
||||||
sourceFilePath,
|
sourceFilePath,
|
||||||
versionsMetadata,
|
versionsMetadata,
|
||||||
);
|
);
|
||||||
const permalink = resolveMarkdownLinkPathname(linkPathname, {
|
return resolveMarkdownLinkPathname(linkPathname, {
|
||||||
sourceFilePath,
|
sourceFilePath,
|
||||||
sourceToPermalink: contentHelpers.sourceToPermalink,
|
sourceToPermalink: contentHelpers.sourceToPermalink,
|
||||||
siteDir,
|
siteDir,
|
||||||
contentPaths: version,
|
contentPaths: version,
|
||||||
});
|
});
|
||||||
if (permalink === null) {
|
|
||||||
logger.report(
|
|
||||||
siteConfig.onBrokenMarkdownLinks,
|
|
||||||
)`Docs markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath} for version number=${version.versionName}`;
|
|
||||||
}
|
|
||||||
return permalink;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdx-js/mdx": "^3.0.0",
|
"@mdx-js/mdx": "^3.0.0",
|
||||||
"@types/history": "^4.7.11",
|
"@types/history": "^4.7.11",
|
||||||
|
"@types/mdast": "^4.0.2",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"commander": "^5.1.0",
|
"commander": "^5.1.0",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.9.2",
|
||||||
|
|
106
packages/docusaurus-types/src/config.d.ts
vendored
106
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -10,12 +10,8 @@ import type {RuleSetRule} from 'webpack';
|
||||||
import type {DeepPartial, Overwrite} from 'utility-types';
|
import type {DeepPartial, Overwrite} from 'utility-types';
|
||||||
import type {I18nConfig} from './i18n';
|
import type {I18nConfig} from './i18n';
|
||||||
import type {PluginConfig, PresetConfig, HtmlTagObject} from './plugin';
|
import type {PluginConfig, PresetConfig, HtmlTagObject} from './plugin';
|
||||||
|
import type {ReportingSeverity} from './reporting';
|
||||||
import type {ProcessorOptions} from '@mdx-js/mdx';
|
import type {MarkdownConfig} from './markdown';
|
||||||
|
|
||||||
export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions'];
|
|
||||||
|
|
||||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
|
||||||
|
|
||||||
export type RouterType = 'browser' | 'hash';
|
export type RouterType = 'browser' | 'hash';
|
||||||
|
|
||||||
|
@ -23,101 +19,6 @@ export type ThemeConfig = {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MarkdownPreprocessor = (args: {
|
|
||||||
filePath: string;
|
|
||||||
fileContent: string;
|
|
||||||
}) => string;
|
|
||||||
|
|
||||||
export type MDX1CompatOptions = {
|
|
||||||
comments: boolean;
|
|
||||||
admonitions: boolean;
|
|
||||||
headingIds: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ParseFrontMatterParams = {filePath: string; fileContent: string};
|
|
||||||
export type ParseFrontMatterResult = {
|
|
||||||
frontMatter: {[key: string]: unknown};
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
export type DefaultParseFrontMatter = (
|
|
||||||
params: ParseFrontMatterParams,
|
|
||||||
) => Promise<ParseFrontMatterResult>;
|
|
||||||
export type ParseFrontMatter = (
|
|
||||||
params: ParseFrontMatterParams & {
|
|
||||||
defaultParseFrontMatter: DefaultParseFrontMatter;
|
|
||||||
},
|
|
||||||
) => Promise<ParseFrontMatterResult>;
|
|
||||||
|
|
||||||
export type MarkdownAnchorsConfig = {
|
|
||||||
/**
|
|
||||||
* Preserves the case of the heading text when generating anchor ids.
|
|
||||||
*/
|
|
||||||
maintainCase: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MarkdownConfig = {
|
|
||||||
/**
|
|
||||||
* The Markdown format to use by default.
|
|
||||||
*
|
|
||||||
* This is the format passed down to the MDX compiler, impacting the way the
|
|
||||||
* content is parsed.
|
|
||||||
*
|
|
||||||
* Possible values:
|
|
||||||
* - `'mdx'`: use the MDX format (JSX support)
|
|
||||||
* - `'md'`: use the CommonMark format (no JSX support)
|
|
||||||
* - `'detect'`: select the format based on file extension (.md / .mdx)
|
|
||||||
*
|
|
||||||
* @see https://mdxjs.com/packages/mdx/#optionsformat
|
|
||||||
* @default 'mdx'
|
|
||||||
*/
|
|
||||||
format: 'mdx' | 'md' | 'detect';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function callback that lets users parse the front matter themselves.
|
|
||||||
* Gives the opportunity to read it from a different source, or process it.
|
|
||||||
*
|
|
||||||
* @see https://github.com/facebook/docusaurus/issues/5568
|
|
||||||
*/
|
|
||||||
parseFrontMatter: ParseFrontMatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allow mermaid language code blocks to be rendered into Mermaid diagrams:
|
|
||||||
*
|
|
||||||
* - `true`: code blocks with language mermaid will be rendered.
|
|
||||||
* - `false` | `undefined` (default): code blocks with language mermaid
|
|
||||||
* will be left as code blocks.
|
|
||||||
*
|
|
||||||
* @see https://docusaurus.io/docs/markdown-features/diagrams/
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
mermaid: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gives opportunity to preprocess the MDX string content before compiling.
|
|
||||||
* A good escape hatch that can be used to handle edge cases.
|
|
||||||
*
|
|
||||||
* @param args
|
|
||||||
*/
|
|
||||||
preprocessor?: MarkdownPreprocessor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set of flags make it easier to upgrade from MDX 1 to MDX 2
|
|
||||||
* See also https://github.com/facebook/docusaurus/issues/4029
|
|
||||||
*/
|
|
||||||
mdx1Compat: MDX1CompatOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ability to provide custom remark-rehype options
|
|
||||||
* See also https://github.com/remarkjs/remark-rehype#options
|
|
||||||
*/
|
|
||||||
remarkRehypeOptions: RemarkRehypeOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options to control the behavior of anchors generated from Markdown headings
|
|
||||||
*/
|
|
||||||
anchors: MarkdownAnchorsConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StorageConfig = {
|
export type StorageConfig = {
|
||||||
type: SiteStorage['type'];
|
type: SiteStorage['type'];
|
||||||
namespace: boolean | string;
|
namespace: boolean | string;
|
||||||
|
@ -258,7 +159,8 @@ export type DocusaurusConfig = {
|
||||||
* @see https://docusaurus.io/docs/api/docusaurus-config#onBrokenMarkdownLinks
|
* @see https://docusaurus.io/docs/api/docusaurus-config#onBrokenMarkdownLinks
|
||||||
* @default "warn"
|
* @default "warn"
|
||||||
*/
|
*/
|
||||||
onBrokenMarkdownLinks: ReportingSeverity;
|
// TODO Docusaurus v4 remove
|
||||||
|
onBrokenMarkdownLinks: ReportingSeverity | undefined;
|
||||||
/**
|
/**
|
||||||
* The behavior of Docusaurus when it detects any [duplicate
|
* The behavior of Docusaurus when it detects any [duplicate
|
||||||
* routes](https://docusaurus.io/docs/creating-pages#duplicate-routes).
|
* routes](https://docusaurus.io/docs/creating-pages#duplicate-routes).
|
||||||
|
|
16
packages/docusaurus-types/src/index.d.ts
vendored
16
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -6,19 +6,27 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ReportingSeverity,
|
|
||||||
RouterType,
|
RouterType,
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
MarkdownConfig,
|
|
||||||
DefaultParseFrontMatter,
|
|
||||||
ParseFrontMatter,
|
|
||||||
DocusaurusConfig,
|
DocusaurusConfig,
|
||||||
FutureConfig,
|
FutureConfig,
|
||||||
|
FutureV4Config,
|
||||||
FasterConfig,
|
FasterConfig,
|
||||||
StorageConfig,
|
StorageConfig,
|
||||||
Config,
|
Config,
|
||||||
} from './config';
|
} from './config';
|
||||||
|
|
||||||
|
export {
|
||||||
|
MarkdownConfig,
|
||||||
|
MarkdownHooks,
|
||||||
|
DefaultParseFrontMatter,
|
||||||
|
ParseFrontMatter,
|
||||||
|
OnBrokenMarkdownLinksFunction,
|
||||||
|
OnBrokenMarkdownImagesFunction,
|
||||||
|
} from './markdown';
|
||||||
|
|
||||||
|
export {ReportingSeverity} from './reporting';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SiteMetadata,
|
SiteMetadata,
|
||||||
DocusaurusContext,
|
DocusaurusContext,
|
||||||
|
|
165
packages/docusaurus-types/src/markdown.d.ts
vendored
Normal file
165
packages/docusaurus-types/src/markdown.d.ts
vendored
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* 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 type {ProcessorOptions} from '@mdx-js/mdx';
|
||||||
|
import type {Image, Definition, Link} from 'mdast';
|
||||||
|
|
||||||
|
import type {ReportingSeverity} from './reporting';
|
||||||
|
|
||||||
|
export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions'];
|
||||||
|
|
||||||
|
export type MarkdownPreprocessor = (args: {
|
||||||
|
filePath: string;
|
||||||
|
fileContent: string;
|
||||||
|
}) => string;
|
||||||
|
|
||||||
|
export type MDX1CompatOptions = {
|
||||||
|
comments: boolean;
|
||||||
|
admonitions: boolean;
|
||||||
|
headingIds: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseFrontMatterParams = {filePath: string; fileContent: string};
|
||||||
|
export type ParseFrontMatterResult = {
|
||||||
|
frontMatter: {[key: string]: unknown};
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
export type DefaultParseFrontMatter = (
|
||||||
|
params: ParseFrontMatterParams,
|
||||||
|
) => Promise<ParseFrontMatterResult>;
|
||||||
|
export type ParseFrontMatter = (
|
||||||
|
params: ParseFrontMatterParams & {
|
||||||
|
defaultParseFrontMatter: DefaultParseFrontMatter;
|
||||||
|
},
|
||||||
|
) => Promise<ParseFrontMatterResult>;
|
||||||
|
|
||||||
|
export type MarkdownAnchorsConfig = {
|
||||||
|
/**
|
||||||
|
* Preserves the case of the heading text when generating anchor ids.
|
||||||
|
*/
|
||||||
|
maintainCase: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnBrokenMarkdownLinksFunction = (params: {
|
||||||
|
/**
|
||||||
|
* Path of the source file on which the broken link was found
|
||||||
|
* Relative to the site dir.
|
||||||
|
* Example: "docs/category/myDoc.mdx"
|
||||||
|
*/
|
||||||
|
sourceFilePath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Markdown link url that couldn't be resolved.
|
||||||
|
* Technically, in this context, it's more a "relative file path", but let's
|
||||||
|
* name it url for consistency with usual Markdown names and the MDX AST
|
||||||
|
* Example: "relative/dir/myTargetDoc.mdx?query#hash"
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* The Markdown Link AST node.
|
||||||
|
*/
|
||||||
|
node: Link | Definition;
|
||||||
|
}) => void | string;
|
||||||
|
|
||||||
|
export type OnBrokenMarkdownImagesFunction = (params: {
|
||||||
|
/**
|
||||||
|
* Path of the source file on which the broken image was found
|
||||||
|
* Relative to the site dir.
|
||||||
|
* Example: "docs/category/myDoc.mdx"
|
||||||
|
*/
|
||||||
|
sourceFilePath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Markdown image url that couldn't be resolved.
|
||||||
|
* Technically, in this context, it's more a "relative file path", but let's
|
||||||
|
* name it url for consistency with usual Markdown names and the MDX AST
|
||||||
|
* Example: "relative/dir/myImage.png"
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* The Markdown Image AST node.
|
||||||
|
*/
|
||||||
|
node: Image;
|
||||||
|
}) => void | string;
|
||||||
|
|
||||||
|
export type MarkdownHooks = {
|
||||||
|
/**
|
||||||
|
* The behavior of Docusaurus when it detects any broken Markdown link.
|
||||||
|
*
|
||||||
|
* // TODO refactor doc links!
|
||||||
|
* @see https://docusaurus.io/docs/api/docusaurus-config#onBrokenMarkdownLinks
|
||||||
|
* @default "warn"
|
||||||
|
*/
|
||||||
|
onBrokenMarkdownLinks: ReportingSeverity | OnBrokenMarkdownLinksFunction;
|
||||||
|
|
||||||
|
onBrokenMarkdownImages: ReportingSeverity | OnBrokenMarkdownImagesFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MarkdownConfig = {
|
||||||
|
/**
|
||||||
|
* The Markdown format to use by default.
|
||||||
|
*
|
||||||
|
* This is the format passed down to the MDX compiler, impacting the way the
|
||||||
|
* content is parsed.
|
||||||
|
*
|
||||||
|
* Possible values:
|
||||||
|
* - `'mdx'`: use the MDX format (JSX support)
|
||||||
|
* - `'md'`: use the CommonMark format (no JSX support)
|
||||||
|
* - `'detect'`: select the format based on file extension (.md / .mdx)
|
||||||
|
*
|
||||||
|
* @see https://mdxjs.com/packages/mdx/#optionsformat
|
||||||
|
* @default 'mdx'
|
||||||
|
*/
|
||||||
|
format: 'mdx' | 'md' | 'detect';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function callback that lets users parse the front matter themselves.
|
||||||
|
* Gives the opportunity to read it from a different source, or process it.
|
||||||
|
*
|
||||||
|
* @see https://github.com/facebook/docusaurus/issues/5568
|
||||||
|
*/
|
||||||
|
parseFrontMatter: ParseFrontMatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow mermaid language code blocks to be rendered into Mermaid diagrams:
|
||||||
|
*
|
||||||
|
* - `true`: code blocks with language mermaid will be rendered.
|
||||||
|
* - `false` | `undefined` (default): code blocks with language mermaid
|
||||||
|
* will be left as code blocks.
|
||||||
|
*
|
||||||
|
* @see https://docusaurus.io/docs/markdown-features/diagrams/
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
mermaid: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives opportunity to preprocess the MDX string content before compiling.
|
||||||
|
* A good escape hatch that can be used to handle edge cases.
|
||||||
|
*
|
||||||
|
* @param args
|
||||||
|
*/
|
||||||
|
preprocessor?: MarkdownPreprocessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of flags make it easier to upgrade from MDX 1 to MDX 2
|
||||||
|
* See also https://github.com/facebook/docusaurus/issues/4029
|
||||||
|
*/
|
||||||
|
mdx1Compat: MDX1CompatOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to provide custom remark-rehype options
|
||||||
|
* See also https://github.com/remarkjs/remark-rehype#options
|
||||||
|
*/
|
||||||
|
remarkRehypeOptions: RemarkRehypeOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to control the behavior of anchors generated from Markdown headings
|
||||||
|
*/
|
||||||
|
anchors: MarkdownAnchorsConfig;
|
||||||
|
|
||||||
|
hooks: MarkdownHooks;
|
||||||
|
};
|
8
packages/docusaurus-types/src/reporting.d.ts
vendored
Normal file
8
packages/docusaurus-types/src/reporting.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
|
@ -42,6 +42,10 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -55,7 +59,6 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -117,6 +120,10 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -130,7 +137,6 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -192,6 +198,10 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -205,7 +215,6 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -267,6 +276,10 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -280,7 +293,6 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -342,6 +354,10 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -355,7 +371,6 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -417,6 +432,10 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -430,7 +449,6 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
@ -492,6 +510,10 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -505,7 +527,6 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"organizationName": "endiliey",
|
"organizationName": "endiliey",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
|
@ -569,6 +590,10 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -582,7 +607,6 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"organizationName": "endiliey",
|
"organizationName": "endiliey",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
|
@ -646,6 +670,10 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -659,7 +687,6 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"organizationName": "endiliey",
|
"organizationName": "endiliey",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
|
@ -726,6 +753,10 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -739,7 +770,6 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"organizationName": "endiliey",
|
"organizationName": "endiliey",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
|
@ -126,6 +126,10 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
"maintainCase": false,
|
"maintainCase": false,
|
||||||
},
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
|
"hooks": {
|
||||||
|
"onBrokenMarkdownImages": "throw",
|
||||||
|
"onBrokenMarkdownLinks": "warn",
|
||||||
|
},
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
"comments": true,
|
"comments": true,
|
||||||
|
@ -139,7 +143,6 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
"noIndex": false,
|
"noIndex": false,
|
||||||
"onBrokenAnchors": "warn",
|
"onBrokenAnchors": "warn",
|
||||||
"onBrokenLinks": "throw",
|
"onBrokenLinks": "throw",
|
||||||
"onBrokenMarkdownLinks": "warn",
|
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {jest} from '@jest/globals';
|
||||||
import {
|
import {
|
||||||
ConfigSchema,
|
ConfigSchema,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
|
@ -16,6 +17,10 @@ import {
|
||||||
DEFAULT_STORAGE_CONFIG,
|
DEFAULT_STORAGE_CONFIG,
|
||||||
validateConfig,
|
validateConfig,
|
||||||
} from '../configValidation';
|
} from '../configValidation';
|
||||||
|
import type {
|
||||||
|
MarkdownConfig,
|
||||||
|
MarkdownHooks,
|
||||||
|
} from '@docusaurus/types/src/markdown';
|
||||||
import type {
|
import type {
|
||||||
FasterConfig,
|
FasterConfig,
|
||||||
FutureConfig,
|
FutureConfig,
|
||||||
|
@ -36,7 +41,7 @@ const normalizeConfig = (config: DeepPartial<Config>) =>
|
||||||
|
|
||||||
describe('normalizeConfig', () => {
|
describe('normalizeConfig', () => {
|
||||||
it('normalizes empty config', () => {
|
it('normalizes empty config', () => {
|
||||||
const value = normalizeConfig({});
|
const value = normalizeConfig({markdown: {}});
|
||||||
expect(value).toEqual({
|
expect(value).toEqual({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
|
@ -108,6 +113,10 @@ describe('normalizeConfig', () => {
|
||||||
remarkRehypeOptions: {
|
remarkRehypeOptions: {
|
||||||
footnoteLabel: 'Pied de page',
|
footnoteLabel: 'Pied de page',
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'log',
|
||||||
|
onBrokenMarkdownImages: 'log',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const normalizedConfig = normalizeConfig(userConfig);
|
const normalizedConfig = normalizeConfig(userConfig);
|
||||||
|
@ -357,20 +366,15 @@ describe('onBrokenLinks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('markdown', () => {
|
describe('markdown', () => {
|
||||||
|
function normalizeMarkdown(markdown: DeepPartial<MarkdownConfig>) {
|
||||||
|
return normalizeConfig({markdown}).markdown;
|
||||||
|
}
|
||||||
it('accepts undefined object', () => {
|
it('accepts undefined object', () => {
|
||||||
expect(
|
expect(normalizeMarkdown(undefined)).toEqual(DEFAULT_CONFIG.markdown);
|
||||||
normalizeConfig({
|
|
||||||
markdown: undefined,
|
|
||||||
}),
|
|
||||||
).toEqual(expect.objectContaining({markdown: DEFAULT_CONFIG.markdown}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts empty object', () => {
|
it('accepts empty object', () => {
|
||||||
expect(
|
expect(normalizeMarkdown({})).toEqual(DEFAULT_CONFIG.markdown);
|
||||||
normalizeConfig({
|
|
||||||
markdown: {},
|
|
||||||
}),
|
|
||||||
).toEqual(expect.objectContaining({markdown: DEFAULT_CONFIG.markdown}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts valid markdown object', () => {
|
it('accepts valid markdown object', () => {
|
||||||
|
@ -393,12 +397,12 @@ describe('markdown', () => {
|
||||||
// @ts-expect-error: we don't validate it on purpose
|
// @ts-expect-error: we don't validate it on purpose
|
||||||
anyKey: 'heck we accept it on purpose',
|
anyKey: 'heck we accept it on purpose',
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'log',
|
||||||
|
onBrokenMarkdownImages: 'warn',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(normalizeMarkdown(markdown)).toEqual(markdown);
|
||||||
normalizeConfig({
|
|
||||||
markdown,
|
|
||||||
}),
|
|
||||||
).toEqual(expect.objectContaining({markdown}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts partial markdown object', () => {
|
it('accepts partial markdown object', () => {
|
||||||
|
@ -408,22 +412,14 @@ describe('markdown', () => {
|
||||||
headingIds: false,
|
headingIds: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(normalizeMarkdown(markdown)).toEqual({
|
||||||
normalizeConfig({
|
...DEFAULT_CONFIG.markdown,
|
||||||
markdown,
|
...markdown,
|
||||||
}),
|
mdx1Compat: {
|
||||||
).toEqual(
|
...DEFAULT_CONFIG.markdown.mdx1Compat,
|
||||||
expect.objectContaining({
|
...markdown.mdx1Compat,
|
||||||
markdown: {
|
},
|
||||||
...DEFAULT_CONFIG.markdown,
|
});
|
||||||
...markdown,
|
|
||||||
mdx1Compat: {
|
|
||||||
...DEFAULT_CONFIG.markdown.mdx1Compat,
|
|
||||||
...markdown.mdx1Compat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw for preprocessor bad arity', () => {
|
it('throw for preprocessor bad arity', () => {
|
||||||
|
@ -436,10 +432,10 @@ describe('markdown', () => {
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeConfig({
|
normalizeMarkdown(
|
||||||
// @ts-expect-error: types forbid this
|
// @ts-expect-error: types forbid this
|
||||||
markdown: {preprocessor: (arg1, arg2) => String(arg1) + String(arg2)},
|
{preprocessor: (arg1, arg2) => String(arg1) + String(arg2)},
|
||||||
}),
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
""markdown.preprocessor" must have an arity of 1
|
""markdown.preprocessor" must have an arity of 1
|
||||||
"
|
"
|
||||||
|
@ -447,18 +443,13 @@ describe('markdown', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts undefined markdown format', () => {
|
it('accepts undefined markdown format', () => {
|
||||||
expect(
|
expect(normalizeMarkdown({format: undefined}).format).toBe('mdx');
|
||||||
normalizeConfig({markdown: {format: undefined}}).markdown.format,
|
|
||||||
).toBe('mdx');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw for bad markdown format', () => {
|
it('throw for bad markdown format', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeConfig({
|
normalizeMarkdown({
|
||||||
markdown: {
|
format: null,
|
||||||
// @ts-expect-error: bad value
|
|
||||||
format: null,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
""markdown.format" must be one of [mdx, md, detect]
|
""markdown.format" must be one of [mdx, md, detect]
|
||||||
|
@ -466,9 +457,9 @@ describe('markdown', () => {
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeConfig(
|
normalizeMarkdown(
|
||||||
// @ts-expect-error: bad value
|
// @ts-expect-error: bad value
|
||||||
{markdown: {format: 'xyz'}},
|
{format: 'xyz'},
|
||||||
),
|
),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
""markdown.format" must be one of [mdx, md, detect]
|
""markdown.format" must be one of [mdx, md, detect]
|
||||||
|
@ -478,15 +469,165 @@ describe('markdown', () => {
|
||||||
|
|
||||||
it('throw for null object', () => {
|
it('throw for null object', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizeConfig({
|
normalizeMarkdown(null);
|
||||||
// @ts-expect-error: bad value
|
|
||||||
markdown: null,
|
|
||||||
});
|
|
||||||
}).toThrowErrorMatchingInlineSnapshot(`
|
}).toThrowErrorMatchingInlineSnapshot(`
|
||||||
""markdown" must be of type object
|
""markdown" must be of type object
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hooks', () => {
|
||||||
|
function normalizeHooks(hooks: DeepPartial<MarkdownHooks>): MarkdownHooks {
|
||||||
|
return normalizeMarkdown({
|
||||||
|
hooks,
|
||||||
|
}).hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('onBrokenMarkdownLinks', () => {
|
||||||
|
function normalizeValue(
|
||||||
|
onBrokenMarkdownLinks?: MarkdownHooks['onBrokenMarkdownLinks'],
|
||||||
|
) {
|
||||||
|
return normalizeHooks({
|
||||||
|
onBrokenMarkdownLinks,
|
||||||
|
}).onBrokenMarkdownLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts undefined', () => {
|
||||||
|
expect(normalizeValue(undefined)).toBe('warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts severity level', () => {
|
||||||
|
expect(normalizeValue('log')).toBe('log');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects number', () => {
|
||||||
|
expect(() =>
|
||||||
|
normalizeValue(
|
||||||
|
// @ts-expect-error: bad value
|
||||||
|
42,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
""markdown.hooks.onBrokenMarkdownLinks" does not match any of the allowed types
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts function', () => {
|
||||||
|
expect(normalizeValue(() => {})).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects null', () => {
|
||||||
|
expect(() => normalizeValue(null)).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
""markdown.hooks.onBrokenMarkdownLinks" does not match any of the allowed types
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onBrokenMarkdownLinks migration', () => {
|
||||||
|
const warnMock = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
warnMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts migrated v3 config', () => {
|
||||||
|
expect(
|
||||||
|
normalizeConfig({
|
||||||
|
onBrokenMarkdownLinks: undefined,
|
||||||
|
markdown: {
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'throw',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
onBrokenMarkdownLinks: undefined,
|
||||||
|
markdown: expect.objectContaining({
|
||||||
|
hooks: expect.objectContaining({
|
||||||
|
onBrokenMarkdownLinks: 'throw',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warnMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts deprecated v3 config with migration warning', () => {
|
||||||
|
expect(
|
||||||
|
normalizeConfig({
|
||||||
|
onBrokenMarkdownLinks: 'log',
|
||||||
|
markdown: {
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'throw',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
onBrokenMarkdownLinks: undefined,
|
||||||
|
markdown: expect.objectContaining({
|
||||||
|
hooks: expect.objectContaining({
|
||||||
|
onBrokenMarkdownLinks: 'log',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
"[WARNING] The \`siteConfig.onBrokenMarkdownLinks\` config option is deprecated and will be removed in Docusaurus v4.
|
||||||
|
Please migrate and move this option to \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` instead.",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onBrokenMarkdownImages', () => {
|
||||||
|
function normalizeValue(
|
||||||
|
onBrokenMarkdownImages?: MarkdownHooks['onBrokenMarkdownImages'],
|
||||||
|
) {
|
||||||
|
return normalizeHooks({
|
||||||
|
onBrokenMarkdownImages,
|
||||||
|
}).onBrokenMarkdownImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts undefined', () => {
|
||||||
|
expect(normalizeValue(undefined)).toBe('throw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts severity level', () => {
|
||||||
|
expect(normalizeValue('log')).toBe('log');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects number', () => {
|
||||||
|
expect(() =>
|
||||||
|
normalizeValue(
|
||||||
|
// @ts-expect-error: bad value
|
||||||
|
42,
|
||||||
|
),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
""markdown.hooks.onBrokenMarkdownImages" does not match any of the allowed types
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts function', () => {
|
||||||
|
expect(normalizeValue(() => {})).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects null', () => {
|
||||||
|
expect(() => normalizeValue(null)).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
""markdown.hooks.onBrokenMarkdownImages" does not match any of the allowed types
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('plugins', () => {
|
describe('plugins', () => {
|
||||||
|
@ -846,7 +987,6 @@ describe('future', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects router - null', () => {
|
it('rejects router - null', () => {
|
||||||
// @ts-expect-error: bad value
|
|
||||||
const router: Config['future']['experimental_router'] = null;
|
const router: Config['future']['experimental_router'] = null;
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeConfig({
|
normalizeConfig({
|
||||||
|
@ -1055,7 +1195,6 @@ describe('future', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects namespace - null', () => {
|
it('rejects namespace - null', () => {
|
||||||
// @ts-expect-error: bad value
|
|
||||||
const storage: Partial<StorageConfig> = {namespace: null};
|
const storage: Partial<StorageConfig> = {namespace: null};
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeConfig({
|
normalizeConfig({
|
||||||
|
|
|
@ -22,11 +22,10 @@ import type {
|
||||||
FutureConfig,
|
FutureConfig,
|
||||||
FutureV4Config,
|
FutureV4Config,
|
||||||
StorageConfig,
|
StorageConfig,
|
||||||
} from '@docusaurus/types/src/config';
|
|
||||||
import type {
|
|
||||||
DocusaurusConfig,
|
DocusaurusConfig,
|
||||||
I18nConfig,
|
I18nConfig,
|
||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
|
MarkdownHooks,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
const DEFAULT_I18N_LOCALE = 'en';
|
const DEFAULT_I18N_LOCALE = 'en';
|
||||||
|
@ -84,6 +83,11 @@ export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
||||||
experimental_router: 'browser',
|
experimental_router: 'browser',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MARKDOWN_HOOKS: MarkdownHooks = {
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
onBrokenMarkdownImages: 'throw',
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||||
format: 'mdx', // TODO change this to "detect" in Docusaurus v4?
|
format: 'mdx', // TODO change this to "detect" in Docusaurus v4?
|
||||||
mermaid: false,
|
mermaid: false,
|
||||||
|
@ -98,6 +102,7 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||||
maintainCase: false,
|
maintainCase: false,
|
||||||
},
|
},
|
||||||
remarkRehypeOptions: undefined,
|
remarkRehypeOptions: undefined,
|
||||||
|
hooks: DEFAULT_MARKDOWN_HOOKS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: Pick<
|
export const DEFAULT_CONFIG: Pick<
|
||||||
|
@ -128,7 +133,7 @@ export const DEFAULT_CONFIG: Pick<
|
||||||
future: DEFAULT_FUTURE_CONFIG,
|
future: DEFAULT_FUTURE_CONFIG,
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
|
onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
|
||||||
onBrokenMarkdownLinks: 'warn',
|
onBrokenMarkdownLinks: undefined,
|
||||||
onDuplicateRoutes: 'warn',
|
onDuplicateRoutes: 'warn',
|
||||||
plugins: [],
|
plugins: [],
|
||||||
themes: [],
|
themes: [],
|
||||||
|
@ -350,7 +355,7 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||||
.default(DEFAULT_CONFIG.onBrokenAnchors),
|
.default(DEFAULT_CONFIG.onBrokenAnchors),
|
||||||
onBrokenMarkdownLinks: Joi.string()
|
onBrokenMarkdownLinks: Joi.string()
|
||||||
.equal('ignore', 'log', 'warn', 'throw')
|
.equal('ignore', 'log', 'warn', 'throw')
|
||||||
.default(DEFAULT_CONFIG.onBrokenMarkdownLinks),
|
.default(() => DEFAULT_CONFIG.onBrokenMarkdownLinks),
|
||||||
onDuplicateRoutes: Joi.string()
|
onDuplicateRoutes: Joi.string()
|
||||||
.equal('ignore', 'log', 'warn', 'throw')
|
.equal('ignore', 'log', 'warn', 'throw')
|
||||||
.default(DEFAULT_CONFIG.onDuplicateRoutes),
|
.default(DEFAULT_CONFIG.onDuplicateRoutes),
|
||||||
|
@ -455,6 +460,20 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||||
DEFAULT_CONFIG.markdown.anchors.maintainCase,
|
DEFAULT_CONFIG.markdown.anchors.maintainCase,
|
||||||
),
|
),
|
||||||
}).default(DEFAULT_CONFIG.markdown.anchors),
|
}).default(DEFAULT_CONFIG.markdown.anchors),
|
||||||
|
hooks: Joi.object<MarkdownHooks>({
|
||||||
|
onBrokenMarkdownLinks: Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.string().equal('ignore', 'log', 'warn', 'throw'),
|
||||||
|
Joi.function(),
|
||||||
|
)
|
||||||
|
.default(DEFAULT_CONFIG.markdown.hooks.onBrokenMarkdownLinks),
|
||||||
|
onBrokenMarkdownImages: Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.string().equal('ignore', 'log', 'warn', 'throw'),
|
||||||
|
Joi.function(),
|
||||||
|
)
|
||||||
|
.default(DEFAULT_CONFIG.markdown.hooks.onBrokenMarkdownImages),
|
||||||
|
}).default(DEFAULT_CONFIG.markdown.hooks),
|
||||||
}).default(DEFAULT_CONFIG.markdown),
|
}).default(DEFAULT_CONFIG.markdown),
|
||||||
}).messages({
|
}).messages({
|
||||||
'docusaurus.configValidationWarning':
|
'docusaurus.configValidationWarning':
|
||||||
|
@ -463,7 +482,16 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||||
|
|
||||||
// Expressing this kind of logic in Joi is a pain
|
// Expressing this kind of logic in Joi is a pain
|
||||||
// We also want to decouple logic from Joi: easier to remove it later!
|
// We also want to decouple logic from Joi: easier to remove it later!
|
||||||
function ensureDocusaurusConfigConsistency(config: DocusaurusConfig) {
|
function postProcessDocusaurusConfig(config: DocusaurusConfig) {
|
||||||
|
if (config.onBrokenMarkdownLinks) {
|
||||||
|
logger.warn`The code=${'siteConfig.onBrokenMarkdownLinks'} config option is deprecated and will be removed in Docusaurus v4.
|
||||||
|
Please migrate and move this option to code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} instead.`;
|
||||||
|
// For v3 retro compatibility we use the old attribute over the new one
|
||||||
|
config.markdown.hooks.onBrokenMarkdownLinks = config.onBrokenMarkdownLinks;
|
||||||
|
// We erase the former one to ensure we don't use it anywhere
|
||||||
|
config.onBrokenMarkdownLinks = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.future.experimental_faster.ssgWorkerThreads &&
|
config.future.experimental_faster.ssgWorkerThreads &&
|
||||||
!config.future.v4.removeLegacyPostBuildHeadAttribute
|
!config.future.v4.removeLegacyPostBuildHeadAttribute
|
||||||
|
@ -528,7 +556,7 @@ export function validateConfig(
|
||||||
throw new Error(formattedError);
|
throw new Error(formattedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDocusaurusConfigConsistency(value);
|
postProcessDocusaurusConfig(value);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,6 +275,14 @@ By default, it prints a warning, to let you know about your broken anchors.
|
||||||
|
|
||||||
### `onBrokenMarkdownLinks` {#onBrokenMarkdownLinks}
|
### `onBrokenMarkdownLinks` {#onBrokenMarkdownLinks}
|
||||||
|
|
||||||
|
:::warning Deprecated
|
||||||
|
|
||||||
|
Deprecated in Docusaurus v3.9, and will be removed in Docusaurus v4.
|
||||||
|
|
||||||
|
Replaced by [`siteConfig.markdown.hooks.onBrokenMarkdownLinks`](#hooks.onBrokenMarkdownLinks)
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
- Type: `'ignore' | 'log' | 'warn' | 'throw'`
|
- Type: `'ignore' | 'log' | 'warn' | 'throw'`
|
||||||
|
|
||||||
The behavior of Docusaurus when it detects any broken Markdown link.
|
The behavior of Docusaurus when it detects any broken Markdown link.
|
||||||
|
@ -511,6 +519,25 @@ type MarkdownAnchorsConfig = {
|
||||||
maintainCase: boolean;
|
maintainCase: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OnBrokenMarkdownLinksFunction = (params: {
|
||||||
|
sourceFilePath: string; // MD/MDX source file relative to cwd
|
||||||
|
url: string; // Link url
|
||||||
|
node: Link | Definition; // mdast Node
|
||||||
|
}) => void | string;
|
||||||
|
|
||||||
|
type OnBrokenMarkdownImagesFunction = (params: {
|
||||||
|
sourceFilePath: string; // MD/MDX source file relative to cwd
|
||||||
|
url: string; // Image url
|
||||||
|
node: Image; // mdast node
|
||||||
|
}) => void | string;
|
||||||
|
|
||||||
|
type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
||||||
|
|
||||||
|
type MarkdownHooks = {
|
||||||
|
onBrokenMarkdownLinks: ReportingSeverity | OnBrokenMarkdownLinksFunction;
|
||||||
|
onBrokenMarkdownImages: ReportingSeverity | OnBrokenMarkdownImagesFunction;
|
||||||
|
};
|
||||||
|
|
||||||
type MarkdownConfig = {
|
type MarkdownConfig = {
|
||||||
format: 'mdx' | 'md' | 'detect';
|
format: 'mdx' | 'md' | 'detect';
|
||||||
mermaid: boolean;
|
mermaid: boolean;
|
||||||
|
@ -519,6 +546,7 @@ type MarkdownConfig = {
|
||||||
mdx1Compat: MDX1CompatOptions;
|
mdx1Compat: MDX1CompatOptions;
|
||||||
remarkRehypeOptions: object; // see https://github.com/remarkjs/remark-rehype#options
|
remarkRehypeOptions: object; // see https://github.com/remarkjs/remark-rehype#options
|
||||||
anchors: MarkdownAnchorsConfig;
|
anchors: MarkdownAnchorsConfig;
|
||||||
|
hooks: MarkdownHooks;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -546,6 +574,10 @@ export default {
|
||||||
anchors: {
|
anchors: {
|
||||||
maintainCase: true,
|
maintainCase: true,
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
onBrokenMarkdownImages: 'throw',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -563,6 +595,9 @@ export default {
|
||||||
| `mdx1Compat` | `MDX1CompatOptions` | `{comments: true, admonitions: true, headingIds: true}` | Compatibility options to make it easier to upgrade to Docusaurus v3+. |
|
| `mdx1Compat` | `MDX1CompatOptions` | `{comments: true, admonitions: true, headingIds: true}` | Compatibility options to make it easier to upgrade to Docusaurus v3+. |
|
||||||
| `anchors` | `MarkdownAnchorsConfig` | `{maintainCase: false}` | Options to control the behavior of anchors generated from Markdown headings |
|
| `anchors` | `MarkdownAnchorsConfig` | `{maintainCase: false}` | Options to control the behavior of anchors generated from Markdown headings |
|
||||||
| `remarkRehypeOptions` | `object` | `undefined` | Makes it possible to pass custom [`remark-rehype` options](https://github.com/remarkjs/remark-rehype#options). |
|
| `remarkRehypeOptions` | `object` | `undefined` | Makes it possible to pass custom [`remark-rehype` options](https://github.com/remarkjs/remark-rehype#options). |
|
||||||
|
| `hooks` | `MarkdownHooks` | `object` | Make it possible to customize the MDX loader behavior with callbacks or built-in options. |
|
||||||
|
| `hooks.onBrokenMarkdownLinks` | `ReportingSeverity \| OnBrokenMarkdownLinksFunction` | `'warn'` | Hook to customize the behavior when encountering a broken Markdown link URL. With the callback function, you can return a new link URL, or alter the link [mdast node](https://github.com/syntax-tree/mdast). |
|
||||||
|
| `hooks.onBrokenMarkdownLinks` | `ReportingSeverity \| OnBrokenMarkdownImagesFunction` | `'throw'` | Hook to customize the behavior when encountering a broken Markdown image URL. With the callback function, you can return a new image URL, or alter the image [mdast node](https://github.com/syntax-tree/mdast). |
|
||||||
|
|
||||||
```mdx-code-block
|
```mdx-code-block
|
||||||
</APITable>
|
</APITable>
|
||||||
|
|
|
@ -15,7 +15,6 @@ export default {
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
// We can only warn now, since we have blog pages linking to non-blog pages...
|
// We can only warn now, since we have blog pages linking to non-blog pages...
|
||||||
onBrokenLinks: 'warn',
|
onBrokenLinks: 'warn',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
favicon: 'img/docusaurus.ico',
|
favicon: 'img/docusaurus.ico',
|
||||||
themes: ['live-codeblock'],
|
themes: ['live-codeblock'],
|
||||||
plugins: ['ideal-image'],
|
plugins: ['ideal-image'],
|
||||||
|
|
|
@ -217,6 +217,9 @@ export default async function createConfigAsync() {
|
||||||
markdown: {
|
markdown: {
|
||||||
format: 'detect',
|
format: 'detect',
|
||||||
mermaid: true,
|
mermaid: true,
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
},
|
||||||
mdx1Compat: {
|
mdx1Compat: {
|
||||||
// comments: false,
|
// comments: false,
|
||||||
},
|
},
|
||||||
|
@ -265,7 +268,6 @@ export default async function createConfigAsync() {
|
||||||
process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale
|
process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale
|
||||||
? 'warn'
|
? 'warn'
|
||||||
: 'throw',
|
: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
favicon: 'img/docusaurus.ico',
|
favicon: 'img/docusaurus.ico',
|
||||||
customFields: {
|
customFields: {
|
||||||
crashTest,
|
crashTest,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue