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>
228 lines
10 KiB
JavaScript
228 lines
10 KiB
JavaScript
// review_geom11.js - аудит Геометрии 11 (только репорт)
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const FILES = [
|
||
'frontend/textbooks/geometry_11_hub.html',
|
||
'frontend/textbooks/geometry_11_ch1.html',
|
||
'frontend/textbooks/geometry_11_ch2.html',
|
||
'frontend/textbooks/geometry_11_ch3.html',
|
||
'frontend/textbooks/geometry_11_ch4.html',
|
||
];
|
||
const ROOT = path.join(__dirname, '..', '..');
|
||
|
||
// Словарь KaTeX-команд (часто встречающиеся)
|
||
const KATEX_CMDS = new Set([
|
||
'dfrac','frac','tfrac','sqrt','cdot','times','pm','mp','ne','le','ge','approx',
|
||
'angle','triangle','square','vec','overline','overrightarrow','overrightarrow',
|
||
'sin','cos','tan','cot','arcsin','arccos','arctan','log','ln','lg','exp',
|
||
'pi','alpha','beta','gamma','delta','varepsilon','theta','lambda','mu','phi','omega','rho','sigma','tau',
|
||
'Delta','Gamma','Omega','Phi','Sigma','Theta','Lambda',
|
||
'boxed','underline','mathbf','mathrm','text','textbf','textit',
|
||
'sum','prod','int','infty','lim','to','rightarrow','leftarrow','Rightarrow','Leftrightarrow',
|
||
'left','right','big','Big','bigg','Bigg','quad','qquad',',','!',
|
||
'cap','cup','in','notin','subset','supset','emptyset','varnothing',
|
||
'parallel','perp','cong','sim','equiv','neq',
|
||
'begin','end','array','matrix','pmatrix','bmatrix','cases','aligned','align',
|
||
'displaystyle','textstyle','scriptstyle',
|
||
'colon','ldots','cdots','vdots','ddots',
|
||
'mathbb','mathcal','mathfrak','operatorname',
|
||
'hat','tilde','bar','dot','widehat','widetilde',
|
||
'binom','choose','over','atop',
|
||
'div','star','ast','circ','bullet',
|
||
'forall','exists','land','lor','neg','iff','implies',
|
||
'leq','geq','prec','succ','subseteq','supseteq',
|
||
'partial','nabla','degree','prime',
|
||
'oplus','ominus','otimes','odot',
|
||
'mathring','space','phantom','vphantom',
|
||
'overset','underset','stackrel',
|
||
'color','textcolor','rgb','href',
|
||
'rule','kern','mskip','hspace','vspace',
|
||
'tag','label','ref',
|
||
'lvert','rvert','lVert','rVert','vert','Vert','|',
|
||
'oint','iint','iiint',
|
||
'because','therefore',
|
||
'leftrightarrow','Leftarrow','longleftarrow','longrightarrow',
|
||
'mapsto','hookrightarrow',
|
||
]);
|
||
|
||
const EMOJI_CHARS = ['✓','✗','⚠','🔥','📐','✅','❌','🎯','🔴','🟢','🟡','📊','📈','📉','🎓','📚','🧮','🔬','🌟','⭐','💡','🚀','🎉','📝','📖','🗒'];
|
||
|
||
const AUTHOR_NAMES = ['Латотин','Чеботаревский','Горбунова','Цыбулько','Шлыков','Подгорная'];
|
||
|
||
function scanFile(rel) {
|
||
const fp = path.join(ROOT, rel);
|
||
const src = fs.readFileSync(fp, 'utf8');
|
||
const out = { file: rel, katex: [], optionsKatex: [], emoji: [], authors: [], pixels: [], g3d: {} };
|
||
|
||
// 1. KaTeX ошибки: ищем backslash перед буквой (одиночный) в JS template literals и в HTML
|
||
// Анализируем строчно
|
||
const lines = src.split(/\r?\n/);
|
||
// Regex: одиночный \ перед 2+ буквами, не предшествуемый \
|
||
const reSingle = /(?<!\\)\\([a-zA-Z]{2,})/g;
|
||
// Regex: двойной \\command (нормальный для JS-литералов)
|
||
// Чтобы не плодить шум: репортим только когда команда из KATEX_CMDS
|
||
|
||
// Делим на блоки: внутри <script> и вне
|
||
let inScript = false;
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i];
|
||
if (/<script(\s|>)/i.test(line)) inScript = true;
|
||
// Сначала проверим текущую строку
|
||
// Внутри <script> — нужен \\cmd (т.к. JS экранирует). Одиночный \cmd = ошибка.
|
||
// Вне <script> (чистый HTML, например в title) — \cmd нормально для KaTeX render.
|
||
|
||
// Найдём все совпадения \cmd
|
||
let m;
|
||
const matches = [];
|
||
const re = /\\+([a-zA-Z]{2,})/g;
|
||
while ((m = re.exec(line)) !== null) {
|
||
const bsCount = m[0].length - m[1].length; // число backslashes
|
||
const cmd = m[1];
|
||
if (!KATEX_CMDS.has(cmd)) continue;
|
||
matches.push({ cmd, bsCount, idx: m.index, full: m[0] });
|
||
}
|
||
for (const mt of matches) {
|
||
if (inScript) {
|
||
// в JS template literals одиночный \X неправильно. Нужно \\X (bsCount>=2)
|
||
if (mt.bsCount === 1) {
|
||
out.katex.push({ line: i+1, cmd: mt.cmd, snippet: line.trim().slice(0, 140) });
|
||
}
|
||
} else {
|
||
// в чистом HTML \cmd ок
|
||
}
|
||
}
|
||
if (/<\/script>/i.test(line)) inScript = false;
|
||
}
|
||
|
||
// 2. <option> с сырым KaTeX (содержит $ или \\sqrt и т.п.)
|
||
const optRe = /<option[^>]*>([^<]*)<\/option>/g;
|
||
let om;
|
||
while ((om = optRe.exec(src)) !== null) {
|
||
const txt = om[1];
|
||
if (/\$[^$]+\$/.test(txt) || /\\\\[a-zA-Z]+/.test(txt) || /\\(dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times)/.test(txt)) {
|
||
out.optionsKatex.push({ snippet: om[0].slice(0,160) });
|
||
}
|
||
}
|
||
// Также проверим динамический innerHTML на option в JS
|
||
const scriptBlocks = [...src.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||
for (const sb of scriptBlocks) {
|
||
// ищем <option ...>$...$</option> или \\sqrt в template-literals
|
||
const re2 = /<option[^>]*>[^<]*(?:\$[^$]+\$|\\\\(?:dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times))[^<]*<\/option>/g;
|
||
let mm;
|
||
while ((mm = re2.exec(sb)) !== null) {
|
||
out.optionsKatex.push({ snippet: mm[0].slice(0,160), inScript: true });
|
||
}
|
||
}
|
||
|
||
// 3. Эмодзи (исключая SVG)
|
||
for (const ch of EMOJI_CHARS) {
|
||
let idx = 0;
|
||
while ((idx = src.indexOf(ch, idx)) !== -1) {
|
||
// Найти строку
|
||
const upto = src.slice(0, idx);
|
||
const lineNum = upto.split('\n').length;
|
||
const lineStart = upto.lastIndexOf('\n') + 1;
|
||
const lineEnd = src.indexOf('\n', idx);
|
||
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
|
||
out.emoji.push({ char: ch, line: lineNum, snippet: lineText.trim().slice(0, 120) });
|
||
idx++;
|
||
}
|
||
}
|
||
|
||
// 4. Авторы
|
||
for (const name of AUTHOR_NAMES) {
|
||
if (src.includes(name)) {
|
||
// Найдём строку
|
||
const idx = src.indexOf(name);
|
||
const upto = src.slice(0, idx);
|
||
const lineNum = upto.split('\n').length;
|
||
const lineStart = upto.lastIndexOf('\n') + 1;
|
||
const lineEnd = src.indexOf('\n', idx);
|
||
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
|
||
out.authors.push({ name, line: lineNum, snippet: lineText.trim().slice(0, 160) });
|
||
}
|
||
}
|
||
|
||
// 5. Пиксели в подписях: ищем sliders с min/max, и подписи рядом
|
||
// Простая эвристика: <input type="range" min="X" max="Y" ...> + value/label рядом
|
||
const sliderRe = /<input[^>]*type=["']range["'][^>]*>/g;
|
||
let sm;
|
||
while ((sm = sliderRe.exec(src)) !== null) {
|
||
const tag = sm[0];
|
||
const minM = tag.match(/\bmin=["'](\d+)["']/);
|
||
const maxM = tag.match(/\bmax=["'](\d+)["']/);
|
||
const idM = tag.match(/\bid=["']([^"']+)["']/);
|
||
if (minM && maxM) {
|
||
const min = +minM[1], max = +maxM[1];
|
||
// подозрительно если min>20 и max>100 (вероятно пиксели)
|
||
const suspicious = min >= 20 && max >= 100;
|
||
if (suspicious) {
|
||
const upto = src.slice(0, sm.index);
|
||
const lineNum = upto.split('\n').length;
|
||
out.pixels.push({ line: lineNum, id: idM ? idM[1] : '', min, max, tag: tag.slice(0,180) });
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. G3D usage
|
||
out.g3d.includesScript = /<script[^>]*src=["']\/js\/g3d\.js["'][^>]*>/.test(src);
|
||
out.g3d.usesPrism = /G3D\.prismMesh/.test(src);
|
||
out.g3d.usesCylinder = /G3D\.cylinderMesh/.test(src);
|
||
out.g3d.usesPyramid = /G3D\.pyramidMesh/.test(src);
|
||
out.g3d.usesCone = /G3D\.coneMesh/.test(src);
|
||
out.g3d.usesSphere = /G3D\.sphereWireframe/.test(src);
|
||
out.g3d.usesAttachOrbit = /G3D\.attachOrbit/.test(src);
|
||
out.g3d.usesPresetView = /G3D\.presetView/.test(src);
|
||
out.g3d.viewButtons = (src.match(/Изо|Спереди|Сверху|Сбоку/g) || []).length;
|
||
|
||
// 7. Структура: theory cards (3), interactives (4), "Я прочитал"
|
||
out.theoryCards = (src.match(/class=["'][^"']*theory[^"']*card/g) || []).length;
|
||
out.interactiveCards = (src.match(/class=["'][^"']*interactive[^"']*/g) || []).length;
|
||
out.readBtns = (src.match(/Я прочитал|я прочитал/g) || []).length;
|
||
out.bossCount = (src.match(/босс|Босс|BOSS/g) || []).length;
|
||
out.paragraphCount = (src.match(/§\d+/g) || []).length;
|
||
out.finalSections = (src.match(/Финал|финал раздела|Финал раздела/g) || []).length;
|
||
out.length = src.length;
|
||
|
||
return out;
|
||
}
|
||
|
||
const results = FILES.map(scanFile);
|
||
|
||
// Report
|
||
console.log('='.repeat(80));
|
||
console.log('AUDIT GEOMETRY 11');
|
||
console.log('='.repeat(80));
|
||
|
||
let totals = { katex: 0, options: 0, emoji: 0, authors: 0, pixels: 0 };
|
||
|
||
for (const r of results) {
|
||
console.log(`\n--- ${r.file} (${(r.length/1024).toFixed(1)} KB) ---`);
|
||
console.log(`KaTeX errors: ${r.katex.length}`);
|
||
if (r.katex.length) {
|
||
r.katex.slice(0, 10).forEach(e => console.log(` L${e.line}: \\${e.cmd} | ${e.snippet}`));
|
||
if (r.katex.length > 10) console.log(` ... +${r.katex.length-10} more`);
|
||
}
|
||
console.log(`<option> KaTeX: ${r.optionsKatex.length}`);
|
||
r.optionsKatex.slice(0,5).forEach(o => console.log(` ${o.inScript?'[JS] ':''}${o.snippet}`));
|
||
console.log(`Emoji: ${r.emoji.length}`);
|
||
r.emoji.slice(0,5).forEach(e => console.log(` L${e.line} '${e.char}': ${e.snippet}`));
|
||
console.log(`Authors: ${r.authors.length}`);
|
||
r.authors.slice(0,5).forEach(a => console.log(` L${a.line} '${a.name}': ${a.snippet}`));
|
||
console.log(`Pixel sliders (susp.): ${r.pixels.length}`);
|
||
r.pixels.slice(0,5).forEach(p => console.log(` L${p.line} #${p.id} min=${p.min} max=${p.max}`));
|
||
console.log(`G3D: script=${r.g3d.includesScript}, prism=${r.g3d.usesPrism}, cyl=${r.g3d.usesCylinder}, pyr=${r.g3d.usesPyramid}, cone=${r.g3d.usesCone}, sphere=${r.g3d.usesSphere}, orbit=${r.g3d.usesAttachOrbit}, presetView=${r.g3d.usesPresetView}, viewBtnHits=${r.g3d.viewButtons}`);
|
||
console.log(`Structure: theoryCards=${r.theoryCards}, interactives=${r.interactiveCards}, readBtns=${r.readBtns}, bossHits=${r.bossCount}, §count=${r.paragraphCount}, finalHits=${r.finalSections}`);
|
||
|
||
totals.katex += r.katex.length;
|
||
totals.options += r.optionsKatex.length;
|
||
totals.emoji += r.emoji.length;
|
||
totals.authors += r.authors.length;
|
||
totals.pixels += r.pixels.length;
|
||
}
|
||
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log('TOTALS:', totals);
|
||
console.log('='.repeat(80));
|