2023-04-30 21:34:36 -03:00

167 lines
4.4 KiB
TypeScript

// Inspired by https://github.dev/modulz/stitches-site code demo
import React from "react";
import refractor from "refractor/core";
import js from "refractor/lang/javascript";
import jsx from "refractor/lang/jsx";
import bash from "refractor/lang/bash";
import css from "refractor/lang/css";
import diff from "refractor/lang/diff";
import hastToHtml from "hast-util-to-html";
import rangeParser from "parse-numeric-range";
import {clsx} from "@nextui-org/shared-utils";
import {Pre} from "./pre";
import {WindowActions} from "./window-actions";
import highlightLine from "@/libs/rehype-highlight-line";
import highlightWord from "@/libs/rehype-highlight-word";
refractor.register(js);
refractor.register(jsx);
refractor.register(bash);
refractor.register(css);
refractor.register(diff);
type PreProps = Omit<React.ComponentProps<typeof Pre>, "css">;
export type CodeBlockProps = PreProps & {
language: "js" | "jsx" | "bash" | "css" | "diff";
title?: string;
value?: string;
highlightLines?: string;
mode?: "static" | "typewriter";
showLineNumbers?: boolean;
showWindowIcons?: boolean;
className?: string;
};
/**
* recursively get all text nodes as an array for a given element
*/
function getTextNodes(node: any): any[] {
let childTextNodes = [];
if (!node.hasChildNodes()) return [];
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeType == Node.TEXT_NODE) {
childTextNodes.push(childNodes[i]);
} else if (childNodes[i].nodeType == Node.ELEMENT_NODE) {
Array.prototype.push.apply(childTextNodes, getTextNodes(childNodes[i]));
}
}
return childTextNodes;
}
/**
* given a text node, wrap each character in the
* given tag.
*/
function wrapEachCharacter(textNode: any, tag: string, count: number) {
const text = textNode.nodeValue;
const parent = textNode.parentNode;
const characters = text.split("");
characters.forEach(function (character: any, letterIndex: any) {
const delay = (count + letterIndex) * 50;
var element = document.createElement(tag);
var characterNode = document.createTextNode(character);
element.appendChild(characterNode);
element.style.opacity = "0";
element.style.transition = `all ease 0ms ${delay}ms`;
parent.insertBefore(element, textNode);
// skip a couple of frames to trigger transition
requestAnimationFrame(() => requestAnimationFrame(() => (element.style.opacity = "1")));
});
parent.removeChild(textNode);
}
function CodeTypewriter({value, className, css, ...props}: any) {
const wrapperRef = React.useRef(null);
React.useEffect(() => {
const wrapper = wrapperRef.current as any;
if (wrapper) {
var allTextNodes = getTextNodes(wrapper);
let count = 0;
allTextNodes?.forEach((textNode) => {
wrapEachCharacter(textNode, "span", count);
count = count + textNode.nodeValue.length;
});
wrapper.style.opacity = "1";
}
return () => (wrapper.innerHTML = value);
}, []);
return (
<Pre className={className} css={css} {...props}>
<code
ref={wrapperRef}
className={className}
dangerouslySetInnerHTML={{__html: value}}
style={{opacity: 0}}
/>
</Pre>
);
}
const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>((_props, forwardedRef) => {
const {
language,
value,
title,
highlightLines = "0",
className = "",
mode,
showLineNumbers,
showWindowIcons,
...props
} = _props;
let result: any = refractor.highlight(value || "", language);
result = highlightLine(result, rangeParser(highlightLines));
result = highlightWord(result);
// convert to html
result = hastToHtml(result);
// TODO reset theme
const classes = `language-${language}`;
const codeClasses = clsx("absolute w-full", showWindowIcons ? "top-8" : "top-0");
if (mode === "typewriter") {
return <CodeTypewriter className={classes} css={css} value={result} {...props} />;
}
return (
<Pre
ref={forwardedRef}
className={clsx("code-block", classes, showWindowIcons ? "pt-8" : "", className)}
data-line-numbers={showLineNumbers}
{...props}
>
<code className={clsx(classes, codeClasses)} dangerouslySetInnerHTML={{__html: result}} />
{showWindowIcons && <WindowActions title={title} />}
</Pre>
);
});
CodeBlock.displayName = "NextUI - CodeBlock";
export default CodeBlock;