import { useState, useRef, useCallback } from 'react'; import { View, StyleSheet } from 'react-native'; import { WebView } from 'react-native-webview'; import { KATEX_CSS, KATEX_JS, KATEX_AUTO_RENDER_JS } from './katex-assets'; type LatexStyle = | 'border' | 'font-size' | 'line-height' | 'color' | 'background-color'; type LatexStyleMap = Partial>; type InnerContainerStyle = | 'padding' | 'border' | 'font-size' | 'line-height' | 'width' | 'height' | 'color' | 'background-color'; type InnerContainerMap = Partial>; type ContainerStyle = | 'padding' | 'width' | 'backgroundColor' | 'borderWidth' | 'borderColor'; type ContainerMap = Partial>; const HEIGHT_CALCULATION_SCRIPT = ` (function() { let lastSentHeight = 0; let rafId = null; const HEIGHT_THRESHOLD = 5; const calculateHeight = () => { const scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight ); const bodyRect = document.body.getBoundingClientRect(); const rectHeight = bodyRect.height + window.pageYOffset; return Math.max(scrollHeight, rectHeight); }; const sendHeightUpdate = () => { if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(() => { try { const height = calculateHeight(); if (Math.abs(height - lastSentHeight) > HEIGHT_THRESHOLD) { lastSentHeight = height; window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'height', height: Math.ceil(height), timestamp: Date.now() })); } } catch (error) { console.error('Height calculation error:', error); } rafId = null; }); }; const setupObservers = () => { if (typeof ResizeObserver !== 'undefined') { const resizeObserver = new ResizeObserver(() => { sendHeightUpdate(); }); resizeObserver.observe(document.body); resizeObserver.observe(document.documentElement); setTimeout(() => { const katexElements = document.querySelectorAll('.katex-display, .katex'); katexElements.forEach(el => { try { resizeObserver.observe(el); } catch (e) { // Element might not be valid, skip } }); }, 100); return; } const mutationObserver = new MutationObserver(() => { sendHeightUpdate(); }); mutationObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'], characterData: false, }); window.addEventListener('resize', sendHeightUpdate); }; const initialize = () => { setupObservers(); sendHeightUpdate(); setTimeout(sendHeightUpdate, 100); setTimeout(sendHeightUpdate, 300); }; initialize(); return true; })(); true; // Required for iOS `; const KaTeXAutoHeightWebView = ({ source, onHeightChange, minHeight = 50, containerStyle, ...webViewProps }: { source: string; onHeightChange?: (height: number) => void; minHeight?: number; containerStyle?: ContainerMap; [key: string]: any; }) => { const [height, setHeight] = useState(minHeight); const webViewRef = useRef(null); const lastHeightRef = useRef(minHeight); const handleMessage = useCallback( (event: any) => { try { const data = JSON.parse(event.nativeEvent.data); if (data.type === 'height' && data.height) { const newHeight = Math.max(data.height, minHeight); if (Math.abs(newHeight - lastHeightRef.current) > 1) { lastHeightRef.current = newHeight; setHeight(newHeight); if (onHeightChange) { onHeightChange(newHeight); } } } } catch (error) { console.error('Error parsing WebView message:', error); } }, [minHeight, onHeightChange] ); return ( , [key, value]) => { if ( typeof value === 'string' && key.includes('borderWidth') && !isNaN(parseFloat(value)) ) { acc[key] = parseFloat(value); } else { acc[key] = value; } return acc; }, {} as Record ), ]} > ); }; const styles = StyleSheet.create({ container: { width: '100%', overflow: 'hidden', backgroundColor: 'transparent', }, webview: { backgroundColor: 'transparent', }, }); const createKaTeXHTML = ( latexContent: string, containerStyles?: InnerContainerMap, latexStyles?: LatexStyleMap ) => { return `
${latexContent}
`; }; const formatContainerStyles = (s?: InnerContainerMap) => { let initialStyles = { 'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', 'font-size': '16px', 'line-height': '1.6', 'padding': '8px', 'background-color': 'transparent', 'color': 'black', }; if (!s || typeof s !== 'object' || Array.isArray(s)) { return {}; } Object.entries(s).forEach(([key, value]) => { initialStyles[key as keyof typeof initialStyles] = value; }); return initialStyles; }; const formatLatexStyles = (s?: LatexStyleMap) => { let initialStyles = { 'color': 'black', 'font-size': '1em', 'line-height': '2', }; if (!s || typeof s !== 'object' || Array.isArray(s)) { return {}; } Object.entries(s).forEach(([key, value]) => { initialStyles[key as keyof typeof initialStyles] = value; }); return initialStyles; }; export { KaTeXAutoHeightWebView, createKaTeXHTML };