diff --git a/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts b/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts index dc7853700..11ea09acc 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts @@ -6,99 +6,3 @@ * ========================================================================== */ /// - -declare module "@docusaurus/theme-common/internal" { - import { CSSProperties, ReactNode, RefObject } from "react"; - - import type { PropDocContent } from "@docusaurus/plugin-content-docs"; - import { MagicCommentConfig } from "@docusaurus/theme-common/lib/utils/codeBlockUtils"; - import { - TabsProps as ITabsProps, - TabValue, - } from "@docusaurus/theme-common/lib/utils/tabsUtils"; - import { Props as ICodeBlockProps } from "@theme/CodeBlock"; - import { Props as ICopyButtonProps } from "@theme/CodeBlock/CopyButton"; - import { Props as ILineProps } from "@theme/CodeBlock/Line"; - import { PrismTheme } from "prism-react-renderer"; - - export interface TabItemProps { - readonly children: ReactNode; - readonly value: string; - readonly default?: boolean; - readonly label?: string; - readonly className?: string; - readonly attributes?: { [key: string]: unknown }; - } - - export interface TabProps extends ITabsProps { - length?: number; - } - - export interface CopyButtonProps extends ICopyButtonProps {} - export interface LineProps extends ILineProps {} - export interface CodeBlockProps extends ICodeBlockProps {} - - export function usePrismTheme(): PrismTheme; - - export function sanitizeTabsChildren(children: TabProps["children"]); - - export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties; - - export function parseCodeBlockTitle(metastring?: string): string; - - export function parseLanguage(className: string): string | undefined; - - export function containsLineNumbers(metastring?: string): boolean; - - export function useScrollPositionBlocker(): { - blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void; - }; - - export function DocProvider({ - children, - content, - }: { - children: ReactNode; - content: PropDocContent; - }); - - export function useTabsContextValue(props: TabProps): { - selectedValue: string; - selectValue: (value: string) => void; - tabValues: readonly TabValue[]; - lazy: boolean; - block: boolean; - }; - - export function useTabs(): { - selectedValue: string; - selectValue: (value: string) => void; - tabValues: readonly TabValue[]; - lazy: boolean; - block: boolean; - }; - - export function TabsProvider(props: { - children: ReactNode; - value: ReturnType; - }): ReactNode; - - export function parseLines( - content: string, - options: { - metastring: string | undefined; - language: string | undefined; - magicComments: MagicCommentConfig[]; - } - ): { - lineClassNames: { [lineIndex: number]: string[] }; - code: string; - }; - - export function useCodeWordWrap(): { - readonly codeBlockRef: RefObject; - readonly isEnabled: boolean; - readonly isCodeScrollable: boolean; - readonly toggle: () => void; - }; -} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx index 68f8dffb2..acf3f8691 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx @@ -8,9 +8,10 @@ import React, { ComponentProps } from "react"; import { ThemeClassNames, usePrismTheme } from "@docusaurus/theme-common"; -import { getPrismCssVariables } from "@docusaurus/theme-common/internal"; import clsx from "clsx"; +import { getPrismCssVariables } from "@theme/utils/codeBlockUtils"; + export default function CodeBlockContainer({ as: As, ...props diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx index 5c0c1791c..fff575b7c 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx @@ -8,13 +8,6 @@ import React from "react"; import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common"; -import { - parseCodeBlockTitle, - parseLanguage, - parseLines, - containsLineNumbers, - useCodeWordWrap, -} from "@docusaurus/theme-common/internal"; import Container from "@theme/ApiExplorer/ApiCodeBlock/Container"; import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton"; import ExpandButton from "@theme/ApiExplorer/ApiCodeBlock/ExpandButton"; @@ -24,6 +17,14 @@ import type { Props } from "@theme/CodeBlock/Content/String"; import clsx from "clsx"; import { Highlight, Language } from "prism-react-renderer"; +import { + containsLineNumbers, + parseCodeBlockTitle, + parseLanguage, + parseLines, +} from "@theme/utils/codeBlockUtils"; +import { useCodeWordWrap } from "@theme/utils/useCodeWordWrap"; + export default function CodeBlockString({ children, className: blockClassName = "", diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx index 94cff38db..e7cc594b6 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx @@ -7,7 +7,7 @@ import React from "react"; -import { LineProps } from "@docusaurus/theme-common/internal"; +import type { Props as LineProps } from "@theme/CodeBlock/Line"; import clsx from "clsx"; export default function CodeBlockLine({ diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx index b09c2dedd..cec02e9e8 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx @@ -7,8 +7,8 @@ import React, { isValidElement, ReactNode } from "react"; -import { CodeBlockProps } from "@docusaurus/theme-common/internal"; import useIsBrowser from "@docusaurus/useIsBrowser"; +import type { Props as CodeBlockProps } from "@theme/CodeBlock"; import ElementContent from "@theme/ApiExplorer/ApiCodeBlock/Content/Element"; import StringContent from "@theme/ApiExplorer/ApiCodeBlock/Content/String"; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx index 513361ced..1ce08df77 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx @@ -7,17 +7,17 @@ import React, { cloneElement, ReactElement, useEffect, useRef } from "react"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import clsx from "clsx"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; import { sanitizeTabsChildren, type TabItemProps, type TabProps, TabsProvider, - useScrollPositionBlocker, useTabsContextValue, -} from "@docusaurus/theme-common/internal"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import clsx from "clsx"; - +} from "@theme/utils/tabsUtils"; import { Language } from "../CodeSnippets/code-snippets-types"; export interface Props { diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx index 86fdd78c0..ff82d82f1 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx @@ -13,18 +13,19 @@ import React, { ReactElement, } from "react"; +import { translate } from "@docusaurus/Translate"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import Heading from "@theme/Heading"; +import clsx from "clsx"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; import { sanitizeTabsChildren, type TabItemProps, TabProps, TabsProvider, - useScrollPositionBlocker, useTabsContextValue, -} from "@docusaurus/theme-common/internal"; -import { translate } from "@docusaurus/Translate"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import Heading from "@theme/Heading"; -import clsx from "clsx"; +} from "@theme/utils/tabsUtils"; export interface TabListProps extends TabProps { label: string; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx index a4108e548..99da7a7f6 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx @@ -13,17 +13,18 @@ import React, { ReactElement, } from "react"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import clsx from "clsx"; +import flatten from "lodash/flatten"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; import { sanitizeTabsChildren, type TabItemProps, TabProps, TabsProvider, - useScrollPositionBlocker, useTabsContextValue, -} from "@docusaurus/theme-common/internal"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import clsx from "clsx"; -import flatten from "lodash/flatten"; +} from "@theme/utils/tabsUtils"; function TabList({ className, diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx index 74841b12b..0feb2bb84 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx @@ -13,14 +13,6 @@ import React, { ReactElement, } from "react"; -import { - sanitizeTabsChildren, - type TabItemProps, - TabProps, - TabsProvider, - useScrollPositionBlocker, - useTabsContextValue, -} from "@docusaurus/theme-common/internal"; import useIsBrowser from "@docusaurus/useIsBrowser"; import { setAccept } from "@theme/ApiExplorer/Accept/slice"; import { setContentType } from "@theme/ApiExplorer/ContentType/slice"; @@ -28,6 +20,15 @@ import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; import { RootState } from "@theme/ApiItem/store"; import clsx from "clsx"; +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; +import { + sanitizeTabsChildren, + type TabItemProps, + TabProps, + TabsProvider, + useTabsContextValue, +} from "@theme/utils/tabsUtils"; + export interface Props { schemaType: any; } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx index 0c149d668..731e4f1c1 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx @@ -13,16 +13,17 @@ import React, { ReactElement, } from "react"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import clsx from "clsx"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; import { sanitizeTabsChildren, type TabItemProps, TabProps, TabsProvider, - useScrollPositionBlocker, useTabsContextValue, -} from "@docusaurus/theme-common/internal"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import clsx from "clsx"; +} from "@theme/utils/tabsUtils"; function TabList({ className, diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx index 85de86472..838c785de 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx @@ -14,17 +14,18 @@ import React, { LegacyRef, } from "react"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import clsx from "clsx"; +import flatten from "lodash/flatten"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; import { sanitizeTabsChildren, type TabItemProps, TabProps, TabsProvider, - useScrollPositionBlocker, useTabsContextValue, -} from "@docusaurus/theme-common/internal"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import clsx from "clsx"; -import flatten from "lodash/flatten"; +} from "@theme/utils/tabsUtils"; export interface SchemaTabsProps extends TabProps { /** diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx new file mode 100644 index 000000000..eebb4ca1b --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx @@ -0,0 +1,61 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Swizzled from @docusaurus/theme-classic/src/theme/TabItem/index.tsx (MIT). + * Re-points useTabs to our vendored tabsUtils so that reads the same + * context our swizzled and OpenAPI tab variants (ApiTabs, MimeTabs, + * SchemaTabs, etc.) provide. See: + * https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * 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 { type TabItemProps, useTabs } from "@theme/utils/tabsUtils"; + +type Props = TabItemProps; +import styles from "./styles.module.css"; + +function TabItemPanel({ + children, + className, + hidden, +}: { + children: ReactNode; + className?: string; + hidden?: boolean; +}) { + return ( + + ); +} + +export default function TabItem({ + children, + className, + value, +}: Props): ReactNode { + const { selectedValue, lazy } = useTabs(); + const isSelected = value === selectedValue; + + if (!isSelected && lazy) { + return null; + } + + return ( + + ); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css new file mode 100644 index 000000000..f448b7fcd --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css @@ -0,0 +1,3 @@ +.tabItem > *:last-child { + margin-bottom: 0; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx new file mode 100644 index 000000000..0f0b44517 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx @@ -0,0 +1,164 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Swizzled from @docusaurus/theme-classic/src/theme/Tabs/index.tsx (MIT). + * Re-points the internal hooks (useTabs, useTabsContextValue, etc.) to our + * vendored tabsUtils so that the entire / pair runs through a + * single React context owned by this package. See: + * https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * 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 { ThemeClassNames } from "@docusaurus/theme-common"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import clsx from "clsx"; + +import { useScrollPositionBlocker } from "@theme/utils/scrollUtils"; +import { + sanitizeTabsChildren, + type TabsProps, + TabsProvider, + useTabs, + useTabsContextValue, +} from "@theme/utils/tabsUtils"; +import styles from "./styles.module.css"; + +type Props = TabsProps; + +function TabList({ className }: { className?: string }) { + const { selectedValue, selectValue, tabValues, block } = useTabs(); + + const tabRefs: (HTMLLIElement | null)[] = []; + const { blockElementScrollPositionUntilNextRender } = + useScrollPositionBlocker(); + + const handleTabChange = ( + event: + | React.FocusEvent + | React.MouseEvent + | React.KeyboardEvent + ) => { + const newTab = event.currentTarget; + const newTabIndex = tabRefs.indexOf(newTab); + const newTabValue = tabValues[newTabIndex]!.value; + + if (newTabValue !== selectedValue) { + blockElementScrollPositionUntilNextRender(newTab); + selectValue(newTabValue); + } + }; + + const handleKeydown = (event: React.KeyboardEvent) => { + let focusElement: HTMLLIElement | null = null; + + switch (event.key) { + case "Enter": { + handleTabChange(event); + break; + } + case "ArrowRight": { + const nextTab = tabRefs.indexOf(event.currentTarget) + 1; + focusElement = tabRefs[nextTab] ?? tabRefs[0]!; + break; + } + case "ArrowLeft": { + const prevTab = tabRefs.indexOf(event.currentTarget) - 1; + focusElement = tabRefs[prevTab] ?? tabRefs[tabRefs.length - 1]!; + break; + } + default: + break; + } + + focusElement?.focus(); + }; + + return ( +
    + {tabValues.map(({ value, label, attributes }) => ( +
  • { + tabRefs.push(ref); + }} + onKeyDown={handleKeydown} + onClick={handleTabChange} + {...attributes} + className={clsx( + "tabs__item", + styles.tabItem, + attributes?.className as string, + { + "tabs__item--active": selectedValue === value, + } + )} + > + {label ?? value} +
  • + ))} +
