import cx from "classnames"; import React, { ReactNode, useRef, useState } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import reactElementToJSXString from "react-element-to-jsx-string"; import unescape from "unescape-html"; import { formatHTML } from "../../utils/formatCode"; import { Button } from "../Button"; import Code from "../Code/"; import { getElementDisplayName } from "./getElementDisplayName"; interface CodeExampleProps { codeJSXOptions?: Record; codeTypes?: string[]; children?: ReactNode; htmlThemeClass?: string; jsxCode?: string; // Pre-generated JSX string (from server-side PreviewAuto) htmlCode?: ReactNode; // Code for HTML rendering [key: string]: any; } const addClassToOpeningTag = (tag: string, className: string): string => { if (tag.includes(' class="')) { return tag.replace(/ class="([^"]*)"/, (_, existingClassName) => { const mergedClassName = [existingClassName, className] .filter(Boolean) .join(" "); return ` class="${mergedClassName}"`; }); } return tag.replace(/^<([\w-]+)/, `<$1 class="${className}"`); }; const applyThemeClassToMarkup = ( markup: string, themeClass?: string, wrapMultipleRoots = false, ): string => { if (!themeClass || !markup.trim()) { return markup; } if (wrapMultipleRoots) { return `
${markup}
`; } return markup.replace(/^<[^>]+>/, (openingTag) => addClassToOpeningTag(openingTag, themeClass), ); }; const renderMarkupToHtml = (markup: ReactNode, themeClass?: string): string => { if (!markup) { return ""; } if (typeof markup === "string") { return markup; } if (Array.isArray(markup)) { const renderedMarkup = markup .map((markupItem) => renderToStaticMarkup(markupItem as React.ReactElement), ) .join(""); return applyThemeClassToMarkup( renderedMarkup, themeClass, markup.length > 1, ); } return applyThemeClassToMarkup( renderToStaticMarkup(markup as React.ReactElement), themeClass, ); }; /** * Remove props that are undefined or null * Don't show React.Fragment in code example */ const cleanUpCode = (markup: any): any => { const markupProps = markup.props || {}; return Object.keys(markupProps).reduce((acc: any, curr: string) => { const currProp = markupProps[curr]; let newProp; // clean up child code if (curr === "children" && typeof currProp !== "string") { newProp = React.Children.map(currProp, (child: any) => { // hide fragments if containing just strings const isFragmentString = child.type === React.Fragment && child.props && typeof child.props.children === "string"; return isFragmentString ? child.props.children : { ...child, props: cleanUpCode(child), }; }); } // hide undefined or null props const isNotDefined = [undefined, null].indexOf(currProp) !== -1; return { ...acc, ...(isNotDefined ? {} : { [curr]: newProp || currProp, }), }; }, {}); }; const getJSXAsStringFromMarkup = ( markup: any, options?: Record, ): string => { const { cleanProps, filterProps = [], ...otherOptions } = options || {}; const reactElementToJSXStringOptions = { showDefaultProps: false, showFunctions: true, functionValue: (fn: any) => fn.name, displayName: getElementDisplayName, filterProps: ["mdxType", "originalType", ...filterProps], ...otherOptions, }; if (cleanProps) { markup = { ...markup, props: cleanUpCode(markup), }; } // valid element can be passed to reactElementToJSXString directly if (React.isValidElement(markup)) { return reactElementToJSXString(markup, reactElementToJSXStringOptions); } // if it's array, we need to pass elemenets one by one if (Array.isArray(markup)) { return markup .map((markupItem: any) => reactElementToJSXString(markupItem, reactElementToJSXStringOptions), ) .join("\n"); } // if it's pure text, return it if (typeof markup === "string") { return markup; } return ""; }; const CodeExample: React.FC = ({ children, codeJSXOptions, codeTypes = ["html", "jsx"], htmlThemeClass, jsxCode, htmlCode, ...other }) => { const [codePreviewType, setCodePreviewType] = useState( (codeTypes && codeTypes[0]) || "html", ); const [copyButtonText, setCopyButtonText] = useState("Copy to clipboard"); const [copyButtonClass, setCopyButtonClass] = useState(""); const codeBlockRef = useRef(null); const handleCodePreviewTypeToggle = (e: React.MouseEvent, type: string) => { setCodePreviewType(type); }; const handleCopyCode = ( e: React.MouseEvent, element: HTMLDivElement | null, ) => { if (!element) return; const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); selection?.removeAllRanges(); selection?.addRange(range); let newText = "Copied!"; let newClass = "success"; try { document.execCommand("copy"); } catch { newText = "Error! Press Ctrl + C"; newClass = "error"; } const originalText = copyButtonText; const originalClass = copyButtonClass; setCopyButtonText(newText); setCopyButtonClass(newClass); setTimeout(() => { setCopyButtonText(originalText); setCopyButtonClass(originalClass); }, 1200); }; let codeToShow: string; switch (codePreviewType) { case "html": if (htmlCode) { codeToShow = typeof htmlCode === "string" ? unescape(htmlCode) : unescape( formatHTML(renderMarkupToHtml(htmlCode, htmlThemeClass)), ); } else { codeToShow = typeof children === "string" ? unescape(children) : unescape( formatHTML(renderMarkupToHtml(children, htmlThemeClass)), ); } break; case "jsx": if (jsxCode) { // Use pre-generated JSX string (from server-side PreviewAuto) codeToShow = jsxCode; } else { // Fallback to client-side generation (may not work well with MDX) codeToShow = getJSXAsStringFromMarkup(children, codeJSXOptions); } break; default: codeToShow = ""; } return (
{codeTypes.map((codeType) => ( ))} {codeToShow}
); }; CodeExample.displayName = "CodeExample"; export default CodeExample;