diff --git a/demo/examples/petstore.yaml b/demo/examples/petstore.yaml index 1e308500c..1de97c514 100644 --- a/demo/examples/petstore.yaml +++ b/demo/examples/petstore.yaml @@ -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: diff --git a/packages/docusaurus-theme-openapi-docs/package.json b/packages/docusaurus-theme-openapi-docs/package.json index fa8558409..2be2c5b5b 100644 --- a/packages/docusaurus-theme-openapi-docs/package.json +++ b/packages/docusaurus-theme-openapi-docs/package.json @@ -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" }, diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx index da3951e38..d595fbe24 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx @@ -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"; @@ -303,7 +304,7 @@ function Body({ {/* @ts-ignore */} - {example.summary &&
{example.summary}
} + {example.summary && {example.summary}} {exampleBody && ( - {example.summary &&
{example.summary}
} + {example.summary && {example.summary}} {example.body && ( {example.body} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js index 13ce11a65..8af1f3e7b 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/index.js @@ -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}`; + default: + return ""; + } + }; + + return ast.map(convertNode).join(""); +} function Markdown({ children }) { return ( -
- {children}; - return !inline && match ? ( - {children} - ) : ( - {children} - ); - }, - }} - /> -
+
, + code({ node, inline, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || ""); + return match ? ( + + {children} + + ) : ( + + {children} + + ); + }, + admonition: ({ node, ...props }) => { + const type = node.data?.hProperties?.type || "note"; + const title = node.data?.hProperties?.title || type; + const content = convertAstToHtmlStr(node.children); + return ( + +
+ + ); + }, + }} + > + {children} + ); } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx index 4a9bf9f08..6d9512576 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx @@ -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"; @@ -97,34 +93,12 @@ function ParamsItem({ param, ...rest }: Props) { deprecated )); - const renderSchema = guard(getQualifierMessage(schema), (message) => ( -
- -
+ const renderQualifier = guard(getQualifierMessage(schema), (qualifier) => ( + {qualifier} )); const renderDescription = guard(description, (description) => ( - <> - {children}; - return !inline && match ? ( - {children} - ) : ( - {children} - ); - }, - }} - rehypePlugins={[rehypeRaw]} - /> - + {description} )); const renderEnumDescriptions = guard( @@ -132,11 +106,7 @@ function ParamsItem({ param, ...rest }: Props) { (value) => { return (
- + {value}
); } @@ -217,7 +187,7 @@ function ParamsItem({ param, ...rest }: Props) { {renderSchemaRequired} {renderDeprecated} - {renderSchema} + {renderQualifier} {renderDescription} {renderEnumDescriptions} {renderDefaultValue()} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index 12d13816c..33def2697 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -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 { @@ -78,7 +78,7 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => {
{body.description && (
- {createDescription(body.description)} + {body.description}
)}
@@ -131,7 +131,7 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => {
{body.description && (
- {createDescription(body.description)} + {body.description}
)}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx index c906d1de0..67eb5361e 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx @@ -7,14 +7,12 @@ import React from "react"; -import ParamsItem from "@theme/ParamsItem"; +import Markdown from "@theme/Markdown"; import ResponseSamples from "@theme/ResponseSamples"; import TabItem from "@theme/TabItem"; -import { createDescription } from "docusaurus-plugin-openapi-docs/lib/markdown/createDescription"; import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; import format from "xml-formatter"; -// Utility function export function json2xml(o: Record, tab: string): string { const toXml = (v: any, name: string, ind: string): string => { let xml = ""; @@ -52,120 +50,14 @@ export function json2xml(o: Record, tab: string): string { return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); } -interface ParameterProps { - in: string; - name: string; - schema?: { - type?: string; - items?: Record; - }; - enumDescriptions?: [string, string][]; -} - -interface ResponseHeaderProps { - description?: string; - example?: string; - schema?: { - type?: string; - }; -} - -interface ResponseExampleProps { - value: any; - summary?: string; -} - -interface Props { - parameters?: ParameterProps[]; - type: string; - responseHeaders?: Record; - responseExamples?: Record; - responseExample?: any; - schema?: any; - mimeType: string; -} - -// React components -export const ParamsDetails: React.FC = ({ parameters, type }) => { - const params = parameters?.filter((param) => param?.in === type); - - if (!params || params.length === 0) { - return null; - } - - return ( -
- -

- {`${type.charAt(0).toUpperCase() + type.slice(1)} Parameters`} -

-
-
-
    - {params.map((param, index) => ( - - ))} -
-
-
- ); -}; - -export const ResponseHeaders: React.FC<{ - responseHeaders?: Record; -}> = ({ responseHeaders }) => { - if (!responseHeaders) { - return null; - } - - return ( -
    - {Object.entries(responseHeaders).map(([headerName, headerObj]) => { - const { description, example, schema } = headerObj; - const type = schema?.type ?? "any"; - - return ( -
  • -
    - - {headerName} - {type && {type}} - -
    - {description && ( -
    - {example && `Example: ${example}`} - {createDescription(description)} -
    - )} -
    -
    -
  • - ); - })} -
- ); -}; - -export const ResponseExamples: React.FC<{ +interface ResponseExamplesProps { responseExamples: any; mimeType: string; -}> = ({ responseExamples, mimeType }): any => { +} +export const ResponseExamples: React.FC = ({ + responseExamples, + mimeType, +}): any => { let language = "shell"; if (mimeType.endsWith("json")) language = "json"; if (mimeType.endsWith("xml")) language = "xml"; @@ -182,9 +74,9 @@ export const ResponseExamples: React.FC<{ // @ts-ignore {exampleValue.summary && ( -
+ {exampleValue.summary} -
+ )} = ({ responseExample, mimeType }) => { +} + +export const ResponseExample: React.FC = ({ + responseExample, + mimeType, +}) => { let language = "shell"; if (mimeType.endsWith("json")) { language = "json"; @@ -219,16 +116,21 @@ export const ResponseExample: React.FC<{ // @ts-ignore {responseExample.summary && ( -
+ {responseExample.summary} -
+ )}
); }; -export const ExampleFromSchema: React.FC<{ schema: any; mimeType: string }> = ({ +interface ExampleFromSchemaProps { + schema: any; + mimeType: string; +} + +export const ExampleFromSchema: React.FC = ({ schema, mimeType, }) => { diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseHeaders/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseHeaders/index.tsx new file mode 100644 index 000000000..d8f2158dd --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseHeaders/index.tsx @@ -0,0 +1,49 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * 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 SchemaItem from "@theme/SchemaItem"; + +import { getQualifierMessage, getSchemaName } from "../../markdown/schema"; + +interface ResponseHeadersProps { + description?: string; + example?: string; + schema?: { + type?: string; + format?: string; + }; +} + +export const ResponseHeaders: React.FC<{ + responseHeaders?: Record; +}> = ({ responseHeaders }) => { + if (!responseHeaders) { + return null; + } + + return ( +
    + {Object.entries(responseHeaders).map(([name, schema]) => { + return ( + + ); + })} +
+ ); +}; + +export default ResponseHeaders; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx index 74f18e7e4..cdb552df9 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -9,6 +9,7 @@ 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 { ExampleFromSchema, @@ -19,7 +20,6 @@ import SchemaNode from "@theme/Schema"; import SchemaTabs from "@theme/SchemaTabs"; 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 { @@ -99,7 +99,7 @@ const ResponseSchemaComponent: React.FC = ({
- {createDescription(body.description)} + {body.description}
)}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx index 548ab682b..9db3b103c 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx @@ -10,21 +10,19 @@ import React from "react"; import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets"; import Details from "@theme/Details"; import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import Markdown from "@theme/Markdown"; import SchemaItem from "@theme/SchemaItem"; import SchemaTabs from "@theme/SchemaTabs"; import TabItem from "@theme/TabItem"; // eslint-disable-next-line import/no-extraneous-dependencies import { merge } from "allof-merge"; import clsx from "clsx"; -import { createDescription } from "docusaurus-plugin-openapi-docs/lib/markdown/createDescription"; import { getQualifierMessage, getSchemaName, } from "docusaurus-plugin-openapi-docs/lib/markdown/schema"; import { SchemaObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; import isEmpty from "lodash/isEmpty"; -import ReactMarkdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; // eslint-disable-next-line import/no-extraneous-dependencies // const jsonSchemaMergeAllOf = require("json-schema-merge-allof"); @@ -44,13 +42,10 @@ interface MarkdownProps { } // Renders string as markdown, useful for descriptions and qualifiers -const Markdown: React.FC = ({ text }) => { +const MarkdownWrapper: React.FC = ({ text }) => { return (
- + {text}
); }; @@ -262,9 +257,11 @@ const PropertyDiscriminator: React.FC = ({ )}
- {schema.description && } + {schema.description && ( + + )} {getQualifierMessage(discriminator) && ( - + )}
@@ -480,9 +477,9 @@ const SchemaNodeDetails: React.FC = ({ } >
- {schema.description && } + {schema.description && } {getQualifierMessage(schema) && ( - + )}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx index 48ab32d80..65984c318 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx @@ -7,13 +7,9 @@ import React, { ReactNode } from "react"; -import CodeBlock from "@theme/CodeBlock"; +import Markdown from "@theme/Markdown"; 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 { guard } from "../../markdown/utils"; export interface Props { @@ -97,44 +93,22 @@ export default function SchemaItem(props: Props) { (value) => { return (
- + {value}
); } ); const renderSchemaDescription = guard(schemaDescription, (description) => ( -
- {children}; - return !inline && match ? ( - {children} - ) : ( - {children} - ); - }, - }} - rehypePlugins={[rehypeRaw]} - /> -
+ <> + {description} + )); const renderQualifierMessage = guard(qualifierMessage, (message) => ( -
- -
+ <> + {message} + )); function renderDefaultValue() { diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/StatusCodes/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/StatusCodes/index.tsx index 422e396a1..e838e9897 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/StatusCodes/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/StatusCodes/index.tsx @@ -9,10 +9,10 @@ import React from "react"; import ApiTabs from "@theme/ApiTabs"; import Details from "@theme/Details"; -import { ResponseHeaders } from "@theme/ResponseExamples"; +import Markdown from "@theme/Markdown"; +import ResponseHeaders from "@theme/ResponseHeaders"; import ResponseSchema from "@theme/ResponseSchema"; import TabItem from "@theme/TabItem"; -import { createDescription } from "docusaurus-plugin-openapi-docs/lib/markdown/createDescription"; import { ApiItem } from "docusaurus-plugin-openapi-docs/lib/types"; interface Props { @@ -39,7 +39,7 @@ const StatusCodes: React.FC = ({ label, id, responses }: any) => {
{response.description && (
- {createDescription(response.description)} + {response.description}
)}