Skip to content

Commit b143240

Browse files
authored
Add admonition support to descriptions/summaries (#1016)
1 parent 6ad497b commit b143240

File tree

12 files changed

+324
-233
lines changed

12 files changed

+324
-233
lines changed

demo/examples/petstore.yaml

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,62 @@ paths:
116116
operationId: addPet
117117
responses:
118118
"200":
119-
description: All good
119+
description: |
120+
All good, here's some MDX:
121+
122+
:::note
123+
124+
Some content but no markdown is supported :(
125+
126+
:::
127+
128+
:::tip
129+
A TIP with no leading or trailing spaces between delimiters.
130+
:::
131+
132+
:::info
133+
134+
Some **content** with _Markdown_ `syntax`. Check [this `api`](#).
135+
136+
| Month | Savings |
137+
| -------- | ------- |
138+
| January | $250 |
139+
| February | $80 |
140+
| March | $420 |
141+
142+
Hmm.....
143+
144+
:::
145+
146+
:::warning
147+
148+
Some **content** with _Markdown_ `syntax`. Check [this `api`](#) which is not supported :( yet
149+
150+
:::
151+
152+
:::danger
153+
154+
Some plain text
155+
156+
Some more plain text
157+
158+
And more
159+
160+
:::
161+
162+
A **code snippet**!
163+
164+
```python
165+
print("hello")
166+
```
167+
168+
_And_ a table!
169+
170+
| Month | Savings |
171+
| -------- | ------- |
172+
| January | $250 |
173+
| February | $80 |
174+
| March | $420 |
120175
content:
121176
application/json:
122177
schema:

packages/docusaurus-theme-openapi-docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"remark-gfm": "3.0.1",
6262
"sass": "^1.80.4",
6363
"sass-loader": "^16.0.2",
64+
"unist-util-visit": "^5.0.0",
6465
"webpack": "^5.61.0",
6566
"xml-formatter": "^2.6.1"
6667
},

packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import FormSelect from "@theme/ApiExplorer/FormSelect";
1414
import FormTextInput from "@theme/ApiExplorer/FormTextInput";
1515
import LiveApp from "@theme/ApiExplorer/LiveEditor";
1616
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
17+
import Markdown from "@theme/Markdown";
1718
import SchemaTabs from "@theme/SchemaTabs";
1819
import TabItem from "@theme/TabItem";
1920
import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
@@ -303,7 +304,7 @@ function Body({
303304
</TabItem>
304305
{/* @ts-ignore */}
305306
<TabItem label="Example" value="example">
306-
{example.summary && <div>{example.summary}</div>}
307+
{example.summary && <Markdown>{example.summary}</Markdown>}
307308
{exampleBody && (
308309
<LiveApp
309310
action={dispatch}
@@ -341,7 +342,7 @@ function Body({
341342
value={example.label}
342343
key={example.label}
343344
>
344-
{example.summary && <div>{example.summary}</div>}
345+
{example.summary && <Markdown>{example.summary}</Markdown>}
345346
{example.body && (
346347
<LiveApp action={dispatch} language={language}>
347348
{example.body}

packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js

Lines changed: 160 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,172 @@
77

88
import React from "react";
99

10+
import Admonition from "@theme/Admonition";
1011
import CodeBlock from "@theme/CodeBlock";
1112
import ReactMarkdown from "react-markdown";
1213
import rehypeRaw from "rehype-raw";
14+
import remarkGfm from "remark-gfm";
15+
16+
function remarkAdmonition() {
17+
return (tree) => {
18+
const openingTagRegex = /^:::(\w+)(?:\[(.*?)\])?\s*$/;
19+
const closingTagRegex = /^:::\s*$/;
20+
const textOnlyAdmonition = /^:::(\w+)(?:\[(.*?)\])?\s*([\s\S]*?)\s*:::$/;
21+
22+
const nodes = [];
23+
let bufferedChildren = [];
24+
25+
let insideAdmonition = false;
26+
let type = null;
27+
let title = null;
28+
29+
tree.children.forEach((node) => {
30+
if (
31+
node.type === "paragraph" &&
32+
node.children.length === 1 &&
33+
node.children[0].type === "text"
34+
) {
35+
const text = node.children[0].value.trim();
36+
const openingMatch = text.match(openingTagRegex);
37+
const closingMatch = text.match(closingTagRegex);
38+
const textOnlyAdmonitionMatch = text.match(textOnlyAdmonition);
39+
40+
if (textOnlyAdmonitionMatch) {
41+
const type = textOnlyAdmonitionMatch[1];
42+
const title = textOnlyAdmonitionMatch[2]
43+
? textOnlyAdmonitionMatch[2]?.trim()
44+
: undefined;
45+
const content = textOnlyAdmonitionMatch[3];
46+
47+
const admonitionNode = {
48+
type: "admonition",
49+
data: {
50+
hName: "Admonition", // Tells ReactMarkdown to replace the node with Admonition component
51+
hProperties: {
52+
type, // Passed as a prop to the Admonition component
53+
title,
54+
},
55+
},
56+
children: [
57+
{
58+
type: "text",
59+
value: content?.trim(), // Trim leading/trailing whitespace
60+
},
61+
],
62+
};
63+
nodes.push(admonitionNode);
64+
return;
65+
}
66+
67+
if (openingMatch) {
68+
type = openingMatch[1];
69+
title = openingMatch[2] || type;
70+
insideAdmonition = true;
71+
return;
72+
}
73+
74+
if (closingMatch && insideAdmonition) {
75+
nodes.push({
76+
type: "admonition",
77+
data: {
78+
hName: "Admonition",
79+
hProperties: { type: type, title: title },
80+
},
81+
children: bufferedChildren,
82+
});
83+
bufferedChildren = [];
84+
insideAdmonition = false;
85+
type = null;
86+
title = null;
87+
return;
88+
}
89+
}
90+
91+
if (insideAdmonition) {
92+
bufferedChildren.push(node);
93+
} else {
94+
nodes.push(node);
95+
}
96+
});
97+
98+
if (bufferedChildren.length > 0 && type) {
99+
nodes.push({
100+
type: "admonition",
101+
data: {
102+
hName: "Admonition",
103+
hProperties: { type: type, title: title },
104+
},
105+
children: bufferedChildren,
106+
});
107+
}
108+
tree.children = nodes;
109+
};
110+
}
111+
112+
function convertAstToHtmlStr(ast) {
113+
if (!ast || !Array.isArray(ast)) {
114+
return "";
115+
}
116+
117+
const convertNode = (node) => {
118+
switch (node.type) {
119+
case "text":
120+
return node.value;
121+
case "element":
122+
const { tagName, properties, children } = node;
123+
124+
// Convert attributes to a string
125+
const attrs = properties
126+
? Object.entries(properties)
127+
.map(([key, value]) => `${key}="${value}"`)
128+
.join(" ")
129+
: "";
130+
131+
// Convert children to HTML
132+
const childrenHtml = children ? children.map(convertNode).join("") : "";
133+
134+
return `<${tagName} ${attrs}>${childrenHtml}</${tagName}>`;
135+
default:
136+
return "";
137+
}
138+
};
139+
140+
return ast.map(convertNode).join("");
141+
}
13142

14143
function Markdown({ children }) {
15144
return (
16-
<div>
17-
<ReactMarkdown
18-
children={children}
19-
rehypePlugins={[rehypeRaw]}
20-
components={{
21-
pre: "div",
22-
code({ node, inline, className, children, ...props }) {
23-
const match = /language-(\w+)/.exec(className || "");
24-
if (inline) return <code>{children}</code>;
25-
return !inline && match ? (
26-
<CodeBlock className={className}>{children}</CodeBlock>
27-
) : (
28-
<CodeBlock>{children}</CodeBlock>
29-
);
30-
},
31-
}}
32-
/>
33-
</div>
145+
<ReactMarkdown
146+
rehypePlugins={[rehypeRaw]}
147+
remarkPlugins={[remarkGfm, remarkAdmonition]}
148+
components={{
149+
pre: (props) => <div {...props} />,
150+
code({ node, inline, className, children, ...props }) {
151+
const match = /language-(\w+)/.exec(className || "");
152+
return match ? (
153+
<CodeBlock className={className} language={match[1]} {...props}>
154+
{children}
155+
</CodeBlock>
156+
) : (
157+
<code className={className} {...props}>
158+
{children}
159+
</code>
160+
);
161+
},
162+
admonition: ({ node, ...props }) => {
163+
const type = node.data?.hProperties?.type || "note";
164+
const title = node.data?.hProperties?.title || type;
165+
const content = convertAstToHtmlStr(node.children);
166+
return (
167+
<Admonition type={type} title={title} {...props}>
168+
<div dangerouslySetInnerHTML={{ __html: content }} />
169+
</Admonition>
170+
);
171+
},
172+
}}
173+
>
174+
{children}
175+
</ReactMarkdown>
34176
);
35177
}
36178

packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@
77

88
import React from "react";
99

10-
import CodeBlock from "@theme/CodeBlock";
10+
import Markdown from "@theme/Markdown";
1111
import SchemaTabs from "@theme/SchemaTabs";
1212
import TabItem from "@theme/TabItem";
1313
/* eslint-disable import/no-extraneous-dependencies*/
1414
import clsx from "clsx";
15-
import ReactMarkdown from "react-markdown";
16-
import rehypeRaw from "rehype-raw";
17-
import remarkGfm from "remark-gfm";
1815

19-
import { createDescription } from "../../markdown/createDescription";
2016
import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
2117
import { guard, toString } from "../../markdown/utils";
2218

@@ -97,46 +93,20 @@ function ParamsItem({ param, ...rest }: Props) {
9793
<span className="openapi-schema__deprecated">deprecated</span>
9894
));
9995

100-
const renderSchema = guard(getQualifierMessage(schema), (message) => (
101-
<div>
102-
<ReactMarkdown
103-
children={createDescription(message)}
104-
rehypePlugins={[rehypeRaw]}
105-
/>
106-
</div>
96+
const renderQualifier = guard(getQualifierMessage(schema), (qualifier) => (
97+
<Markdown>{qualifier}</Markdown>
10798
));
10899

109100
const renderDescription = guard(description, (description) => (
110-
<>
111-
<ReactMarkdown
112-
children={createDescription(description)}
113-
components={{
114-
pre: "div",
115-
code({ node, inline, className, children, ...props }) {
116-
const match = /language-(\w+)/.exec(className || "");
117-
if (inline) return <code>{children}</code>;
118-
return !inline && match ? (
119-
<CodeBlock className={className}>{children}</CodeBlock>
120-
) : (
121-
<CodeBlock>{children}</CodeBlock>
122-
);
123-
},
124-
}}
125-
rehypePlugins={[rehypeRaw]}
126-
/>
127-
</>
101+
<Markdown>{description}</Markdown>
128102
));
129103

130104
const renderEnumDescriptions = guard(
131105
getEnumDescriptionMarkdown(enumDescriptions),
132106
(value) => {
133107
return (
134108
<div style={{ marginTop: ".5rem" }}>
135-
<ReactMarkdown
136-
rehypePlugins={[rehypeRaw]}
137-
remarkPlugins={[remarkGfm]}
138-
children={value}
139-
/>
109+
<Markdown>{value}</Markdown>
140110
</div>
141111
);
142112
}
@@ -217,7 +187,7 @@ function ParamsItem({ param, ...rest }: Props) {
217187
{renderSchemaRequired}
218188
{renderDeprecated}
219189
</span>
220-
{renderSchema}
190+
{renderQualifier}
221191
{renderDescription}
222192
{renderEnumDescriptions}
223193
{renderDefaultValue()}

packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import React, { Suspense } from "react";
99

1010
import BrowserOnly from "@docusaurus/BrowserOnly";
1111
import Details from "@theme/Details";
12+
import Markdown from "@theme/Markdown";
1213
import MimeTabs from "@theme/MimeTabs"; // Assume these components exist
1314
import SchemaNode from "@theme/Schema";
1415
import SkeletonLoader from "@theme/SkeletonLoader";
1516
import TabItem from "@theme/TabItem";
16-
import { createDescription } from "docusaurus-plugin-openapi-docs/lib/markdown/createDescription";
1717
import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types";
1818

1919
interface Props {
@@ -78,7 +78,7 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
7878
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
7979
{body.description && (
8080
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
81-
{createDescription(body.description)}
81+
<Markdown>{body.description}</Markdown>
8282
</div>
8383
)}
8484
</div>
@@ -131,7 +131,7 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
131131
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
132132
{body.description && (
133133
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
134-
{createDescription(body.description)}
134+
<Markdown>{body.description}</Markdown>
135135
</div>
136136
)}
137137
</div>

0 commit comments

Comments
 (0)