Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add new site config option siteConfig.markdown.anchors.maintainCase #10064

Merged
merged 9 commits into from
Apr 25, 2024
19 changes: 12 additions & 7 deletions packages/docusaurus-mdx-loader/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ type SimpleProcessor = {
}) => Promise<SimpleProcessorResult>;
};

async function getDefaultRemarkPlugins(): Promise<MDXPlugin[]> {
const {default: emoji} = await import('remark-emoji');
return [headings, emoji, toc];
}

export type MDXPlugin = Pluggable;

export type MDXOptions = {
Expand Down Expand Up @@ -86,8 +81,18 @@ async function createProcessorFactory() {
const {default: comment} = await import('@slorber/remark-comment');
const {default: directive} = await import('remark-directive');
const {VFile} = await import('vfile');
const {default: emoji} = await import('remark-emoji');

const defaultRemarkPlugins = await getDefaultRemarkPlugins();
function getDefaultRemarkPlugins({options}: {options: Options}): MDXPlugin[] {
return [
[
headings,
{anchorsMaintainCase: options.markdownConfig.anchors.maintainCase},
],
emoji,
toc,
];
}

// /!\ this method is synchronous on purpose
// Using async code here can create cache entry race conditions!
Expand All @@ -104,7 +109,7 @@ async function createProcessorFactory() {
directive,
[contentTitle, {removeContentTitle: options.removeContentTitle}],
...getAdmonitionsPlugins(options.admonitions ?? false),
...defaultRemarkPlugins,
...getDefaultRemarkPlugins({options}),
details,
head,
...(options.markdownConfig.mermaid ? [mermaid] : []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ import u from 'unist-builder';
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 plugin 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 = {anchorsMaintainCase: false},
) {
const {remark} = await import('remark');
const processor = await remark().use({plugins: [...plugins, slug]});
const processor = await remark().use({
plugins: [...plugins, [plugin, options]],
});
const result = await processor.run(processor.parse(doc));
removePosition(result, {force: true});
return result;
Expand Down Expand Up @@ -312,4 +319,25 @@ describe('headings remark plugin', () => {
},
]);
});

it('preserve anchors case then "anchorsMaintainCase" option is set', async () => {
const result = await process('# Case Sensitive Heading', [], {
anchorsMaintainCase: true,
});
const expected = u('root', [
u(
'heading',
{
depth: 1,
data: {
hProperties: {id: 'Case-Sensitive-Heading'},
id: 'Case-Sensitive-Heading',
},
},
[u('text', 'Case Sensitive Heading')],
),
]);

expect(result).toEqual(expected);
});
});
12 changes: 10 additions & 2 deletions packages/docusaurus-mdx-loader/src/remark/headings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
anchorsMaintainCase: boolean;
}

export default function plugin({
anchorsMaintainCase,
}: PluginOptions): Transformer {
return async (root) => {
const {toString} = await import('mdast-util-to-string');
const {visit} = await import('unist-util-visit');
Expand All @@ -38,7 +44,9 @@ 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: anchorsMaintainCase});

if (parsedHeading.id) {
// When there's an id, it is always in the last child node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const processFixture = async (name: string) => {

const result = await compile(file, {
format: 'mdx',
remarkPlugins: [headings, gfm, plugin],
remarkPlugins: [[headings, {anchorsMaintainCase: false}], gfm, plugin],
rehypePlugins: [],
});

Expand Down
12 changes: 12 additions & 0 deletions packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export type ParseFrontMatter = (
},
) => 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.
Expand Down Expand Up @@ -101,6 +108,11 @@ export type MarkdownConfig = {
* See also https://github.com/remarkjs/remark-rehype#options
*/
remarkRehypeOptions: RemarkRehypeOptions;

/**
* Options to control the behavior of anchors generated from Markdown headings
*/
anchors: MarkdownAnchorsConfig;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -68,6 +71,9 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -119,6 +125,9 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -170,6 +179,9 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -221,6 +233,9 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -272,6 +287,9 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -323,6 +341,9 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -376,6 +397,9 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -429,6 +453,9 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down Expand Up @@ -485,6 +512,9 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ exports[`load loads props for site with custom i18n path 1`] = `
"path": "i18n",
},
"markdown": {
"anchors": {
"maintainCase": false,
},
"format": "mdx",
"mdx1Compat": {
"admonitions": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ describe('normalizeConfig', () => {
admonitions: false,
headingIds: true,
},
anchors: {
maintainCase: true,
},
remarkRehypeOptions: {
footnoteLabel: 'Pied de page',
},
Expand Down Expand Up @@ -517,6 +520,9 @@ describe('markdown', () => {
admonitions: true,
headingIds: false,
},
anchors: {
maintainCase: true,
},
remarkRehypeOptions: {
footnoteLabel: 'Notes de bas de page',
// @ts-expect-error: we don't validate it on purpose
Expand Down
8 changes: 8 additions & 0 deletions packages/docusaurus/src/server/configValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
admonitions: true,
headingIds: true,
},
anchors: {
maintainCase: false,
},
remarkRehypeOptions: undefined,
};

Expand Down Expand Up @@ -320,6 +323,11 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
// Not sure if it's a good idea, validation is likely to become stale
// See https://github.com/remarkjs/remark-rehype#options
Joi.object().unknown(),
anchors: Joi.object({
maintainCase: Joi.boolean().default(
DEFAULT_CONFIG.markdown.anchors.maintainCase,
),
}).default(DEFAULT_CONFIG.markdown.anchors),
}).default(DEFAULT_CONFIG.markdown),
}).messages({
'docusaurus.configValidationWarning':
Expand Down
9 changes: 9 additions & 0 deletions website/docs/api/docusaurus.config.js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,18 @@ export type ParseFrontMatter = (params: {
content: string;
}>;

type MarkdownAnchorsConfig = {
maintainCase: boolean;
};

type MarkdownConfig = {
format: 'mdx' | 'md' | 'detect';
mermaid: boolean;
preprocessor?: MarkdownPreprocessor;
parseFrontMatter?: ParseFrontMatter;
mdx1Compat: MDX1CompatOptions;
remarkRehypeOptions: object; // see https://github.com/remarkjs/remark-rehype#options
anchors: MarkdownAnchorsConfig;
};
```

Expand All @@ -469,6 +474,9 @@ export default {
admonitions: true,
headingIds: true,
},
anchors: {
maintainCase: true,
},
},
};
```
Expand All @@ -484,6 +492,7 @@ export default {
| `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. |
| `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. |
| `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 |
| `remarkRehypeOptions` | `object` | `undefined` | Makes it possible to pass custom [`remark-rehype` options](https://github.com/remarkjs/remark-rehype#options). |

```mdx-code-block
Expand Down
Loading