diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts index 6865f721a999..a4172a7c324a 100644 --- a/packages/docusaurus-mdx-loader/src/processor.ts +++ b/packages/docusaurus-mdx-loader/src/processor.ts @@ -47,11 +47,6 @@ type SimpleProcessor = { }) => Promise; }; -async function getDefaultRemarkPlugins(): Promise { - const {default: emoji} = await import('remark-emoji'); - return [headings, emoji, toc]; -} - export type MDXPlugin = Pluggable; export type MDXOptions = { @@ -86,8 +81,7 @@ async function createProcessorFactory() { const {default: comment} = await import('@slorber/remark-comment'); const {default: directive} = await import('remark-directive'); const {VFile} = await import('vfile'); - - const defaultRemarkPlugins = await getDefaultRemarkPlugins(); + const {default: emoji} = await import('remark-emoji'); // /!\ this method is synchronous on purpose // Using async code here can create cache entry race conditions! @@ -104,7 +98,9 @@ async function createProcessorFactory() { directive, [contentTitle, {removeContentTitle: options.removeContentTitle}], ...getAdmonitionsPlugins(options.admonitions ?? false), - ...defaultRemarkPlugins, + [headings, options.markdownConfig.anchors], + emoji, + toc, details, head, ...(options.markdownConfig.mermaid ? [mermaid] : []), diff --git a/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts index f9385ca71fba..0f0ab6317edb 100644 --- a/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts @@ -12,12 +12,19 @@ import {removePosition} from 'unist-util-remove-position'; import {toString} from 'mdast-util-to-string'; import {visit} from 'unist-util-visit'; import slug from '../index'; +import type {PluginOptions} from '../index'; import type {Plugin} from 'unified'; import type {Parent} from 'unist'; -async function process(doc: string, plugins: Plugin[] = []) { +async function process( + doc: string, + plugins: Plugin[] = [], + options: PluginOptions = {maintainCase: false}, +) { const {remark} = await import('remark'); - const processor = await remark().use({plugins: [...plugins, slug]}); + const processor = await remark().use({ + plugins: [...plugins, [slug, options]], + }); const result = await processor.run(processor.parse(doc)); removePosition(result, {force: true}); return result; @@ -312,4 +319,20 @@ describe('headings remark plugin', () => { }, ]); }); + + it('preserve anchors case then "maintainCase" option is set', async () => { + const result = await process('# Normal\n', [], {maintainCase: true}); + const expected = u('root', [ + u( + 'heading', + { + depth: 1, + data: {hProperties: {id: 'Normal'}, id: 'Normal'}, + }, + [u('text', 'Normal')], + ), + ]); + + expect(result).toEqual(expected); + }); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts index 90f90e75a5ad..a43aafd5efc4 100644 --- a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts @@ -12,7 +12,13 @@ import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils'; import type {Transformer} from 'unified'; import type {Heading, Text} from 'mdast'; -export default function plugin(): Transformer { +export interface PluginOptions { + maintainCase: boolean; +} + +export default function plugin( + {maintainCase}: PluginOptions = {maintainCase: false}, +): Transformer { return async (root) => { const {toString} = await import('mdast-util-to-string'); const {visit} = await import('unist-util-visit'); @@ -38,7 +44,7 @@ export default function plugin(): Transformer { // Support explicit heading IDs const parsedHeading = parseMarkdownHeadingId(heading); - id = parsedHeading.id ?? slugs.slug(heading); + id = parsedHeading.id ?? slugs.slug(heading, {maintainCase}); if (parsedHeading.id) { // When there's an id, it is always in the last child node diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 47bfde898d44..4016466ff641 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -101,6 +101,15 @@ export type MarkdownConfig = { * See also https://github.com/remarkjs/remark-rehype#options */ remarkRehypeOptions: RemarkRehypeOptions; + + /** + * Ability to preserve the case of the heading anchor links. + * See also https://github.com/facebook/docusaurus/issues/7946 + */ + + anchors?: { + maintainCase: boolean; + }; }; /** diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 5a187dfcaaff..049422e84bcd 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -445,6 +445,9 @@ type MarkdownConfig = { parseFrontMatter?: ParseFrontMatter; mdx1Compat: MDX1CompatOptions; remarkRehypeOptions: object; // see https://github.com/remarkjs/remark-rehype#options + anchors?: { + maintainCase: boolean; + }; }; ``` @@ -469,6 +472,9 @@ export default { admonitions: true, headingIds: true, }, + anchors: { + maintainCase: true, + }, }, }; ```