#!/usr/bin/env node // tools/embed-fish.mjs — встраивает 37 SVG-портретов рыб (assets/fish/fish-.svg) // в index.html как СКРЫТЫЙ SVG-спрайт из (id="fish-", viewBox 0 0 160 100). // // Запуск: node tools/embed-fish.mjs (из каталога репо или откуда угодно — пути от репо) // - идемпотентен: если маркеры FISH-SVG:BEGIN/END уже есть — заменяет ВСЁ между ними; // если нет — вставляет блок сразу ПОСЛЕ открывающего тега (в начале файла). // - после записи — самопроверка: 37 вхождений id="fish-, оба маркера, затем node tools/check.mjs. // Чистый Node, без npm-зависимостей. Детерминирован: одинаковый ввод → одинаковый index.html. // // ВАЖНО: это НЕ подключение UI. Дневник/карточка/JS-логику НЕ трогаем — // подключит следующий этап. import { readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const INDEX = join(ROOT, 'index.html'); const ASSETS = join(ROOT, 'assets', 'fish'); // Порядок = порядок SPECIES в index.html (карасс → кит → дух болота). const IDS = [ 'carass', 'perch', 'bream', 'carp', 'pike', 'catfish', 'sturge', 'gold', 'zander', 'chuch', 'crayfish', 'bleak', 'roundel', 'eelpout', 'tench', 'navaga', 'rudd', 'cisco', 'herring', 'mackerel', 'flounder', 'tuna', 'swordfish', 'moray', 'grouper', 'turtle', 'angler', 'octopus', 'manta', 'shark', 'squid', 'whale', 'glowfin', 'moonfish', 'emberfin', 'violetlure', 'spiritfin', ]; const BEGIN = ''; const END = ''; const VIEWBOX = 'viewBox="0 0 160 100"'; function fail(msg) { console.error(`embed-fish: ОШИБКА: ${msg}`); process.exit(1); } // Чтение и валидация одного fish-.svg → ВНУТРЕННЕЕ содержимое (между '>' корневого // и ''). Корневой тег (width/height) в symbol не нужен — только viewBox. function loadInner(id) { const file = `fish-${id}.svg`; let s; try { s = readFileSync(join(ASSETS, file), 'utf8'); } catch (e) { fail(`${file} (вид ${id}): не удалось прочитать: ${e.message}`); } // BOM допустим if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1); // Перед /.exec(s.slice(i)); if (cm) { i += cm[0].length; continue; } break; } // Корневой тег: ' if (!/^])/.test(s.slice(i))) { fail(`${file} (вид ${id}): файл не начинается с ')) { fail(`${file} (вид ${id}): файл не заканчивается на `); } const openEnd = s.indexOf('>', i); // закрывающая '>' корневого const closeStart = s.lastIndexOf(''); // финальное if (openEnd === -1 || closeStart === -1 || closeStart < openEnd) { fail(`${file} (вид ${id}): нераспознана структура (корневой / )`); } return s.slice(openEnd + 1, closeStart); } // Сборка блока спрайта (строка на symbol; внутреннее содержимое — как в исходнике). function buildBlock() { const lines = [BEGIN, '', END); return lines.join('\n'); } // --- основная логика --- let html; try { html = readFileSync(INDEX, 'utf8'); } catch (e) { fail(`не удалось прочитать ${INDEX}: ${e.message}`); } const block = buildBlock(); const b = html.indexOf(BEGIN); const e = html.indexOf(END); if (b !== -1 || e !== -1) { // Маркеры есть (хотя бы один) — заменяем ВСЁ между ними (идемпотентность повторного запуска). if (b === -1 || e === -1 || e < b) fail('маркеры FISH-SVG:BEGIN/END найдены, но парно/порядком — index.html повреждён'); html = html.slice(0, b) + block + html.slice(e + END.length); console.log('embed-fish: маркеры уже есть — блок заменён (повторный запуск)'); } else { // Маркеров нет — вставляем сразу ПОСЛЕ открывающего тега (с атрибутами, первый в файле). const m = /]*)?>/.exec(html); if (!m) fail('в index.html не найден открывающий тег '); const pos = m.index + m[0].length; html = html.slice(0, pos) + '\n' + block + html.slice(pos); console.log(`embed-fish: блок вставлен после (строка ${html.slice(0, pos).split('\n').length})`); } writeFileSync(INDEX, html); // --- самопроверка: перечитываем файл и убеждаемся, что всё на месте --- const check = readFileSync(INDEX, 'utf8'); const nSymbols = (check.match(/id="fish-/g) || []).length; if (nSymbols !== IDS.length) fail(`самопроверка: ожидалось ${IDS.length} вхождений id="fish-, найдено ${nSymbols}`); if (check.split(BEGIN).length - 1 !== 1) fail('самопроверка: маркер FISH-SVG:BEGIN не найден (или дублируется)'); if (check.split(END).length - 1 !== 1) fail('самопроверка: маркер FISH-SVG:END не найден (или дублируется)'); // Синтаксис JS в index.html (node --check на вынутых блоках) — как п.1 «Проверки» из AGENTS.md. let out; try { out = execFileSync(process.execPath, [join(ROOT, 'tools', 'check.mjs')], { encoding: 'utf8', cwd: ROOT }); } catch (err) { process.stdout.write(err.stdout || ''); process.stderr.write(err.stderr || String(err)); fail('node tools/check.mjs завершился с ошибкой'); } process.stdout.write(out); const blockBytes = Buffer.byteLength(block, 'utf8'); const blockLines = block.split('\n').length; console.log(`embed-fish: OK — ${IDS.length} символов, блок ${blockBytes} байт / ${blockLines} строк, маркеры на месте, check.mjs пройден`);