Skip to content

Commit

Permalink
feat: code highlighting using CodeBlock (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Feb 11, 2024
1 parent 34d0312 commit 967083b
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 38 deletions.
36 changes: 36 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
1 change: 1 addition & 0 deletions packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-hook/size": "^2.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const CurlExample: React.FC<CurlExample.Props> = ({ curlLines, selectedPr
isSelected ? "bg-accent-highlight" : "bg-transparent",
)}
>
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-1" />}
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-0.5" />}
{renderJsonLine(part.line)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const JsonExample = React.memo<JsonExample.Props>(function JsonExample({
)}
key={i}
>
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-1" />}
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-0.5" />}
{renderJsonLine(line)}
</div>
);
Expand Down Expand Up @@ -81,7 +81,7 @@ export const JsonExampleVirtualized: React.FC<JsonExampleVirtualized.Props> = ({
isSelected ? "bg-accent-highlight" : "bg-transparent",
)}
>
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-1" />}
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-0.5" />}
{renderJsonLine(row)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const JsonExampleLine = React.forwardRef<HTMLDivElement, PropsWithChildre
)}
ref={ref}
>
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-1" />}
{isSelected && <div className="bg-accent absolute inset-y-0 left-0 w-0.5" />}
{" ".repeat(depth * TAB_WIDTH)}
{children}
</div>
Expand Down
81 changes: 77 additions & 4 deletions packages/ui/app/src/commons/CodeBlockSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import classNames from "classnames";
import { useTheme } from "next-themes";
import React from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import React, { CSSProperties } from "react";
import { createElement, Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import * as prism from "react-syntax-highlighter/dist/cjs/styles/prism";

// [number, number] is a range of lines to highlight
type HighlightLine = number | [number, number];

type CodeBlockSkeletonProps = {
className?: string;
language: string;
content: string;
usePlainStyles?: boolean;
fontSize: "sm" | "lg";
style?: React.CSSProperties;
highlightLines?: HighlightLine[];
highlightStyle?: "highlight" | "focus";
};

export const CodeBlockSkeleton: React.FC<CodeBlockSkeletonProps> = ({
Expand All @@ -20,14 +25,16 @@ export const CodeBlockSkeleton: React.FC<CodeBlockSkeletonProps> = ({
usePlainStyles = false,
fontSize,
style,
highlightLines,
highlightStyle,
}) => {
const { resolvedTheme: theme } = useTheme();
return (
<div
className={classNames(
"bg-gray-100/90 dark:bg-gray-950/90",
"font-mono",
{
"w-full border-l border-r border-b rounded-bl-lg rounded-br-lg border-default": !usePlainStyles,
"w-full rounded-bl-lg rounded-br-lg": !usePlainStyles,
},
className,
)}
Expand All @@ -40,13 +47,79 @@ export const CodeBlockSkeleton: React.FC<CodeBlockSkeletonProps> = ({
fontSize: fontSize === "sm" ? 12 : 14,
lineHeight: fontSize === "sm" ? "20px" : "24px",
}}
renderer={createHighlightRenderer(highlightLines, highlightStyle, theme as "light" | "dark")}
>
{content}
</FernSyntaxHighlighter>
</div>
);
};

function flattenHighlightLines(highlightLines: HighlightLine[]): number[] {
return highlightLines.flatMap((line) => {
if (Array.isArray(line)) {
const [start, end] = line;
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return [line];
});
}

function createHighlightRenderer(
highlightLines: HighlightLine[] | undefined,
highlightStyle: "highlight" | "focus" = "highlight",
theme: "light" | "dark",
) {
if (highlightLines == null || highlightLines.length === 0) {
return undefined;
}
const flattenedLines = flattenHighlightLines(highlightLines);
return function renderer({ rows, stylesheet, useInlineStyles }: rendererProps) {
return (
<>
{rows.map((row, i) =>
createElement({
node: row,
stylesheet,
style: createStyle(flattenedLines.includes(i), highlightStyle, theme),
useInlineStyles,
key: i,
}),
)}
</>
);
};
}

function createStyle(
isHighlighted: boolean,
highlightStyle: "highlight" | "focus",
theme: "light" | "dark",
): CSSProperties {
if (highlightStyle === "highlight") {
return {
display: "block",
marginLeft: "-16px",
marginRight: "-16px",
paddingLeft: "14px",
paddingRight: "16px",
borderLeft: isHighlighted
? theme === "light"
? "2px solid rgb(var(--accent-primary-light))"
: "2px solid rgb(var(--accent-primary-dark))"
: "2px solid transparent",
backgroundColor: isHighlighted
? theme === "light"
? "rgba(var(--accent-primary-light), 20%)"
: "rgba(var(--accent-primary-dark), 20%)"
: "unset",
};
}
return {
opacity: isHighlighted ? 1 : 0.33,
};
}

export const FernSyntaxHighlighter: React.FC<React.ComponentProps<typeof SyntaxHighlighter>> = (props) => {
const { resolvedTheme: theme } = useTheme();
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const CodeBlockWithClipboardButton: React.FC<CodeBlockWithClipboardButton
content,
}) => {
return (
<div className="group/cb-container border-default relative mb-5 flex w-full rounded-t-lg border-t">
<div className="group/cb-container bg-tag-default-soft after:ring-border-default relative mb-5 flex w-full rounded-lg after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-1 after:ring-inset after:content-['']">
<CopyToClipboardButton
className={classNames("absolute", "transition opacity-0 group-hover/cb-container:opacity-100", {
"right-0.5 top-0.5": variant === "sm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`CopyToClipboardButton renders correctly 1`] = `
<button
aria-disabled={true}
className="group cursor-pointer fern-button minimal disabled rounded square"
data-state="off"
data-testid="copy-btn"
disabled={true}
>
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/app/src/mdx/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@ export declare namespace CodeBlock {
export interface Props {
title?: string;
children: unknown;
highlightLines?: (number | [number, number])[];
highlightStyle?: "highlight" | "focus";
}
}

export const CodeBlock: React.FC<React.PropsWithChildren<CodeBlock.Props>> = ({ title, children }) => {
return <_CodeBlocks items={[transformCodeBlockChildrenToCodeBlockItem(title, children)]} />;
export const CodeBlock: React.FC<React.PropsWithChildren<CodeBlock.Props>> = ({
title,
children,
highlightLines,
highlightStyle,
}) => {
return (
<_CodeBlocks
items={[transformCodeBlockChildrenToCodeBlockItem(title, children, highlightLines, highlightStyle)]}
/>
);
};
62 changes: 36 additions & 26 deletions packages/ui/app/src/mdx/components/_CodeBlocks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import classNames from "classnames";
import * as Tabs from "@radix-ui/react-tabs";
import dynamic from "next/dynamic";
import { useState } from "react";
import { CopyToClipboardButton } from "../../commons/CopyToClipboardButton";
Expand All @@ -22,32 +22,42 @@ export const _CodeBlocks: React.FC<React.PropsWithChildren<_CodeBlocks.Props>> =
return null;
}
return (
<div className="mb-5 w-full min-w-0 max-w-full">
<div className="border-default flex items-center justify-between rounded-t-lg border bg-gray-200/90 dark:bg-[#19181C]">
<div className="flex overflow-x-auto">
{items.map((item, idx) => (
<button
className={classNames("border-b py-2.5 px-4 transition text-xs", {
"t-accent border-accent-primary": selectedTabIndex === idx,
"t-muted border-transparent hover:t-accent hover:dark:text-text-default-dark":
selectedTabIndex !== idx,
})}
key={idx}
onClick={() => setSelectedTabIndex(idx)}
>
{item.title}
</button>
))}
</div>
<Tabs.Root
className="after:ring-border-default relative mb-5 w-full min-w-0 max-w-full after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-1 after:ring-inset after:content-['']"
onValueChange={(value) => setSelectedTabIndex(parseInt(value, 10))}
defaultValue="0"
>
<div className="bg-tag-default rounded-t-lg">
<div className="shadow-border-default mx-px flex min-h-10 items-center justify-between shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)]">
<Tabs.List className="flex min-h-10 overflow-x-auto">
{items.map((item, idx) => (
<Tabs.Trigger
key={idx}
value={idx.toString()}
className="data-[state=active]:shadow-accent group min-h-10 px-2 py-1.5 data-[state=active]:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.1)]"
>
<span className="t-muted group-data-[state=active]:t-default group-hover:bg-tag-default rounded px-2 py-1 text-sm group-data-[state=active]:font-semibold">
{item.title}
</span>
</Tabs.Trigger>
))}
</Tabs.List>

<CopyToClipboardButton className="ml-2 mr-1" content={codeBlockItem.content} />
<CopyToClipboardButton className="ml-2 mr-1" content={codeBlockItem.content} />
</div>
</div>
<CodeBlockSkeleton
className="max-h-[350px] overflow-y-auto"
language={codeBlockItem.language}
content={codeBlockItem.content}
fontSize="lg"
/>
</div>
{items.map((item, idx) => (
<Tabs.Content value={idx.toString()} key={idx}>
<CodeBlockSkeleton
className="bg-tag-default-soft max-h-[350px] overflow-y-auto"
language={item.language}
content={item.content}
highlightLines={item.highlightLines}
highlightStyle={item.highlightStyle}
fontSize="lg"
/>
</Tabs.Content>
))}
</Tabs.Root>
);
};
2 changes: 2 additions & 0 deletions packages/ui/app/src/mdx/components/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export interface CodeBlockItem {
language: string;
title: string;
content: string;
highlightLines?: (number | [number, number])[];
highlightStyle?: "highlight" | "focus";
}
9 changes: 8 additions & 1 deletion packages/ui/app/src/mdx/components/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ const fallbackItemForBadlyFormattedCodeBlock: CodeBlockItem = {
/**
* Transforms the user-provided `CodeBlock` to a `CodeBlockItem` with a cleaner interface
*/
export function transformCodeBlockChildrenToCodeBlockItem(title: string | undefined, children: unknown): CodeBlockItem {
export function transformCodeBlockChildrenToCodeBlockItem(
title: string | undefined,
children: unknown,
highlightLines?: (number | [number, number])[],
highlightStyle?: "highlight" | "focus",
): CodeBlockItem {
if (!isExpectedCodeBlockChildren(children)) {
return fallbackItemForBadlyFormattedCodeBlock;
}
Expand All @@ -81,6 +86,8 @@ export function transformCodeBlockChildrenToCodeBlockItem(title: string | undefi
language,
title: title ?? language,
content: children?.props?.children?.props?.children ?? DEFAULT_CODE_BLOCK_CONTENT,
highlightLines,
highlightStyle,
};
}

Expand Down
3 changes: 3 additions & 0 deletions packages/ui/public-docs-bundle/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ module.exports = {
".border-danger": {
"@apply border-border-danger": {},
},
".shadow-accent": {
"@apply shadow-accent-primary-light dark:shadow-accent-primary-dark": {},
},
".outline-accent-primary": {
"@apply outline-accent-primary-light dark:outline-accent-primary-dark": {},
},
Expand Down
Loading

0 comments on commit 967083b

Please sign in to comment.