Files
Learn_System/backend/scripts/gen_chem8_skeletons.js
T
Maxim Dolgolyov 6ea140af54 @
feat(chemistry-8): Phase 1 — раздел «Количественные понятия» (§1–9 + ПР1)

Полноценная интерактивная страница chemistry_8_intro.html (9 § + ПР1 + босс):
- §1 карта элементов (Z, название, Ar), §2 калькулятор Mr по формуле
- §3 «порция вещества» n⇒N,m, §4 счётчик частиц N=n·N_A, §5 M+молярный объём
- §6 звёздный виджет: интерактивный треугольник n–m–M
- §7 универсальный калькулятор газа (m–n–V–N), §8 балансировщик уравнений
- §9 пошаговый решатель по уравнению; босс раздела (4 задачи) + ачивка «Счёт в химии»
- прогресс/XP через /api/textbooks/chemistry-8-intro/progress, scrollspy, тема

chem8_svg.js: реализованы движки — molarMass (школьные Ar: Mr(H2O)=18),
elementCounts, moleTriangle, equationBalancer (+ fmt, arOf).

Фикс порядка загрузки: инициализация обёрнута в DOMContentLoaded (defer-скрипты
готовы к этому моменту). Генератор каркасов получил skip-if-exists (--force для перезаписи).

Тесты: chemistry8.test.js (14) + chemistry8-dom.test.js (jsdom-смоук виджетов, 3) — 17/17.

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