+ ); +} + +function TabContent({ children }: { children: ReactNode }) { + return
{children}
; +} + +function TabsContainer({ + className, + children, +}: { + className?: string; + children: ReactNode; +}): ReactNode { + return ( +
+ + {children} +
+ ); +} + +export default function Tabs(props: Props): ReactNode { + const isBrowser = useIsBrowser(); + const value = useTabsContextValue(props); + return ( + + + {sanitizeTabsChildren(props.children)} + + + ); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css new file mode 100644 index 000000000..0c79270e7 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css @@ -0,0 +1,7 @@ +.tabList { + margin-bottom: var(--ifm-leading); +} + +.tabItem { + margin-top: 0 !important; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts new file mode 100644 index 000000000..df830d5dc --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts @@ -0,0 +1,296 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored subset of @docusaurus/theme-common/src/utils/codeBlockUtils.tsx (MIT) + * to remove the dependency on @docusaurus/theme-common/internal. + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import type { CSSProperties } from "react"; + +import rangeParser from "parse-numeric-range"; +import type { PrismTheme, PrismThemeEntry } from "prism-react-renderer"; + +const codeBlockTitleRegex = /title=(?["'])(?.*?)\1/; +const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/; + +const popularCommentPatterns = { + js: { start: "\\/\\/", end: "" }, + jsBlock: { start: "\\/\\*", end: "\\*\\/" }, + jsx: { start: "\\{\\s*\\/\\*", end: "\\*\\/\\s*\\}" }, + bash: { start: "#", end: "" }, + html: { start: "<!--", end: "-->" }, +} as const; + +const commentPatterns = { + ...popularCommentPatterns, + lua: { start: "--", end: "" }, + wasm: { start: "\\;\\;", end: "" }, + tex: { start: "%", end: "" }, + vb: { start: "['‘’]", end: "" }, + vbnet: { start: "(?:_\\s*)?['‘’]", end: "" }, + rem: { start: "[Rr][Ee][Mm]\\b", end: "" }, + f90: { start: "!", end: "" }, + ml: { start: "\\(\\*", end: "\\*\\)" }, + cobol: { start: "\\*>", end: "" }, +} as const; + +type CommentType = keyof typeof commentPatterns; +const popularCommentTypes = Object.keys( + popularCommentPatterns +) as CommentType[]; + +export type MagicCommentConfig = { + className: string; + line?: string; + block?: { start: string; end: string }; +}; + +function getCommentPattern( + languages: CommentType[], + magicCommentDirectives: MagicCommentConfig[] +) { + const commentPattern = languages + .map((lang) => { + const { start, end } = commentPatterns[lang]; + return `(?:${start}\\s*(${magicCommentDirectives + .flatMap((d) => [d.line, d.block?.start, d.block?.end].filter(Boolean)) + .join("|")})\\s*${end})`; + }) + .join("|"); + return new RegExp(`^\\s*(?:${commentPattern})\\s*$`); +} + +function getAllMagicCommentDirectiveStyles( + lang: string, + magicCommentDirectives: MagicCommentConfig[] +) { + switch (lang) { + case "js": + case "javascript": + case "ts": + case "typescript": + return getCommentPattern(["js", "jsBlock"], magicCommentDirectives); + + case "jsx": + case "tsx": + return getCommentPattern( + ["js", "jsBlock", "jsx"], + magicCommentDirectives + ); + + case "html": + return getCommentPattern( + ["js", "jsBlock", "html"], + magicCommentDirectives + ); + + case "python": + case "py": + case "bash": + return getCommentPattern(["bash"], magicCommentDirectives); + + case "markdown": + case "md": + return getCommentPattern(["html", "jsx", "bash"], magicCommentDirectives); + + case "tex": + case "latex": + case "matlab": + return getCommentPattern(["tex"], magicCommentDirectives); + + case "lua": + case "haskell": + return getCommentPattern(["lua"], magicCommentDirectives); + + case "sql": + return getCommentPattern(["lua", "jsBlock"], magicCommentDirectives); + + case "wasm": + return getCommentPattern(["wasm"], magicCommentDirectives); + + case "vb": + case "vba": + case "visual-basic": + return getCommentPattern(["vb", "rem"], magicCommentDirectives); + case "vbnet": + return getCommentPattern(["vbnet", "rem"], magicCommentDirectives); + + case "batch": + return getCommentPattern(["rem"], magicCommentDirectives); + + case "basic": + return getCommentPattern(["rem", "f90"], magicCommentDirectives); + + case "fsharp": + return getCommentPattern(["js", "ml"], magicCommentDirectives); + + case "ocaml": + case "sml": + return getCommentPattern(["ml"], magicCommentDirectives); + + case "fortran": + return getCommentPattern(["f90"], magicCommentDirectives); + + case "cobol": + return getCommentPattern(["cobol"], magicCommentDirectives); + + default: + return getCommentPattern(popularCommentTypes, magicCommentDirectives); + } +} + +export function parseCodeBlockTitle(metastring?: string): string { + return metastring?.match(codeBlockTitleRegex)?.groups!.title ?? ""; +} + +export function containsLineNumbers(metastring?: string): boolean { + return Boolean(metastring?.includes("showLineNumbers")); +} + +type ParseCodeLinesParam = { + metastring: string | undefined; + language: string | undefined; + magicComments: MagicCommentConfig[]; +}; + +type CodeLineClassNames = { [lineIndex: number]: string[] }; + +type ParsedCodeLines = { + code: string; + lineClassNames: CodeLineClassNames; +}; + +function parseCodeLinesFromMetastring( + code: string, + { metastring, magicComments }: ParseCodeLinesParam +): ParsedCodeLines | null { + if (metastring && metastringLinesRangeRegex.test(metastring)) { + const linesRange = metastring.match(metastringLinesRangeRegex)!.groups! + .range!; + if (magicComments.length === 0) { + throw new Error( + `A highlight range has been given in code block's metastring (\`\`\` ${metastring}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.` + ); + } + const metastringRangeClassName = magicComments[0]!.className; + const lines = rangeParser(linesRange) + .filter((n) => n > 0) + .map((n) => [n - 1, [metastringRangeClassName]] as [number, string[]]); + return { lineClassNames: Object.fromEntries(lines), code }; + } + return null; +} + +function parseCodeLinesFromContent( + code: string, + params: ParseCodeLinesParam +): ParsedCodeLines { + const { language, magicComments } = params; + if (language === undefined) { + return { lineClassNames: {}, code }; + } + const directiveRegex = getAllMagicCommentDirectiveStyles( + language, + magicComments + ); + const lines = code.split(/\r?\n/); + const blocks = Object.fromEntries( + magicComments.map((d) => [d.className, { start: 0, range: "" }]) + ); + const lineToClassName: { [comment: string]: string } = Object.fromEntries( + magicComments + .filter((d) => d.line) + .map(({ className, line }) => [line!, className] as [string, string]) + ); + const blockStartToClassName: { [comment: string]: string } = + Object.fromEntries( + magicComments + .filter((d) => d.block) + .map(({ className, block }) => [block!.start, className]) + ); + const blockEndToClassName: { [comment: string]: string } = Object.fromEntries( + magicComments + .filter((d) => d.block) + .map(({ className, block }) => [block!.end, className]) + ); + for (let lineNumber = 0; lineNumber < lines.length; ) { + const line = lines[lineNumber]!; + const match = line.match(directiveRegex); + if (!match) { + lineNumber += 1; + continue; + } + const directive = match + .slice(1) + .find((item: string | undefined) => item !== undefined)!; + if (lineToClassName[directive]) { + blocks[lineToClassName[directive]!]!.range += `${lineNumber},`; + } else if (blockStartToClassName[directive]) { + blocks[blockStartToClassName[directive]!]!.start = lineNumber; + } else if (blockEndToClassName[directive]) { + blocks[blockEndToClassName[directive]!]!.range += `${ + blocks[blockEndToClassName[directive]!]!.start + }-${lineNumber - 1},`; + } + lines.splice(lineNumber, 1); + } + + const lineClassNames: { [lineIndex: number]: string[] } = {}; + Object.entries(blocks).forEach(([className, { range }]) => { + rangeParser(range).forEach((l) => { + lineClassNames[l] ??= []; + lineClassNames[l]!.push(className); + }); + }); + + return { code: lines.join("\n"), lineClassNames }; +} + +export function parseLines( + code: string, + params: ParseCodeLinesParam +): ParsedCodeLines { + const newCode = code.replace(/\r?\n$/, ""); + return ( + parseCodeLinesFromMetastring(newCode, { ...params }) ?? + parseCodeLinesFromContent(newCode, { ...params }) + ); +} + +function parseClassNameLanguage( + className: string | undefined +): string | undefined { + if (!className) { + return undefined; + } + const languageClassName = className + .split(" ") + .find((str) => str.startsWith("language-")); + return languageClassName?.replace(/language-/, ""); +} + +// Upstream renamed `parseLanguage` to `parseClassNameLanguage`; the back-compat +// shim in @docusaurus/theme-common/internal re-exports it under the old name. +// We keep the old name here since our call sites still use it. +export { parseClassNameLanguage as parseLanguage }; + +export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties { + const mapping: PrismThemeEntry = { + color: "--prism-color", + backgroundColor: "--prism-background-color", + }; + + const properties: { [key: string]: string } = {}; + Object.entries(prismTheme.plain).forEach(([key, value]) => { + const varName = mapping[key as keyof PrismThemeEntry]; + if (varName && typeof value === "string") { + properties[varName] = value; + } + }); + return properties; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts new file mode 100644 index 000000000..0a2edd37c --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts @@ -0,0 +1,48 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored subset of @docusaurus/theme-common/src/utils/reactUtils.tsx (MIT) + * to remove the dependency on @docusaurus/theme-common/internal, which is + * scheduled for removal in Docusaurus v4. + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { useCallback, useMemo, useRef } from "react"; + +import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect"; + +export function useEvent<T extends (...args: never[]) => unknown>( + callback: T +): T { + const ref = useRef<T>(callback); + + useIsomorphicLayoutEffect(() => { + ref.current = callback; + }, [callback]); + + // @ts-expect-error: TS is right that this callback may be a supertype of T, + // but good enough for our use + return useCallback<T>((...args) => ref.current(...args), []); +} + +export function useShallowMemoObject<O extends object>(obj: O): O { + const deps = Object.entries(obj); + deps.sort((a, b) => a[0].localeCompare(b[0])); + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => obj, deps.flat()); +} + +export class ReactContextError extends Error { + constructor(providerName: string, additionalInfo?: string) { + super(); + this.name = "ReactContextError"; + this.message = `Hook ${ + this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?<name>\w+)/)?.groups! + .name ?? "" + } is called outside the <${providerName}>. ${additionalInfo ?? ""}`; + } +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx new file mode 100644 index 000000000..6490a2ca6 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx @@ -0,0 +1,154 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored subset of @docusaurus/theme-common/src/utils/scrollUtils.tsx (MIT) + * to remove the dependency on @docusaurus/theme-common/internal. Only the + * ScrollControllerProvider + useScrollPositionBlocker surface is kept, since + * that's all our tab renderers need. The ScrollControllerProvider must be + * mounted in the React tree above any consumer of useScrollPositionBlocker + * (see ApiItem/index.tsx). + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React, { + useCallback, + useContext, + useMemo, + useRef, + type ReactNode, +} from "react"; + +import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect"; + +import { ReactContextError } from "./reactUtils"; + +type ScrollController = { + scrollEventsEnabledRef: React.RefObject<boolean>; + enableScrollEvents: () => void; + disableScrollEvents: () => void; +}; + +function useScrollControllerContextValue(): ScrollController { + const scrollEventsEnabledRef = useRef(true); + + return useMemo( + () => ({ + scrollEventsEnabledRef, + enableScrollEvents: () => { + scrollEventsEnabledRef.current = true; + }, + disableScrollEvents: () => { + scrollEventsEnabledRef.current = false; + }, + }), + [] + ); +} + +const ScrollMonitorContext = React.createContext<ScrollController | undefined>( + undefined +); + +export function ScrollControllerProvider({ + children, +}: { + children: ReactNode; +}): ReactNode { + const value = useScrollControllerContextValue(); + return ( + <ScrollMonitorContext.Provider value={value}> + {children} + </ScrollMonitorContext.Provider> + ); +} + +function useScrollController(): ScrollController { + const context = useContext(ScrollMonitorContext); + if (context == null) { + throw new ReactContextError("ScrollControllerProvider"); + } + return context; +} + +type UseScrollPositionSaver = { + save: (elem: HTMLElement) => void; + restore: () => { restored: boolean }; +}; + +function useScrollPositionSaver(): UseScrollPositionSaver { + const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({ + elem: null, + top: 0, + }); + + const save = useCallback((elem: HTMLElement) => { + lastElementRef.current = { + elem, + top: elem.getBoundingClientRect().top, + }; + }, []); + + const restore = useCallback(() => { + const { + current: { elem, top }, + } = lastElementRef; + if (!elem) { + return { restored: false }; + } + const newTop = elem.getBoundingClientRect().top; + const heightDiff = newTop - top; + if (heightDiff) { + window.scrollBy({ left: 0, top: heightDiff }); + } + lastElementRef.current = { elem: null, top: 0 }; + + return { restored: heightDiff !== 0 }; + }, []); + + return useMemo(() => ({ save, restore }), [restore, save]); +} + +export function useScrollPositionBlocker(): { + blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void; +} { + const scrollController = useScrollController(); + const scrollPositionSaver = useScrollPositionSaver(); + + const nextLayoutEffectCallbackRef = useRef<(() => void) | undefined>( + undefined + ); + + const blockElementScrollPositionUntilNextRender = useCallback( + (el: HTMLElement) => { + scrollPositionSaver.save(el); + scrollController.disableScrollEvents(); + nextLayoutEffectCallbackRef.current = () => { + const { restored } = scrollPositionSaver.restore(); + nextLayoutEffectCallbackRef.current = undefined; + + if (restored) { + const handleScrollRestoreEvent = () => { + scrollController.enableScrollEvents(); + window.removeEventListener("scroll", handleScrollRestoreEvent); + }; + window.addEventListener("scroll", handleScrollRestoreEvent); + } else { + scrollController.enableScrollEvents(); + } + }; + }, + [scrollController, scrollPositionSaver] + ); + + useIsomorphicLayoutEffect(() => { + queueMicrotask(() => nextLayoutEffectCallbackRef.current?.()); + }); + + return { + blockElementScrollPositionUntilNextRender, + }; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx new file mode 100644 index 000000000..0ed562496 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx @@ -0,0 +1,329 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored from @docusaurus/theme-common/src/utils/tabsUtils.tsx (MIT) to + * remove the dependency on @docusaurus/theme-common/internal. The + * useQueryStringValue dependency from theme-common's historyUtils is inlined + * below to avoid pulling another internal module. + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React, { + createContext, + isValidElement, + useCallback, + useMemo, + useState, + useSyncExternalStore, + type ReactElement, + type ReactNode, +} from "react"; + +import { useHistory } from "@docusaurus/router"; +import { duplicates, useStorageSlot } from "@docusaurus/theme-common"; +import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect"; + +import { ScrollControllerProvider } from "./scrollUtils"; + +export interface TabValue { + readonly value: string; + readonly label?: string; + readonly attributes?: { [key: string]: unknown }; + readonly default?: boolean; +} + +export interface TabsProps { + readonly lazy?: boolean; + readonly block?: boolean; + readonly children: ReactNode; + readonly defaultValue?: string | null; + readonly values?: readonly TabValue[]; + readonly groupId?: string; + readonly className?: string; + readonly queryString?: string | boolean; +} + +// Extended Tabs type used across the OpenAPI theme; preserves the historical +// shape that included an optional `length` field. +export interface TabProps extends TabsProps { + length?: number; +} + +export interface TabItemProps { + readonly children: ReactNode; + readonly value: string; + readonly default?: boolean; + readonly label?: string; + readonly className?: string; + readonly attributes?: { [key: string]: unknown }; +} + +export function sanitizeTabsChildren(children: ReactNode): ReactNode { + return React.Children.toArray(children).filter((child) => child !== "\n"); +} + +function extractChildrenTabValues(children: ReactNode): TabValue[] { + function isTabItemWithValueProp( + comp: ReactElement + ): comp is ReactElement<TabItemProps> { + const { props } = comp; + return !!props && typeof props === "object" && "value" in props; + } + + const elements = React.Children.toArray(children).flatMap((child) => { + if (!child) { + return []; + } + if (isValidElement(child) && isTabItemWithValueProp(child)) { + return [child]; + } + const badChildTypeName = + // @ts-expect-error: guarding against unexpected cases + typeof child.type === "string" ? child.type : child.type.name; + throw new Error( + `Docusaurus error: Bad <Tabs> child <${badChildTypeName}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop. +If you do not want to pass on a "value" prop to the direct children of <Tabs>, you can also pass an explicit <Tabs values={...}> prop.` + ); + }); + + return elements.map( + ({ props: { value, label, attributes, default: isDefault } }) => ({ + value, + label, + attributes, + default: isDefault, + }) + ); +} + +function ensureNoDuplicateValue(values: readonly TabValue[]) { + const dup = duplicates(values, (a, b) => a.value === b.value); + if (dup.length > 0) { + throw new Error( + `Docusaurus error: Duplicate values "${dup + .map((a) => `'${a.value}'`) + .join(", ")}" found in <Tabs>. Every value needs to be unique.` + ); + } +} + +function useTabValues( + props: Pick<TabsProps, "values" | "children"> +): readonly TabValue[] { + const { values: valuesProp, children } = props; + return useMemo(() => { + const values = valuesProp ?? extractChildrenTabValues(children); + ensureNoDuplicateValue(values); + return values; + }, [valuesProp, children]); +} + +function isValidValue({ + value, + tabValues, +}: { + value: string | null | undefined; + tabValues: readonly TabValue[]; +}) { + return tabValues.some((a) => a.value === value); +} + +function getInitialStateValue({ + defaultValue, + tabValues, +}: { + defaultValue: TabsProps["defaultValue"]; + tabValues: readonly TabValue[]; +}): string { + if (tabValues.length === 0) { + throw new Error( + "Docusaurus error: the <Tabs> component requires at least one <TabItem> children component" + ); + } + if (defaultValue) { + if (!isValidValue({ value: defaultValue, tabValues })) { + throw new Error( + `Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${tabValues + .map((a) => a.value) + .join( + ", " + )}. If you intend to show no default tab, use defaultValue={null} instead.` + ); + } + return defaultValue; + } + const defaultTabValue = + tabValues.find((tabValue) => tabValue.default) ?? tabValues[0]; + if (!defaultTabValue) { + throw new Error("Unexpected error: 0 tabValues"); + } + return defaultTabValue.value; +} + +function getStorageKey(groupId: string | undefined) { + if (!groupId) { + return null; + } + return `docusaurus.tab.${groupId}`; +} + +function getQueryStringKey({ + queryString = false, + groupId, +}: Pick<TabsProps, "queryString" | "groupId">) { + if (typeof queryString === "string") { + return queryString; + } + if (queryString === false) { + return null; + } + if (queryString === true && !groupId) { + throw new Error( + `Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".` + ); + } + return groupId ?? null; +} + +// Inlined from @docusaurus/theme-common/internal historyUtils.useQueryStringValue +function useQueryStringValue(key: string | null): string | null { + const history = useHistory(); + return useSyncExternalStore( + history.listen, + () => + key === null + ? null + : new URLSearchParams(history.location.search).get(key), + () => null + ); +} + +function useTabQueryString({ + queryString = false, + groupId, +}: Pick<TabsProps, "queryString" | "groupId">) { + const history = useHistory(); + const key = getQueryStringKey({ queryString, groupId }); + const value = useQueryStringValue(key); + + const setValue = useCallback( + (newValue: string) => { + if (!key) { + return; + } + const searchParams = new URLSearchParams(history.location.search); + searchParams.set(key, newValue); + history.replace({ ...history.location, search: searchParams.toString() }); + }, + [key, history] + ); + + return [value, setValue] as const; +} + +function useTabStorage({ groupId }: Pick<TabsProps, "groupId">) { + const key = getStorageKey(groupId); + const [value, storageSlot] = useStorageSlot(key); + + const setValue = useCallback( + (newValue: string) => { + if (!key) { + return; + } + storageSlot.set(newValue); + }, + [key, storageSlot] + ); + + return [value, setValue] as const; +} + +type TabsContextValue = { + selectedValue: string; + selectValue: (value: string) => void; + tabValues: readonly TabValue[]; + lazy: boolean; + block: boolean; +}; + +export function useTabsContextValue(props: TabsProps): TabsContextValue { + const { defaultValue, queryString = false, groupId } = props; + const tabValues = useTabValues(props); + + const [selectedValue, setSelectedValue] = useState(() => + getInitialStateValue({ defaultValue, tabValues }) + ); + + const [queryStringValue, setQueryString] = useTabQueryString({ + queryString, + groupId, + }); + + const [storageValue, setStorageValue] = useTabStorage({ + groupId, + }); + + const valueToSync = (() => { + const value = queryStringValue ?? storageValue; + if (!isValidValue({ value, tabValues })) { + return null; + } + return value; + })(); + + useIsomorphicLayoutEffect(() => { + if (valueToSync) { + setSelectedValue(valueToSync); + } + }, [valueToSync]); + + const selectValue = useCallback( + (newValue: string) => { + if (!isValidValue({ value: newValue, tabValues })) { + throw new Error(`Can't select invalid tab value=${newValue}`); + } + setSelectedValue(newValue); + setQueryString(newValue); + setStorageValue(newValue); + }, + [setQueryString, setStorageValue, tabValues] + ); + + return { + selectedValue, + selectValue, + tabValues, + lazy: props.lazy ?? false, + block: props.block ?? false, + }; +} + +const TabsContext = createContext<TabsContextValue | null>(null); + +export function useTabs(): TabsContextValue { + const contextValue = React.useContext(TabsContext); + if (!contextValue) { + throw new Error("useTabsContext() must be used within a Tabs component"); + } + return contextValue; +} + +export function TabsProvider(props: { + children: ReactNode; + value: TabsContextValue; +}): ReactNode { + // ScrollControllerProvider is mounted here so every tab consumer + // (our six OpenAPI tab variants + the swizzled @theme/Tabs) gets a working + // useScrollPositionBlocker without callers needing a separate provider. + return ( + <ScrollControllerProvider> + <TabsContext.Provider value={props.value}> + {props.children} + </TabsContext.Provider> + </ScrollControllerProvider> + ); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts new file mode 100644 index 000000000..820f4d801 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts @@ -0,0 +1,110 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored from @docusaurus/theme-common/src/hooks/useCodeWordWrap.ts (MIT) + * to remove the dependency on @docusaurus/theme-common/internal. + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { useCallback, useEffect, useRef, useState } from "react"; +import type { RefObject } from "react"; + +import { useMutationObserver } from "./useMutationObserver"; + +// Callback fires when the "hidden" attribute of a tabpanel changes +// See https://github.com/facebook/docusaurus/pull/7485 +function useTabBecameVisibleCallback( + codeBlockRef: RefObject<HTMLPreElement | null>, + callback: () => void +) { + const [hiddenTabElement, setHiddenTabElement] = useState< + Element | null | undefined + >(); + + const updateHiddenTabElement = useCallback(() => { + setHiddenTabElement( + codeBlockRef.current?.closest("[role=tabpanel][hidden]") + ); + }, [codeBlockRef, setHiddenTabElement]); + + useEffect(() => { + updateHiddenTabElement(); + }, [updateHiddenTabElement]); + + useMutationObserver( + hiddenTabElement, + (mutations: MutationRecord[]) => { + mutations.forEach((mutation) => { + if ( + mutation.type === "attributes" && + mutation.attributeName === "hidden" + ) { + callback(); + updateHiddenTabElement(); + } + }); + }, + { + attributes: true, + characterData: false, + childList: false, + subtree: false, + } + ); +} + +export type WordWrap = { + readonly codeBlockRef: RefObject<HTMLPreElement | null>; + readonly isEnabled: boolean; + readonly isCodeScrollable: boolean; + readonly toggle: () => void; +}; + +export function useCodeWordWrap(): WordWrap { + const [isEnabled, setIsEnabled] = useState(false); + const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false); + const codeBlockRef = useRef<HTMLPreElement>(null); + + const toggle = useCallback(() => { + const codeElement = codeBlockRef.current!.querySelector("code")!; + + if (isEnabled) { + codeElement.removeAttribute("style"); + } else { + codeElement.style.whiteSpace = "pre-wrap"; + codeElement.style.overflowWrap = "anywhere"; + } + + setIsEnabled((value) => !value); + }, [codeBlockRef, isEnabled]); + + const updateCodeIsScrollable = useCallback(() => { + const { scrollWidth, clientWidth } = codeBlockRef.current!; + const isScrollable = + scrollWidth > clientWidth || + codeBlockRef.current!.querySelector("code")!.hasAttribute("style"); + setIsCodeScrollable(isScrollable); + }, [codeBlockRef]); + + useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable); + + useEffect(() => { + updateCodeIsScrollable(); + }, [isEnabled, updateCodeIsScrollable]); + + useEffect(() => { + window.addEventListener("resize", updateCodeIsScrollable, { + passive: true, + }); + + return () => { + window.removeEventListener("resize", updateCodeIsScrollable); + }; + }, [updateCodeIsScrollable]); + + return { codeBlockRef, isEnabled, isCodeScrollable, toggle }; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts new file mode 100644 index 000000000..d81a83c48 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts @@ -0,0 +1,41 @@ +/* ============================================================================ + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions Copyright (c) Palo Alto Networks + * + * Vendored from @docusaurus/theme-common/src/hooks/useMutationObserver.ts (MIT) + * to remove the dependency on @docusaurus/theme-common/internal. + * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140 + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { useEffect } from "react"; + +import { useEvent, useShallowMemoObject } from "./reactUtils"; + +type Options = MutationObserverInit; + +const DefaultOptions: Options = { + attributes: true, + characterData: true, + childList: true, + subtree: true, +}; + +export function useMutationObserver( + target: Element | undefined | null, + callback: MutationCallback, + options: Options = DefaultOptions +): void { + const stableCallback = useEvent(callback); + const stableOptions: Options = useShallowMemoObject(options); + + useEffect(() => { + const observer = new MutationObserver(stableCallback); + if (target) { + observer.observe(target, stableOptions); + } + return () => observer.disconnect(); + }, [target, stableCallback, stableOptions]); +}