UNPKG

@redocly/theme

Version:

Shared UI components lib

166 lines (140 loc) 4.94 kB
import { useContext, useCallback, useEffect, useState, useMemo } from 'react'; import type { CodeWalkthroughFile, CodeWalkthroughNode, CodeWalkthroughConditionsObject, } from '@redocly/config'; import { CodeWalkthroughControlsStateContext, CodeWalkthroughStepsContext, } from '@redocly/theme/core/contexts'; import { useThemeHooks } from '@redocly/theme/core/hooks'; const ACTIVE_FILE_MOCK = { content: [], path: '', basename: '', metadata: {}, language: '', }; export function useCodePanel(files: CodeWalkthroughFile[]) { const { activeStep } = useContext(CodeWalkthroughStepsContext); const { areConditionsMet, populateInputsWithValue } = useContext( CodeWalkthroughControlsStateContext, ); const { useCodeHighlight } = useThemeHooks(); const { highlight } = useCodeHighlight(); const findFileIndexByName = useCallback( (name: string) => { return files.findIndex((file) => file.path === name); }, [files], ); const findFileIndexByStepId = useCallback( (id: string) => files.findIndex((file) => file.metadata.steps.includes(id)), [files], ); const activeStepFileIndex = activeStep ? findFileIndexByStepId(activeStep) : 0; const initialActiveFileIndex = activeStepFileIndex !== -1 ? activeStepFileIndex : 0; const [activeFileIndex, setActiveFileIndex] = useState(initialActiveFileIndex); useEffect(() => { setActiveFileIndex(initialActiveFileIndex); }, [initialActiveFileIndex, activeStep, files]); const handleTabSwitch = useCallback( (name: string) => { const index = findFileIndexByName(name); if (index !== -1) { setActiveFileIndex(index); } }, [findFileIndexByName], ); const activeFile = files[activeFileIndex] || // Fallback to default. Needed when switching from language with more files to a language with less files files[initialActiveFileIndex] || // Final fallback for dev mode when no files were added yet ACTIVE_FILE_MOCK; const highlightedCode = useMemo(() => { const { highlightedLines, code, isWholeFileSelected } = getRenderableCode( activeFile, activeStep, areConditionsMet, populateInputsWithValue, ); return highlight(code, activeFile.language, { withLineNumbers: true, // Shiki transformerMetaHighlight meta to highlight lines // If the whole file is selected for a step, do not apply highlighting highlight: isWholeFileSelected ? '' : `{${Array.from(highlightedLines).join(',')}}`, customTransformer: { // Add greyed-out class to lines that are not highlighted line(hast, number) { if (!highlightedLines.has(number)) { this.addClassToHast(hast, 'greyed-out'); } }, }, }); }, [activeFile, activeStep, highlight, areConditionsMet, populateInputsWithValue]); return { activeFile, handleTabSwitch, highlightedCode } as const; } function getRenderableCode( activeFile: CodeWalkthroughFile, activeStep: string | null, areConditionsMet: (conditions: CodeWalkthroughConditionsObject) => boolean, populateInputsWithValue: (node: string) => string, ): { highlightedLines: Set<number>; code: string; isWholeFileSelected: boolean; } { const codeLines = activeFile.content.flatMap((node) => getCodeLinesFromNode(node, activeStep, areConditionsMet, populateInputsWithValue), ); const codeLinesContent: string[] = []; const highlightedLines = new Set<number>(); codeLines.forEach(({ lineContent, highlighted }, idx) => { codeLinesContent.push(lineContent); if (highlighted) { highlightedLines.add(idx + 1); } }); return { highlightedLines, code: codeLinesContent.join('\n'), isWholeFileSelected: highlightedLines.size === codeLinesContent.length, }; } /** * Convert code node to code line objects with content to render and their highlighted status */ function getCodeLinesFromNode( node: CodeWalkthroughNode, activeStep: string | null, areConditionsMet: (conditions: CodeWalkthroughConditionsObject) => boolean, populateInputsWithValue: (node: string) => string, parentHighlighted: boolean = false, ): { lineContent: string; highlighted: boolean }[] { if (typeof node === 'string') { const replacedNode = populateInputsWithValue(node); return [{ lineContent: replacedNode, highlighted: parentHighlighted }]; } else { const shouldRenderChunk = areConditionsMet(node.condition); const isHighlighted = parentHighlighted || (activeStep != null && node.condition.steps.length > 0 && node.condition.steps.includes(activeStep)); return shouldRenderChunk ? node.children.flatMap((child) => getCodeLinesFromNode( child, activeStep, areConditionsMet, populateInputsWithValue, isHighlighted, ), ) : []; } }