feat(textbooks): миграция контента Физики 9 — §1-36 + ЛР11
- migrate_phys9_ch4.js: первая итерация (§31-36 → ch4) - migrate_phys9_content.js: обобщённый скрипт для ch1-3 (§1-30) + ch5 (ЛР11 из монолита) Каждая глава: - Получает CSS-блок монолита (стили .para-hero, .fcard, .def-box и т.д.) - Подключает Font Awesome CDN для иконок в section-title - HTML-тела параграфов вставляются в STUB-builder'ы заменой по regex - Эмодзи (нарушают правило проекта) и orphaned <i> теги удаляются на этапе clean() Размеры после миграции: - ch1 (кинематика, §1-14): 136 КБ - ch2 (динамика, §15-24): 127 КБ - ch3 (статика, §25-30): 100 КБ - ch4 (законы сохранения, §31-36): 133 КБ - ch5 (лаб. практикум): 90 КБ (только ЛР11 заполнен, ЛР1-10 и ЛР12 — STUB) Источник physics_9.html сохранён для возможной повторной миграции.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
// Перенос всего содержимого physics_9.html в physics_9_ch1..ch5.html.
|
||||
// - Извлекает CSS-блок монолита, инжектит в каждую ch-файл (стили нужны для рендера)
|
||||
// - Извлекает HTML-тело каждого §1..§36 + лабораторного блока
|
||||
// - Чистит emoji и Font Awesome <i>
|
||||
// - Подключает FA CDN для совместимости
|
||||
// - Заменяет STUB-builder для каждого pid на реальный контент
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TBOOKS = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
|
||||
const SRC = path.join(TBOOKS, 'physics_9.html');
|
||||
|
||||
const src = fs.readFileSync(SRC, 'utf8');
|
||||
|
||||
// === Распределение §N → главе ===
|
||||
const CH_OF = {};
|
||||
for (let n = 1; n <= 14; n++) CH_OF[n] = 1;
|
||||
for (let n = 15; n <= 24; n++) CH_OF[n] = 2;
|
||||
for (let n = 25; n <= 30; n++) CH_OF[n] = 3;
|
||||
for (let n = 31; n <= 36; n++) CH_OF[n] = 4;
|
||||
|
||||
// Заголовки § (для матчинга STUB) — должны точно совпадать с PARA_NAMES в gen_phys9_ch.js
|
||||
const PARA_NAMES = {
|
||||
1:'Механическое движение',
|
||||
2:'Относительность движения. Система отсчёта',
|
||||
3:'Скалярные и векторные величины. Действия над векторами',
|
||||
4:'Проекция вектора на ось',
|
||||
5:'Путь и перемещение',
|
||||
6:'Равномерное прямолинейное движение. Скорость',
|
||||
7:'Графическое представление равномерного движения',
|
||||
8:'Неравномерное движение. Средняя и мгновенная скорость',
|
||||
9:'Сложение скоростей',
|
||||
10:'Ускорение',
|
||||
11:'Скорость при равноускоренном движении',
|
||||
12:'Перемещение, координата и путь при равноускоренном движении',
|
||||
13:'Линейная и угловая скорости',
|
||||
14:'Ускорение точки при движении по окружности',
|
||||
15:'Взаимодействие тел. Сила. ИСО. 1-й закон Ньютона',
|
||||
16:'Масса',
|
||||
17:'Второй закон Ньютона',
|
||||
18:'Третий закон Ньютона. Принцип относительности Галилея',
|
||||
19:'Деформация тел. Сила упругости. Закон Гука',
|
||||
20:'Силы трения. Силы сопротивления среды',
|
||||
21:'Движение тела под действием силы тяжести',
|
||||
22:'Движение тела, брошенного под углом к горизонту',
|
||||
23:'Закон всемирного тяготения',
|
||||
24:'Вес. Невесомость и перегрузки',
|
||||
25:'Условия равновесия тел. Момент силы',
|
||||
26:'Простые механизмы. Рычаги. Блоки',
|
||||
27:'Наклонная плоскость. «Золотое правило» механики. КПД',
|
||||
28:'Центр тяжести. Виды равновесия',
|
||||
29:'Закон Архимеда. Выталкивающая сила',
|
||||
30:'Плавание судов. Воздухоплавание',
|
||||
31:'Импульс тела. Импульс системы тел',
|
||||
32:'Закон сохранения импульса. Реактивное движение',
|
||||
33:'Механическая работа. Мощность',
|
||||
34:'Потенциальная энергия',
|
||||
35:'Кинетическая энергия. Полная энергия системы тел',
|
||||
36:'Закон сохранения энергии',
|
||||
};
|
||||
|
||||
// === Извлекаем CSS ===
|
||||
const styleStart = src.indexOf('<style>') + '<style>'.length;
|
||||
const styleEnd = src.indexOf('</style>', styleStart);
|
||||
const monolithCss = src.slice(styleStart, styleEnd);
|
||||
|
||||
// === Извлекаем §1..§36 ===
|
||||
// Boundary для §36 — позиция h2 лабораторной секции
|
||||
const labH2Pos = src.indexOf('Проверка закона сохранения импульса');
|
||||
const labBoundary = labH2Pos > 0 ? src.lastIndexOf('<!-- ═', labH2Pos) : src.length;
|
||||
|
||||
const PARAS = {};
|
||||
for (let n = 1; n <= 36; n++) {
|
||||
const tag = `id="tab-ref${n}"`;
|
||||
const i = src.indexOf(tag);
|
||||
if (i < 0) { console.warn('miss §' + n); continue; }
|
||||
let j;
|
||||
if (n < 36) {
|
||||
j = src.indexOf(`id="tab-ref${n+1}"`, i);
|
||||
const cm = src.lastIndexOf('<!--', j);
|
||||
if (cm > i + 1000) j = cm;
|
||||
} else {
|
||||
j = labBoundary;
|
||||
}
|
||||
const divStart = src.lastIndexOf('<div class="content', i);
|
||||
PARAS[n] = src.slice(divStart, j);
|
||||
}
|
||||
|
||||
// === Извлекаем лабораторный блок ЛР11 (для Ch5) ===
|
||||
// В монолите есть одна секция id="tab-lab11" — "Проверка закона сохранения импульса".
|
||||
let LAB_BLOCK = null;
|
||||
{
|
||||
const labStart = src.indexOf('id="tab-lab11"');
|
||||
if (labStart >= 0) {
|
||||
const divStart = src.lastIndexOf('<div class="content', labStart);
|
||||
const labEnd = src.indexOf('<!-- ═', labStart + 200);
|
||||
if (labEnd > 0) {
|
||||
LAB_BLOCK = src.slice(divStart, labEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Очистка от emoji + FA ===
|
||||
function clean(s) {
|
||||
return s
|
||||
.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{27BF}]|[\u{1F000}-\u{1F2FF}]|[\u{FE0F}]/gu, '')
|
||||
.replace(/<i\s+class="fa[s ][^"]*"[^>]*>\s*<\/i>/g, '')
|
||||
.replace(/(\s)\s+/g, '$1')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// === Замена STUB в ch-файле ===
|
||||
function migrateChapter(chN, paraNums) {
|
||||
const dstPath = path.join(TBOOKS, `physics_9_ch${chN}.html`);
|
||||
let h = fs.readFileSync(dstPath, 'utf8');
|
||||
const before = h.length;
|
||||
|
||||
for (const n of paraNums) {
|
||||
const pid = 'p' + n;
|
||||
if (!PARAS[n]) { console.warn(`skip ${pid} — no source`); continue; }
|
||||
let body = clean(PARAS[n]);
|
||||
// Удаляем внешний контейнер
|
||||
body = body.replace(/^<div\s+class="content[^"]*"\s+id="tab-ref\d+">/, '');
|
||||
body = body.replace(/<\/div>\s*$/, '');
|
||||
// Экранируем для template literal
|
||||
const esc = body.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
||||
|
||||
// Найти STUB makeCard блок для этого pid
|
||||
const titleEsc = PARA_NAMES[n].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const stubRegex = new RegExp(
|
||||
`makeCard\\('theory', "${titleEsc}", "\\u00a7${n}", \`[\\s\\S]*?\`\\);`
|
||||
);
|
||||
const match = h.match(stubRegex);
|
||||
if (!match) {
|
||||
console.error(`STUB not found for ${pid} (ch${chN})`);
|
||||
continue;
|
||||
}
|
||||
const replacement = `makeCard('theory', ${JSON.stringify(PARA_NAMES[n])}, "§${n}", \`\n${esc}\n \`);`;
|
||||
h = h.replace(stubRegex, () => replacement);
|
||||
console.log(` §${n} → ${body.length} bytes`);
|
||||
}
|
||||
|
||||
// Инжект CSS монолита
|
||||
if (!h.includes('/* === MONOLITH CSS (migrated from physics_9.html) === */')) {
|
||||
const inject = `\n/* === MONOLITH CSS (migrated from physics_9.html) === */\n${monolithCss}\n/* === END MONOLITH CSS === */\n`;
|
||||
h = h.replace('</style>', inject + '</style>');
|
||||
}
|
||||
|
||||
// FA CDN
|
||||
if (!h.includes('font-awesome')) {
|
||||
h = h.replace(
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">',
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">\n<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">'
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(dstPath, h);
|
||||
console.log(`ch${chN}: ${before} → ${h.length} bytes`);
|
||||
|
||||
// Sanity: parse inline scripts
|
||||
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
||||
for (const m of scripts) {
|
||||
try { new Function(m[1]); }
|
||||
catch(e) { console.error(`JS PARSE FAIL in ch${chN}:`, e.message); process.exit(1); }
|
||||
}
|
||||
console.log(`ch${chN}: inline JS parses OK`);
|
||||
}
|
||||
|
||||
// === Ch5: лабораторный блок целиком в первую ЛР (lr11 — единственная описанная) ===
|
||||
function migrateCh5(chN = 5) {
|
||||
if (!LAB_BLOCK) {
|
||||
console.log('ch5: no lab block found in source, skipping');
|
||||
return;
|
||||
}
|
||||
const dstPath = path.join(TBOOKS, `physics_9_ch${chN}.html`);
|
||||
let h = fs.readFileSync(dstPath, 'utf8');
|
||||
const before = h.length;
|
||||
|
||||
// Очищаем лаб-блок
|
||||
let body = clean(LAB_BLOCK);
|
||||
const esc = body.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
||||
|
||||
// В монолите есть единственная ЛР "Проверка закона сохранения импульса" — ставим её в lr11.
|
||||
// Остальные 11 ЛР остаются STUB.
|
||||
const stubRegex = /makeCard\('theory', "Проверка закона сохранения импульса", "ЛР 11", `[\s\S]*?`\);/;
|
||||
const match = h.match(stubRegex);
|
||||
if (match) {
|
||||
const replacement = `makeCard('lab', "Проверка закона сохранения импульса", "ЛР 11", \`\n${esc}\n \`);`;
|
||||
h = h.replace(stubRegex, () => replacement);
|
||||
console.log(` ЛР11 → ${body.length} bytes`);
|
||||
} else {
|
||||
console.warn('ЛР11 stub not found — leaving Ch5 untouched');
|
||||
}
|
||||
|
||||
// Инжект CSS и FA как в других
|
||||
if (!h.includes('/* === MONOLITH CSS (migrated from physics_9.html) === */')) {
|
||||
const inject = `\n/* === MONOLITH CSS (migrated from physics_9.html) === */\n${monolithCss}\n/* === END MONOLITH CSS === */\n`;
|
||||
h = h.replace('</style>', inject + '</style>');
|
||||
}
|
||||
if (!h.includes('font-awesome')) {
|
||||
h = h.replace(
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">',
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">\n<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">'
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(dstPath, h);
|
||||
console.log(`ch5: ${before} → ${h.length} bytes`);
|
||||
|
||||
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
||||
for (const m of scripts) {
|
||||
try { new Function(m[1]); }
|
||||
catch(e) { console.error(`JS PARSE FAIL in ch5:`, e.message); process.exit(1); }
|
||||
}
|
||||
console.log(`ch5: inline JS parses OK`);
|
||||
}
|
||||
|
||||
// === Run ===
|
||||
console.log('=== ch1 (§1-14) ===');
|
||||
migrateChapter(1, [1,2,3,4,5,6,7,8,9,10,11,12,13,14]);
|
||||
console.log('=== ch2 (§15-24) ===');
|
||||
migrateChapter(2, [15,16,17,18,19,20,21,22,23,24]);
|
||||
console.log('=== ch3 (§25-30) ===');
|
||||
migrateChapter(3, [25,26,27,28,29,30]);
|
||||
// ch4 — уже мигрирована migrate_phys9_ch4.js, не трогаем повторно
|
||||
console.log('=== ch5 (lab) ===');
|
||||
migrateCh5();
|
||||
|
||||
console.log('Done.');
|
||||
Reference in New Issue
Block a user