Files
thu-maic--openmaic/lib/generation/interactive-post-processor.ts
wehub-resource-sync c48612c494
Docs Build / Build docs site (push) Waiting to run
Publish @openmaic packages / Build, validate & publish (push) Waiting to run
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

158 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Interactive HTML Post-Processor
*
* Ported from Python's PostProcessor class (learn-your-way/concept_to_html.py:287-385)
*
* Handles:
* - LaTeX delimiter conversion ($$...$$ -> \[...\], $...$ -> \(...\))
* - KaTeX CSS/JS injection with auto-render and MutationObserver
* - Script tag protection during LaTeX conversion
*/
/**
* Main entry point: post-process generated interactive HTML
* Converts LaTeX delimiters and injects KaTeX rendering resources.
*/
export function postProcessInteractiveHtml(html: string): string {
// Convert LaTeX delimiters while protecting script tags
let processed = convertLatexDelimiters(html);
// Inject KaTeX resources if not already present
if (!processed.toLowerCase().includes('katex')) {
processed = injectKatex(processed);
}
return processed;
}
/**
* Convert LaTeX delimiters while protecting <script> tags.
*
* - Protects script blocks from modification
* - Converts $$...$$ to \[...\] (display math)
* - Converts $...$ to \(...\) (inline math)
* - Restores script blocks after conversion
*/
function convertLatexDelimiters(html: string): string {
const scriptBlocks: string[] = [];
// Protect script tags by replacing them with placeholders
let processed = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, (match) => {
scriptBlocks.push(match);
return `__SCRIPT_BLOCK_${scriptBlocks.length - 1}__`;
});
// Convert display math: $$...$$ -> \[...\]
processed = processed.replace(/\$\$([^$]+)\$\$/g, '\\[$1\\]');
// Convert inline math: $...$ -> \(...\)
// Use non-greedy match and exclude newlines to avoid false positives
processed = processed.replace(/\$([^$\n]+?)\$/g, '\\($1\\)');
// Restore script blocks in a single pass. A replacer FUNCTION (not a string)
// is safe even when script content contains `$` — a function's return value
// is inserted literally, with no `$&`/`$1` substitution. The previous
// indexOf+substring loop rebuilt the entire string once per block, i.e.
// O(blocks × length), which balloons memory and blocks the event loop when
// the generated widget HTML contains many <script> tags.
processed = processed.replace(
/__SCRIPT_BLOCK_(\d+)__/g,
(whole, index) => scriptBlocks[Number(index)] ?? whole,
);
return processed;
}
/**
* Inject KaTeX CSS, JS, auto-render, and MutationObserver before </head>.
* Falls back to appending at end if </head> is not found.
*/
function injectKatex(html: string): string {
const katexInjection = `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const katexOptions = {
delimiters: [
{left: '\\\\[', right: '\\\\]', display: true},
{left: '\\\\(', right: '\\\\)', display: false},
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}
],
throwOnError: false,
strict: false,
trust: true
};
let renderTimeout;
function safeRender() {
if (renderTimeout) clearTimeout(renderTimeout);
renderTimeout = setTimeout(() => {
renderMathInElement(document.body, katexOptions);
}, 100);
}
renderMathInElement(document.body, katexOptions);
const observer = new MutationObserver((mutations) => {
let shouldRender = false;
mutations.forEach((mutation) => {
if (mutation.target &&
mutation.target.className &&
typeof mutation.target.className === 'string' &&
mutation.target.className.includes('katex')) {
return;
}
shouldRender = true;
});
if (shouldRender) {
safeRender();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
setInterval(() => {
const text = document.body.innerText;
if (text.includes('\\\\(') || text.includes('$$')) {
safeRender();
}
}, 2000);
});
</script>`;
// Use indexOf + substring instead of String.replace() because the
// katexInjection string contains '$' characters that .replace() would
// interpret as special substitution patterns ($$ → $, $' → post-match text).
const headCloseIdx = html.indexOf('</head>');
if (headCloseIdx !== -1) {
return (
html.substring(0, headCloseIdx) +
katexInjection +
'\n</head>' +
html.substring(headCloseIdx + 7)
);
}
// Fallback: inject before </body> if </head> is missing
const bodyCloseIdx = html.indexOf('</body>');
if (bodyCloseIdx !== -1) {
return (
html.substring(0, bodyCloseIdx) +
katexInjection +
'\n</body>' +
html.substring(bodyCloseIdx + 7)
);
}
// Last resort: append at end
return html + katexInjection;
}