Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(docs): revise code block #3922

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 85 additions & 23 deletions apps/docs/components/docs/components/codeblock.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -142,21 +148,36 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
{...props}
>
{({className, style, tokens, getLineProps, getTokenProps}) => (
<div className="w-full" data-language={language}>
<pre
ref={ref}
className={clsx(className, classNameProp, "flex max-w-full", {
<pre
ref={ref}
className={clsx(
className,
classNameProp,
`language-${codeLang}`,
"max-w-full contents",
{
"flex-col": isMultiLine,
"scrollbar-hide overflow-x-scroll": hideScrollBar,
})}
style={style}
translate="no"
>
{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 (
<div
<Tag
{...omit(lineProps, ["key"])}
key={`${i}-${getUniqueID("line-wrapper")}`}
className={clsx(
Expand All @@ -167,25 +188,48 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
"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 && (
<span className="select-none text-xs mr-6 opacity-30">{i + 1}</span>
<span
className={cn(
"mr-6 select-none text-xs opacity-30",
i + 1 >= 10 ? "mr-4" : "",
i + 1 >= 100 ? "mr-2" : "",
i + 1 >= 1000 ? "mr-0" : "",
)}
>
{i + 1}
</span>
)}

{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 ? (
<span key={key} className="copy-token" style={{whiteSpace: "inherit"}}>
{token.folderContent?.map((folderTokens) => {
return folderTokens.map((token, index) => {
// Hack for wrap line
return token.content === "" ? (
<div key={`${index}-${getUniqueID("line")}`} />
) : (
<span key={`${index}-${getUniqueID("line")}`}>{token.content}</span>
);
});
})}
</span>
) : (
<span
{...omit(props, ["key"])}
key={`${key}-${getUniqueID("line")}`}
className={className}
className={cn(className, token.class)}
style={{
...props.style,
...(highlightStyleToken.some((t) => {
Expand All @@ -201,11 +245,29 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
/>
);
})}
</div>
</Tag>
);
})}
</pre>
</div>
};
const renderFolderTokens = (tokens: TransformTokensTypes[][]) => {
return tokens.map((token, key) => {
const index = key + folderLine.index! + 1;

return renderLine(token, index);
});
};

return isFolder ? (
<details key={`${folderLine.index}`} open={folderLine.open ? true : undefined}>
<summary className="cursor-pointer">
{renderLine(folderLine.summaryContent as any, folderLine.index!, "span")}
</summary>
{renderFolderTokens(folderLine.folderContent as any)}
</details>
) : (
renderLine(line, folderLine.index!)
);
})}
</pre>
)}
</BaseHighlight>
);
Expand Down
189 changes: 189 additions & 0 deletions apps/docs/components/docs/components/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type Highlight from "prism-react-renderer";

export type TransformTokens = Parameters<Highlight["props"]["children"]>[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"];
wingkwong marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
wingkwong marked this conversation as resolved.
Show resolved Hide resolved
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);
});
wingkwong marked this conversation as resolved.
Show resolved Hide resolved

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;
}
wingkwong marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion apps/docs/components/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,12 @@ const Code = ({
});
}}
>
<Codeblock codeString={codeString} language={language} metastring={meta} />
<Codeblock
className="sp-editor"
codeString={codeString}
language={language}
metastring={meta}
/>
</Components.Snippet>
);
};
Expand Down
Loading