Skip to content

Commit

Permalink
refactor(docs): revise code block (#3922)
Browse files Browse the repository at this point in the history
* refactor(docs): revise code block

* chore(docs): resolve pr comments
  • Loading branch information
wingkwong authored Nov 5, 2024
1 parent 8de427b commit 93f274b
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 24 deletions.
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"];

/**
* 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;
}
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

0 comments on commit 93f274b

Please sign in to comment.