mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-18 11:36:53 +02:00
feat(core): add new site config option siteConfig.markdown.anchors.maintainCase
(#10064)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com> Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
9418786b26
commit
daba917e7c
10 changed files with 122 additions and 13 deletions
|
@ -47,11 +47,6 @@ type SimpleProcessor = {
|
||||||
}) => Promise<SimpleProcessorResult>;
|
}) => Promise<SimpleProcessorResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getDefaultRemarkPlugins(): Promise<MDXPlugin[]> {
|
|
||||||
const {default: emoji} = await import('remark-emoji');
|
|
||||||
return [headings, emoji, toc];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MDXPlugin = Pluggable;
|
export type MDXPlugin = Pluggable;
|
||||||
|
|
||||||
export type MDXOptions = {
|
export type MDXOptions = {
|
||||||
|
@ -86,8 +81,18 @@ async function createProcessorFactory() {
|
||||||
const {default: comment} = await import('@slorber/remark-comment');
|
const {default: comment} = await import('@slorber/remark-comment');
|
||||||
const {default: directive} = await import('remark-directive');
|
const {default: directive} = await import('remark-directive');
|
||||||
const {VFile} = await import('vfile');
|
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
|
// /!\ this method is synchronous on purpose
|
||||||
// Using async code here can create cache entry race conditions!
|
// Using async code here can create cache entry race conditions!
|
||||||
|
@ -104,7 +109,7 @@ async function createProcessorFactory() {
|
||||||
directive,
|
directive,
|
||||||
[contentTitle, {removeContentTitle: options.removeContentTitle}],
|
[contentTitle, {removeContentTitle: options.removeContentTitle}],
|
||||||
...getAdmonitionsPlugins(options.admonitions ?? false),
|
...getAdmonitionsPlugins(options.admonitions ?? false),
|
||||||
...defaultRemarkPlugins,
|
...getDefaultRemarkPlugins({options}),
|
||||||
details,
|
details,
|
||||||
head,
|
head,
|
||||||
...(options.markdownConfig.mermaid ? [mermaid] : []),
|
...(options.markdownConfig.mermaid ? [mermaid] : []),
|
||||||
|
|
|
@ -11,13 +11,20 @@ import u from 'unist-builder';
|
||||||
import {removePosition} from 'unist-util-remove-position';
|
import {removePosition} from 'unist-util-remove-position';
|
||||||
import {toString} from 'mdast-util-to-string';
|
import {toString} from 'mdast-util-to-string';
|
||||||
import {visit} from 'unist-util-visit';
|
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 {Plugin} from 'unified';
|
||||||
import type {Parent} from 'unist';
|
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 {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));
|
const result = await processor.run(processor.parse(doc));
|
||||||
removePosition(result, {force: true});
|
removePosition(result, {force: true});
|
||||||
return result;
|
return result;
|
||||||
|
@ -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,7 +12,13 @@ import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
|
||||||
import type {Transformer} from 'unified';
|
import type {Transformer} from 'unified';
|
||||||
import type {Heading, Text} from 'mdast';
|
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) => {
|
return async (root) => {
|
||||||
const {toString} = await import('mdast-util-to-string');
|
const {toString} = await import('mdast-util-to-string');
|
||||||
const {visit} = await import('unist-util-visit');
|
const {visit} = await import('unist-util-visit');
|
||||||
|
@ -38,7 +44,9 @@ export default function plugin(): Transformer {
|
||||||
// Support explicit heading IDs
|
// Support explicit heading IDs
|
||||||
const parsedHeading = parseMarkdownHeadingId(heading);
|
const parsedHeading = parseMarkdownHeadingId(heading);
|
||||||
|
|
||||||
id = parsedHeading.id ?? slugs.slug(heading);
|
id =
|
||||||
|
parsedHeading.id ??
|
||||||
|
slugs.slug(heading, {maintainCase: anchorsMaintainCase});
|
||||||
|
|
||||||
if (parsedHeading.id) {
|
if (parsedHeading.id) {
|
||||||
// When there's an id, it is always in the last child node
|
// When there's an id, it is always in the last child node
|
||||||
|
|
|
@ -25,7 +25,7 @@ const processFixture = async (name: string) => {
|
||||||
|
|
||||||
const result = await compile(file, {
|
const result = await compile(file, {
|
||||||
format: 'mdx',
|
format: 'mdx',
|
||||||
remarkPlugins: [headings, gfm, plugin],
|
remarkPlugins: [[headings, {anchorsMaintainCase: false}], gfm, plugin],
|
||||||
rehypePlugins: [],
|
rehypePlugins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
12
packages/docusaurus-types/src/config.d.ts
vendored
12
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -45,6 +45,13 @@ export type ParseFrontMatter = (
|
||||||
},
|
},
|
||||||
) => Promise<ParseFrontMatterResult>;
|
) => Promise<ParseFrontMatterResult>;
|
||||||
|
|
||||||
|
export type MarkdownAnchorsConfig = {
|
||||||
|
/**
|
||||||
|
* Preserves the case of the heading text when generating anchor ids.
|
||||||
|
*/
|
||||||
|
maintainCase: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type MarkdownConfig = {
|
export type MarkdownConfig = {
|
||||||
/**
|
/**
|
||||||
* The Markdown format to use by default.
|
* The Markdown format to use by default.
|
||||||
|
@ -101,6 +108,11 @@ export type MarkdownConfig = {
|
||||||
* See also https://github.com/remarkjs/remark-rehype#options
|
* See also https://github.com/remarkjs/remark-rehype#options
|
||||||
*/
|
*/
|
||||||
remarkRehypeOptions: RemarkRehypeOptions;
|
remarkRehypeOptions: RemarkRehypeOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to control the behavior of anchors generated from Markdown headings
|
||||||
|
*/
|
||||||
|
anchors: MarkdownAnchorsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,6 +17,9 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -68,6 +71,9 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -119,6 +125,9 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -170,6 +179,9 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -221,6 +233,9 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -272,6 +287,9 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -323,6 +341,9 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -376,6 +397,9 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -429,6 +453,9 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
@ -485,6 +512,9 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
|
|
@ -97,6 +97,9 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
"path": "i18n",
|
"path": "i18n",
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
|
"anchors": {
|
||||||
|
"maintainCase": false,
|
||||||
|
},
|
||||||
"format": "mdx",
|
"format": "mdx",
|
||||||
"mdx1Compat": {
|
"mdx1Compat": {
|
||||||
"admonitions": true,
|
"admonitions": true,
|
||||||
|
|
|
@ -69,6 +69,9 @@ describe('normalizeConfig', () => {
|
||||||
admonitions: false,
|
admonitions: false,
|
||||||
headingIds: true,
|
headingIds: true,
|
||||||
},
|
},
|
||||||
|
anchors: {
|
||||||
|
maintainCase: true,
|
||||||
|
},
|
||||||
remarkRehypeOptions: {
|
remarkRehypeOptions: {
|
||||||
footnoteLabel: 'Pied de page',
|
footnoteLabel: 'Pied de page',
|
||||||
},
|
},
|
||||||
|
@ -517,6 +520,9 @@ describe('markdown', () => {
|
||||||
admonitions: true,
|
admonitions: true,
|
||||||
headingIds: false,
|
headingIds: false,
|
||||||
},
|
},
|
||||||
|
anchors: {
|
||||||
|
maintainCase: true,
|
||||||
|
},
|
||||||
remarkRehypeOptions: {
|
remarkRehypeOptions: {
|
||||||
footnoteLabel: 'Notes de bas de page',
|
footnoteLabel: 'Notes de bas de page',
|
||||||
// @ts-expect-error: we don't validate it on purpose
|
// @ts-expect-error: we don't validate it on purpose
|
||||||
|
|
|
@ -41,6 +41,9 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||||
admonitions: true,
|
admonitions: true,
|
||||||
headingIds: true,
|
headingIds: true,
|
||||||
},
|
},
|
||||||
|
anchors: {
|
||||||
|
maintainCase: false,
|
||||||
|
},
|
||||||
remarkRehypeOptions: undefined,
|
remarkRehypeOptions: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -320,6 +323,11 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||||
// Not sure if it's a good idea, validation is likely to become stale
|
// Not sure if it's a good idea, validation is likely to become stale
|
||||||
// See https://github.com/remarkjs/remark-rehype#options
|
// See https://github.com/remarkjs/remark-rehype#options
|
||||||
Joi.object().unknown(),
|
Joi.object().unknown(),
|
||||||
|
anchors: Joi.object({
|
||||||
|
maintainCase: Joi.boolean().default(
|
||||||
|
DEFAULT_CONFIG.markdown.anchors.maintainCase,
|
||||||
|
),
|
||||||
|
}).default(DEFAULT_CONFIG.markdown.anchors),
|
||||||
}).default(DEFAULT_CONFIG.markdown),
|
}).default(DEFAULT_CONFIG.markdown),
|
||||||
}).messages({
|
}).messages({
|
||||||
'docusaurus.configValidationWarning':
|
'docusaurus.configValidationWarning':
|
||||||
|
|
|
@ -438,6 +438,10 @@ export type ParseFrontMatter = (params: {
|
||||||
content: string;
|
content: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type MarkdownAnchorsConfig = {
|
||||||
|
maintainCase: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type MarkdownConfig = {
|
type MarkdownConfig = {
|
||||||
format: 'mdx' | 'md' | 'detect';
|
format: 'mdx' | 'md' | 'detect';
|
||||||
mermaid: boolean;
|
mermaid: boolean;
|
||||||
|
@ -445,6 +449,7 @@ type MarkdownConfig = {
|
||||||
parseFrontMatter?: ParseFrontMatter;
|
parseFrontMatter?: ParseFrontMatter;
|
||||||
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;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -469,6 +474,9 @@ export default {
|
||||||
admonitions: true,
|
admonitions: true,
|
||||||
headingIds: true,
|
headingIds: true,
|
||||||
},
|
},
|
||||||
|
anchors: {
|
||||||
|
maintainCase: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -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. |
|
| `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. |
|
| `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+. |
|
| `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). |
|
| `remarkRehypeOptions` | `object` | `undefined` | Makes it possible to pass custom [`remark-rehype` options](https://github.com/remarkjs/remark-rehype#options). |
|
||||||
|
|
||||||
```mdx-code-block
|
```mdx-code-block
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue