);
};
// Custom renderer for text nodes to handle source references and math
const renderWithSourceRefsAndMath = (elementType) => {
const ElementComponent = elementType; // e.g., 'p', 'li'
// Helper to gather plain text
const getFullText = (something) => {
if (typeof something === 'string') return something;
if (Array.isArray(something)) return something.map(getFullText).join('');
if (React.isValidElement(something) && something.props?.children)
return getFullText(React.Children.toArray(something.props.children));
return '';
};
return ({ children, ...props }) => {
// Plain‑text version of this block (paragraph / list‑item)
const fullText = getFullText(children);
// Same regex the backend used
const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`'"]*|[^.!?\n]+$/g;
const sentencesArr = fullText.match(sentenceRegex) || [fullText];
// Helper function to find the sentence that contains position `pos`
const sentenceByPos = (pos) => {
let run = 0;
for (const s of sentencesArr) {
const end = run + s.length;
if (pos >= run && pos < end) return s.trim();
run = end;
}
return fullText.trim();
};
// Cursor that advances through fullText so each subsequent
// indexOf search starts AFTER the previous match
let searchCursor = 0;
// Recursive renderer that preserves existing markup and adds math support
const processNode = (node, keyPrefix = 'node') => {
if (typeof node === 'string') {
const parts = [];
let lastIndex = 0;
// First process block math
const blockMathRegex = /\$\$([\s\S]*?)\$\$/g;
let blockMatch;
while ((blockMatch = blockMathRegex.exec(node))) {
// Process text before the math expression for citations
const textBefore = node.slice(lastIndex, blockMatch.index);
if (textBefore) {
parts.push(...processCitations(textBefore, keyPrefix, lastIndex));
}
// Add the block math component
parts.push(
{blockMatch[1]}
);
lastIndex = blockMatch.index + blockMatch[0].length;
}
// Process remaining text for inline math and citations
const remainingText = node.slice(lastIndex);
if (remainingText) {
parts.push(...processInlineMathAndCitations(remainingText, keyPrefix, lastIndex));
}
return parts;
}
// For non‑string children, recurse (preserves , , links, etc.)
if (React.isValidElement(node) && node.props?.children) {
const processed = React.Children.map(node.props.children, (child, i) =>
processNode(child, `${keyPrefix}-${i}`)
);
return React.cloneElement(node, { children: processed });
}
return node; // element without children or unknown type
};
// Helper function to process inline math and citations
const processInlineMathAndCitations = (text, keyPrefix, offset) => {
const parts = [];
let lastIndex = 0;
// Combined regex for inline math and citations
const combinedRegex = /\$([^\$\n]+?)\$|\[(\d+)\]/g;
let match;
while ((match = combinedRegex.exec(text))) {
// Add text before the match
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
if (match[1] !== undefined) {
// It's inline math
parts.push(
{match[1]}
);
} else if (match[2] !== undefined) {
// It's a citation
const num = parseInt(match[2], 10);
const absIdx = fullText.indexOf(match[0], searchCursor);
if (absIdx !== -1) searchCursor = absIdx + match[0].length;
const sentenceForPopup = sentenceByPos(absIdx);
parts.push(
showSourcePopup &&
showSourcePopup(num - 1, e.target, sentenceForPopup)
}
onMouseLeave={hideSourcePopup}
>
{num}
);
}
lastIndex = match.index + match[0].length;
}
// Add remaining text
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts;
};
// Helper function to process only citations
const processCitations = (text, keyPrefix, offset) => {
const citationRegex = /\[(\d+)\]/g;
let last = 0;
let parts = [];
let m;
while ((m = citationRegex.exec(text))) {
const sliceBefore = text.slice(last, m.index);
if (sliceBefore) parts.push(sliceBefore);
const localIdx = m.index;
const num = parseInt(m[1], 10);
const citStr = m[0];
// Find this specific occurrence in fullText, starting at searchCursor
const absIdx = fullText.indexOf(citStr, searchCursor);
if (absIdx !== -1) searchCursor = absIdx + citStr.length;
const sentenceForPopup = sentenceByPos(absIdx);
parts.push(
showSourcePopup &&
showSourcePopup(num - 1, e.target, sentenceForPopup)
}
onMouseLeave={hideSourcePopup}
>
{num}
);
last = localIdx + citStr.length;
}
if (last < text.length) parts.push(text.slice(last));
return parts;
};
const processedChildren = React.Children.map(children, (child, i) =>
processNode(child, `root-${i}`)
);
// Render original element (p, li, …) with processed children
return {processedChildren};
};
};
return (
{
let codeElement = null;
let codeString = '';
let language = '';
React.Children.forEach(children, (child) => {
if (React.isValidElement(child)) { // Check for element, no type restriction
codeElement = child;
const className = child.props?.className || '';
const match = /(?:lang|language)-(\w+)/.exec(className);
if (match) {
language = match[1];
}
// Flatten children safely
codeString = React.Children.toArray(child.props?.children).join('');
}
});
// Always start code blocks on a new line
return (
<>
{codeElement ? (
) : (