From c9a6b611a22087760236777e68ad5182cad989d4 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Mon, 29 Jun 2026 19:12:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20=D0=B0=D0=B2=D1=82=D0=BE-KaTeX?= =?UTF-8?q?=20=D0=B2=D0=BE=20=D0=92=D0=A1=D0=95=D0=A5=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=81=D1=82-=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D1=8F=D1=85?= =?UTF-8?q?=20(=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D1=8B=20=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=D0=B5=D1=80=D1=8F=D1=82=D1=81=D1=8F=20=D0=B2=D0=B5?= =?UTF-8?q?=D0=B7=D0=B4=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderMixed теперь авто-детектит математику в любом текст-условии (compute/verify/ choice/word), без обязательных $...$: латино-цифровые «прогоны» (дроби, уравнения, степени x²/(x+1)², проценты %, градусы °, десятичные) → KaTeX через exprToLatex; кириллица/слова остаются текстом. Одиночные числа НЕ рендерятся (только выражения с оператором/степенью/%/°), чтобы не «утяжелять» прозу. Безопасность: любой неразобравшийся фрагмент остаётся обычным экранированным текстом (худший случай = прежнее поведение). $...$ по-прежнему форсирует рендер. showStatement применяет авто-рендер ко всем текст-условиям (целые уравнения solve/ roots идут единым latex как раньше). Проверка: реплика логики прогнана по ВСЕМ генераторам (2388 дисплеев / 1609 матем. фрагментов): 0 кириллицы в формулах, все фрагменты валидны, проза сохранена — 3997/0. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/trainer.html | 47 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/frontend/trainer.html b/frontend/trainer.html index af7d4c5..c3ae587 100644 --- a/frontend/trainer.html +++ b/frontend/trainer.html @@ -622,17 +622,47 @@ // Смешанный текст «проза + математика»: фрагменты в $...$ рендерятся KaTeX // (через exprToLatex), остальное экранируется. Так формулы видны и в текст-условиях // (проценты/дроби/verify/choice), где нет единого latex уравнения. + // Возврат «косметики» prettyMath к виду, понятному SimExpr-парсеру. + function _unpretty(s) { + return String(s).replace(/[·×]/g, '*').replace(/−/g, '-').replace(/÷/g, '/').replace(/²/g, '^2').replace(/³/g, '^3'); + } + // Один математический фрагмент → KaTeX-html или null (если не математика / не разобрался). + function _mathRun(run) { + var suffix = '', core = run, t = core.match(/(%|°)$/); + if (t) { suffix = (t[1] === '%') ? '\\%' : '^{\\circ}'; core = core.slice(0, -1); } + // рендерим только если есть оператор/степень/дробь/проценты-градусы (одиночные числа/слова — текстом) + var worth = /[\/^=<>+\-−·×÷√]|[0-9][A-Za-z]|²|³/.test(run) || suffix; + if (!worth) return null; + core = _unpretty(core); + if (!/[0-9A-Za-z]/.test(core)) return null; + var lx = TE.exprToLatex(core); + if (lx == null) return null; + return kat(lx + suffix, false); + } + // Авто-рендер математики в прозе: латино-цифровые «прогоны» → KaTeX, кириллица/слова — текст. + function _autoProse(s) { + var out = '', re = /[0-9A-Za-z(][\w ().,^*\/+\-=<>≤≥≠·×÷√²³°%]*/g, last = 0, m; + while ((m = re.exec(s))) { + out += esc(s.slice(last, m.index)); + var run = m[0], tail = '', tm = run.match(/[ ,.;:]+$/); + if (tm) { tail = run.slice(run.length - tm[0].length); run = run.slice(0, run.length - tm[0].length); } + var r = _mathRun(run); + out += (r != null ? r : esc(run)) + esc(tail); + last = m.index + m[0].length; + } + out += esc(s.slice(last)); + return out; + } + // Смешанный текст: $...$ — принудительная математика; вне — авто-детект формул. + // Безопасно: любой неразобравшийся фрагмент остаётся обычным экранированным текстом. function renderMixed(text) { - var parts = String(text == null ? '' : text).split('$'); - var html = ''; + var parts = String(text == null ? '' : text).split('$'), html = ''; for (var i = 0; i < parts.length; i++) { - if (i % 2 === 1) { // нечётные сегменты — математика - // prettyMath уже мог заменить * → ·, − и т.п.; возвращаем к виду для SimExpr-парсера - var src = parts[i].replace(/[·×]/g, '*').replace(/−/g, '-').replace(/÷/g, '/'); - var lx = TE.exprToLatex(src); + if (i % 2 === 1) { + var lx = TE.exprToLatex(_unpretty(parts[i])); html += lx ? (kat(lx, false) || esc(parts[i])) : esc(parts[i]); } else { - html += esc(parts[i]); + html += _autoProse(parts[i]); } } return html; @@ -645,8 +675,7 @@ var latex = useFig ? null : problem.latex; eq.classList.toggle('tr-eq-text', !latex); if (latex) { setMath(eq, latex, text, true); return; } // целое уравнение (solve/roots/…) - if (text && text.indexOf('$') !== -1) eq.innerHTML = renderMixed(text); // текст с инлайн-формулами - else eq.textContent = text; + eq.innerHTML = renderMixed(text); // текст-условие: авто-рендер формул в KaTeX } // Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием. function renderFigureToggle() {