Skip to content

Commit 6de29c7

Browse files
authored
feat(site): 课程正文数学公式 KaTeX 渲染 (#25)
* feat(site): 课程正文数学公式 KaTeX 渲染(zh 特化) 上游课程把公式写成 inline code(如 \`h_t = f(h_{t-1}, x_t)\`), code 样式下下标/希腊字母完全不可读(上游英文站同病)。 渲染层方案,源文档零改动、与上游零漂移: - looksLikeMath():保守启发式判定数学 span——命中强信号(_{ ^{、 数学 Unicode 符号、^上标)且无代码特征(中文、引号字符串、* 乘法、 snake_case 长下标、-> 等操作符)才转;prime 记法 V(s') 与字符串 字面量按开引号位置区分 - texPreprocess():伪 LaTeX 整理(²³ᵀ→^、组合字符 α̅/V̂→\bar/\hat、 √→\sqrt、多字符上下标补花括号、log/softmax 等函数名 \operatorname) - renderMathSpans():按需加载 KaTeX 0.16.21(页面含公式才拉 CDN), 逐个 try/catch 渲染,失败回退原 code 样式,绝不变差 验证:431 种全仓数学样本命中 376、渲染成功率 100%;23 种典型代码 span(文件名/f-string/snake_case/含中文)误伤 0;本地实测 07-why-transformers(4 公式)、09-policy-gradients(34 公式)全渲染 零回退;暗色模式颜色随主题正常。 * fix(site): KaTeX 判定按审查意见加固 审查(全语料 Node+katex 仿真)发现 4 处问题,全部修复: - looksLikeMath 拒绝含字面 &lt; 的 span:em 正则跨 code 配对的既有 行为会把 <em> 注入 code 内容,* 防线失效(7 处 net 变差) - ^ 上标信号要求前面有底数字符:拒掉脱敏正则 ^Bearer 等锚点; 字符类保留 | 以免误杀双范数 ||q||^2 - texPreprocess 花括号补全 {2,}→+:修 (Σ_r·Σ_g)^0.5 渲染成 「上标0+基线.5」的错误 - ~→\sim:修 ε ~ N(0,I) 等分布记法在 KaTeX 中不可见的问题 回归:369 命中/369 渲染成功,对抗样本(^Bearer、^1.2.3 semver、 正则锚点、em 污染 span)误伤 0。
1 parent 8f29083 commit 6de29c7

1 file changed

Lines changed: 69 additions & 1 deletion

File tree

site/lesson.html

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,10 @@
425425
color: var(--blueprint);
426426
}
427427

428+
.lesson-article .math-inline .katex {
429+
font-size: 1.04em;
430+
}
431+
428432
.lesson-article pre {
429433
position: relative;
430434
margin: 20px 0;
@@ -1971,6 +1975,7 @@
19711975

19721976
initCodeCopy();
19731977
renderMermaidBlocks();
1978+
renderMathSpans();
19741979
if (window.mountLessonFigures) window.mountLessonFigures(el);
19751980
renderAIPanels();
19761981
buildTOC();
@@ -2484,12 +2489,34 @@
24842489
return cells.map(function (c) { return c.trim(); });
24852490
}
24862491

2492+
// zh 特化:上游课程把数学公式写成 inline code(如 `h_t = f(h_{t-1}, x_t)`),
2493+
// 命中强数学信号且无代码特征的 span 改走 KaTeX 渲染,其余保持 code 原样。
2494+
// 输入是 HTML 转义后的文本;判定必须保守——漏判只是维持现状,误判会把代码渲染成数学。
2495+
function looksLikeMath(s) {
2496+
if (/[-鿿]/.test(s)) return false;
2497+
if (s.indexOf('&quot;') !== -1 || s.indexOf('$') !== -1) return false;
2498+
// 字面 &lt; 只可能来自 em/strong 注入残留(真实 < 已是 &amp;lt;),整 span 拒绝
2499+
if (s.indexOf('&lt;') !== -1) return false;
2500+
// 单引号:开引号(前面不是标识符/右括号)按字符串字面量排除;prime 记法如 V(s') 放行
2501+
if (/(^|[^a-zA-Z0-9)\]])&#39;/.test(s)) return false;
2502+
if (s.indexOf('*') !== -1) return false;
2503+
if (/_[a-z]{4,}/.test(s)) return false;
2504+
if (/--|::|=&gt;|-&gt;|\/\//.test(s)) return false;
2505+
// ^ 上标要求前面有底数字符(拒正则锚点如 ^Bearer);| 别漏,双范数 ||q||^2 靠它
2506+
return /[_^]\{/.test(s) ||
2507+
/[½¼·±×÷ΣπθμσεαβγλδηρτφψωΩΔ²³]/.test(s) ||
2508+
/[0-9a-zA-Z)\]}|]\^[0-9a-zA-Z(]/.test(s);
2509+
}
2510+
24872511
function inlineFormat(text) {
24882512
text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
24892513
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
24902514
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
24912515
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
2492-
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
2516+
text = text.replace(/`([^`]+)`/g, function (m, c) {
2517+
if (looksLikeMath(c)) return '<code class="math-tex" data-tex="' + c + '">' + c + '</code>';
2518+
return '<code>' + c + '</code>';
2519+
});
24932520
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (m, label, href) {
24942521
if (/^https?:\/\/|^mailto:/i.test(href)) {
24952522
return '<a href="' + href + '" target="_blank" rel="noopener">' + label + '</a>';
@@ -2503,6 +2530,47 @@
25032530
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
25042531
}
25052532

2533+
// 把课程里的伪 LaTeX 记法整理成 KaTeX 能吃的形式。
2534+
// getAttribute 已还原 HTML 实体,这里拿到的是原始文本。
2535+
function texPreprocess(s) {
2536+
s = s.replace(/²/g, '^2').replace(/³/g, '^3').replace(//g, '^T');
2537+
s = s.replace(/([A-Za-zα-ωΑ-Ω])̅/g, '\\bar{$1}');
2538+
s = s.replace(/([A-Za-zα-ωΑ-Ω])̂/g, '\\hat{$1}');
2539+
s = s.replace(/ŵ/g, '\\hat{w}').replace(/â/g, '\\hat{a}');
2540+
s = s.replace(//g, '\\sqrt ');
2541+
s = s.replace(/~/g, '\\sim ');
2542+
s = s.replace(/([_^])([a-zA-Z0-9]+(?:\.[0-9]+)?)/g, '$1{$2}');
2543+
s = s.replace(/\b(softmax|argmax|argmin|log10|log|exp|sin|cos|tanh|max|min|KL|Tr|Var|sqrt)\b(\s*\()/g, '\\operatorname{$1}$2');
2544+
return s;
2545+
}
2546+
2547+
// 按需加载 KaTeX 并渲染 math-tex span;任何一步失败都回退原 code 样式。
2548+
function renderMathSpans() {
2549+
var spans = document.querySelectorAll('code.math-tex');
2550+
if (!spans.length) return;
2551+
var KATEX = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min';
2552+
if (!document.getElementById('katex-css')) {
2553+
var link = document.createElement('link');
2554+
link.id = 'katex-css'; link.rel = 'stylesheet'; link.href = KATEX + '.css';
2555+
document.head.appendChild(link);
2556+
}
2557+
function renderAll() {
2558+
spans.forEach(function (el) {
2559+
try {
2560+
var span = document.createElement('span');
2561+
span.className = 'math-inline';
2562+
window.katex.render(texPreprocess(el.getAttribute('data-tex')), span, { throwOnError: true, strict: false });
2563+
el.replaceWith(span);
2564+
} catch (e) { el.classList.remove('math-tex'); }
2565+
});
2566+
}
2567+
if (window.katex) { renderAll(); return; }
2568+
var script = document.createElement('script');
2569+
script.src = KATEX + '.js';
2570+
script.onload = renderAll;
2571+
document.head.appendChild(script);
2572+
}
2573+
25062574
function renderCodeBlock(code, lang) {
25072575
var highlighted = highlightSyntax(escapeHtml(code), lang);
25082576
var langLabel = lang ? '<span class="code-lang">' + escapeHtml(lang) + '</span>' : '';

0 commit comments

Comments
 (0)