Skip to content

Commit

Permalink
Add admonition support to descriptions/summaries (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
sserrata authored Nov 22, 2024
1 parent 6ad497b commit b143240
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 233 deletions.
57 changes: 56 additions & 1 deletion demo/examples/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,62 @@ paths:
operationId: addPet
responses:
"200":
description: All good
description: |
All good, here's some MDX:
:::note
Some content but no markdown is supported :(
:::
:::tip
A TIP with no leading or trailing spaces between delimiters.
:::
:::info
Some **content** with _Markdown_ `syntax`. Check [this `api`](#).
| Month | Savings |
| -------- | ------- |
| January | $250 |
| February | $80 |
| March | $420 |
Hmm.....
:::
:::warning
Some **content** with _Markdown_ `syntax`. Check [this `api`](#) which is not supported :( yet
:::
:::danger
Some plain text
Some more plain text
And more
:::
A **code snippet**!
```python
print("hello")
```
_And_ a table!
| Month | Savings |
| -------- | ------- |
| January | $250 |
| February | $80 |
| March | $420 |
content:
application/json:
schema:
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-theme-openapi-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"remark-gfm": "3.0.1",
"sass": "^1.80.4",
"sass-loader": "^16.0.2",
"unist-util-visit": "^5.0.0",
"webpack": "^5.61.0",
"xml-formatter": "^2.6.1"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import FormSelect from "@theme/ApiExplorer/FormSelect";
import FormTextInput from "@theme/ApiExplorer/FormTextInput";
import LiveApp from "@theme/ApiExplorer/LiveEditor";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import Markdown from "@theme/Markdown";
import SchemaTabs from "@theme/SchemaTabs";
import TabItem from "@theme/TabItem";
import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
Expand Down Expand Up @@ -303,7 +304,7 @@ function Body({
</TabItem>
{/* @ts-ignore */}
<TabItem label="Example" value="example">
{example.summary && <div>{example.summary}</div>}
{example.summary && <Markdown>{example.summary}</Markdown>}
{exampleBody && (
<LiveApp
action={dispatch}
Expand Down Expand Up @@ -341,7 +342,7 @@ function Body({
value={example.label}
key={example.label}
>
{example.summary && <div>{example.summary}</div>}
{example.summary && <Markdown>{example.summary}</Markdown>}
{example.body && (
<LiveApp action={dispatch} language={language}>
{example.body}
Expand Down
178 changes: 160 additions & 18 deletions packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,172 @@

import React from "react";

import Admonition from "@theme/Admonition";
import CodeBlock from "@theme/CodeBlock";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";

function remarkAdmonition() {
return (tree) => {
const openingTagRegex = /^:::(\w+)(?:\[(.*?)\])?\s*$/;
const closingTagRegex = /^:::\s*$/;
const textOnlyAdmonition = /^:::(\w+)(?:\[(.*?)\])?\s*([\s\S]*?)\s*:::$/;

const nodes = [];
let bufferedChildren = [];

let insideAdmonition = false;
let type = null;
let title = null;

tree.children.forEach((node) => {
if (
node.type === "paragraph" &&
node.children.length === 1 &&
node.children[0].type === "text"
) {
const text = node.children[0].value.trim();
const openingMatch = text.match(openingTagRegex);
const closingMatch = text.match(closingTagRegex);
const textOnlyAdmonitionMatch = text.match(textOnlyAdmonition);

if (textOnlyAdmonitionMatch) {
const type = textOnlyAdmonitionMatch[1];
const title = textOnlyAdmonitionMatch[2]
? textOnlyAdmonitionMatch[2]?.trim()
: undefined;
const content = textOnlyAdmonitionMatch[3];

const admonitionNode = {
type: "admonition",
data: {
hName: "Admonition", // Tells ReactMarkdown to replace the node with Admonition component
hProperties: {
type, // Passed as a prop to the Admonition component
title,
},
},
children: [
{
type: "text",
value: content?.trim(), // Trim leading/trailing whitespace
},
],
};
nodes.push(admonitionNode);
return;
}

if (openingMatch) {
type = openingMatch[1];
title = openingMatch[2] || type;
insideAdmonition = true;
return;
}

if (closingMatch && insideAdmonition) {
nodes.push({
type: "admonition",
data: {
hName: "Admonition",
hProperties: { type: type, title: title },
},
children: bufferedChildren,
});
bufferedChildren = [];
insideAdmonition = false;
type = null;
title = null;
return;
}
}

if (insideAdmonition) {
bufferedChildren.push(node);
} else {
nodes.push(node);
}
});

if (bufferedChildren.length > 0 && type) {
nodes.push({
type: "admonition",
data: {
hName: "Admonition",
hProperties: { type: type, title: title },
},
children: bufferedChildren,
});
}
tree.children = nodes;
};
}

function convertAstToHtmlStr(ast) {
if (!ast || !Array.isArray(ast)) {
return "";
}

const convertNode = (node) => {
switch (node.type) {
case "text":
return node.value;
case "element":
const { tagName, properties, children } = node;

// Convert attributes to a string
const attrs = properties
? Object.entries(properties)
.map(([key, value]) => `${key}="${value}"`)
.join(" ")
: "";

// Convert children to HTML
const childrenHtml = children ? children.map(convertNode).join("") : "";

return `<${tagName} ${attrs}>${childrenHtml}</${tagName}>`;
default:
return "";
}
};

return ast.map(convertNode).join("");
}

function Markdown({ children }) {
return (
<div>
<ReactMarkdown
children={children}
rehypePlugins={[rehypeRaw]}
components={{
pre: "div",
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
if (inline) return <code>{children}</code>;
return !inline && match ? (
<CodeBlock className={className}>{children}</CodeBlock>
) : (
<CodeBlock>{children}</CodeBlock>
);
},
}}
/>
</div>
<ReactMarkdown
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm, remarkAdmonition]}
components={{
pre: (props) => <div {...props} />,
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
return match ? (
<CodeBlock className={className} language={match[1]} {...props}>
{children}
</CodeBlock>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
admonition: ({ node, ...props }) => {
const type = node.data?.hProperties?.type || "note";
const title = node.data?.hProperties?.title || type;
const content = convertAstToHtmlStr(node.children);
return (
<Admonition type={type} title={title} {...props}>
<div dangerouslySetInnerHTML={{ __html: content }} />
</Admonition>
);
},
}}
>
{children}
</ReactMarkdown>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@

import React from "react";

import CodeBlock from "@theme/CodeBlock";
import Markdown from "@theme/Markdown";
import SchemaTabs from "@theme/SchemaTabs";
import TabItem from "@theme/TabItem";
/* eslint-disable import/no-extraneous-dependencies*/
import clsx from "clsx";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";

import { createDescription } from "../../markdown/createDescription";
import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
import { guard, toString } from "../../markdown/utils";

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

const renderSchema = guard(getQualifierMessage(schema), (message) => (
<div>
<ReactMarkdown
children={createDescription(message)}
rehypePlugins={[rehypeRaw]}
/>
</div>
const renderQualifier = guard(getQualifierMessage(schema), (qualifier) => (
<Markdown>{qualifier}</Markdown>
));

const renderDescription = guard(description, (description) => (
<>
<ReactMarkdown
children={createDescription(description)}
components={{
pre: "div",
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
if (inline) return <code>{children}</code>;
return !inline && match ? (
<CodeBlock className={className}>{children}</CodeBlock>
) : (
<CodeBlock>{children}</CodeBlock>
);
},
}}
rehypePlugins={[rehypeRaw]}
/>
</>
<Markdown>{description}</Markdown>
));

const renderEnumDescriptions = guard(
getEnumDescriptionMarkdown(enumDescriptions),
(value) => {
return (
<div style={{ marginTop: ".5rem" }}>
<ReactMarkdown
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
children={value}
/>
<Markdown>{value}</Markdown>
</div>
);
}
Expand Down Expand Up @@ -217,7 +187,7 @@ function ParamsItem({ param, ...rest }: Props) {
{renderSchemaRequired}
{renderDeprecated}
</span>
{renderSchema}
{renderQualifier}
{renderDescription}
{renderEnumDescriptions}
{renderDefaultValue()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import React, { Suspense } from "react";

import BrowserOnly from "@docusaurus/BrowserOnly";
import Details from "@theme/Details";
import Markdown from "@theme/Markdown";
import MimeTabs from "@theme/MimeTabs"; // Assume these components exist
import SchemaNode from "@theme/Schema";
import SkeletonLoader from "@theme/SkeletonLoader";
import TabItem from "@theme/TabItem";
import { createDescription } from "docusaurus-plugin-openapi-docs/lib/markdown/createDescription";
import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types";

interface Props {
Expand Down Expand Up @@ -78,7 +78,7 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
{body.description && (
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
{createDescription(body.description)}
<Markdown>{body.description}</Markdown>
</div>
)}
</div>
Expand Down Expand Up @@ -131,7 +131,7 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
{body.description && (
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
{createDescription(body.description)}
<Markdown>{body.description}</Markdown>
</div>
)}
</div>
Expand Down
Loading

0 comments on commit b143240

Please sign in to comment.