Created
September 9, 2025 07:07
-
-
Save hrdyjan1/4c96d63d3adee44deb93eca15a664964 to your computer and use it in GitHub Desktop.
react-native-own-katex.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { memo, useMemo, useRef, useState } from 'react'; | |
| import { Platform, StyleProp, View, ViewStyle } from 'react-native'; | |
| import { WebView, WebViewMessageEvent } from 'react-native-webview'; | |
| interface RNKaTeXProps { | |
| math: string; | |
| displayMode?: boolean; | |
| fontSize?: number; | |
| macros?: Record<string, string>; | |
| transparent?: boolean; | |
| style?: StyleProp<ViewStyle>; | |
| minHeight?: number; | |
| textColor?: string; | |
| } | |
| const KATEX_VERSION = '0.16.11'; | |
| export default memo(function RNKaTeX({ | |
| math, | |
| displayMode = false, | |
| fontSize = 18, | |
| macros = {}, | |
| transparent = true, | |
| textColor = '#000', | |
| style, | |
| minHeight = 8, | |
| }: RNKaTeXProps) { | |
| const [contentHeight, setContentHeight] = useState(minHeight); | |
| const webRef = useRef<WebView>(null); | |
| const html = useMemo( | |
| () => ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist/katex.min.css"> | |
| <style> | |
| html, body { margin: 0; padding: 0; background: ${transparent ? 'transparent' : '#fff'}; } | |
| #root { ${displayMode ? 'display:block;' : 'display:inline-block;'} font-size: ${fontSize}px; color: ${textColor}; contain: content; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script src="https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist/katex.min.js"></script> | |
| <script> | |
| (function () { | |
| // keep backslashes intact | |
| const math = ${JSON.stringify(math)}; | |
| const macros = ${JSON.stringify(macros)}; | |
| const displayMode = ${displayMode ? 'true' : 'false'}; | |
| function render() { | |
| try { | |
| katex.render(math, document.getElementById('root'), { | |
| displayMode, throwOnError:false, trust:true, strict:'ignore', macros | |
| }); | |
| } catch (e) { document.getElementById('root').textContent = String(e); } | |
| postHeight(); | |
| } | |
| let lastH = -1, rafId = 0, tId = 0; | |
| function measureH() { | |
| const r = document.getElementById('root'); | |
| const h = Math.ceil(r.getBoundingClientRect().height); | |
| return h | 0; | |
| } | |
| function postHeight() { | |
| cancelAnimationFrame(rafId); clearTimeout(tId); | |
| rafId = requestAnimationFrame(() => { | |
| tId = setTimeout(() => { | |
| const h = measureH(); | |
| if (Math.abs(h - lastH) >= 1) { | |
| lastH = h; | |
| window.ReactNativeWebView?.postMessage(JSON.stringify({type:'height', h})); | |
| } | |
| }, 50); | |
| }); | |
| } | |
| const ro = new ResizeObserver(postHeight); | |
| ro.observe(document.getElementById('root')); | |
| render(); | |
| window.addEventListener('load', () => setTimeout(postHeight, 60)); | |
| setTimeout(postHeight, 120); // fallback | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |
| `, | |
| [math, macros, displayMode, fontSize, textColor, transparent] | |
| ); | |
| const onMessage = (e: WebViewMessageEvent) => { | |
| try { | |
| const data = JSON.parse(e.nativeEvent.data); | |
| if (data?.type === 'height') | |
| setContentHeight(Math.max(minHeight, data.h | 0)); | |
| } catch {} | |
| }; | |
| // Software layer for block math on Android reduces shimmer | |
| const androidProps = | |
| Platform.OS === 'android' && displayMode | |
| ? { | |
| androidHardwareAccelerationDisabled: true as const, | |
| androidLayerType: 'software' as const, | |
| } | |
| : {}; | |
| return ( | |
| <View style={[{ minHeight: contentHeight }, style]}> | |
| <WebView | |
| ref={webRef} | |
| originWhitelist={['*']} | |
| onMessage={onMessage} | |
| source={{ html }} | |
| style={{ backgroundColor: 'transparent', height: contentHeight }} | |
| scrollEnabled={false} | |
| showsVerticalScrollIndicator={false} | |
| showsHorizontalScrollIndicator={false} | |
| {...androidProps} | |
| /> | |
| </View> | |
| ); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment