import * as React from 'react'; import { CodeBlock as CodeBlockComponent, type CodeBlockProps, } from '@redocly/theme/components/CodeBlock/CodeBlock'; import { langToName } from '@redocly/theme/core/utils'; import { useActiveCodeSnippetId } from '@redocly/theme/core/contexts'; type SnippetData = { name: string; languageName: string; lang: string; props: CodeBlockProps; id: string; }; export function CodeGroup(props: React.PropsWithChildren<{ mode?: 'tabs' | 'dropdown' }>) { const mode = props.mode || 'tabs'; const isTabsMode = mode === 'tabs'; const rawSnippets = React.useMemo( () => parseSnippetsFromChildren(props.children), [props.children], ); const groupId = React.useMemo(() => generateGroupId(rawSnippets, mode), [rawSnippets, mode]); const [activeSnippetId, setActiveSnippetId] = useActiveCodeSnippetId(groupId, rawSnippets); const snippets = React.useMemo(() => { const items = createItemsFromSnippets(rawSnippets, isTabsMode); const itemsProps = { items, onChange: (id: string | string[]) => setActiveSnippetId(id as string), value: activeSnippetId, }; return Object.fromEntries( rawSnippets.map((snippet: SnippetData) => { const snippetProps = { ...snippet.props, header: { ...snippet.props.header, title: isTabsMode ? undefined : snippet.name, }, ...(isTabsMode ? { tabs: itemsProps } : { dropdown: itemsProps }), }; return [snippet.id, snippetProps]; }), ); }, [rawSnippets, activeSnippetId, isTabsMode, setActiveSnippetId]); const activeSnippet = snippets[activeSnippetId]; if (!activeSnippet) { return null; } return ; } function generateContentHash(content: string): number { let hash = 0; for (let i = 0; i < content.length; i++) { hash = content.charCodeAt(i) + ((hash << 5) - hash); } return Math.abs(hash); } // Generate unique group ID for CodeGroup instance // Examples: "dropdown-8901234", "tabs-1234567" function generateGroupId(rawSnippets: SnippetData[], mode: string): string { const content = rawSnippets.map((s) => s.id + (s.props.source || '')).join('|') + `|${mode}`; const hash = generateContentHash(content); return `${mode}-${hash}`; } function getTabName(props: CodeBlockProps, idx: number): string { const fallbackName = `Tab ${idx + 1}`; return String(props.header?.title || props.file || langToName(props.lang || '') || fallbackName); } function parseSnippetsFromChildren(children: React.ReactNode): SnippetData[] { return React.Children.toArray(children).map((child, idx) => { const childProps = child as React.ReactElement; const props = childProps.props; return { name: getTabName(props, idx), languageName: String(langToName(props.lang || 'Default') || ''), lang: props.lang || '', props, id: `${props.lang || ''}-${idx}`, }; }); } function createItemsFromSnippets(snippets: SnippetData[], isTabsMode: boolean) { return snippets.map((snippet) => ({ name: isTabsMode ? snippet.name : snippet.languageName || '', lang: snippet.lang, id: snippet.id, })); }