306 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* gen_chem8_skeletons.js — генерирует каркасы 7 глав «Химия 8» (Phase 0).
* Запуск: node backend/scripts/gen_chem8_skeletons.js
* Выход: frontend/textbooks/chemistry_8_intro.html, _ch1.html ... _ch6.html
*
* Каркас = валидная брендированная страница: header (водяной знак), hero,
* оглавление § (read-only), баннер «в разработке», ссылка назад в хаб, тема.
* Полный интерактивный SPA-контент каждой главы добавляется в Phase 1–6
* (файлы перезаписываются), пока скелет обеспечивает навигацию и структуру.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
const P = (t, n) => ({ t, n }); // параграф
const NOTE = (note) => ({ note }); // лаб. опыт / практическая работа
const CHAPTERS = [
{
file: 'chemistry_8_intro.html', slug: 'chemistry-8-intro',
kicker: 'Вводный раздел', title: 'Количественные понятия в химии',
range: '§ 19', wm: 'mol',
color: { p:'#d97706', d:'#b45309', l:'#fbbf24', soft:'#fef3c7', bgd:'#1c1410', cardd:'#271c14', textd:'#fef3c7' },
items: [
P('§ 1', 'Атомы. Химические элементы. Относительная атомная масса'),
P('§ 2', 'Молекулы. Простые и сложные вещества. Химические формулы. Относительная молекулярная масса'),
P('§ 3', 'Химическое количество вещества'),
P('§ 4', 'Моль — единица химического количества вещества. Постоянная Авогадро'),
P('§ 5', 'Молярная масса. Молярный объём газов'),
P('§ 6', 'Вычисление химического количества вещества по его массе и массы вещества по его химическому количеству'),
P('§ 7', 'Вычисление химического количества газа по его объёму и объёма газа по его химическому количеству'),
NOTE('Практическая работа 1. Химическое количество вещества'),
P('§ 8', 'Химические реакции'),
P('§ 9', 'Количественные расчёты по уравнениям химических реакций')
]
},
{
file: 'chemistry_8_ch1.html', slug: 'chemistry-8-ch1',
kicker: 'Глава 1', title: 'Важнейшие классы неорганических соединений',
range: '§ 1023', wm: 'OH',
color: { p:'#0d9488', d:'#0f766e', l:'#14b8a6', soft:'#ccfbf1', bgd:'#0c1a18', cardd:'#102825', textd:'#ccfbf1' },
items: [
P('§ 10', 'Оксиды. Состав и классификация оксидов'),
P('§ 11', 'Химические свойства оксидов'),
P('§ 12', 'Получение и применение оксидов'),
P('§ 13', 'Кислоты. Состав и классификация кислот'),
P('§ 14', 'Химические свойства кислот'),
P('§ 15', 'Получение и применение кислот'),
P('§ 16', 'Основания'),
P('§ 17', 'Химические свойства оснований'),
P('§ 18', 'Получение и применение оснований'),
NOTE('Лабораторный опыт 1. Получение нерастворимого основания'),
NOTE('Практическая работа 2. Изучение реакции нейтрализации'),
P('§ 19', 'Соли. Состав и классификация солей'),
P('§ 20', 'Химические свойства солей'),
NOTE('Лабораторный опыт 2. Взаимодействие растворов солей с металлами'),
P('§ 21', 'Получение и применение солей'),
P('§ 22', 'Взаимосвязь между классами основных неорганических веществ'),
NOTE('Практическая работа 3. Решение экспериментальных задач'),
P('§ 23', 'Решение расчётных задач по теме «Основные классы неорганических соединений»')
]
},
{
file: 'chemistry_8_ch2.html', slug: 'chemistry-8-ch2',
kicker: 'Глава 2', title: 'Периодический закон и периодическая система химических элементов',
range: '§ 2428', wm: '№',
color: { p:'#4f46e5', d:'#4338ca', l:'#818cf8', soft:'#e0e7ff', bgd:'#12122b', cardd:'#1b1b3a', textd:'#e0e7ff' },
items: [
P('§ 24', 'Систематизация химических элементов'),
P('§ 25', 'Понятие об амфотерности'),
NOTE('Лабораторный опыт 3. Получение гидроксида цинка и изучение его амфотерных свойств'),
P('§ 26', 'Естественные семейства элементов'),
P('§ 27', 'Периодический закон Д. И. Менделеева'),
P('§ 28', 'Периодическая система химических элементов')
]
},
{
file: 'chemistry_8_ch3.html', slug: 'chemistry-8-ch3',
kicker: 'Глава 3', title: 'Строение атома и периодичность изменения свойств',
range: '§ 2935', wm: 'e',
color: { p:'#2563eb', d:'#1d4ed8', l:'#60a5fa', soft:'#dbeafe', bgd:'#0a1428', cardd:'#102137', textd:'#dbeafe' },
items: [
P('§ 29', 'Строение атома. Атомный номер химического элемента'),
P('§ 30', 'Массовое число атома. Нуклиды'),
P('§ 31', 'Изотопы. Явление радиоактивности'),
P('§ 32', 'Состояние электронов в атоме. Электронное облако. Атомная орбиталь'),
P('§ 33', 'Строение электронных оболочек атомов'),
P('§ 34', 'Периодичность изменения свойств атомов химических элементов'),
P('§ 35', 'Характеристика химического элемента по его положению в периодической системе')
]
},
{
file: 'chemistry_8_ch4.html', slug: 'chemistry-8-ch4',
kicker: 'Глава 4', title: 'Химическая связь',
range: '§ 3641', wm: 'H₂O',
color: { p:'#059669', d:'#047857', l:'#34d399', soft:'#d1fae5', bgd:'#0a1a12', cardd:'#10271c', textd:'#d1fae5' },
items: [
P('§ 36', 'Природа химической связи'),
P('§ 37', 'Ковалентная связь'),
P('§ 38', 'Неполярная и полярная ковалентная связь. Электроотрицательность'),
NOTE('Лабораторный опыт 4. Составление моделей молекул'),
P('§ 39', 'Ионная связь'),
P('§ 40', 'Металлическая связь. Межмолекулярное взаимодействие'),
P('§ 41', 'Кристаллическое состояние вещества')
]
},
{
file: 'chemistry_8_ch5.html', slug: 'chemistry-8-ch5',
kicker: 'Глава 5', title: 'Окислительно-восстановительные реакции',
range: '§ 4245', wm: 'O₂',
color: { p:'#ea580c', d:'#c2410c', l:'#fb923c', soft:'#ffedd5', bgd:'#1c1208', cardd:'#2a1c10', textd:'#ffedd5' },
items: [
P('§ 42', 'Степень окисления'),
P('§ 43', 'Процессы окисления и восстановления'),
P('§ 44', 'Окислительно-восстановительные реакции'),
P('§ 45', 'Окислительно-восстановительные реакции вокруг нас')
]
},
{
file: 'chemistry_8_ch6.html', slug: 'chemistry-8-ch6',
kicker: 'Глава 6', title: 'Растворы',
range: '§ 4652', wm: 'aq',
color: { p:'#0891b2', d:'#0e7490', l:'#22d3ee', soft:'#cffafe', bgd:'#08191c', cardd:'#10282d', textd:'#cffafe' },
items: [
P('§ 46', 'Смеси веществ'),
P('§ 47', 'Растворение веществ в воде'),
P('§ 48', 'Характеристики растворимости веществ'),
P('§ 49', 'Качественные характеристики состава растворов'),
P('§ 50', 'Количественные характеристики растворённых веществ. Массовая доля растворённого вещества'),
P('§ 51', 'Молярная концентрация растворённых веществ'),
NOTE('Практическая работа 4. Приготовление раствора с заданной массовой долей и молярной концентрацией'),
P('§ 52', 'Вода и растворы в жизни и деятельности человека')
]
}
];
function esc(s) {
return String(s).replace(/[&<>]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[c]));
}
function outlineHtml(items) {
return items.map(it => {
if (it.note) {
return ' <li class="ol-note"><span class="ol-note-ic">' +
'<svg viewBox="0 0 24 24"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>' +
'</span><span>' + esc(it.note) + '</span></li>';
}
return ' <li class="ol-para"><span class="ol-num">' + esc(it.t) + '</span><span class="ol-name">' + esc(it.n) + '</span></li>';
}).join('\n');
}
function pageHtml(ch) {
const c = ch.color;
const wmHeader = ch.kicker.toUpperCase();
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Химия 8 · ${esc(ch.kicker)} · «${esc(ch.title)}»</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/biochem-core.js" defer></script>
<script src="/js/chem8_svg.js" defer></script>
<style>
:root{
--bg:#fffbeb; --card:#fff; --text:#1c1917; --muted:#78716c; --border:#e7e5e4;
--pri:${c.p}; --pri-d:${c.d}; --pri-l:${c.l}; --pri-soft:${c.soft};
--sh:0 4px 16px rgba(0,0,0,.06); --sh-h:0 12px 32px rgba(0,0,0,.12);
}
html.dark{ --bg:${c.bgd}; --card:${c.cardd}; --text:${c.textd}; --muted:#a8a29e; --border:#3a3026; --pri-soft:rgba(0,0,0,.2); }
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.hdr{position:relative;background:linear-gradient(110deg,${c.d} 0%,${c.p} 55%,${c.l} 100%);color:#fff;padding:34px 24px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
.hdr::before{content:'${wmHeader}';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(4rem,13vw,10rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.13);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-inner{position:relative;z-index:1;max-width:1000px;margin:0 auto;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.16);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.26)}
.hdr-kicker{font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.14em;opacity:.85}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.55rem;font-weight:900;letter-spacing:-.01em;line-height:1.25;margin-top:3px}
.hdr-side{margin-left:auto}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
.hdr-btn:hover{background:rgba(255,255,255,.26)}
main{max-width:1000px;margin:0 auto;padding:28px 24px 60px}
.wip{display:flex;gap:14px;align-items:flex-start;background:linear-gradient(135deg,var(--pri-soft),rgba(0,0,0,.02));border:1.5px dashed var(--pri);border-radius:16px;padding:18px 20px;margin-bottom:26px}
.wip-ic{width:42px;height:42px;border-radius:11px;background:var(--pri);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.wip-ic svg{width:22px;height:22px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.wip h2{font-family:'Outfit',sans-serif;font-size:1.05rem;color:var(--pri-d);margin-bottom:4px}
html.dark .wip h2{color:var(--pri-l)}
.wip p{font-size:.9rem;color:var(--muted);line-height:1.55}
.ol-title{font-family:'Outfit',sans-serif;font-size:1.15rem;font-weight:800;margin:6px 0 14px;display:flex;align-items:center;gap:9px}
.ol-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ol-list{list-style:none;background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;box-shadow:var(--sh)}
.ol-para,.ol-note{display:flex;gap:12px;align-items:baseline;padding:12px 18px;border-bottom:1px solid var(--border)}
.ol-list li:last-child{border-bottom:0}
.ol-num{flex-shrink:0;min-width:46px;font-weight:800;color:var(--pri);font-size:.92rem}
.ol-name{font-size:.94rem;color:var(--text)}
.ol-note{background:var(--pri-soft);align-items:center;gap:10px}
.ol-note-ic{display:inline-flex;color:var(--pri-d)}
html.dark .ol-note-ic{color:var(--pri-l)}
.ol-note-ic svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ol-note span:last-child{font-size:.88rem;font-weight:600;color:var(--pri-d)}
html.dark .ol-note span:last-child{color:var(--pri-l)}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<a href="/textbook/chemistry-8" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К разделам
</a>
<div>
<div class="hdr-kicker">${esc(ch.kicker)} &middot; ${esc(ch.range)}</div>
<h1>${esc(ch.title)}</h1>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main>
<section class="wip">
<div class="wip-ic">
<svg viewBox="0 0 24 24"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4l-6.3 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l6.3-6.3a4 4 0 0 0 5.4-5.4l-2.6 2.6-2-2 2.6-2.6z"/></svg>
</div>
<div>
<h2>Раздел в разработке</h2>
<p>Интерактивное наглядное наполнение этого раздела (теория, модели, симуляторы, тренажёры и боссы) добавляется поэтапно. Ниже — план параграфов раздела согласно учебнику.</p>
</div>
</section>
<div class="ol-title">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
Содержание раздела
</div>
<ul class="ol-list">
${outlineHtml(ch.items)}
</ul>
</main>
<footer class="foot">
Интерактивный учебник «Химия — 8 класс» &middot; ${esc(ch.kicker)} &middot; LearnSpace
</footer>
<script>
'use strict';
const _TB_SLUG = '${ch.slug}';
(function(){
var saved = localStorage.getItem('chemistry8_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab');
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('chemistry8_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
</script>
</body>
</html>
`;
}
// --force перезапишет уже существующие файлы; по умолчанию — пропускаем
// готовые (наполненные в фазах) страницы, чтобы не затереть контент.
const FORCE = process.argv.includes('--force');
let count = 0, skipped = 0;
for (const ch of CHAPTERS) {
const target = path.join(OUT, ch.file);
if (!FORCE && fs.existsSync(target)) {
skipped++;
console.log('skip ', ch.file, '(уже существует — наполнен в фазе)');
continue;
}
fs.writeFileSync(target, pageHtml(ch), 'utf8');
count++;
console.log('written', ch.file, '(' + ch.items.filter(i => i.t).length + ' §)');
}
console.log('done:', count, 'written,', skipped, 'skipped');