@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
JavaScript
;
"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);