项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

158 行
5.0 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* 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;
}