diff --git a/packages/docusaurus-mdx-loader/src/loader.ts b/packages/docusaurus-mdx-loader/src/loader.ts index 764d62c2bd..3d6747771d 100644 --- a/packages/docusaurus-mdx-loader/src/loader.ts +++ b/packages/docusaurus-mdx-loader/src/loader.ts @@ -53,7 +53,7 @@ export type MDXPlugin = [Plugin, any] | Plugin; export type MDXOptions = { - admonitions: boolean | AdmonitionOptions; + admonitions: boolean | Partial; remarkPlugins: MDXPlugin[]; rehypePlugins: MDXPlugin[]; beforeDefaultRemarkPlugins: MDXPlugin[]; diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap index 227eb28b33..0b7bdca847 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/__snapshots__/index.test.ts.snap @@ -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`] = ` "

The blog feature enables you to deploy in no time a full-featured blog.

Check the Blog Plugin API Reference documentation for an exhaustive list of options.

Initial setup {#initial-setup}

@@ -11,11 +11,9 @@ exports[`admonitions remark plugin base 1`] = `

++++

" `; -exports[`admonitions remark plugin custom keywords 1`] = ` +exports[`admonitions remark plugin base 1`] = ` "

The blog feature enables you to deploy in no time a full-featured blog.

-

:::info Sample Title

-

Check the Blog Plugin API Reference documentation for an exhaustive list of options.

-

:::

+

Check the Blog Plugin API Reference documentation for an exhaustive list of options.

Initial setup {#initial-setup}

To set up your site's blog, start by creating a blog directory.

Use the Fast Track to understand Docusaurus in 5 minutes ⏱!

Use docusaurus.new to test Docusaurus immediately in your browser!

@@ -38,7 +36,33 @@ exports[`admonitions remark plugin custom tag 1`] = `

Admonition with different syntax

" `; +exports[`admonitions remark plugin default behavior for custom keyword 1`] = ` +"

The blog feature enables you to deploy in no time a full-featured blog.

+

:::info Sample Title

+

Check the Blog Plugin API Reference documentation for an exhaustive list of options.

+

:::

+

Initial setup {#initial-setup}

+

To set up your site's blog, start by creating a blog directory.

+

Use the Fast Track to understand Docusaurus in 5 minutes ⏱!

Use docusaurus.new to test Docusaurus immediately in your browser!

+

++++tip

+

Admonition with different syntax

+

++++

" +`; + exports[`admonitions remark plugin interpolation 1`] = ` "

Test admonition with interpolated title/body

My interpolated title <button style={{color: "red"}} onClick={() => alert("click")}>test

body interpolated content

" `; + +exports[`admonitions remark plugin replace custom keyword 1`] = ` +"

The blog feature enables you to deploy in no time a full-featured blog.

+

:::info Sample Title

+

Check the Blog Plugin API Reference documentation for an exhaustive list of options.

+

:::

+

Initial setup {#initial-setup}

+

To set up your site's blog, start by creating a blog directory.

+

Use the Fast Track to understand Docusaurus in 5 minutes ⏱!

Use docusaurus.new to test Docusaurus immediately in your browser!

+

++++tip

+

Admonition with different syntax

+

++++

" +`; diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts index 3794562bb1..eb950e627e 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/__tests__/index.test.ts @@ -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(); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts index d6393b8c45..779ef71873 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts @@ -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, + providedOptions: Partial, ): 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 diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 29f89dbb49..c0378ab47e 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -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: { diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 86a25cb4e8..39799fb78d 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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; + }; + + 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; } diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Caution.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Caution.tsx new file mode 100644 index 0000000000..ae47231d1f --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Caution.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Danger.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 0000000000..90d4a5515f --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Danger.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Info.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Info.tsx new file mode 100644 index 0000000000..7591ccaea1 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Info.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Note.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Note.tsx new file mode 100644 index 0000000000..a0b97a0dfa --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Note.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Tip.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 0000000000..df23616232 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Icon/Tip.tsx @@ -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 ( + + + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Layout/index.tsx new file mode 100644 index 0000000000..d72cc05461 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Layout/index.tsx @@ -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 & {children: ReactNode}) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({icon, title}: Pick) { + return ( +
+ {icon} + {title} +
+ ); +} + +function AdmonitionContent({children}: Pick) { + return
{children}
; +} + +export default function AdmonitionLayout(props: Props): JSX.Element { + const {type, icon, title, children, className} = props; + return ( + + + {children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Admonition/Layout/styles.module.css similarity index 100% rename from packages/docusaurus-theme-classic/src/theme/Admonition/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/Admonition/Layout/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Caution.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Caution.tsx new file mode 100644 index 0000000000..b929f63540 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Caution.tsx @@ -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: , + title: ( + + caution + + ), +}; + +export default function AdmonitionTypeCaution(props: Props): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Danger.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Danger.tsx new file mode 100644 index 0000000000..2115729b8b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Danger.tsx @@ -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: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Info.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Info.tsx new file mode 100644 index 0000000000..0ffbfaa185 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Info.tsx @@ -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: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Note.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Note.tsx new file mode 100644 index 0000000000..2831d7b975 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Note.tsx @@ -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: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Tip.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Tip.tsx new file mode 100644 index 0000000000..881944b3dd --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Type/Tip.tsx @@ -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: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/Types.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/Types.tsx new file mode 100644 index 0000000000..ac36bfe20b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/Types.tsx @@ -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) => , + important: (props) => , + success: (props) => , + // TODO bad legacy mapping, warning is usually yellow, not red... + warning: (props) => , +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx index aa64d04618..d64b461636 100644 --- a/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx @@ -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 ( - - - - ); -} - -function TipIcon() { - return ( - - - - ); -} - -function DangerIcon() { - return ( - - - - ); -} - -function InfoIcon() { - return ( - - - - ); -} - -function CautionIcon() { - return ( - - - - ); -} - -type AdmonitionConfig = { - iconComponent: React.ComponentType; - infimaClassName: string; - label: ReactNode; -}; - -// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -const AdmonitionConfigs: Record = { - note: { - infimaClassName: 'secondary', - iconComponent: NoteIcon, - label: ( - - note - - ), - }, - tip: { - infimaClassName: 'success', - iconComponent: TipIcon, - label: ( - - tip - - ), - }, - danger: { - infimaClassName: 'danger', - iconComponent: DangerIcon, - label: ( - - danger - - ), - }, - info: { - infimaClassName: 'info', - iconComponent: InfoIcon, - label: ( - - info - - ), - }, - caution: { - infimaClassName: 'warning', - iconComponent: CautionIcon, - label: ( - - caution - - ), - }, -}; - -// 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 { + 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 ?? ; - return ( -
-
- {icon} - {titleLabel} -
-
{children}
-
- ); +export default function Admonition(unprocessedProps: Props): JSX.Element { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; } diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 7f7427c414..7df15c4516 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -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'; diff --git a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts index 8f82f5213b..e5f859f428 100644 --- a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts +++ b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts @@ -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 diff --git a/packages/docusaurus-theme-common/src/utils/admonitionUtils.tsx b/packages/docusaurus-theme-common/src/utils/admonitionUtils.tsx new file mode 100644 index 0000000000..a8442064e8 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/admonitionUtils.tsx @@ -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, + }; +} diff --git a/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap index d49280b414..b2ef0a4a25 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap +++ b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap @@ -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"`; diff --git a/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts b/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts index ad66dede97..46515774bb 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts +++ b/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts @@ -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({ diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index f6bd54d6f3..cee204e492 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -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, diff --git a/packages/docusaurus/src/commands/swizzle/components.ts b/packages/docusaurus/src/commands/swizzle/components.ts index 9475ce33db..50ae374a67 100644 --- a/packages/docusaurus/src/commands/swizzle/components.ts +++ b/packages/docusaurus/src/commands/swizzle/components.ts @@ -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[] { diff --git a/website/_dogfooding/_pages tests/markdownPageTests.md b/website/_dogfooding/_pages tests/markdownPageTests.md index 323c741ee3..3097fa7792 100644 --- a/website/_dogfooding/_pages tests/markdownPageTests.md +++ b/website/_dogfooding/_pages tests/markdownPageTests.md @@ -239,3 +239,9 @@ Can be arbitrarily nested: Admonition body ::: + +:::important + +Admonition alias `:::important` should have Important title + +::: diff --git a/website/_dogfooding/testSwizzleThemeClassic.mjs b/website/_dogfooding/testSwizzleThemeClassic.mjs index 0f6af4da06..0bb8f2eb72 100644 --- a/website/_dogfooding/testSwizzleThemeClassic.mjs +++ b/website/_dogfooding/testSwizzleThemeClassic.mjs @@ -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 */ diff --git a/website/docs/guides/markdown-features/markdown-features-admonitions.mdx b/website/docs/guides/markdown-features/markdown-features-admonitions.mdx index 94f7cb7b61..f76ec875f7 100644 --- a/website/docs/guides/markdown-features/markdown-features-admonitions.mdx +++ b/website/docs/guides/markdown-features/markdown-features-admonitions.mdx @@ -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 ; + return ; } - return } {...props} />; + return } {...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 ( +
+
{props.title}
+
{props.children}
+
+ ); +} + +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! + +::: +``` + + + +:::my-custom-admonition Custom Admonition + +It works! + +::: + + diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 2377c40d67..b2a6c80aae 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -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}]], diff --git a/website/src/theme/Admonition/Types.tsx b/website/src/theme/Admonition/Types.tsx new file mode 100644 index 0000000000..35c5723607 --- /dev/null +++ b/website/src/theme/Admonition/Types.tsx @@ -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 ( +
+
{props.title}
+
{props.children}
+
+ ); +} + +const AdmonitionTypes = { + ...DefaultAdmonitionTypes, + + // Add all your custom admonition types here... + // you can also override the default ones + 'my-custom-admonition': MyCustomAdmonition, +}; + +export default AdmonitionTypes;