refactor(theme): split admonitions, make swizzle easier, better retrocompatibility (#7945)

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
Sébastien Lorber 2022-09-07 17:49:44 +02:00 committed by GitHub
parent f1415525c0
commit 6f63ffe0a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 914 additions and 236 deletions

View file

@ -53,7 +53,7 @@ export type MDXPlugin =
[Plugin<any[]>, any] | Plugin<any[]>;
export type MDXOptions = {
admonitions: boolean | AdmonitionOptions;
admonitions: boolean | Partial<AdmonitionOptions>;
remarkPlugins: MDXPlugin[];
rehypePlugins: MDXPlugin[];
beforeDefaultRemarkPlugins: MDXPlugin[];

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`admonitions remark plugin base 1`] = `
exports[`admonitions remark plugin add custom keyword 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<admonition title="Sample Title" type="info"><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></admonition>
<h2>Initial setup {#initial-setup}</h2>
@ -11,11 +11,9 @@ exports[`admonitions remark plugin base 1`] = `
<p>++++</p>"
`;
exports[`admonitions remark plugin custom keywords 1`] = `
exports[`admonitions remark plugin base 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<p>:::info Sample Title</p>
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
<p>:::</p>
<admonition title="Sample Title" type="info"><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></admonition>
<h2>Initial setup {#initial-setup}</h2>
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
@ -38,7 +36,33 @@ exports[`admonitions remark plugin custom tag 1`] = `
<admonition type="tip"><p>Admonition with different syntax</p></admonition>"
`;
exports[`admonitions remark plugin default behavior for custom keyword 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<p>:::info Sample Title</p>
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
<p>:::</p>
<h2>Initial setup {#initial-setup}</h2>
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
<p>++++tip</p>
<p>Admonition with different syntax</p>
<p>++++</p>"
`;
exports[`admonitions remark plugin interpolation 1`] = `
"<p>Test admonition with interpolated title/body</p>
<admonition type="tip"><mdxAdmonitionTitle>My <code>interpolated</code> <strong>title</strong> &#x3C;button style={{color: "red"}} onClick={() => alert("click")}>test</mdxAdmonitionTitle><p><code>body</code> <strong>interpolated</strong> content</p></admonition>"
`;
exports[`admonitions remark plugin replace custom keyword 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<p>:::info Sample Title</p>
<p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p>
<p>:::</p>
<h2>Initial setup {#initial-setup}</h2>
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>
<p>++++tip</p>
<p>Admonition with different syntax</p>
<p>++++</p>"
`;

View file

@ -36,13 +36,34 @@ describe('admonitions remark plugin', () => {
expect(result).toMatchSnapshot();
});
it('custom keywords', async () => {
const result = await processFixture('base', {keywords: ['tip']});
it('default behavior for custom keyword', async () => {
const result = await processFixture('base', {
keywords: ['tip'],
// extendDefaults: false, // By default we don't extend
});
expect(result).toMatchSnapshot();
});
it('add custom keyword', async () => {
const result = await processFixture('base', {
keywords: ['tip'],
extendDefaults: true,
});
expect(result).toMatchSnapshot();
});
it('replace custom keyword', async () => {
const result = await processFixture('base', {
keywords: ['tip'],
extendDefaults: false,
});
expect(result).toMatchSnapshot();
});
it('custom tag', async () => {
const result = await processFixture('base', {tag: '++++'});
const result = await processFixture('base', {
tag: '++++',
});
expect(result).toMatchSnapshot();
});

View file

@ -11,9 +11,14 @@ import type {Literal} from 'mdast';
const NEWLINE = '\n';
// TODO not ideal option shape
// First let upgrade to MDX 2.0
// Maybe we'll want to provide different tags for different admonition types?
// Also maybe rename "keywords" to "types"?
export type AdmonitionOptions = {
tag: string;
keywords: string[];
extendDefaults: boolean;
};
export const DefaultAdmonitionOptions: AdmonitionOptions = {
@ -29,6 +34,7 @@ export const DefaultAdmonitionOptions: AdmonitionOptions = {
'important',
'caution',
],
extendDefaults: false, // TODO make it true by default: breaking change
};
function escapeRegExp(s: string): string {
@ -36,9 +42,20 @@ function escapeRegExp(s: string): string {
}
function normalizeOptions(
options: Partial<AdmonitionOptions>,
providedOptions: Partial<AdmonitionOptions>,
): AdmonitionOptions {
return {...DefaultAdmonitionOptions, ...options};
const options = {...DefaultAdmonitionOptions, ...providedOptions};
// By default it makes more sense to append keywords to the default ones
// Adding custom keywords is more common than disabling existing ones
if (options.extendDefaults) {
options.keywords = [
...DefaultAdmonitionOptions.keywords,
...options.keywords,
];
}
return options;
}
// This string value does not matter much

View file

@ -12,6 +12,113 @@ import type {SwizzleConfig} from '@docusaurus/types';
export default function getSwizzleConfig(): SwizzleConfig {
return {
components: {
'Admonition/Icon': {
actions: {
eject: 'safe',
wrap: 'forbidden', // Can't wrap a folder
},
description: 'The folder containing all admonition icons',
},
'Admonition/Icon/Caution': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The admonition caution icon',
},
'Admonition/Icon/Danger': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The admonition danger icon',
},
'Admonition/Icon/Info': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The admonition info icon',
},
'Admonition/Icon/Note': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The admonition note icon',
},
'Admonition/Icon/Tip': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The admonition tip icon',
},
'Admonition/Layout': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The standard admonition layout applied to all default admonition types',
},
'Admonition/Type': {
actions: {
eject: 'safe',
wrap: 'forbidden',
},
description:
'The folder containing all the admonition type components.',
},
'Admonition/Type/Caution': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The component responsible for rendering a :::caution admonition type',
},
'Admonition/Type/Danger': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The component responsible for rendering a :::danger admonition type',
},
'Admonition/Type/Info': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The component responsible for rendering a :::info admonition type',
},
'Admonition/Type/Note': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The component responsible for rendering a :::note admonition type',
},
'Admonition/Type/Tip': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The component responsible for rendering a :::tip admonition type',
},
'Admonition/Types': {
actions: {
eject: 'safe',
// TODO the swizzle CLI should provide a way to wrap such objects
wrap: 'forbidden',
},
description:
'The object mapping admonition type to a React component.\nUse it to add custom admonition type components, or replace existing ones.\nCan be ejected or wrapped (only manually, see our documentation).',
},
CodeBlock: {
actions: {
eject: 'safe',
@ -20,6 +127,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The component used to render multi-line code blocks, generally used in Markdown files.',
},
'CodeBlock/Content': {
actions: {
eject: 'unsafe',
wrap: 'forbidden',
},
description:
'The folder containing components responsible for rendering different types of CodeBlock content.',
},
ColorModeToggle: {
actions: {
eject: 'safe',
@ -36,6 +151,17 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The component responsible for rendering a list of sidebar items cards.\nNotable used on the category generated-index pages.',
},
'DocItem/TOC': {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
// TODO the CLI should rather support --eject
// Subfolders can be swizzled
eject: 'forbidden',
wrap: 'forbidden',
},
description:
'The DocItem TOC is not directly swizzle-able, but you can swizzle its sub-components.',
},
DocSidebar: {
actions: {
eject: 'unsafe', // Too much technical code in sidebar, not very safe atm
@ -101,6 +227,17 @@ export default function getSwizzleConfig(): SwizzleConfig {
},
description: 'The footer logo',
},
Icon: {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
// TODO the CLI should rather support --eject
// Subfolders can be swizzled
eject: 'forbidden',
wrap: 'forbidden',
},
description:
'The Icon folder is not directly swizzle-able, but you can swizzle its sub-components.',
},
'Icon/Arrow': {
actions: {
eject: 'safe',
@ -220,7 +357,7 @@ export default function getSwizzleConfig(): SwizzleConfig {
wrap: 'forbidden',
},
description:
'The Navbar item components mapping. Can be ejected to add custom navbar item types. See https://github.com/facebook/docusaurus/issues/7227.',
'The Navbar item components mapping. Can be ejected to add custom navbar item types.\nSee https://github.com/facebook/docusaurus/issues/7227.',
},
NotFound: {
actions: {

View file

@ -44,13 +44,114 @@ declare module '@theme/Admonition' {
export interface Props {
readonly children: ReactNode;
readonly type: 'note' | 'tip' | 'danger' | 'info' | 'caution';
readonly type: string;
readonly icon?: ReactNode;
readonly title?: ReactNode;
readonly className?: string;
}
export default function Admonition(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Type/Note' {
import type {Props as AdmonitionProps} from '@theme/Admonition';
export interface Props extends AdmonitionProps {}
export default function AdmonitionTypeNote(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Type/Info' {
import type {Props as AdmonitionProps} from '@theme/Admonition';
export interface Props extends AdmonitionProps {}
export default function AdmonitionTypeInfo(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Type/Tip' {
import type {Props as AdmonitionProps} from '@theme/Admonition';
export interface Props extends AdmonitionProps {}
export default function AdmonitionTypeTip(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Type/Caution' {
import type {Props as AdmonitionProps} from '@theme/Admonition';
export interface Props extends AdmonitionProps {}
export default function AdmonitionTypeCaution(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Type/Danger' {
import type {Props as AdmonitionProps} from '@theme/Admonition';
export interface Props extends AdmonitionProps {}
export default function AdmonitionTypeDanger(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Types' {
import type {ComponentType} from 'react';
import type {Props} from '@theme/Admonition';
const AdmonitionTypes: {
[admonitionType: string]: ComponentType<Props>;
};
export default AdmonitionTypes;
}
declare module '@theme/Admonition/Layout' {
import type {ReactNode} from 'react';
export interface Props {
readonly children: ReactNode;
readonly type: string;
readonly icon?: ReactNode;
readonly title?: ReactNode;
readonly className?: string;
}
export default function AdmonitionLayout(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Icon/Note' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function AdmonitionIconNote(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Icon/Tip' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function AdmonitionIconTip(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Icon/Caution' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function AdmonitionIconCaution(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Icon/Danger' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function AdmonitionIconDanger(props: Props): JSX.Element;
}
declare module '@theme/Admonition/Icon/Info' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function AdmonitionIconInfo(props: Props): JSX.Element;
}
declare module '@theme/AnnouncementBar' {
export default function AnnouncementBar(): JSX.Element | null;
}

View file

@ -0,0 +1,20 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition/Icon/Caution';
export default function AdmonitionIconCaution(props: Props): JSX.Element {
return (
<svg viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"
/>
</svg>
);
}

View file

@ -0,0 +1,20 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition/Icon/Danger';
export default function AdmonitionIconDanger(props: Props): JSX.Element {
return (
<svg viewBox="0 0 12 16" {...props}>
<path
fillRule="evenodd"
d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"
/>
</svg>
);
}

View file

@ -0,0 +1,20 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition/Icon/Info';
export default function AdmonitionIconInfo(props: Props): JSX.Element {
return (
<svg viewBox="0 0 14 16" {...props}>
<path
fillRule="evenodd"
d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"
/>
</svg>
);
}

View file

@ -0,0 +1,20 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition/Icon/Note';
export default function AdmonitionIconNote(props: Props): JSX.Element {
return (
<svg viewBox="0 0 14 16" {...props}>
<path
fillRule="evenodd"
d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"
/>
</svg>
);
}

View file

@ -0,0 +1,20 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition/Icon/Tip';
export default function AdmonitionIconTip(props: Props): JSX.Element {
return (
<svg viewBox="0 0 12 16" {...props}>
<path
fillRule="evenodd"
d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"
/>
</svg>
);
}

View file

@ -0,0 +1,55 @@
/**
* 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 React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {ThemeClassNames} from '@docusaurus/theme-common';
import type {Props} from '@theme/Admonition/Layout';
import styles from './styles.module.css';
function AdmonitionContainer({
type,
className,
children,
}: Pick<Props, 'type' | 'className'> & {children: ReactNode}) {
return (
<div
className={clsx(
ThemeClassNames.common.admonition,
ThemeClassNames.common.admonitionType(type),
styles.admonition,
className,
)}>
{children}
</div>
);
}
function AdmonitionHeading({icon, title}: Pick<Props, 'icon' | 'title'>) {
return (
<div className={styles.admonitionHeading}>
<span className={styles.admonitionIcon}>{icon}</span>
{title}
</div>
);
}
function AdmonitionContent({children}: Pick<Props, 'children'>) {
return <div className={styles.admonitionContent}>{children}</div>;
}
export default function AdmonitionLayout(props: Props): JSX.Element {
const {type, icon, title, children, className} = props;
return (
<AdmonitionContainer type={type} className={className}>
<AdmonitionHeading title={title} icon={icon} />
<AdmonitionContent>{children}</AdmonitionContent>
</AdmonitionContainer>
);
}

View file

@ -0,0 +1,37 @@
/**
* 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 React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/Admonition/Type/Caution';
import AdmonitionLayout from '@theme/Admonition/Layout';
import IconCaution from '@theme/Admonition/Icon/Caution';
const infimaClassName = 'alert alert--warning';
const defaultProps = {
icon: <IconCaution />,
title: (
<Translate
id="theme.admonition.caution"
description="The default label used for the Caution admonition (:::caution)">
caution
</Translate>
),
};
export default function AdmonitionTypeCaution(props: Props): JSX.Element {
return (
<AdmonitionLayout
{...defaultProps}
{...props}
className={clsx(infimaClassName, props.className)}>
{props.children}
</AdmonitionLayout>
);
}

View file

@ -0,0 +1,37 @@
/**
* 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 React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/Admonition/Type/Danger';
import AdmonitionLayout from '@theme/Admonition/Layout';
import IconDanger from '@theme/Admonition/Icon/Danger';
const infimaClassName = 'alert alert--danger';
const defaultProps = {
icon: <IconDanger />,
title: (
<Translate
id="theme.admonition.danger"
description="The default label used for the Danger admonition (:::danger)">
danger
</Translate>
),
};
export default function AdmonitionTypeDanger(props: Props): JSX.Element {
return (
<AdmonitionLayout
{...defaultProps}
{...props}
className={clsx(infimaClassName, props.className)}>
{props.children}
</AdmonitionLayout>
);
}

View file

@ -0,0 +1,37 @@
/**
* 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 React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/Admonition/Type/Info';
import AdmonitionLayout from '@theme/Admonition/Layout';
import IconInfo from '@theme/Admonition/Icon/Info';
const infimaClassName = 'alert alert--info';
const defaultProps = {
icon: <IconInfo />,
title: (
<Translate
id="theme.admonition.info"
description="The default label used for the Info admonition (:::info)">
info
</Translate>
),
};
export default function AdmonitionTypeInfo(props: Props): JSX.Element {
return (
<AdmonitionLayout
{...defaultProps}
{...props}
className={clsx(infimaClassName, props.className)}>
{props.children}
</AdmonitionLayout>
);
}

View file

@ -0,0 +1,37 @@
/**
* 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 React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/Admonition/Type/Note';
import AdmonitionLayout from '@theme/Admonition/Layout';
import IconNote from '@theme/Admonition/Icon/Note';
const infimaClassName = 'alert alert--secondary';
const defaultProps = {
icon: <IconNote />,
title: (
<Translate
id="theme.admonition.note"
description="The default label used for the Note admonition (:::note)">
note
</Translate>
),
};
export default function AdmonitionTypeNote(props: Props): JSX.Element {
return (
<AdmonitionLayout
{...defaultProps}
{...props}
className={clsx(infimaClassName, props.className)}>
{props.children}
</AdmonitionLayout>
);
}

View file

@ -0,0 +1,37 @@
/**
* 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 React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/Admonition/Type/Tip';
import AdmonitionLayout from '@theme/Admonition/Layout';
import IconTip from '@theme/Admonition/Icon/Tip';
const infimaClassName = 'alert alert--success';
const defaultProps = {
icon: <IconTip />,
title: (
<Translate
id="theme.admonition.tip"
description="The default label used for the Tip admonition (:::tip)">
tip
</Translate>
),
};
export default function AdmonitionTypeTip(props: Props): JSX.Element {
return (
<AdmonitionLayout
{...defaultProps}
{...props}
className={clsx(infimaClassName, props.className)}>
{props.children}
</AdmonitionLayout>
);
}

View file

@ -0,0 +1,38 @@
/**
* 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 React from 'react';
import AdmonitionTypeNote from '@theme/Admonition/Type/Note';
import AdmonitionTypeTip from '@theme/Admonition/Type/Tip';
import AdmonitionTypeInfo from '@theme/Admonition/Type/Info';
import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution';
import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger';
import type AdmonitionTypes from '@theme/Admonition/Types';
const admonitionTypes: typeof AdmonitionTypes = {
note: AdmonitionTypeNote,
tip: AdmonitionTypeTip,
info: AdmonitionTypeInfo,
caution: AdmonitionTypeCaution,
danger: AdmonitionTypeDanger,
};
// Undocumented legacy admonition type aliases
// Provide hardcoded/untranslated retrocompatible label
// See also https://github.com/facebook/docusaurus/issues/7767
const admonitionAliases: typeof AdmonitionTypes = {
secondary: (props) => <AdmonitionTypeNote title="secondary" {...props} />,
important: (props) => <AdmonitionTypeInfo title="important" {...props} />,
success: (props) => <AdmonitionTypeTip title="success" {...props} />,
// TODO bad legacy mapping, warning is usually yellow, not red...
warning: (props) => <AdmonitionTypeDanger title="warning" {...props} />,
};
export default {
...admonitionTypes,
...admonitionAliases,
};

View file

@ -5,205 +5,24 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {ThemeClassNames} from '@docusaurus/theme-common';
import Translate from '@docusaurus/Translate';
import React, {type ComponentType} from 'react';
import {processAdmonitionProps} from '@docusaurus/theme-common';
import type {Props} from '@theme/Admonition';
import AdmonitionTypes from '@theme/Admonition/Types';
import styles from './styles.module.css';
function NoteIcon() {
return (
<svg viewBox="0 0 14 16">
<path
fillRule="evenodd"
d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"
/>
</svg>
);
}
function TipIcon() {
return (
<svg viewBox="0 0 12 16">
<path
fillRule="evenodd"
d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"
/>
</svg>
);
}
function DangerIcon() {
return (
<svg viewBox="0 0 12 16">
<path
fillRule="evenodd"
d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"
/>
</svg>
);
}
function InfoIcon() {
return (
<svg viewBox="0 0 14 16">
<path
fillRule="evenodd"
d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"
/>
</svg>
);
}
function CautionIcon() {
return (
<svg viewBox="0 0 16 16">
<path
fillRule="evenodd"
d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"
/>
</svg>
);
}
type AdmonitionConfig = {
iconComponent: React.ComponentType;
infimaClassName: string;
label: ReactNode;
};
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
const AdmonitionConfigs: Record<Props['type'], AdmonitionConfig> = {
note: {
infimaClassName: 'secondary',
iconComponent: NoteIcon,
label: (
<Translate
id="theme.admonition.note"
description="The default label used for the Note admonition (:::note)">
note
</Translate>
),
},
tip: {
infimaClassName: 'success',
iconComponent: TipIcon,
label: (
<Translate
id="theme.admonition.tip"
description="The default label used for the Tip admonition (:::tip)">
tip
</Translate>
),
},
danger: {
infimaClassName: 'danger',
iconComponent: DangerIcon,
label: (
<Translate
id="theme.admonition.danger"
description="The default label used for the Danger admonition (:::danger)">
danger
</Translate>
),
},
info: {
infimaClassName: 'info',
iconComponent: InfoIcon,
label: (
<Translate
id="theme.admonition.info"
description="The default label used for the Info admonition (:::info)">
info
</Translate>
),
},
caution: {
infimaClassName: 'warning',
iconComponent: CautionIcon,
label: (
<Translate
id="theme.admonition.caution"
description="The default label used for the Caution admonition (:::caution)">
caution
</Translate>
),
},
};
// Legacy aliases, undocumented but kept for retro-compatibility
const aliases = {
secondary: 'note',
important: 'info',
success: 'tip',
warning: 'danger',
} as const;
function getAdmonitionConfig(unsafeType: string): AdmonitionConfig {
const type =
(aliases as {[key: string]: Props['type']})[unsafeType] ?? unsafeType;
const config = (AdmonitionConfigs as {[key: string]: AdmonitionConfig})[type];
if (config) {
return config;
function getAdmonitionTypeComponent(type: string): ComponentType<Props> {
const component = AdmonitionTypes[type];
if (component) {
return component;
}
console.warn(
`No admonition config found for admonition type "${type}". Using Info as fallback.`,
`No admonition component found for admonition type "${type}". Using Info as fallback.`,
);
return AdmonitionConfigs.info;
return AdmonitionTypes.info!;
}
// Workaround because it's difficult in MDX v1 to provide a MDX title as props
// See https://github.com/facebook/docusaurus/pull/7152#issuecomment-1145779682
function extractMDXAdmonitionTitle(children: ReactNode): {
mdxAdmonitionTitle: ReactNode | undefined;
rest: ReactNode;
} {
const items = React.Children.toArray(children);
const mdxAdmonitionTitle = items.find(
(item) =>
React.isValidElement(item) &&
(item.props as {mdxType: string} | null)?.mdxType ===
'mdxAdmonitionTitle',
);
const rest = <>{items.filter((item) => item !== mdxAdmonitionTitle)}</>;
return {
mdxAdmonitionTitle,
rest,
};
}
function processAdmonitionProps(props: Props): Props {
const {mdxAdmonitionTitle, rest} = extractMDXAdmonitionTitle(props.children);
return {
...props,
title: props.title ?? mdxAdmonitionTitle,
children: rest,
};
}
export default function Admonition(props: Props): JSX.Element {
const {children, type, title, icon: iconProp} = processAdmonitionProps(props);
const typeConfig = getAdmonitionConfig(type);
const titleLabel = title ?? typeConfig.label;
const {iconComponent: IconComponent} = typeConfig;
const icon = iconProp ?? <IconComponent />;
return (
<div
className={clsx(
ThemeClassNames.common.admonition,
ThemeClassNames.common.admonitionType(props.type),
'alert',
`alert--${typeConfig.infimaClassName}`,
styles.admonition,
)}>
<div className={styles.admonitionHeading}>
<span className={styles.admonitionIcon}>{icon}</span>
{titleLabel}
</div>
<div className={styles.admonitionContent}>{children}</div>
</div>
);
export default function Admonition(unprocessedProps: Props): JSX.Element {
const props = processAdmonitionProps(unprocessedProps);
const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type);
return <AdmonitionTypeComponent {...props} />;
}

View file

@ -78,3 +78,5 @@ export {duplicates, uniq} from './utils/jsUtils';
export {usePrismTheme} from './hooks/usePrismTheme';
export {useDocsPreferredVersion} from './contexts/docsPreferredVersion';
export {processAdmonitionProps} from './utils/admonitionUtils';

View file

@ -40,8 +40,7 @@ export const ThemeClassNames = {
backToTopButton: 'theme-back-to-top-button',
codeBlock: 'theme-code-block',
admonition: 'theme-admonition',
admonitionType: (type: 'note' | 'tip' | 'danger' | 'info' | 'caution') =>
`theme-admonition-${type}`,
admonitionType: (type: string) => `theme-admonition-${type}`,
},
layout: {
// TODO add other stable classNames here

View file

@ -0,0 +1,43 @@
/**
* 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 React, {type ReactNode} from 'react';
// Workaround because it's difficult in MDX v1 to provide a MDX title as props
// See https://github.com/facebook/docusaurus/pull/7152#issuecomment-1145779682
function extractMDXAdmonitionTitle(children: ReactNode): {
mdxAdmonitionTitle: ReactNode | undefined;
rest: ReactNode;
} {
const items = React.Children.toArray(children);
const mdxAdmonitionTitle = items.find(
(item) =>
React.isValidElement(item) &&
(item.props as {mdxType: string} | null)?.mdxType ===
'mdxAdmonitionTitle',
);
const rest = <>{items.filter((item) => item !== mdxAdmonitionTitle)}</>;
return {
mdxAdmonitionTitle,
rest,
};
}
export function processAdmonitionProps<
Props extends {readonly children: ReactNode; readonly title?: ReactNode},
>(props: Props): Props {
const {mdxAdmonitionTitle, rest} = extractMDXAdmonitionTitle(props.children);
const title = props.title ?? mdxAdmonitionTitle;
return {
...props,
// Do not return "title: undefined" prop
// this might create unwanted props overrides when merging props
// For example: {...default,...props}
...(title && {title}),
children: rest,
};
}

View file

@ -20,7 +20,7 @@ You now need to swizzle the admonitions component to provide UI customizations s
Please refer to https://github.com/facebook/docusaurus/pull/7152 for detailed upgrade instructions."
`;
exports[`validation schemas admonitionsSchema: for value={"keywords":[]} 1`] = `""keywords" does not contain 1 required value(s)"`;
exports[`validation schemas admonitionsSchema: for value={"keywords":["custom-keyword"],"extendDefaults":42} 1`] = `""extendDefaults" must be a boolean"`;
exports[`validation schemas admonitionsSchema: for value={"tag":""} 1`] = `""tag" is not allowed to be empty"`;

View file

@ -96,13 +96,20 @@ describe('validation schemas', () => {
testOK({});
testOK({tag: '+++'});
testOK({keywords: ['info', 'tip']});
testOK({keywords: ['info', 'tip'], extendDefaults: true});
testOK({keywords: ['info', 'tip'], extendDefaults: false});
testOK({keywords: []});
testOK({keywords: [], extendDefaults: true}); // noop
testOK({keywords: [], extendDefaults: false}); // disable admonitions
testOK({tag: '+++', keywords: ['info', 'tip']});
testOK({tag: '+++', keywords: ['custom-keyword'], extendDefaults: true});
testOK({tag: '+++', keywords: ['custom-keyword'], extendDefaults: false});
testFail(3);
testFail([]);
testFail({unknownAttribute: 'val'});
testFail({tag: ''});
testFail({keywords: []});
testFail({keywords: ['custom-keyword'], extendDefaults: 42});
// Legacy types
testFail({

View file

@ -44,8 +44,12 @@ export const AdmonitionsSchema = JoiFrontMatter.alternatives()
JoiFrontMatter.object({
tag: JoiFrontMatter.string(),
keywords: JoiFrontMatter.array().items(
JoiFrontMatter.string().required(),
JoiFrontMatter.string(),
// Apparently this is how we tell job to accept empty arrays...
// .required(),
),
extendDefaults: JoiFrontMatter.boolean(),
// TODO Remove before 2023
customTypes: LegacyAdmonitionConfigSchema,
icons: LegacyAdmonitionConfigSchema,

View file

@ -50,7 +50,7 @@ function sortComponentNames(componentNames: string[]): string[] {
*
* @param componentNames the original list of component names
*/
function getMissingIntermediateComponentFolderNames(
export function getMissingIntermediateComponentFolderNames(
componentNames: string[],
): string[] {
function getAllIntermediatePaths(componentName: string): string[] {

View file

@ -239,3 +239,9 @@ Can be arbitrarily nested:
Admonition body
:::
:::important
Admonition alias `:::important` should have Important title
:::

View file

@ -13,7 +13,10 @@ import logger from '@docusaurus/logger';
import classicTheme from '@docusaurus/theme-classic';
// Unsafe imports
import {readComponentNames} from '@docusaurus/core/lib/commands/swizzle/components.js';
import {
readComponentNames,
getMissingIntermediateComponentFolderNames,
} from '@docusaurus/core/lib/commands/swizzle/components.js';
import {normalizeSwizzleConfig} from '@docusaurus/core/lib/commands/swizzle/config.js';
import {wrap, eject} from '@docusaurus/core/lib/commands/swizzle/actions.js';
@ -50,7 +53,33 @@ console.log('\n');
await fs.remove(toPath);
let componentNames = await readComponentNames(themePath);
function filterComponentNames(componentNames) {
// TODO temp workaround: non-comps should be forbidden to wrap
if (action === 'wrap') {
const WrapBlocklist = [
'Layout', // Due to theme-fallback?
];
return componentNames.filter((componentName) => {
const blocked = WrapBlocklist.includes(componentName);
if (blocked) {
logger.warn(`${componentName} is blocked and will not be wrapped`);
}
return !blocked;
});
}
return componentNames;
}
async function getAllComponentNames() {
const names = await readComponentNames(themePath);
const allNames = names.concat(
await getMissingIntermediateComponentFolderNames(names),
);
return filterComponentNames(allNames);
}
const componentNames = await getAllComponentNames();
const componentsNotFound = Object.keys(swizzleConfig.components).filter(
(componentName) => !componentNames.includes(componentName),
@ -67,21 +96,6 @@ Please double-check or clean up these components from the config:
process.exit(1);
}
// TODO temp workaround: non-comps should be forbidden to wrap
if (action === 'wrap') {
const WrapBlocklist = [
'Layout', // Due to theme-fallback?
];
componentNames = componentNames.filter((componentName) => {
const blocked = WrapBlocklist.includes(componentName);
if (blocked) {
logger.warn(`${componentName} is blocked and will not be wrapped`);
}
return !blocked;
});
}
/**
* @param {string} componentName
*/

View file

@ -216,13 +216,13 @@ You can customize how each individual admonition type is rendered through [swizz
```jsx title="src/theme/Admonition.js"
import React from 'react';
import Admonition from '@theme-original/Admonition';
import MyIcon from '@site/static/img/info.svg';
import MyCustomNoteIcon from '@site/static/img/info.svg';
export default function AdmonitionWrapper(props) {
if (props.type !== 'info') {
return <Admonition {...props} />;
return <Admonition title="My Custom Admonition Title" {...props} />;
}
return <Admonition icon={<MyIcon />} {...props} />;
return <Admonition icon={<MyCustomNoteIcon />} {...props} />;
}
```
@ -240,6 +240,7 @@ module.exports = {
admonitions: {
tag: ':::',
keywords: ['note', 'tip', 'info', 'caution', 'danger'],
extendDefaults: true,
},
},
},
@ -248,9 +249,82 @@ module.exports = {
};
```
The plugin accepts two options:
The plugin accepts the following options:
- `tag`: The tag that encloses the admonition. Defaults to `:::`.
- `keywords`: An array of keywords that can be used as the type for the admonition. Note that if you override this, the default values will not be applied.
- `keywords`: An array of keywords that can be used as the type for the admonition.
- `extendDefaults`: Should the provided options (such as `keywords`) be merged into the existing defaults. Defaults to `false`.
The `keyword` will be passed as the `type` prop of the `Admonition` component. If you register more types than the default, you are also responsible for providing their implementation—including the container's style, icon, default title text, etc. You would usually need to [eject](../../swizzling.md#ejecting) the `@theme/Admonition` component, so you could re-use the same infrastructure as the other types.
The `keyword` will be passed as the `type` prop of the `Admonition` component.
### Custom admonition type components {#custom-admonition-type-components}
By default, the theme doesn't know what do to with custom admonition keywords such as `:::my-custom-admonition`. It is your responsibility to map each admonition keyword to a React component so that the theme knows how to render them.
If you registered a new admonition type `my-custom-admonition` via the following config:
````js title="docusaurus.config.js"
module.exports = {
// ...
presets: [
[
'classic',
{
// ...
docs: {
admonitions: {
tag: ':::',
keywords: ['my-custom-admonition'],
extendDefaults: true,
},
},
},
],
],
};
You can provide the corresponding React component for `:::my-custom-admonition` by creating the following file (unfortunately, since it's not a React component file, it's not swizzlable):
```js title="src/theme/Admonition/Types.js"
import React from 'react';
import DefaultAdmonitionTypes from '@theme-original/Admonition/Types';
function MyCustomAdmonition(props) {
return (
<div style={{border: 'solid red', padding: 10}}>
<h5 style={{color: 'blue', fontSize: 30}}>{props.title}</h5>
<div>{props.children}</div>
</div>
);
}
const AdmonitionTypes = {
...DefaultAdmonitionTypes,
// Add all your custom admonition types here...
// You can also override the default ones if you want
'my-custom-admonition': MyCustomAdmonition,
};
export default AdmonitionTypes;
````
Now you can use your new admonition keyword in a Markdown file, and it will be parsed and rendered with your custom logic:
```md
:::my-custom-admonition Custom Admonition
It works!
:::
```
<BrowserWindow>
:::my-custom-admonition Custom Admonition
It works!
:::
</BrowserWindow>

View file

@ -303,6 +303,9 @@ const config = {
const nextVersionDocsDirPath = 'docs';
return `https://github.com/facebook/docusaurus/edit/main/website/${nextVersionDocsDirPath}/${docPath}`;
},
admonitions: {
keywords: ['my-custom-admonition'],
},
showLastUpdateAuthor: true,
showLastUpdateTime: true,
remarkPlugins: [math, [npm2yarn, {sync: true}]],

View file

@ -0,0 +1,29 @@
/**
* 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 React from 'react';
import type {Props} from '@theme/Admonition';
import DefaultAdmonitionTypes from '@theme-original/Admonition/Types';
function MyCustomAdmonition(props: Props): JSX.Element {
return (
<div style={{border: 'solid red', padding: 10}}>
<h5 style={{color: 'blue', fontSize: 30}}>{props.title}</h5>
<div>{props.children}</div>
</div>
);
}
const AdmonitionTypes = {
...DefaultAdmonitionTypes,
// Add all your custom admonition types here...
// you can also override the default ones
'my-custom-admonition': MyCustomAdmonition,
};
export default AdmonitionTypes;