Skip to content

Commit daba917

Browse files
iAdramelkJosh-Cenaslorber
authored
feat(core): add new site config option siteConfig.markdown.anchors.maintainCase (#10064)
Co-authored-by: Joshua Chen <[email protected]> Co-authored-by: Sébastien Lorber <[email protected]> Co-authored-by: sebastien <[email protected]>
1 parent 9418786 commit daba917

File tree

10 files changed

+122
-13
lines changed

10 files changed

+122
-13
lines changed

packages/docusaurus-mdx-loader/src/processor.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ type SimpleProcessor = {
4747
}) => Promise<SimpleProcessorResult>;
4848
};
4949

50-
async function getDefaultRemarkPlugins(): Promise<MDXPlugin[]> {
51-
const {default: emoji} = await import('remark-emoji');
52-
return [headings, emoji, toc];
53-
}
54-
5550
export type MDXPlugin = Pluggable;
5651

5752
export type MDXOptions = {
@@ -86,8 +81,18 @@ async function createProcessorFactory() {
8681
const {default: comment} = await import('@slorber/remark-comment');
8782
const {default: directive} = await import('remark-directive');
8883
const {VFile} = await import('vfile');
84+
const {default: emoji} = await import('remark-emoji');
8985

90-
const defaultRemarkPlugins = await getDefaultRemarkPlugins();
86+
function getDefaultRemarkPlugins({options}: {options: Options}): MDXPlugin[] {
87+
return [
88+
[
89+
headings,
90+
{anchorsMaintainCase: options.markdownConfig.anchors.maintainCase},
91+
],
92+
emoji,
93+
toc,
94+
];
95+
}
9196

9297
// /!\ this method is synchronous on purpose
9398
// Using async code here can create cache entry race conditions!
@@ -104,7 +109,7 @@ async function createProcessorFactory() {
104109
directive,
105110
[contentTitle, {removeContentTitle: options.removeContentTitle}],
106111
...getAdmonitionsPlugins(options.admonitions ?? false),
107-
...defaultRemarkPlugins,
112+
...getDefaultRemarkPlugins({options}),
108113
details,
109114
head,
110115
...(options.markdownConfig.mermaid ? [mermaid] : []),

packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@ import u from 'unist-builder';
1111
import {removePosition} from 'unist-util-remove-position';
1212
import {toString} from 'mdast-util-to-string';
1313
import {visit} from 'unist-util-visit';
14-
import slug from '../index';
14+
import plugin from '../index';
15+
import type {PluginOptions} from '../index';
1516
import type {Plugin} from 'unified';
1617
import type {Parent} from 'unist';
1718

18-
async function process(doc: string, plugins: Plugin[] = []) {
19+
async function process(
20+
doc: string,
21+
plugins: Plugin[] = [],
22+
options: PluginOptions = {anchorsMaintainCase: false},
23+
) {
1924
const {remark} = await import('remark');
20-
const processor = await remark().use({plugins: [...plugins, slug]});
25+
const processor = await remark().use({
26+
plugins: [...plugins, [plugin, options]],
27+
});
2128
const result = await processor.run(processor.parse(doc));
2229
removePosition(result, {force: true});
2330
return result;
@@ -312,4 +319,25 @@ describe('headings remark plugin', () => {
312319
},
313320
]);
314321
});
322+
323+
it('preserve anchors case then "anchorsMaintainCase" option is set', async () => {
324+
const result = await process('# Case Sensitive Heading', [], {
325+
anchorsMaintainCase: true,
326+
});
327+
const expected = u('root', [
328+
u(
329+
'heading',
330+
{
331+
depth: 1,
332+
data: {
333+
hProperties: {id: 'Case-Sensitive-Heading'},
334+
id: 'Case-Sensitive-Heading',
335+
},
336+
},
337+
[u('text', 'Case Sensitive Heading')],
338+
),
339+
]);
340+
341+
expect(result).toEqual(expected);
342+
});
315343
});

packages/docusaurus-mdx-loader/src/remark/headings/index.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
1212
import type {Transformer} from 'unified';
1313
import type {Heading, Text} from 'mdast';
1414

15-
export default function plugin(): Transformer {
15+
export interface PluginOptions {
16+
anchorsMaintainCase: boolean;
17+
}
18+
19+
export default function plugin({
20+
anchorsMaintainCase,
21+
}: PluginOptions): Transformer {
1622
return async (root) => {
1723
const {toString} = await import('mdast-util-to-string');
1824
const {visit} = await import('unist-util-visit');
@@ -38,7 +44,9 @@ export default function plugin(): Transformer {
3844
// Support explicit heading IDs
3945
const parsedHeading = parseMarkdownHeadingId(heading);
4046

41-
id = parsedHeading.id ?? slugs.slug(heading);
47+
id =
48+
parsedHeading.id ??
49+
slugs.slug(heading, {maintainCase: anchorsMaintainCase});
4250

4351
if (parsedHeading.id) {
4452
// When there's an id, it is always in the last child node

packages/docusaurus-mdx-loader/src/remark/toc/__tests__/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const processFixture = async (name: string) => {
2525

2626
const result = await compile(file, {
2727
format: 'mdx',
28-
remarkPlugins: [headings, gfm, plugin],
28+
remarkPlugins: [[headings, {anchorsMaintainCase: false}], gfm, plugin],
2929
rehypePlugins: [],
3030
});
3131

packages/docusaurus-types/src/config.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export type ParseFrontMatter = (
4545
},
4646
) => Promise<ParseFrontMatterResult>;
4747

48+
export type MarkdownAnchorsConfig = {
49+
/**
50+
* Preserves the case of the heading text when generating anchor ids.
51+
*/
52+
maintainCase: boolean;
53+
};
54+
4855
export type MarkdownConfig = {
4956
/**
5057
* The Markdown format to use by default.
@@ -101,6 +108,11 @@ export type MarkdownConfig = {
101108
* See also https://github.com/remarkjs/remark-rehype#options
102109
*/
103110
remarkRehypeOptions: RemarkRehypeOptions;
111+
112+
/**
113+
* Options to control the behavior of anchors generated from Markdown headings
114+
*/
115+
anchors: MarkdownAnchorsConfig;
104116
};
105117

106118
/**

packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap

+30
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
1717
"path": "i18n",
1818
},
1919
"markdown": {
20+
"anchors": {
21+
"maintainCase": false,
22+
},
2023
"format": "mdx",
2124
"mdx1Compat": {
2225
"admonitions": true,
@@ -68,6 +71,9 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
6871
"path": "i18n",
6972
},
7073
"markdown": {
74+
"anchors": {
75+
"maintainCase": false,
76+
},
7177
"format": "mdx",
7278
"mdx1Compat": {
7379
"admonitions": true,
@@ -119,6 +125,9 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
119125
"path": "i18n",
120126
},
121127
"markdown": {
128+
"anchors": {
129+
"maintainCase": false,
130+
},
122131
"format": "mdx",
123132
"mdx1Compat": {
124133
"admonitions": true,
@@ -170,6 +179,9 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
170179
"path": "i18n",
171180
},
172181
"markdown": {
182+
"anchors": {
183+
"maintainCase": false,
184+
},
173185
"format": "mdx",
174186
"mdx1Compat": {
175187
"admonitions": true,
@@ -221,6 +233,9 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
221233
"path": "i18n",
222234
},
223235
"markdown": {
236+
"anchors": {
237+
"maintainCase": false,
238+
},
224239
"format": "mdx",
225240
"mdx1Compat": {
226241
"admonitions": true,
@@ -272,6 +287,9 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
272287
"path": "i18n",
273288
},
274289
"markdown": {
290+
"anchors": {
291+
"maintainCase": false,
292+
},
275293
"format": "mdx",
276294
"mdx1Compat": {
277295
"admonitions": true,
@@ -323,6 +341,9 @@ exports[`loadSiteConfig website with valid async config 1`] = `
323341
"path": "i18n",
324342
},
325343
"markdown": {
344+
"anchors": {
345+
"maintainCase": false,
346+
},
326347
"format": "mdx",
327348
"mdx1Compat": {
328349
"admonitions": true,
@@ -376,6 +397,9 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
376397
"path": "i18n",
377398
},
378399
"markdown": {
400+
"anchors": {
401+
"maintainCase": false,
402+
},
379403
"format": "mdx",
380404
"mdx1Compat": {
381405
"admonitions": true,
@@ -429,6 +453,9 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
429453
"path": "i18n",
430454
},
431455
"markdown": {
456+
"anchors": {
457+
"maintainCase": false,
458+
},
432459
"format": "mdx",
433460
"mdx1Compat": {
434461
"admonitions": true,
@@ -485,6 +512,9 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
485512
"path": "i18n",
486513
},
487514
"markdown": {
515+
"anchors": {
516+
"maintainCase": false,
517+
},
488518
"format": "mdx",
489519
"mdx1Compat": {
490520
"admonitions": true,

packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ exports[`load loads props for site with custom i18n path 1`] = `
9797
"path": "i18n",
9898
},
9999
"markdown": {
100+
"anchors": {
101+
"maintainCase": false,
102+
},
100103
"format": "mdx",
101104
"mdx1Compat": {
102105
"admonitions": true,

packages/docusaurus/src/server/__tests__/configValidation.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ describe('normalizeConfig', () => {
6969
admonitions: false,
7070
headingIds: true,
7171
},
72+
anchors: {
73+
maintainCase: true,
74+
},
7275
remarkRehypeOptions: {
7376
footnoteLabel: 'Pied de page',
7477
},
@@ -517,6 +520,9 @@ describe('markdown', () => {
517520
admonitions: true,
518521
headingIds: false,
519522
},
523+
anchors: {
524+
maintainCase: true,
525+
},
520526
remarkRehypeOptions: {
521527
footnoteLabel: 'Notes de bas de page',
522528
// @ts-expect-error: we don't validate it on purpose

packages/docusaurus/src/server/configValidation.ts

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
4141
admonitions: true,
4242
headingIds: true,
4343
},
44+
anchors: {
45+
maintainCase: false,
46+
},
4447
remarkRehypeOptions: undefined,
4548
};
4649

@@ -320,6 +323,11 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
320323
// Not sure if it's a good idea, validation is likely to become stale
321324
// See https://github.com/remarkjs/remark-rehype#options
322325
Joi.object().unknown(),
326+
anchors: Joi.object({
327+
maintainCase: Joi.boolean().default(
328+
DEFAULT_CONFIG.markdown.anchors.maintainCase,
329+
),
330+
}).default(DEFAULT_CONFIG.markdown.anchors),
323331
}).default(DEFAULT_CONFIG.markdown),
324332
}).messages({
325333
'docusaurus.configValidationWarning':

website/docs/api/docusaurus.config.js.mdx

+9
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,18 @@ export type ParseFrontMatter = (params: {
438438
content: string;
439439
}>;
440440

441+
type MarkdownAnchorsConfig = {
442+
maintainCase: boolean;
443+
};
444+
441445
type MarkdownConfig = {
442446
format: 'mdx' | 'md' | 'detect';
443447
mermaid: boolean;
444448
preprocessor?: MarkdownPreprocessor;
445449
parseFrontMatter?: ParseFrontMatter;
446450
mdx1Compat: MDX1CompatOptions;
447451
remarkRehypeOptions: object; // see https://github.com/remarkjs/remark-rehype#options
452+
anchors: MarkdownAnchorsConfig;
448453
};
449454
```
450455

@@ -469,6 +474,9 @@ export default {
469474
admonitions: true,
470475
headingIds: true,
471476
},
477+
anchors: {
478+
maintainCase: true,
479+
},
472480
},
473481
};
474482
```
@@ -484,6 +492,7 @@ export default {
484492
| `preprocessor` | `MarkdownPreprocessor` | `undefined` | Gives you the ability to alter the Markdown content string before parsing. Use it as a last-resort escape hatch or workaround: it is almost always better to implement a Remark/Rehype plugin. |
485493
| `parseFrontMatter` | `ParseFrontMatter` | `undefined` | Gives you the ability to provide your own front matter parser, or to enhance the default parser. Read our [front matter guide](../guides/markdown-features/markdown-features-intro.mdx#front-matter) for details. |
486494
| `mdx1Compat` | `MDX1CompatOptions` | `{comments: true, admonitions: true, headingIds: true}` | Compatibility options to make it easier to upgrade to Docusaurus v3+. |
495+
| `anchors` | `MarkdownAnchorsConfig` | `{maintainCase: false}` | Options to control the behavior of anchors generated from Markdown headings |
487496
| `remarkRehypeOptions` | `object` | `undefined` | Makes it possible to pass custom [`remark-rehype` options](https://github.com/remarkjs/remark-rehype#options). |
488497
489498
```mdx-code-block

0 commit comments

Comments
 (0)