Files
Learn_System/frontend/js/chem8_svg.js
T
Maxim Dolgolyov 67b95234d0 @
feat(chemistry-8): Phase 0 — каркас учебника «Химия 8» (hub + 7 глав)

Архитектура hub + главы (как физика 7–11, алгебра, геометрия), не монолит.
- chemistry_8_hub.html: хаб-каталог 7 разделов, amber-палитра, прогресс из
  /api/textbooks/chemistry-8/children, achievement «Химик 8 класса»
- 7 каркасов глав (вводный + гл.1–6, §1–52) с оглавлением и баннером «в разработке»
- /js/chem8_svg.js: неймспейс Chem8 (formula/ionLabel/chemEq готовы, 13 хелперов-заглушек)
- миграция 041: родитель chemistry-8 + 7 детей (parent_slug), para_count сумма = 52
- gen_chem8_skeletons.js: генератор каркасов глав
- tests/chemistry8.test.js: 9 тестов (примитивы + целостность каркаса), все зелёные
- PLAN_CHEMISTRY_8.md обновлён под hub-архитектуру

Источник: Шиманович, Красицкий, Сечко, Хвалюк. Химия 8, Народная асвета, 2018.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-05-30 14:10:21 +03:00

140 lines
6.9 KiB
JavaScript

/* chem8_svg.js — химические наглядные примитивы для учебника «Химия 8».
*
* Неймспейс: window.Chem8.*
* Молекулярные модели (структурные / шаростержневые / 3D) — НЕ здесь, а через
* biochem-core.js (window.BioChem). Здесь только то, чего там нет: рендер формул и
* уравнений, ионы, степени окисления, интерактивные виджеты (растворимость, ряд
* активности, индикаторы, классификаторы, калькуляторы расчётов и т. п.).
*
* Phase 0: реализованы чистые текстовые примитивы (ionLabel, chemEq, formula).
* Остальные хелперы — каркасы-заглушки, наполняются по фазам (см. PLAN_CHEMISTRY_8.md, разд. B).
*
* Правила (CLAUDE.md / план):
* - без эмоджи, только inline SVG .ic;
* - в KaTeX-шаблонах двойной backslash (\\to, \\downarrow, \\rightleftharpoons);
* - drag/слайдеры: window-listeners + state ВЫШЕ redraw(), без setPointerCapture.
*/
(function (global) {
'use strict';
var SUB = { '0':'₀','1':'₁','2':'₂','3':'₃','4':'₄',
'5':'₅','6':'₆','7':'₇','8':'₈','9':'₉' };
var SUP = { '0':'⁰','1':'¹','2':'²','3':'³','4':'⁴',
'5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹',
'+':'⁺','-':'⁻' };
function toSub(digits) {
return String(digits).replace(/[0-9]/g, function (d) { return SUB[d]; });
}
function toSup(s) {
return String(s).replace(/[0-9+\-]/g, function (c) { return SUP[c] || c; });
}
/* formula('CaCO3') -> 'CaCO₃' : числовые индексы атомов в подстрочные.
Не трогает множители-коэффициенты в начале (их рендерит chemEq). */
function formula(src) {
if (src == null) return '';
return String(src).replace(/([A-Za-z\)\]])(\d+)/g, function (_, a, n) {
return a + toSub(n);
});
}
/* ionLabel('SO4', -2) -> 'SO₄²⁻' ; ionLabel('Ca', 2) -> 'Ca²⁺' ; ionLabel('Na', 1) -> 'Na⁺' */
function ionLabel(form, charge) {
var body = formula(form);
var c = Number(charge) || 0;
if (c === 0) return body;
var mag = Math.abs(c);
var sign = c > 0 ? '+' : '-';
var num = mag === 1 ? '' : String(mag);
return body + toSup(num + sign);
}
/* chemEq('2Na + 2H2O -> 2NaOH + H2^', {arrow:'->'}) -> HTML-строка с индексами,
стрелками (= → ⇌), значками газа (↑) и осадка (↓), условием над стрелкой.
Токены: '->'/'=' необратимая, '<->'/'<=>' обратимая, '^' газ, 'v' осадок.
opts.cond — подпись над стрелкой (например 't', 'кат.', 'эл. ток'). */
function chemEq(src, opts) {
opts = opts || {};
var s = String(src == null ? '' : src).trim();
var arrowHtml = ' <span class="ceq-arrow">' + arrowGlyph(s, opts) + condHtml(opts) + '</span> ';
// выделяем стрелку
var parts = s.split(/<->|<=>|->|⇌|=(?![^(]*\))|→/);
var left = parts[0] || '';
var right = parts.length > 1 ? parts.slice(1).join(' ') : '';
var html = renderSide(left);
if (right) html += arrowHtml + renderSide(right);
return '<span class="ceq">' + html + '</span>';
}
function arrowGlyph(s, opts) {
if (opts.arrow === '<->' || opts.arrow === '<=>' || /<->|<=>|⇌/.test(s)) return '⇌';
return '→'; // →
}
function condHtml(opts) {
if (!opts.cond) return '';
return '<sup class="ceq-cond">' + escapeHtml(opts.cond) + '</sup>';
}
/* одна сторона уравнения: разбор на вещества по '+', значки ↑/↓ */
function renderSide(side) {
return side.split('+').map(function (term) {
var t = term.trim();
if (!t) return '';
var gas = false, prec = false;
t = t.replace(/\^|↑/g, function () { gas = true; return ''; })
.replace(/(^|[A-Za-z0-9\)])v(\b|$)|↓/g, function (m) {
prec = true; return m.replace(/v|↓/, '');
});
// коэффициент в начале
var coef = '';
t = t.replace(/^(\d+)/, function (_, n) { coef = n; return ''; });
var out = (coef ? coef : '') + formula(t.trim());
if (gas) out += '↑';
if (prec) out += '↓';
return out;
}).filter(Boolean).join(' + ');
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c];
});
}
/* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */
function notImplemented(name) {
return function () {
if (global.console && console.warn) {
console.warn('[Chem8] ' + name + ' ещё не реализован (Phase 0 заглушка)');
}
return null;
};
}
var Chem8 = {
// готово (Phase 0)
formula: formula,
ionLabel: ionLabel,
chemEq: chemEq,
toSub: toSub,
toSup: toSup,
// заглушки (см. план, разд. B) — наполняются в Phase 1–6
testTube: notImplemented('testTube'), // §18,25 — пробирка: осадок/газ/окраска
moleTriangle: notImplemented('moleTriangle'), // §6 — треугольник n–m–M
equationBalancer: notImplemented('equationBalancer'),// §8 — балансировщик уравнений
oxStateCalc: notImplemented('oxStateCalc'), // §42 — калькулятор степени окисления
redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР
orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма
solubilityTable: notImplemented('solubilityTable'), // §19,20,48 — таблица растворимости
activitySeries: notImplemented('activitySeries'), // §14,20 — ряд активности металлов
miniPeriodic: notImplemented('miniPeriodic'), // §1,26,34 — мини-ПСХЭ с подсветкой
indicatorScale: notImplemented('indicatorScale'), // §13,14,16,17 — индикатор + шкала pH
dissociationAnim: notImplemented('dissociationAnim'),// §47 — анимация растворения
classifier: notImplemented('classifier'), // §10,13,16,19,46 — DnD-классификатор
geneticMap: notImplemented('geneticMap') // §22 — генетическая карта-граф классов
};
global.Chem8 = Chem8;
})(typeof window !== 'undefined' ? window : this);