5381679c68
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест — оставлены незакоммиченными по запросу). Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges, пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend- страницы и lab/textbooks-правки параллельной сессии. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
5.2 KiB
JavaScript
111 lines
5.2 KiB
JavaScript
/* 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(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
|
|
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
|
|
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
|
|
Object.keys(inl).forEach(src => {
|
|
html = html.replace(new RegExp('<script src="' + src + '" defer><\\/script>'), () => '<script>\n' + inl[src] + '\n</script>');
|
|
});
|
|
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);
|
|
})();
|