feat(trainer): авто-KaTeX во ВСЕХ текст-условиях (формулы рендерятся везде)

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) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-29 19:12:14 +03:00
parent 27d5308a04
commit c9a6b611a2
+38 -9
View File
@@ -622,17 +622,47 @@
// Смешанный текст «проза + математика»: фрагменты в $...$ рендерятся KaTeX // Смешанный текст «проза + математика»: фрагменты в $...$ рендерятся KaTeX
// (через exprToLatex), остальное экранируется. Так формулы видны и в текст-условиях // (через exprToLatex), остальное экранируется. Так формулы видны и в текст-условиях
// (проценты/дроби/verify/choice), где нет единого latex уравнения. // (проценты/дроби/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) { function renderMixed(text) {
var parts = String(text == null ? '' : text).split('$'); var parts = String(text == null ? '' : text).split('$'), html = '';
var html = '';
for (var i = 0; i < parts.length; i++) { for (var i = 0; i < parts.length; i++) {
if (i % 2 === 1) { // нечётные сегменты — математика if (i % 2 === 1) {
// prettyMath уже мог заменить * → ·, − и т.п.; возвращаем к виду для SimExpr-парсера var lx = TE.exprToLatex(_unpretty(parts[i]));
var src = parts[i].replace(/[·×]/g, '*').replace(//g, '-').replace(/÷/g, '/');
var lx = TE.exprToLatex(src);
html += lx ? (kat(lx, false) || esc(parts[i])) : esc(parts[i]); html += lx ? (kat(lx, false) || esc(parts[i])) : esc(parts[i]);
} else { } else {
html += esc(parts[i]); html += _autoProse(parts[i]);
} }
} }
return html; return html;
@@ -645,8 +675,7 @@
var latex = useFig ? null : problem.latex; var latex = useFig ? null : problem.latex;
eq.classList.toggle('tr-eq-text', !latex); eq.classList.toggle('tr-eq-text', !latex);
if (latex) { setMath(eq, latex, text, true); return; } // целое уравнение (solve/roots/…) if (latex) { setMath(eq, latex, text, true); return; } // целое уравнение (solve/roots/…)
if (text && text.indexOf('$') !== -1) eq.innerHTML = renderMixed(text); // текст с инлайн-формулами eq.innerHTML = renderMixed(text); // текст-условие: авто-рендер формул в KaTeX
else eq.textContent = text;
} }
// Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием. // Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием.
function renderFigureToggle() { function renderFigureToggle() {