this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 164 lines 5.5 kB view raw
1import 'temml/dist/Temml-Local.css'; 2 3import { useLingui } from '@lingui/react/macro'; 4import { useCallback, useState } from 'preact/hooks'; 5 6import showToast from '../utils/show-toast'; 7 8import Icon from './icon'; 9 10// Follow https://mathstodon.xyz/about 11// > You can use LaTeX in toots here! Use \( and \) for inline, and \[ and \] for display mode. 12const DELIMITERS_PATTERNS = [ 13 // '\\$\\$[\\s\\S]*?\\$\\$', // $$...$$ 14 '\\\\\\[[\\s\\S]*?\\\\\\]', // \[...\] 15 '\\\\\\([\\s\\S]*?\\\\\\)', // \(...\) 16 // '\\\\begin\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}[\\s\\S]*?\\\\end\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}', // AMS environments 17 // '\\\\(?:ref|eqref)\\{[^}]*\\}', // \ref{...}, \eqref{...} 18]; 19const DELIMITERS_REGEX = new RegExp(DELIMITERS_PATTERNS.join('|'), 'g'); 20 21function cleanDOMForTemml(dom) { 22 // Define start and end delimiter patterns 23 const START_DELIMITERS = ['\\\\\\[', '\\\\\\(']; // \[ and \( 24 const startRegex = new RegExp(`(${START_DELIMITERS.join('|')})`); 25 26 // Walk through all text nodes 27 const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT); 28 const textNodes = []; 29 let node; 30 while ((node = walker.nextNode())) { 31 textNodes.push(node); 32 } 33 34 for (const textNode of textNodes) { 35 const text = textNode.textContent; 36 const startMatch = text.match(startRegex); 37 38 if (!startMatch) continue; // No start delimiter in this text node 39 40 // Find the matching end delimiter 41 const startDelimiter = startMatch[0]; 42 const endDelimiter = startDelimiter === '\\[' ? '\\]' : '\\)'; 43 44 // Collect nodes from start delimiter until end delimiter 45 const nodesToCombine = [textNode]; 46 let currentNode = textNode; 47 let foundEnd = false; 48 let combinedText = text; 49 50 // Check if end delimiter is in the same text node 51 if (text.includes(endDelimiter)) { 52 foundEnd = true; 53 } else { 54 // Look through sibling nodes 55 while (currentNode.nextSibling && !foundEnd) { 56 const nextSibling = currentNode.nextSibling; 57 58 if (nextSibling.nodeType === Node.TEXT_NODE) { 59 nodesToCombine.push(nextSibling); 60 combinedText += nextSibling.textContent; 61 if (nextSibling.textContent.includes(endDelimiter)) { 62 foundEnd = true; 63 } 64 } else if ( 65 nextSibling.nodeType === Node.ELEMENT_NODE && 66 nextSibling.tagName === 'BR' 67 ) { 68 nodesToCombine.push(nextSibling); 69 combinedText += '\n'; 70 } else { 71 // Found a non-BR element, stop and don't process 72 break; 73 } 74 75 currentNode = nextSibling; 76 } 77 } 78 79 // Only process if we found the end delimiter and have nodes to combine 80 if (foundEnd && nodesToCombine.length > 1) { 81 // Replace the first text node with combined text 82 textNode.textContent = combinedText; 83 84 // Remove the other nodes 85 for (let i = 1; i < nodesToCombine.length; i++) { 86 nodesToCombine[i].remove(); 87 } 88 } 89 } 90} 91 92const MathBlock = ({ content, contentRef, onRevert }) => { 93 DELIMITERS_REGEX.lastIndex = 0; // Reset index to prevent g trap 94 const hasLatexContent = DELIMITERS_REGEX.test(content); 95 96 if (!hasLatexContent) return null; 97 98 const { t } = useLingui(); 99 const [mathRendered, setMathRendered] = useState(false); 100 const toggleMathRendering = useCallback( 101 async (e) => { 102 e.preventDefault(); 103 e.stopPropagation(); 104 if (mathRendered) { 105 // Revert to original content by refreshing PostContent 106 setMathRendered(false); 107 onRevert(); 108 } else { 109 // Render math 110 try { 111 // This needs global because the codebase inside temml is calling a function from global.temml 🤦‍♂️ 112 const temml = 113 window.temml || (window.temml = (await import('temml'))?.default); 114 115 cleanDOMForTemml(contentRef.current); 116 const originalContentRefHTML = contentRef.current.innerHTML; 117 temml.renderMathInElement(contentRef.current, { 118 fences: '(', // This should sync with DELIMITERS_REGEX 119 annotate: true, 120 throwOnError: true, 121 errorCallback: (err) => { 122 console.warn('Failed to render LaTeX:', err); 123 }, 124 }); 125 126 const hasMath = contentRef.current.querySelector('math.tml-display'); 127 const htmlChanged = 128 contentRef.current.innerHTML !== originalContentRefHTML; 129 if (hasMath && htmlChanged) { 130 setMathRendered(true); 131 } else { 132 showToast(t`Unable to format math`); 133 setMathRendered(false); 134 onRevert(); // Revert because DOM modified by cleanDOMForTemml 135 } 136 } catch (e) { 137 console.error('Failed to LaTeX:', e); 138 } 139 } 140 }, 141 [mathRendered], 142 ); 143 144 return ( 145 <div class="math-block"> 146 <Icon icon="formula" size="s" /> <span>{t`Math expressions found.`}</span>{' '} 147 <button type="button" class="light small" onClick={toggleMathRendering}> 148 {mathRendered 149 ? t({ 150 comment: 151 'Action to switch from rendered math back to raw (LaTeX) markup', 152 message: 'Show markup', 153 }) 154 : t({ 155 comment: 156 'Action to render math expressions from raw (LaTeX) markup', 157 message: 'Format math', 158 })} 159 </button> 160 </div> 161 ); 162}; 163 164export default MathBlock;