@
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> @
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
/* 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: '§ 1–9', 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: '§ 10–23', 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: '§ 24–28', 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: '§ 29–35', 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: '§ 36–41', 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: '§ 42–45', 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: '§ 46–52', 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 => ({ '&':'&', '<':'<', '>':'>' }[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)} · ${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 класс» · ${esc(ch.kicker)} · 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>
|
||||
`;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const ch of CHAPTERS) {
|
||||
const html = pageHtml(ch);
|
||||
fs.writeFileSync(path.join(OUT, ch.file), html, 'utf8');
|
||||
count++;
|
||||
console.log('written', ch.file, '(' + ch.items.filter(i => i.t).length + ' §)');
|
||||
}
|
||||
console.log('done:', count, 'chapter skeletons');
|
||||
@@ -0,0 +1,56 @@
|
||||
-- Chemistry 8 hub migration.
|
||||
-- Creates chemistry-8 as a full hub textbook (intro + 6 chapters) in the style of physics-9:
|
||||
-- chemistry-8 (hub, html_path = chemistry_8_hub.html)
|
||||
-- chemistry-8-intro (Количественные понятия, §§1–9) → chemistry_8_intro.html
|
||||
-- chemistry-8-ch1 (Важнейшие классы соединений, §§10–23) → chemistry_8_ch1.html
|
||||
-- chemistry-8-ch2 (Периодический закон и ПСХЭ, §§24–28) → chemistry_8_ch2.html
|
||||
-- chemistry-8-ch3 (Строение атома, §§29–35) → chemistry_8_ch3.html
|
||||
-- chemistry-8-ch4 (Химическая связь, §§36–41) → chemistry_8_ch4.html
|
||||
-- chemistry-8-ch5 (ОВР, §§42–45) → chemistry_8_ch5.html
|
||||
-- chemistry-8-ch6 (Растворы, §§46–52) → chemistry_8_ch6.html
|
||||
--
|
||||
-- Source: Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н.,
|
||||
-- «Химия 8», Народная асвета, 2018. Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
-- 1. Insert the parent chemistry-8 hub row (does not exist yet in the catalog).
|
||||
INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('chemistry-8', 'chemistry', 8, 'Химия — 8 класс',
|
||||
'',
|
||||
'Полный курс химии за 8 класс: количественные понятия (моль, молярная масса и объём, расчёты по уравнениям), важнейшие классы неорганических соединений, периодический закон и строение атома, химическая связь, окислительно-восстановительные реакции, растворы. 7 разделов, 52 параграфа, 4 лабораторных опыта, 4 практические работы.',
|
||||
'chemistry_8_hub.html', 52, 'amber', 8, 1, NULL);
|
||||
|
||||
-- 2. Insert the 7 children (intro section + 6 chapters).
|
||||
INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('chemistry-8-intro', 'chemistry', 8, 'Химия 8 · Количественные понятия в химии',
|
||||
'',
|
||||
'§§1–9: атомы и химические элементы, простые и сложные вещества, химическое количество вещества, моль и постоянная Авогадро, молярная масса и молярный объём газов, расчёты по массе/объёму и по уравнениям реакций. Практическая работа 1.',
|
||||
'chemistry_8_intro.html', 9, 'amber', 1, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch1', 'chemistry', 8, 'Химия 8 · Важнейшие классы неорганических соединений',
|
||||
'',
|
||||
'§§10–23: оксиды, кислоты, основания и соли — состав, классификация, химические свойства, получение и применение; генетическая связь между классами. 2 лабораторных опыта, 2 практические работы.',
|
||||
'chemistry_8_ch1.html', 14, 'teal', 2, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch2', 'chemistry', 8, 'Химия 8 · Периодический закон и периодическая система',
|
||||
'',
|
||||
'§§24–28: систематизация элементов, амфотерность, естественные семейства элементов, периодический закон Д. И. Менделеева и строение периодической системы. Лабораторный опыт 3.',
|
||||
'chemistry_8_ch2.html', 5, 'indigo', 3, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch3', 'chemistry', 8, 'Химия 8 · Строение атома',
|
||||
'',
|
||||
'§§29–35: строение атома и атомный номер, массовое число и нуклиды, изотопы и радиоактивность, электронное облако и атомная орбиталь, строение электронных оболочек, периодичность свойств, характеристика элемента по положению в ПС.',
|
||||
'chemistry_8_ch3.html', 7, 'blue', 4, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch4', 'chemistry', 8, 'Химия 8 · Химическая связь',
|
||||
'',
|
||||
'§§36–41: природа химической связи, ковалентная связь (неполярная и полярная, электроотрицательность), ионная и металлическая связь, межмолекулярное взаимодействие, кристаллическое состояние вещества. Лабораторный опыт 4.',
|
||||
'chemistry_8_ch4.html', 6, 'green', 5, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch5', 'chemistry', 8, 'Химия 8 · Окислительно-восстановительные реакции',
|
||||
'',
|
||||
'§§42–45: степень окисления, процессы окисления и восстановления, окислительно-восстановительные реакции и метод электронного баланса, ОВР вокруг нас.',
|
||||
'chemistry_8_ch5.html', 4, 'orange', 6, 1, 'chemistry-8'),
|
||||
('chemistry-8-ch6', 'chemistry', 8, 'Химия 8 · Растворы',
|
||||
'',
|
||||
'§§46–52: смеси веществ, растворение веществ в воде, характеристики растворимости, качественные и количественные характеристики состава растворов, массовая доля и молярная концентрация, вода и растворы в жизни человека. Практическая работа 4.',
|
||||
'chemistry_8_ch6.html', 7, 'cyan', 7, 1, 'chemistry-8');
|
||||
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Phase 0 тесты учебника «Химия 8» (hub + 7 глав).
|
||||
* 1. Чистые примитивы frontend/js/chem8_svg.js (window.Chem8): formula/ionLabel/chemEq.
|
||||
* 2. Целостность каркаса: хаб + 7 файлов глав существуют, slug'и согласованы,
|
||||
* сумма параграфов = 52, миграция 041 содержит родителя + 7 детей.
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
const TB = path.join(ROOT, 'frontend', 'textbooks');
|
||||
|
||||
// --- shim browser global, load the frontend module ---
|
||||
global.window = global;
|
||||
require(path.join(ROOT, 'frontend', 'js', 'chem8_svg.js'));
|
||||
const C = global.Chem8;
|
||||
|
||||
test('Chem8.formula — числовые индексы в подстрочные', () => {
|
||||
assert.equal(C.formula('CaCO3'), 'CaCO₃');
|
||||
assert.equal(C.formula('H2O'), 'H₂O');
|
||||
assert.equal(C.formula('Al2(SO4)3'), 'Al₂(SO₄)₃');
|
||||
assert.equal(C.formula('NaCl'), 'NaCl');
|
||||
});
|
||||
|
||||
test('Chem8.ionLabel — заряд ионов надстрочным', () => {
|
||||
assert.equal(C.ionLabel('Na', 1), 'Na⁺');
|
||||
assert.equal(C.ionLabel('Ca', 2), 'Ca²⁺');
|
||||
assert.equal(C.ionLabel('Cl', -1), 'Cl⁻');
|
||||
assert.equal(C.ionLabel('SO4', -2), 'SO₄²⁻');
|
||||
assert.equal(C.ionLabel('Fe', 3), 'Fe³⁺');
|
||||
assert.equal(C.ionLabel('Na', 0), 'Na');
|
||||
});
|
||||
|
||||
test('Chem8.chemEq — стрелка, индексы, газ', () => {
|
||||
const html = C.chemEq('2Na + 2H2O -> 2NaOH + H2^');
|
||||
assert.ok(html.includes('2H₂O'), 'индексы воды');
|
||||
assert.ok(html.includes('→'), 'стрелка реакции');
|
||||
assert.ok(html.includes('H₂↑'), 'значок газа');
|
||||
assert.ok(html.includes('class="ceq"'), 'обёртка');
|
||||
});
|
||||
|
||||
test('Chem8.chemEq — обратимая реакция и осадок', () => {
|
||||
const rev = C.chemEq('N2 + 3H2 <-> 2NH3');
|
||||
assert.ok(rev.includes('⇌'), 'обратимая стрелка');
|
||||
const prec = C.chemEq('AgNO3 + NaCl -> AgClv + NaNO3');
|
||||
assert.ok(prec.includes('AgCl↓'), 'значок осадка');
|
||||
});
|
||||
|
||||
test('Chem8 — заглушки возвращают null и не падают', () => {
|
||||
for (const fn of ['testTube', 'moleTriangle', 'solubilityTable', 'oxStateCalc', 'geneticMap']) {
|
||||
assert.equal(typeof C[fn], 'function', fn + ' определён');
|
||||
assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null');
|
||||
}
|
||||
});
|
||||
|
||||
// --- каркас страниц ---
|
||||
const CHILDREN = [
|
||||
{ slug: 'chemistry-8-intro', file: 'chemistry_8_intro.html', paras: 9 },
|
||||
{ slug: 'chemistry-8-ch1', file: 'chemistry_8_ch1.html', paras: 14 },
|
||||
{ slug: 'chemistry-8-ch2', file: 'chemistry_8_ch2.html', paras: 5 },
|
||||
{ slug: 'chemistry-8-ch3', file: 'chemistry_8_ch3.html', paras: 7 },
|
||||
{ slug: 'chemistry-8-ch4', file: 'chemistry_8_ch4.html', paras: 6 },
|
||||
{ slug: 'chemistry-8-ch5', file: 'chemistry_8_ch5.html', paras: 4 },
|
||||
{ slug: 'chemistry-8-ch6', file: 'chemistry_8_ch6.html', paras: 7 }
|
||||
];
|
||||
|
||||
test('сумма параграфов глав = 52', () => {
|
||||
assert.equal(CHILDREN.reduce((a, c) => a + c.paras, 0), 52);
|
||||
});
|
||||
|
||||
test('хаб chemistry_8_hub.html существует и ссылается на все 7 глав', () => {
|
||||
const hub = fs.readFileSync(path.join(TB, 'chemistry_8_hub.html'), 'utf8');
|
||||
assert.ok(hub.includes('var TOTAL = 52'), 'TOTAL=52');
|
||||
for (const ch of CHILDREN) {
|
||||
assert.ok(hub.includes('/textbook/' + ch.slug), 'ссылка на ' + ch.slug);
|
||||
}
|
||||
assert.ok(hub.includes('/api/textbooks/chemistry-8/children'), 'грузит детей');
|
||||
});
|
||||
|
||||
test('каждая глава существует и задаёт свой _TB_SLUG', () => {
|
||||
for (const ch of CHILDREN) {
|
||||
const html = fs.readFileSync(path.join(TB, ch.file), 'utf8');
|
||||
assert.ok(html.includes("const _TB_SLUG = '" + ch.slug + "'"), ch.file + ' slug');
|
||||
assert.ok(html.includes('/textbook/chemistry-8"'), ch.file + ' ссылка назад в хаб');
|
||||
assert.ok(html.includes('/js/chem8_svg.js'), ch.file + ' подключает chem8_svg');
|
||||
assert.ok(html.includes('/js/biochem-core.js'), ch.file + ' подключает biochem-core');
|
||||
}
|
||||
});
|
||||
|
||||
test('миграция 041 — родитель chemistry-8 + 7 детей, нет эмоджи', () => {
|
||||
const sql = fs.readFileSync(
|
||||
path.join(ROOT, 'backend', 'src', 'db', 'migrations', '041_chemistry8_hub.sql'), 'utf8');
|
||||
assert.ok(/'chemistry-8'.*NULL/s.test(sql) || sql.includes("'chemistry-8', 'chemistry', 8"), 'родитель');
|
||||
for (const ch of CHILDREN) {
|
||||
assert.ok(sql.includes("'" + ch.slug + "'"), 'дитя ' + ch.slug);
|
||||
}
|
||||
// запрет эмоджи (правило проекта)
|
||||
assert.ok(!/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(sql), 'без эмоджи');
|
||||
});
|
||||
Reference in New Issue
Block a user