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> @
140 lines
6.9 KiB
JavaScript
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 { '&':'&','<':'<','>':'>','"':'"',"'":''' }[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);
|