flowtoken-next
Version:

298 lines (297 loc) • 18.7 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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.customCodeRenderer = void 0;
const react_1 = __importStar(require("react"));
const react_markdown_1 = __importDefault(require("react-markdown"));
const remark_gfm_1 = __importDefault(require("remark-gfm"));
const react_syntax_highlighter_1 = require("react-syntax-highlighter");
const docco_1 = __importDefault(require("react-syntax-highlighter/dist/esm/styles/hljs/docco"));
const regex_splitter_1 = require("../utils/regex-splitter");
const styles_1 = require("../styles");
const AnimatedImage = ({ src, alt, animation, animationDuration, animationTimingFunction, animationIterationCount }) => {
const [isLoaded, setIsLoaded] = react_1.default.useState(false);
const imageStyle = isLoaded ? {
animationName: animation,
animationDuration: animationDuration,
animationTimingFunction: animationTimingFunction,
animationIterationCount: animationIterationCount,
whiteSpace: 'pre-wrap',
} : {
display: 'none',
};
return (react_1.default.createElement("img", { src: src, alt: alt, onLoad: () => setIsLoaded(true), style: imageStyle }));
};
const TokenizedText = ({ input, sep, animation, animationDuration, animationTimingFunction, animationIterationCount }) => {
const tokens = react_1.default.useMemo(() => {
if (react_1.default.isValidElement(input))
return [input];
if (typeof input !== 'string')
return null;
let splitRegex;
if (sep === 'word') {
splitRegex = /(\s+)/;
}
else if (sep === 'char') {
splitRegex = /(.)/;
}
else {
throw new Error('Invalid separator');
}
return input.split(splitRegex).filter(token => token.length > 0);
}, [input, sep]);
return (react_1.default.createElement(react_1.default.Fragment, null, tokens?.map((token, index) => (react_1.default.createElement("span", { key: `token-${index}-${typeof token === 'string' ? token.charCodeAt(0) : index}`, style: {
animationName: animation,
animationDuration,
animationTimingFunction,
animationIterationCount,
whiteSpace: 'pre-wrap',
display: 'inline-block',
} }, token)))));
};
const customCodeRenderer = ({ animation, animationDuration, animationTimingFunction }) => {
return ({ rows, stylesheet, useInlineStyles }) => rows.map((node, i) => (react_1.default.createElement("div", { key: `code-row-${i}`, style: node.properties?.style || {} }, node.children.map((token, tokenIndex) => {
// Extract and apply styles from the stylesheet if available and inline styles are used
const tokenStyles = useInlineStyles && stylesheet ? { ...stylesheet[token?.properties?.className[1]], ...token.properties?.style } : token.properties?.style || {};
return (react_1.default.createElement("span", { key: `token-${i}-${tokenIndex}`, style: tokenStyles }, token.children && token.children[0].value.split(' ').map((word, wordIndex) => (react_1.default.createElement("span", { key: `word-${i}-${tokenIndex}-${wordIndex}`, style: {
animationName: animation || '',
animationDuration,
animationTimingFunction,
animationIterationCount: 1,
whiteSpace: 'pre-wrap',
display: 'inline-block',
} }, word + (wordIndex < token.children[0].value.split(' ').length - 1 ? ' ' : ''))))));
}))));
};
exports.customCodeRenderer = customCodeRenderer;
const MarkdownAnimateText = ({ content, sep = "word", animation = "fadeIn", animationDuration = "1s", animationTimingFunction = "ease-in-out", codeStyle = null, htmlComponents = {}, customComponents = {}, }) => {
// Inject styles on component mount
(0, react_1.useEffect)(() => {
(0, styles_1.injectStyles)();
}, []);
customComponents = react_1.default.useMemo(() => {
return Object.entries(customComponents).reduce((acc, [pattern, component]) => {
if (!pattern.startsWith('/') && !pattern.endsWith('/')) {
// Convert simple component name to HTML-style tag pattern
const regexPattern = `/<${pattern}.*\\s*\\/\\>/`;
acc[regexPattern] = component;
delete acc[pattern];
}
return acc;
}, {});
}, [customComponents]);
codeStyle = codeStyle || docco_1.default.docco;
const animationStyle = {
'--marker-animation': `${animation} ${animationDuration} ${animationTimingFunction}`,
};
// Add this new memoized function
const generatePatterns = react_1.default.useMemo(() => {
const generatePartialPatterns = (pattern) => {
const components = (0, regex_splitter_1.splitRegexPattern)(pattern);
return components.reduce((acc, _, index) => {
if (index < components.length - 1) {
acc.push(components.slice(0, index + 1).join(''));
}
return acc;
}, []);
};
const fullPatterns = Object.keys(customComponents).map(pattern => new RegExp(pattern.slice(1, -1)));
const partialPatterns = fullPatterns.flatMap(generatePartialPatterns)
.sort((a, b) => b.length - a.length);
return { fullPatterns, partialPatterns };
}, [customComponents]);
const processCustomComponents = react_1.default.useCallback((text) => {
if (text === '\n')
return [];
const { fullPatterns, partialPatterns } = generatePatterns;
// Process the entire text as it ends with a complete pattern
let remainingText = text;
// Split text by full matches
const regex = new RegExp(`(${fullPatterns.map(pattern => pattern.source).join('|')})`, 'g');
let parts = [];
let lastIndex = 0;
if (fullPatterns.length === 0) {
return [
react_1.default.createElement(TokenizedText, { key: "single-tokenized-text", input: text, sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }),
];
}
// Use matchAll to find each match and its position
const matches = Array.from(remainingText.matchAll(regex));
matches.forEach((match, matchIndex) => {
// Add the substring before the match
if (match.index > lastIndex) {
parts.push(react_1.default.createElement(TokenizedText, { key: `before-match-${matchIndex}`, input: remainingText.slice(lastIndex, match.index), sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }));
}
// Add the match itself - either as custom component or tokenized text
const matchText = match[0];
const matchPattern = fullPatterns.find(pattern => new RegExp(pattern).test(matchText));
if (matchPattern && customComponents[matchPattern]) {
const CustomComponent = customComponents[matchPattern];
// Only extract props if it's an HTML-style component (starts with <)
if (matchText.startsWith('<')) {
// Extract props from the matched text
const propsMatch = matchText.match(/\s+([\w-]+)=(?:"([^"]*)"|{([^}]*)})/g);
const props = propsMatch?.reduce((acc, prop) => {
const [name, value] = prop.trim().split('=');
// Handle both string values ("value") and JSX expressions ({value})
if (value.startsWith('"') && value.endsWith('"')) {
acc[name] = value.slice(1, -1); // Remove quotes
}
else if (value.startsWith('{') && value.endsWith('}')) {
acc[name] = value.slice(1, -1); // Remove braces
}
return acc;
}, {});
parts.push(react_1.default.createElement(TokenizedText, { key: `custom-component-${matchIndex}`, input: react_1.default.createElement(CustomComponent, { key: `component-${matchIndex}`, ...props, content: matchText }), sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }));
}
else {
// For non-HTML regex matches, just pass the content
parts.push(react_1.default.createElement(TokenizedText, { key: `custom-regex-${matchIndex}`, input: react_1.default.createElement(CustomComponent, { key: `component-regex-${matchIndex}`, content: matchText }), sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }));
}
}
// Update the last index to be after the match
lastIndex = match.index + match[0].length;
});
// Add any remaining part after the last match
if (lastIndex < remainingText.length) {
// Split the remaining text into before and after the partial pattern match
const partialRegex = new RegExp(`(${partialPatterns.join('|')})$`);
const partialMatch = remainingText.slice(lastIndex).match(partialRegex);
if (partialMatch && partialMatch.index) {
const beforePartial = remainingText.slice(lastIndex, lastIndex + partialMatch.index);
if (beforePartial) {
parts.push(react_1.default.createElement(TokenizedText, { key: `before-partial-${lastIndex}`, input: beforePartial, sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }));
}
}
else {
parts.push(react_1.default.createElement(TokenizedText, { key: `remaining-${lastIndex}`, input: remainingText.slice(lastIndex), sep: sep, animation: animation, animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }));
}
}
return parts;
}, [animation, animationDuration, animationTimingFunction, sep, generatePatterns]);
// Memoize animateText function to prevent recalculations if props do not change
const animateText = react_1.default.useCallback((text) => {
text = Array.isArray(text) ? text : [text];
const processText = (input, index) => {
if (Array.isArray(input)) {
// Process each element in the array
return input.map((element, idx) => processText(element, idx));
}
else if (typeof input === 'string') {
if (!animation)
return input;
return processCustomComponents(input);
}
else if (react_1.default.isValidElement(input)) {
// If the element is a React component or element, clone it with a key if needed
if (index !== undefined && !input.key) {
return react_1.default.cloneElement(input, { key: `animated-element-${index}` });
}
return input;
}
else {
// Return non-string, non-element inputs unchanged (null, undefined, etc.)
return input;
}
};
if (!animation) {
return text;
}
return processText(text);
}, [animation, animationDuration, animationTimingFunction, sep, processCustomComponents]);
// Memoize components object to avoid redefining components unnecessarily
const components = react_1.default.useMemo(() => ({
text: ({ node, ...props }) => animateText(props.children),
h1: ({ node, ...props }) => react_1.default.createElement("h1", { ...props }, animateText(props.children)),
h2: ({ node, ...props }) => react_1.default.createElement("h2", { ...props }, animateText(props.children)),
h3: ({ node, ...props }) => react_1.default.createElement("h3", { ...props }, animateText(props.children)),
h4: ({ node, ...props }) => react_1.default.createElement("h4", { ...props }, animateText(props.children)),
h5: ({ node, ...props }) => react_1.default.createElement("h5", { ...props }, animateText(props.children)),
h6: ({ node, ...props }) => react_1.default.createElement("h6", { ...props }, animateText(props.children)),
p: ({ node, ...props }) => react_1.default.createElement("p", { ...props }, animateText(props.children)),
li: ({ node, ...props }) => react_1.default.createElement("li", { ...props, className: "custom-li", style: animationStyle }, animateText(props.children)),
a: ({ node, ...props }) => react_1.default.createElement("a", { ...props, href: props.href, target: "_blank", rel: "noopener noreferrer" }, animateText(props.children)),
strong: ({ node, ...props }) => react_1.default.createElement("strong", { ...props }, animateText(props.children)),
em: ({ node, ...props }) => react_1.default.createElement("em", { ...props }, animateText(props.children)),
code: ({ node, className, children, ...props }) => {
const [copied, setCopied] = react_1.default.useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
if (!className || !className.startsWith("language-")) {
return react_1.default.createElement("code", { ...props }, animateText(children));
}
return react_1.default.createElement("div", { ...props, style: animationStyle, className: `relative` },
react_1.default.createElement("button", { onClick: handleCopy, style: {
// Add your custom styles here
backgroundColor: 'rgba(0, 0, 0, 0.2)',
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
zIndex: 10,
opacity: 0.7,
cursor: 'pointer',
borderRadius: '0.5rem',
padding: '0.25rem 0.25rem',
color: 'white',
// or any other CSS properties you want to modify
}, "aria-label": copied ? 'Copied!' : 'Copy code' }, copied ? (react_1.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
react_1.default.createElement("path", { d: "M20 6L9 17l-5-5" }))) : (react_1.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
react_1.default.createElement("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
react_1.default.createElement("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })))),
react_1.default.createElement(react_syntax_highlighter_1.Prism, { style: codeStyle, language: className?.substring(9).trim() || '', renderer: (0, exports.customCodeRenderer)({ animation, animationDuration, animationTimingFunction }) }, children));
},
hr: ({ node, ...props }) => react_1.default.createElement("hr", { ...props, style: {
animationName: animation,
animationDuration,
animationTimingFunction,
animationIterationCount: 1,
whiteSpace: 'pre-wrap',
} }),
img: ({ node, ...props }) => react_1.default.createElement(AnimatedImage, { src: props.src, alt: props.alt, animation: animation || '', animationDuration: animationDuration, animationTimingFunction: animationTimingFunction, animationIterationCount: 1 }),
table: ({ node, ...props }) => react_1.default.createElement("table", { ...props, className: "code-block" }, props.children),
tr: ({ node, ...props }) => react_1.default.createElement("tr", { ...props }, animateText(props.children)),
td: ({ node, ...props }) => react_1.default.createElement("td", { ...props }, animateText(props.children)),
...htmlComponents
}), [animateText]);
return react_1.default.createElement(react_markdown_1.default, { components: components, remarkPlugins: [remark_gfm_1.default] }, content);
};
exports.default = MarkdownAnimateText;