diff --git a/apps/docs/components/docs/components/codeblock.tsx b/apps/docs/components/docs/components/codeblock.tsx index d14a14365b..43761c523a 100644 --- a/apps/docs/components/docs/components/codeblock.tsx +++ b/apps/docs/components/docs/components/codeblock.tsx @@ -1,7 +1,13 @@ +import type {Language, PrismTheme} from "prism-react-renderer"; +import type {TransformTokensTypes} from "./helper"; + import React, {forwardRef, useEffect} from "react"; import {clsx, dataAttr, getUniqueID} from "@nextui-org/shared-utils"; -import BaseHighlight, {Language, PrismTheme, defaultProps} from "prism-react-renderer"; import {debounce, omit} from "lodash"; +import BaseHighlight, {defaultProps} from "prism-react-renderer"; +import {cn} from "@nextui-org/react"; + +import {transformTokens} from "./helper"; import defaultTheme from "@/libs/prism-theme"; @@ -142,21 +148,36 @@ const Codeblock = forwardRef( {...props} > {({className, style, tokens, getLineProps, getTokenProps}) => ( -
-
-              {tokens.map((line, i) => {
+                "overflow-x-scroll scrollbar-hide": hideScrollBar,
+              },
+            )}
+            data-language={language}
+            style={style}
+          >
+            {transformTokens(tokens).map((line) => {
+              const folderLine = line[0] as TransformTokensTypes;
+
+              const isFolder = folderLine.types?.includes("folderStart");
+
+              const renderLine = (
+                line: TransformTokensTypes[],
+                i: number,
+                as: "div" | "span" = "div",
+              ) => {
+                const Tag = as;
                 const lineProps = getLineProps({line, key: i});
 
                 return (
-                  
( "px-2": showLines, }, { - "before:content-[''] before:w-full before:h-full before:absolute before:z-0 before:left-0 before:bg-gradient-to-r before:from-white/10 before:to-code-background": - shouldHighlightLine(i), + "before:to-code-background before:absolute before:left-0 before:z-0 before:h-full before:w-full before:bg-gradient-to-r before:from-white/10 before:content-['']": + isFolder ? false : shouldHighlightLine(i), }, )} data-deleted={dataAttr(highlightStyle?.[i] === "deleted")} data-inserted={dataAttr(highlightStyle?.[i] === "inserted")} > {showLines && ( - {i + 1} + = 10 ? "mr-4" : "", + i + 1 >= 100 ? "mr-2" : "", + i + 1 >= 1000 ? "mr-0" : "", + )} + > + {i + 1} + )} + {line.map((token, key) => { - // Bun has no color style by default in the code block, so hack add in here const props = getTokenProps({token, key}) || {}; - - return ( + const isCopy = token.types.includes("copy"); + + return isCopy ? ( + + {token.folderContent?.map((folderTokens) => { + return folderTokens.map((token, index) => { + // Hack for wrap line + return token.content === "" ? ( +
+ ) : ( + {token.content} + ); + }); + })} + + ) : ( { @@ -201,11 +245,29 @@ const Codeblock = forwardRef( /> ); })} -
+ ); - })} -
-
+ }; + const renderFolderTokens = (tokens: TransformTokensTypes[][]) => { + return tokens.map((token, key) => { + const index = key + folderLine.index! + 1; + + return renderLine(token, index); + }); + }; + + return isFolder ? ( +
+ + {renderLine(folderLine.summaryContent as any, folderLine.index!, "span")} + + {renderFolderTokens(folderLine.folderContent as any)} +
+ ) : ( + renderLine(line, folderLine.index!) + ); + })} + )} ); diff --git a/apps/docs/components/docs/components/helper.ts b/apps/docs/components/docs/components/helper.ts new file mode 100644 index 0000000000..451c106139 --- /dev/null +++ b/apps/docs/components/docs/components/helper.ts @@ -0,0 +1,189 @@ +import type Highlight from "prism-react-renderer"; + +export type TransformTokens = Parameters[0]["tokens"]; + +export type TransformTokensTypes = TransformTokens[0][0] & { + folderContent?: TransformTokens; + summaryContent?: TransformTokens[0]; + class?: string; + index?: number; + open?: boolean; +}; + +const startFlag = ["{", "["]; +const endFlag = ["}", "]"]; +const specialStartFlag = ["("]; +const specialEndFlag = [")"]; +const defaultFoldFlagList = ["cn", "HTMLAttributes"]; +const defaultShowFlagList = ["Component", "forwardRef", "App"]; + +/** + * Transform tokens from `prism-react-renderer` to wrap them in folder structure + * + * @example + * transformTokens(tokens) -> wrap tokens in folder structure + */ +export function transformTokens(tokens: TransformTokens, folderLine = 10) { + const result: TransformTokens = []; + let lastIndex = 0; + let isShowFolder = false; + let fold = false; + + tokens.forEach((token, index) => { + if (index < lastIndex) { + return; + } + + let startToken: TransformTokens[0][0] = null as any; + let mergedStartFlagList = [...startFlag]; + + token.forEach((t) => { + if (defaultFoldFlagList.some((text) => t.content.includes(text))) { + // If cn then need to judge whether it is import token + if (t.content.includes("cn") && token.some((t) => t.content === "import")) { + return; + } + + // If HTMLAttributes then need to judge whether it have start flag + if ( + t.content.includes("HTMLAttributes") && + !token.some((t) => startFlag.includes(t.content)) + ) { + return; + } + + fold = true; + mergedStartFlagList.push(...specialStartFlag); + } + + if (mergedStartFlagList.includes(t.content)) { + startToken = t; + } + + if (defaultShowFlagList.some((text) => t.content.includes(text))) { + isShowFolder = true; + } + }); + + const mergedOptions = fold + ? { + specialEndFlag, + specialStartFlag, + } + : undefined; + const isFolder = checkIsFolder(token, mergedOptions); + + if (isFolder && startToken) { + const endIndex = findEndIndex(tokens, index + 1, mergedOptions); + + // Greater than or equal to folderLine then will folder otherwise it will show directly + if (endIndex !== -1 && (endIndex - index >= folderLine || isShowFolder || fold)) { + lastIndex = endIndex; + const folder = tokens.slice(index + 1, endIndex); + const endToken = tokens[endIndex]; + const ellipsisToken: TransformTokensTypes = { + types: ["ellipsis"], + content: "", + class: "custom-folder ellipsis-token", + }; + const copyContent: TransformTokensTypes = { + types: ["copy"], + content: "", + folderContent: folder, + class: "custom-folder copy-token", + }; + + endToken.forEach((t, _, arr) => { + let className = ""; + + className += "custom-folder"; + if (t.content.trim() === "" && (arr.length === 3 || arr.length === 4)) { + // Add length check to sure it's added to } token + className += " empty-token"; + } + (t as TransformTokensTypes).class = className; + }); + + startToken.types = ["folderStart"]; + (startToken as TransformTokensTypes).folderContent = folder; + (startToken as TransformTokensTypes).summaryContent = [ + ...token, + ellipsisToken, + copyContent, + ...endToken, + ]; + (startToken as TransformTokensTypes).index = index; + if (isShowFolder && !fold) { + (startToken as TransformTokensTypes).open = true; + } + + result.push([startToken]); + + isShowFolder = false; + fold = false; + + return; + } + } + token.forEach((t) => { + (t as TransformTokensTypes).index = index; + }); + result.push(token); + }); + + return result; +} + +interface SpecialOptions { + specialStartFlag?: string[]; + specialEndFlag?: string[]; +} + +function checkIsFolder( + token: TransformTokens[0], + {specialStartFlag, specialEndFlag}: SpecialOptions = {}, +) { + const stack: string[] = []; + const mergedStartFlagList = specialStartFlag ? [...startFlag, ...specialStartFlag] : startFlag; + const mergedEndFlagList = specialEndFlag ? [...endFlag, ...specialEndFlag] : endFlag; + + for (const t of token) { + if (mergedStartFlagList.includes(t.content)) { + stack.push(t.content); + } else if (mergedEndFlagList.includes(t.content)) { + stack.pop(); + } + } + + return stack.length !== 0; +} + +function findEndIndex( + tokens: TransformTokens, + startIndex: number, + {specialStartFlag, specialEndFlag}: SpecialOptions = {}, +) { + const stack: string[] = ["flag"]; + const mergedStartFlagList = specialStartFlag ? [...startFlag, ...specialStartFlag] : startFlag; + const mergedEndFlagList = specialEndFlag ? [...endFlag, ...specialEndFlag] : endFlag; + + for (let i = startIndex; i < tokens.length; i++) { + const token = tokens[i]; + + for (const line of token) { + const transformLine = line.content.replace(/\$/g, ""); + + if (mergedStartFlagList.includes(transformLine)) { + stack.push("flag"); + } else if (mergedEndFlagList.includes(transformLine)) { + stack.pop(); + } + + if (stack.length === 0) { + return i; + } + } + } + + return -1; +} diff --git a/apps/docs/components/mdx-components.tsx b/apps/docs/components/mdx-components.tsx index 1071270d37..b4586728ec 100644 --- a/apps/docs/components/mdx-components.tsx +++ b/apps/docs/components/mdx-components.tsx @@ -152,7 +152,12 @@ const Code = ({ }); }} > - + ); }; diff --git a/apps/docs/styles/globals.css b/apps/docs/styles/globals.css index 21033242d3..f17f8eee5d 100644 --- a/apps/docs/styles/globals.css +++ b/apps/docs/styles/globals.css @@ -96,3 +96,63 @@ z-index: 0; content: counter(step); } } + +pre details[open]>summary>span:first-child::before { + transform: rotate(90deg); +} + +pre details[open]>summary span.custom-folder { + display: none; +} + +pre details:not([open])>summary span.ellipsis-token::after { + content: '...'; +} + +pre details:not([open])>summary span.copy-token { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +pre details[open]>summary span.copy-token { + display: none; +} + +pre details:not([open]) { + display: inline; +} + +pre details:not([open])>summary span.empty-token { + display: none; +} + +pre details:not([open])+div.token-line { + display: none; +} + +pre summary { + display: inline; + position: relative; + list-style: none; +} + +pre summary>span:first-child::before { + position: absolute; + display: inline-flex; + align-items: center; + height: 21px; + margin-left: -16px; + margin-top: 1px; + content: "▶"; + font-size: 12px; + font-style: normal; + transition: transform 100ms; + color: #999; +} \ No newline at end of file diff --git a/apps/docs/styles/sandpack.css b/apps/docs/styles/sandpack.css index 56d23993e9..7c523c2bbe 100644 --- a/apps/docs/styles/sandpack.css +++ b/apps/docs/styles/sandpack.css @@ -26,6 +26,22 @@ max-height: 100%; overflow: hidden; } + +.sp-editor, +.sp-editor-viewer { + height: auto !important; +} + +.sp-editor { + & .token-line { + padding: 0px 24px; + } +} + +.sp-code-viewer { + padding: 6px; +} + .cm-scroller { overflow: hidden; max-height: 600px;