Skip to content

Add admonition support to descriptions/summaries #1016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b0002c0
experimental admonition support
sserrata Nov 7, 2024
818403e
base admonition theme color on type
sserrata Nov 7, 2024
9a5f3c0
Use @theme/Admonition component
sserrata Nov 7, 2024
eb3f771
concatenate and process all node children and add support for title prop
sserrata Nov 7, 2024
4718e2a
update example
sserrata Nov 7, 2024
f4dcd06
use docusaurus examples
sserrata Nov 7, 2024
99e451a
fall back to type if no title found
sserrata Nov 7, 2024
65c8d7c
Merge branch 'main' into remark-admonitions
sserrata Nov 18, 2024
a9acde9
update demo
sserrata Nov 18, 2024
7ebeb93
allow buffering children and rendering ast to preserve markdown
sserrata Nov 18, 2024
221f37f
cleanup variables to improve readability
sserrata Nov 18, 2024
9ce4b03
add more complex examples
sserrata Nov 19, 2024
91d0dab
ensure all layers of nested children are captured
sserrata Nov 19, 2024
b1e0a4d
replace react markdown with markdown component
sserrata Nov 20, 2024
7c25186
replace createDescription with Markdown
sserrata Nov 20, 2024
e178a5e
cleanup unused component
sserrata Nov 21, 2024
86b12bf
cleanup props
sserrata Nov 21, 2024
c341b78
cleanup unused import
sserrata Nov 21, 2024
ad741ea
move ResponseHeaders to standalone component
sserrata Nov 21, 2024
de31539
implement new markdown component as wrapper
sserrata Nov 21, 2024
d085180
implement markdown support in schema item
sserrata Nov 21, 2024
79f56e7
support markdown in example summaries
sserrata Nov 21, 2024
8465687
wrap body summaries in markdown
sserrata Nov 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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