UNPKG

@nvq/flowtoken

Version:

Animated React components for streaming text and markdown with GitHub theme syntax highlighting (forked from flowtoken)

228 lines (227 loc) 13.3 kB
"use strict"; "use client"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importStar(require("react")); const react_markdown_1 = __importDefault(require("react-markdown")); const remark_gfm_1 = __importDefault(require("remark-gfm")); const rehype_raw_1 = __importDefault(require("rehype-raw")); const docco_1 = __importDefault(require("react-syntax-highlighter/dist/esm/styles/hljs/docco")); const SplitText_1 = __importDefault(require("./SplitText")); const AnimatedImage_1 = __importDefault(require("./AnimatedImage")); const animations_1 = require("../utils/animations"); const DefaultCode_1 = __importDefault(require("./DefaultCode")); const DEFAULT_CUSTOM_COMPONENTS = {}; // Function to create animation style object - extracted outside of component const createAnimationStyle = (animation, animationDuration, animationTimingFunction) => ({ animation: animation ? `${animation} ${animationDuration} ${animationTimingFunction}` : "none", }); // Memoized component for text elements to avoid creating many instances const MemoizedText = react_1.default.memo(({ children, animation, animationDuration, animationTimingFunction, sep, }) => (react_1.default.createElement(SplitText_1.default, { input: children, sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }))); // Optimize rendering for lists to reduce memory usage const MemoizedList = react_1.default.memo(({ children, style, }) => (react_1.default.createElement("li", { className: "custom-li", style: style }, children))); const MarkdownAnimateText = ({ content, sep = "diff", animation: animationName = "none", animationDuration = "1s", animationTimingFunction = "ease-in-out", codeStyle = null, customComponents = DEFAULT_CUSTOM_COMPONENTS, imgHeight = "20rem", isStreaming = false, }) => { const animation = animations_1.animations[animationName] || animationName; codeStyle = codeStyle || docco_1.default.docco; // Memoize the animation style to avoid recreating it on every render const animationStyle = (0, react_1.useMemo)(() => createAnimationStyle(animation, animationDuration, animationTimingFunction), [animation, animationDuration, animationTimingFunction]); // Enhanced hidePartialCustomComponents function that also handles tag attributes const hidePartialCustomComponents = react_1.default.useCallback((input) => { if (!input || Object.keys(customComponents).length === 0) return input; // Check for any opening tag without a closing '>' const lastOpeningBracketIndex = input.lastIndexOf("<"); if (lastOpeningBracketIndex !== -1) { const textAfterLastOpeningBracket = input.substring(lastOpeningBracketIndex); // If there's no closing bracket, then it's potentially a partial tag if (!textAfterLastOpeningBracket.includes(">")) { // Check if it starts with any of our custom component names for (const tag of Object.keys(customComponents)) { // Check if the text starts with the tag name (allowing for partial tag name) // For example, '<Cus' would match a component named 'CustomTag' if (textAfterLastOpeningBracket .substring(1) .startsWith(tag.substring(0, textAfterLastOpeningBracket.length - 1)) || // Or it's a complete tag name followed by attributes textAfterLastOpeningBracket.match(new RegExp(`^<${tag}(\\s|$)`))) { // Remove the partial tag return input.substring(0, lastOpeningBracketIndex); } } } } return input; }, [customComponents]); // Stable img component function to prevent re-rendering const imgComponent = react_1.default.useCallback((_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement(AnimatedImage_1.default, { src: props.src, height: imgHeight, alt: props.alt, animation: animation || "", animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 })); }, [animation, animationDuration, animationTimingFunction, imgHeight]); // V2: Simplified animateText function for SplitTextV4 compatibility const animateText = react_1.default.useCallback((text) => { text = Array.isArray(text) ? text : [text]; if (!animation) return text; return text.map((item, index) => { if (typeof item === "string") { // Let SplitTextV4 handle all string animation and cleanup return (react_1.default.createElement(MemoizedText, { key: `text-${index}`, children: hidePartialCustomComponents(item), animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, sep: sep })); } else if (react_1.default.isValidElement(item)) { // Check if the React element is one of the types we don't want to animate const noAnimateElementTypes = [ "br", "ul", "ol", "td", "th", "pre", ]; let typeName = item.type; if (typeof typeName === "function") { typeName = typeName.name; } if (typeof typeName === "string" && noAnimateElementTypes.includes(typeName)) { // Render these elements directly without an animation wrapper return item; } // V2: No extra span wrapper - let SplitTextV4 handle animation return react_1.default.cloneElement(item, { key: `element-${index}` }); } // V2: Return raw content without wrapper spans return item; }); }, [ animation, animationDuration, animationTimingFunction, sep, hidePartialCustomComponents, ]); // Memoize components object to avoid recreating on every render // Using proper React types instead of trying to import Components type const components = (0, react_1.useMemo)(() => (Object.assign({ // Handle text node with specific memoization for performance text: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return animateText(props.children); }, h1: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h1", Object.assign({}, props), animateText(props.children))); }, h2: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h2", Object.assign({}, props), animateText(props.children))); }, h3: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h3", Object.assign({}, props), animateText(props.children))); }, h4: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h4", Object.assign({}, props), animateText(props.children))); }, h5: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h5", Object.assign({}, props), animateText(props.children))); }, h6: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("h6", Object.assign({}, props), animateText(props.children))); }, p: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("p", Object.assign({}, props), animateText(props.children))); }, li: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("li", Object.assign({}, props, { className: "ft-custom-li" }), animateText(props.children))); }, a: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("a", Object.assign({}, props, { href: props.href, target: "_blank", rel: "noopener noreferrer" }), animateText(props.children))); }, strong: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("strong", Object.assign({}, props), animateText(props.children))); }, em: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("em", Object.assign({}, props), animateText(props.children))); }, code: (_a) => { var { node, className, children } = _a, props = __rest(_a, ["node", "className", "children"]); return (react_1.default.createElement(DefaultCode_1.default, Object.assign({ node: node, className: className, style: animationStyle, codeStyle: codeStyle, animateText: animateText, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction }, props), children)); }, hr: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return react_1.default.createElement("hr", Object.assign({}, props)); }, img: imgComponent, table: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("table", Object.assign({}, props), props.children)); }, tr: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("tr", Object.assign({}, props), animateText(props.children))); }, td: (_a) => { var { node } = _a, props = __rest(_a, ["node"]); return (react_1.default.createElement("td", Object.assign({}, props), animateText(props.children))); } }, Object.entries(customComponents).reduce((acc, [key, value]) => { acc[key] = (elements) => value(Object.assign(Object.assign({}, elements), { animateText })); return acc; }, {}))), [ animateText, customComponents, animation, animationDuration, animationTimingFunction, animationStyle, codeStyle, imgHeight, imgComponent, ]); // Optimize for large content by chunking if needed const optimizedContent = (0, react_1.useMemo)(() => { // For extremely large content (>50KB), we could implement chunking or virtualization here return content; }, [content]); return (react_1.default.createElement(react_markdown_1.default, { components: components, remarkPlugins: [remark_gfm_1.default], rehypePlugins: [rehype_raw_1.default] }, optimizedContent)); }; // Wrap the entire component in React.memo to prevent unnecessary rerenders exports.default = react_1.default.memo(MarkdownAnimateText);