/* audit_chem8.js — аудит KaTeX и оформления учебника «Химия 8».
* Загружает каждую страницу в jsdom (renderMathInElement застаблен → $…$ остаются
* литералами с уже раскрытыми JS-эскейпами), строит все §, извлекает формулы и
* проверяет: баланс $, баланс {}, отсутствие управляющих символов (следы \t/\n),
* пустые формулы, «сырые» $…$ вне рендера. Запуск: node backend/scripts/audit_chem8.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { JSDOM, VirtualConsole } = require('jsdom');
const ROOT = path.join(__dirname, '..', '..');
const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8');
const wait = ms => new Promise(r => setTimeout(r, ms));
const PAGES = [
['chemistry_8_intro.html', 'chem8_intro_widgets'],
['chemistry_8_ch1.html', 'chem8_ch1_widgets'],
['chemistry_8_ch2.html', 'chem8_ch2_widgets'],
['chemistry_8_ch3.html', 'chem8_ch3_widgets'],
['chemistry_8_ch4.html', 'chem8_ch4_widgets'],
['chemistry_8_ch5.html', 'chem8_ch5_widgets'],
['chemistry_8_ch6.html', 'chem8_ch6_widgets']
];
function buildPage(file, widgets) {
let html = readF('frontend/textbooks/' + file);
const inl = {
'/js/biochem-core.js': readF('frontend/js/biochem-core.js'),
'/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
'/js/chem8_mol.js': readF('frontend/js/chem8_mol.js'),
['/js/' + widgets + '.js']: readF('frontend/js/' + widgets + '.js'),
'/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
};
html = html
.replace(/')
.replace(/');
});
return html;
}
function extractMath(s) {
const out = [];
// $$...$$ затем $...$
let re = /\$\$([\s\S]+?)\$\$/g, m;
let masked = s;
while ((m = re.exec(s)) !== null) out.push({ disp: true, body: m[1] });
masked = s.replace(/\$\$[\s\S]+?\$\$/g, '');
re = /\$([^$]*)\$/g;
while ((m = re.exec(masked)) !== null) out.push({ disp: false, body: m[1] });
return out;
}
function checkBraces(b) { let d = 0; for (const c of b) { if (c === '{') d++; else if (c === '}') d--; if (d < 0) return false; } return d === 0; }
function hasCtrl(b) { return /[\t\n\r\f\v\b]/.test(b); }
async function auditPage(file, widgets) {
const issues = [];
const vc = new VirtualConsole(); const errs = [];
vc.on('jsdomError', e => errs.push(e.message));
const dom = new JSDOM(buildPage(file, widgets), {
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
beforeParse(w) { w.scrollTo = function () {}; }
});
await wait(120);
const doc = dom.window.document;
const paras = (dom.window.PARAS || []).map(p => p.id);
for (const id of paras) { try { dom.window.goTo(id); } catch (e) {} }
await wait(120);
if (errs.length) issues.push('script errors: ' + errs.join(' | '));
// собрать все § тела + sidebar
let html = '';
doc.querySelectorAll('[id$="-body"]').forEach(el => { html += el.innerHTML + '\n'; });
const sidebar = doc.getElementById('sidebar-content'); if (sidebar) html += sidebar.innerHTML;
// баланс $ (нечётное число одиночных $ вне $$)
const noDisp = html.replace(/\$\$[\s\S]+?\$\$/g, '');
const singles = (noDisp.match(/\$/g) || []).length;
if (singles % 2 !== 0) issues.push('нечётное число одиночных $ (' + singles + ')');
const maths = extractMath(html);
let bad = 0;
for (const m of maths) {
const b = m.body;
if (!b.trim()) { issues.push('пустая формула $' + (m.disp ? '$' : '') + '$'); bad++; continue; }
if (!checkBraces(b)) { issues.push('несбалансированные {} в: ' + b.slice(0, 50)); bad++; }
if (hasCtrl(b)) { issues.push('управляющий символ (след \\t/\\n?) в: ' + JSON.stringify(b.slice(0, 50))); bad++; }
// одиночный backslash перед буквой, не часть известной команды? — грубая эвристика: \ в конце
if (/\\$/.test(b)) { issues.push('формула заканчивается на \\: ' + b.slice(-20)); bad++; }
}
return { file, mathCount: maths.length, badCount: bad, issues };
}
(async () => {
let total = 0, totalBad = 0;
for (const [file, w] of PAGES) {
const r = await auditPage(file, w);
total += r.mathCount; totalBad += r.badCount;
console.log('\n=== ' + file + ' — формул: ' + r.mathCount + ', проблем: ' + r.issues.length + ' ===');
if (r.issues.length) r.issues.slice(0, 25).forEach(i => console.log(' ! ' + i));
else console.log(' OK');
}
console.log('\nИТОГО формул: ' + total + ', проблемных: ' + totalBad);
process.exit(0);
})();