/* 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); })();