Skip to content

Commit

Permalink
wip... punting on line numbers and syntax highlighting for now
Browse files Browse the repository at this point in the history
  • Loading branch information
cody-dot-js committed Oct 19, 2023
1 parent 85016ff commit 1e3b535
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 84 deletions.
53 changes: 44 additions & 9 deletions src/components/code-block/code-block.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";

import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockHeader } from ".";
import { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle } from ".";

const meta = {
title: "CodeBlock",
Expand All @@ -25,13 +25,47 @@ const CodeIcon = () => (

export const WithHeaderHighlightsAndHighlights: Story = {
render: () => (
<div className="max-w-96 mx-auto">
<div className="mx-auto max-w-screen-md">
<CodeBlock>
<CodeBlockHeader className="flex items-center gap-2">
<CodeIcon />
hello.js
<CodeBlockTitle>ngrok-example.js</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockBody>
<CodeBlockCopyButton />
<CodeBlockContent language="js" showLineNumbers highlightLines={[1, 2, "10-13"]}>
{`
const http = require('http');
const ngrok = require("@ngrok/ngrok");
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end("Hello!");
});
// Consumes authtoken from env automatically
ngrok.listen(server).then(() => {
console.log("url:", server.tunnel.url());
});
// really long line here that should wrap around and stuff Officia ipsum sint eu labore esse deserunt aliqua quis irure.
`}
</CodeBlockContent>
</CodeBlockBody>
</CodeBlock>
</div>
),
};

export const WithHeaderHighlightsAndHighlightsScrollyBoi: Story = {
render: () => (
<div className="mx-auto max-w-screen-md">
<CodeBlock className="h-40">
<CodeBlockHeader className="flex items-center gap-2">
<CodeIcon />
<CodeBlockTitle>ngrok-example.js</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockBody>
<CodeBlockCopyButton />
<CodeBlockContent language="js" showLineNumbers highlightLines={[1, 2, "10-12"]}>
{`
const http = require('http');
Expand All @@ -56,11 +90,11 @@ ngrok.listen(server).then(() => {

export const WithHeaderAndLineNumbers: Story = {
render: () => (
<div className="max-w-96 mx-auto">
<div className="mx-auto max-w-screen-md">
<CodeBlock>
<CodeBlockHeader className="flex items-center gap-2">
<CodeIcon />
hello.js
<CodeBlockTitle>ngrok-example.js</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockBody>
<CodeBlockContent language="js" showLineNumbers>
Expand All @@ -77,6 +111,7 @@ const server = http.createServer((req, res) => {
ngrok.listen(server).then(() => {
console.log("url:", server.tunnel.url());
});
// really long line here that should wrap around and stuff Officia ipsum sint eu labore esse deserunt aliqua quis irure.
`}
</CodeBlockContent>
</CodeBlockBody>
Expand All @@ -87,11 +122,11 @@ ngrok.listen(server).then(() => {

export const WithHeaderNoLineNumbers: Story = {
render: () => (
<div className="max-w-96 mx-auto">
<div className="mx-auto max-w-screen-md">
<CodeBlock>
<CodeBlockHeader className="flex items-center gap-2">
<CodeIcon />
hello.js
<CodeBlockTitle>ngrok-example.js</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockBody>
<CodeBlockContent language="js">
Expand All @@ -118,7 +153,7 @@ ngrok.listen(server).then(() => {

export const WithoutHeaderNoLineNumbers: Story = {
render: () => (
<div className="max-w-96 mx-auto">
<div className="mx-auto max-w-screen-md">
<CodeBlock>
<CodeBlockBody>
<CodeBlockContent language="js">
Expand All @@ -145,7 +180,7 @@ ngrok.listen(server).then(() => {

export const WithoutHeaderNoLineNumbersButHighlights: Story = {
render: () => (
<div className="max-w-96 mx-auto">
<div className="mx-auto max-w-screen-md">
<CodeBlock>
<CodeBlockBody>
<CodeBlockContent language="js" highlightLines={[1, 2, "10-12"]}>
Expand Down
177 changes: 102 additions & 75 deletions src/components/code-block/index.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
import { PropsWithChildren, createContext, forwardRef, useContext, useEffect, useState } from "react";
import { HTMLAttributes, createContext, forwardRef, useContext, useEffect, useState } from "react";
import type { WithStyleProps } from "@/types/with-style-props";
import { cx } from "@/lib/cx";
import { SupportedLanguage } from "./utils/supported-languages";
import { formatLanguageClassName } from "./utils/format-language-classname";
import { LineRange, resolveLineNumbers } from "./utils/line-numbers";
import { Slot } from "@radix-ui/react-slot";

/**
* TODO(cody):
* - implement syntax highlighting w/ prism or highlightjs (spike on both, figure out which is easier/less bs)
* - fix overflow-y-auto on CodeBlockBody
* - fix line numbers, maybe try grid instead of :before and flex?
* - fix line hightlighting
* - fix line wrapping? horizontal scrolling has problems w/ line highlighting :(
*/

type CodeBlockContextType = (newCopyText: string) => void;

const CodeBlockContext = createContext<CodeBlockContextType>(() => {});

const CodeBlockCopyContext = createContext<string>("");

type CodeBlockProps = PropsWithChildren & WithStyleProps;

const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(({ className, children, style }, ref) => {
const CodeBlock = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
const [copyText, setCopyText] = useState("");

return (
<CodeBlockContext.Provider value={setCopyText}>
<CodeBlockCopyContext.Provider value={copyText}>
<div className={cx("relative rounded-md border border-gray-200 bg-gray-50", className)} ref={ref} style={style}>
{children}
</div>
<div
className={cx("overflow-hidden rounded-md border border-gray-200 bg-gray-50", className)}
ref={ref}
{...props}
/>
</CodeBlockCopyContext.Provider>
</CodeBlockContext.Provider>
);
});
CodeBlock.displayName = "CodeBlock";

type CodeBlockBodyProps = WithStyleProps & PropsWithChildren;

const CodeBlockBody = forwardRef<HTMLDivElement, CodeBlockBodyProps>(({ className, children, style }, ref) => (
<div className={cx("relative", className)} ref={ref} style={style}>
<CodeBlockCopyButton className="absolute right-2 top-2" />
{children}
</div>
const CodeBlockBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div className={cx("relative h-full", className)} ref={ref} {...props} />
));
CodeBlockBody.displayName = "CodeBlockBody";

Expand All @@ -45,102 +50,124 @@ type CodeBlockContentProps = WithStyleProps & {
showLineNumbers?: boolean;
};

const CodeBlockContent = forwardRef<HTMLPreElement, CodeBlockContentProps>(
({ children, className, highlightLines, language, showLineNumbers, style }, ref) => {
const setCopyText = useContext(CodeBlockContext);
const CodeBlockContent = forwardRef<HTMLPreElement, CodeBlockContentProps>((props, ref) => {
const { children, className, /* highlightLines, */ language = "sh", /* showLineNumbers = false, */ style } = props;
const highlightLines = undefined; // debug only, punting for now
const showLineNumbers = false; // debug only, punting for now

// trim any leading and trailing whitespace/empty lines
const trimmedCode = children?.trim() ?? "";
const lines = trimmedCode.split("\n");
const setCopyText = useContext(CodeBlockContext);

const highlightLineNumberSet = resolveLineNumbers(...(highlightLines ?? []));
// trim any leading and trailing whitespace/empty lines
const trimmedCode = children?.trim() ?? "";
const lines = trimmedCode.split("\n");

useEffect(() => {
setCopyText(trimmedCode);
}, [trimmedCode, setCopyText]);
const highlightLineNumberSet = resolveLineNumbers(...(highlightLines ?? []));

return (
<pre className={cx(formatLanguageClassName(language), "flex py-4 font-mono", className)} ref={ref} style={style}>
{showLineNumbers && (
<div aria-hidden className="pointer-events-none flex-shrink-0 select-none text-right">
{lines.map((line, index) => {
const lineNumber = index + 1;
const shouldHighlight = highlightLineNumberSet.has(lineNumber);

return (
<span
key={line + lineNumber}
className={cx(
"block border-r px-2 text-gray-400",
shouldHighlight && "border-l-4 border-l-blue-200 bg-blue-100 text-gray-600",
)}
>
{lineNumber}
</span>
);
})}
</div>
)}
<code className="block min-w-0 flex-1">
useEffect(() => {
setCopyText(trimmedCode);
}, [trimmedCode, setCopyText]);

return (
<pre
className={cx(
formatLanguageClassName(language),
"block h-full overflow-auto py-4 font-mono text-sm leading-normal",
className,
)}
data-lang={language}
data-line-numbers={showLineNumbers || undefined}
ref={ref}
style={style}
>
{/* TODO(cody): maybe retry this, but use grid instead? */}
{/* {showLineNumbers && (
<div aria-hidden className="pointer-events-none flex-shrink-0 select-none text-right">
{lines.map((line, index) => {
const lineNumber = index + 1;
const shouldHighlight = highlightLineNumberSet.has(lineNumber);
return (
<span
key={line + lineNumber}
data-line-number={lineNumber}
data-highlight={shouldHighlight || undefined}
className={cx("block px-4", shouldHighlight && "bg-blue-100")}
ref={ref}
style={style}
className={cx(
"block border-r px-2 text-gray-400",
shouldHighlight && "border-l-4 border-l-brand-primary-200 bg-brand-primary-100 ",
)}
>
{line === "" ? "\n" : line}
{lineNumber}
</span>
);
})}
</code>
</pre>
);
},
);
</div>
)} */}
<code className="">
{lines.map((line, index) => {
const lineNumber = index + 1;
const shouldHighlight = highlightLineNumberSet.has(lineNumber);

return (
<span
key={line + lineNumber}
data-line-number={showLineNumbers ? lineNumber : undefined}
data-highlight={shouldHighlight || undefined}
className={cx(
"relative block whitespace-pre-wrap before:sticky before:left-0 before:inline-block before:border-brand-primary-200 before:bg-gray-50 before:text-right before:text-gray-400",
showLineNumbers ? "before:w-14 before:pr-4 before:content-[attr(data-line-number)]" : "px-4",
shouldHighlight && "bg-brand-primary-100 before:border-l-4 before:bg-brand-primary-100",
)}
>
{line === "" ? "\n" : line}
</span>
);
})}
</code>
</pre>
);
});
CodeBlockContent.displayName = "CodeBlockContent";

const CodeBlockHeader = forwardRef<HTMLDivElement, PropsWithChildren & WithStyleProps>(
({ children, className, style }, ref) => (
<div
className={cx("border-bottom border-gray-300 bg-gray-100 px-4 py-2 font-mono text-base text-gray-700", className)}
ref={ref}
style={style}
>
{children}
</div>
),
);
const CodeBlockHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div
className={cx(
"flex-shrink-0 border-b border-gray-200 bg-gray-100 px-4 py-2 font-mono text-base text-gray-700",
className,
)}
ref={ref}
{...props}
/>
));
CodeBlockHeader.displayName = "CodeBlockHeader";

const CodeBlockTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement> & { asChild?: boolean }>(
({ asChild = false, className, ...props }, ref) => {
const Comp = asChild ? Slot : "h3";
return <Comp ref={ref} className={cx("font-mono text-[13px] font-normal leading-[22px]", className)} {...props} />;
},
);
CodeBlockTitle.displayName = "CodeBlockTitle";

type CodeBlockCopyButtonProps = WithStyleProps & {
onCopy?: (value: string) => void;
onCopyError?: (error: unknown) => void;
};

const CodeBlockCopyButton = forwardRef<HTMLButtonElement, CodeBlockCopyButtonProps>(
({ className, onCopy, onCopyError, style }, ref) => {
const ctx = useContext(CodeBlockCopyContext);
const [, setCopied] = useState(false);
const copyText = useContext(CodeBlockCopyContext);
const [, setCopied] = useState(false); // todo: useme

return (
<button
type="button"
className={cx("p-2", className)}
className={cx("absolute right-2 top-2 z-50 p-2", className)}
ref={ref}
style={style}
onClick={() => {
window.navigator.clipboard
.writeText(ctx)
.writeText(copyText)
.then(() => {
setCopied(true);
onCopy?.(ctx);
onCopy?.(copyText);
setTimeout(() => {
setCopied(false);
}, 1000);
Expand All @@ -157,7 +184,7 @@ const CodeBlockCopyButton = forwardRef<HTMLButtonElement, CodeBlockCopyButtonPro
);
CodeBlockCopyButton.displayName = "CodeBlockCopyButton";

export { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader };
export { CodeBlock, CodeBlockBody, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle };

const CopyIcon = ({ className, style }: WithStyleProps) => (
<svg
Expand Down

0 comments on commit 1e3b535

Please sign in to comment.