bd7dd06e47
Root cause: в seed-ах вариантов 101–121 опции писались как mc('$\sqrt..$') в
обычных кавычках вместо R`…` → JS-парсер съедал \s→s, \d→d и т.п., в БД легло
«$sqrt{17}$», «$dfrac{pi}{3}$» (KaTeX рисует «sqrt17», «dfracpi3»). Плюс литеральные
<,> внутри $…$ ломали HTML до KaTeX. Скрипт fix_ctmath_render.js (dry-run/--apply,
идемпотентный): восстанавливает \ перед командами (+нормализует управляющие символы)
в opts_json и заменяет <,>→\lt,\gt внутри $…$ в text/opts/solution. DRY-RUN: 307
строк (143 opts/128 text/157 sol), остаточных багов 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
172 lines
8.9 KiB
JavaScript
172 lines
8.9 KiB
JavaScript
'use strict';
|
||
/* ───────────────────────────────────────────────────────────────────────────
|
||
fix_ctmath_render.js — починка двух дефектов рендеринга в exam_tasks (ctmath).
|
||
|
||
ПРИЧИНА (root cause):
|
||
В seed-скриптах вариантов 101–121 опции писались как mc('$\sqrt{17}$', ...) —
|
||
в ОБЫЧНЫХ кавычках, а не в String.raw `…`. JS-парсер съедал управляющие
|
||
эскейпы: \s→s, \d→d (теряется «\»), а \f→0x0C, \t→0x09, \b→0x08, \v,\n,\r —
|
||
превращались в УПРАВЛЯЮЩИЕ символы. Итог в БД: «$sqrt{17}$», «$dfrac{pi}{3}$»,
|
||
KaTeX рендерит их как «sqrt17», «dfracpi3». (text/solution писались через R`…`
|
||
и НЕ пострадали — там «\» на месте.)
|
||
|
||
ВТОРОЙ ДЕФЕКТ: литеральные < и > ВНУТРИ $…$ (напр. «$-1{,}6<x<-1$»). При вставке
|
||
в innerHTML браузер парсит «<x…» как HTML-тег ДО запуска KaTeX → ломает карточку.
|
||
Лечится заменой < → \lt, > → \gt (только внутри $…$).
|
||
|
||
ЧТО ДЕЛАЕТ СКРИПТ (идемпотентно, повторный запуск безопасен):
|
||
• opts_json: (1) нормализует управляющие символы обратно в \f \t \b \v \n \r;
|
||
(2) восстанавливает «\» перед известными KaTeX-командами; (3) < > → \lt \gt.
|
||
• text_html, solution_html: только (3) < > → \lt \gt внутри $…$ (HTML-теги вне
|
||
математики не трогаются).
|
||
Восстановление «\» применяется ТОЛЬКО к opts_json (text/sol не повреждены).
|
||
|
||
Запуск:
|
||
node backend/scripts/fix_ctmath_render.js # DRY-RUN (показывает правки)
|
||
node backend/scripts/fix_ctmath_render.js --apply # запись в БД
|
||
⚠️ Запись запускает ПОЛЬЗОВАТЕЛЬ. После --apply — перезапуск сервера не нужен
|
||
(данные в БД; фронт перечитает их при следующем запросе), но hard-refresh браузера.
|
||
─────────────────────────────────────────────────────────────────────────── */
|
||
|
||
const { DatabaseSync } = require('node:sqlite');
|
||
const path = require('path');
|
||
|
||
const APPLY = process.argv.includes('--apply');
|
||
const EXAM = 'ctmath';
|
||
|
||
/* ── Управляющие символы → их LaTeX-эскейп (обратная нормализация) ── */
|
||
const CTRL_MAP = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\v': '\\v', '\f': '\\f', '\r': '\\r' };
|
||
function normalizeCtrl(s) {
|
||
return s.replace(/[\b\t\n\v\f\r]/g, ch => CTRL_MAP[ch] || ch);
|
||
}
|
||
|
||
/* ── Команды, у которых первая буква НЕ из {f,t,b,v,n,r} (их «\» просто пропал,
|
||
без управляющего символа). Длинные/составные — РАНЬШЕ коротких префиксов,
|
||
чтобы не разорвать слово (dfrac до frac, arccos до cos, leq до le, …). ── */
|
||
const BARE_CMDS = [
|
||
'arccos', 'arcsin', 'arctg',
|
||
'overline', 'operatorname', 'varnothing', 'varphi', 'varepsilon',
|
||
'dfrac', 'cdots', 'cdot', 'sqrt', 'left',
|
||
'lambda', 'gamma', 'delta', 'sigma', 'omega', 'alpha', 'angle', 'approx',
|
||
'infty', 'ldots', 'oplus',
|
||
'cos', 'sin', 'cot', 'ctg', 'cup', 'cap', 'leq', 'geq', 'neq',
|
||
'sim', 'lim', 'log',
|
||
'pm', 'mp', 'le', 'ge', 'ln', 'lg', 'pi', 'mu', 'in',
|
||
'phi', 'psi', 'rho', 'chi', 'tau',
|
||
];
|
||
/* (frac, tfrac, times, theta, tan, tg, text, beta, vec, ne, nu, right, nabla —
|
||
приходят из управляющих символов и чинятся normalizeCtrl, поэтому в этом
|
||
списке их НЕТ: иначе «dfrac»→«d\frac».) */
|
||
|
||
function restoreBackslashes(math) {
|
||
let s = normalizeCtrl(math);
|
||
for (const cmd of BARE_CMDS) {
|
||
// «\bcmd», не уже-экранированное (нет «\» перед), как самостоятельное слово
|
||
const re = new RegExp('(^|[^\\\\A-Za-z])' + cmd + '(?![A-Za-z])', 'g');
|
||
s = s.replace(re, (m, pre) => pre + '\\' + cmd);
|
||
}
|
||
return s;
|
||
}
|
||
|
||
/* ── < > → \lt \gt ТОЛЬКО внутри $…$ / $$…$$ ── */
|
||
function fixAngles(field) {
|
||
if (!field) return field;
|
||
return String(field).replace(/\$\$[\s\S]*?\$\$|\$[^$]*\$/g, seg =>
|
||
seg.replace(/</g, '\\lt ').replace(/>/g, '\\gt '));
|
||
}
|
||
|
||
/* ── Полная починка одной опции (внутри $…$): \ + < > ── */
|
||
function fixOptionText(t) {
|
||
if (!t) return t;
|
||
// обрабатываем содержимое каждого $…$: восстановить «\», затем < >
|
||
return String(t).replace(/\$\$[\s\S]*?\$\$|\$[^$]*\$/g, seg => {
|
||
const open = seg.startsWith('$$') ? '$$' : '$';
|
||
const inner = seg.slice(open.length, seg.length - open.length);
|
||
let fixed = restoreBackslashes(inner);
|
||
fixed = fixed.replace(/</g, '\\lt ').replace(/>/g, '\\gt ');
|
||
return open + fixed + open;
|
||
});
|
||
}
|
||
|
||
/* ── Открытие БД ── */
|
||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||
const db = new DatabaseSync(DB);
|
||
|
||
const rows = db.prepare(
|
||
`SELECT id, variant, task_idx, task_type, text_html, opts_json, solution_html
|
||
FROM exam_tasks WHERE exam_key=? ORDER BY variant, task_idx`).all(EXAM);
|
||
|
||
let changedRows = 0, changedOpts = 0, changedText = 0, changedSol = 0;
|
||
const samples = [];
|
||
|
||
const upd = db.prepare(
|
||
`UPDATE exam_tasks SET text_html=?, opts_json=?, solution_html=? WHERE id=?`);
|
||
|
||
if (APPLY) db.exec('BEGIN');
|
||
try {
|
||
for (const r of rows) {
|
||
let newOpts = r.opts_json;
|
||
let newText = r.text_html;
|
||
let newSol = r.solution_html;
|
||
let touched = false;
|
||
|
||
// opts_json — восстановление «\» + < >
|
||
if (r.opts_json) {
|
||
try {
|
||
const arr = JSON.parse(r.opts_json);
|
||
const fixed = arr.map(([l, t]) => [l, fixOptionText(t)]);
|
||
const cand = JSON.stringify(fixed);
|
||
if (cand !== r.opts_json) { newOpts = cand; changedOpts++; touched = true; }
|
||
} catch { /* не-JSON — пропускаем */ }
|
||
}
|
||
// text_html / solution_html — только < >
|
||
const ft = fixAngles(r.text_html);
|
||
if (ft !== r.text_html) { newText = ft; changedText++; touched = true; }
|
||
const fs = fixAngles(r.solution_html);
|
||
if (fs !== r.solution_html) { newSol = fs; changedSol++; touched = true; }
|
||
|
||
if (touched) {
|
||
changedRows++;
|
||
if (samples.length < 12) {
|
||
samples.push({ v: r.variant, i: r.task_idx,
|
||
beforeOpts: r.opts_json && r.opts_json.length > 90 ? r.opts_json.slice(0, 90) + '…' : r.opts_json,
|
||
afterOpts: newOpts && newOpts.length > 90 ? newOpts.slice(0, 90) + '…' : newOpts });
|
||
}
|
||
if (APPLY) upd.run(newText, newOpts, newSol, r.id);
|
||
}
|
||
}
|
||
if (APPLY) db.exec('COMMIT');
|
||
} catch (e) {
|
||
if (APPLY) db.exec('ROLLBACK');
|
||
console.error('✗ Ошибка, откат:', e.message);
|
||
db.close();
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`\n=== fix_ctmath_render (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||
console.log(`Всего задач ctmath: ${rows.length}`);
|
||
console.log(`Будет изменено строк: ${changedRows} (opts: ${changedOpts}, text: ${changedText}, sol: ${changedSol})`);
|
||
console.log(`\nПримеры (opts до → после):`);
|
||
for (const s of samples) {
|
||
console.log(`\n v${s.v}#${s.i}`);
|
||
console.log(` ДО: ${s.beforeOpts}`);
|
||
console.log(` ПОСЛЕ: ${s.afterOpts}`);
|
||
}
|
||
|
||
/* контроль остаточных «голых» команд после починки (для self-check в dry-run) */
|
||
if (!APPLY) {
|
||
const after = db.prepare(`SELECT opts_json FROM exam_tasks WHERE exam_key=? AND opts_json IS NOT NULL`).all(EXAM);
|
||
let leftover = 0;
|
||
for (const r of after) {
|
||
let arr; try { arr = JSON.parse(r.opts_json); } catch { continue; }
|
||
for (const [, t] of arr) {
|
||
const fixedNow = fixOptionText(t);
|
||
// ищем подозрительные «\bdfrac/sqrt/frac…» БЕЗ слэша уже ПОСЛЕ починки
|
||
if (/(^|[^\\A-Za-z])(sqrt|dfrac|frac|tfrac|cdot|times|alpha|beta|theta|pi)(?![A-Za-z])/.test(fixedNow.replace(/\$/g,''))) leftover++;
|
||
}
|
||
}
|
||
console.log(`\nКонтроль: потенциально не починенных опций после прогона: ${leftover}`);
|
||
console.log(`\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/fix_ctmath_render.js --apply\n`);
|
||
}
|
||
db.close();
|