mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
167 lines
4.4 KiB
TypeScript
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;
|