139 lines
7.4 KiB
JavaScript
139 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// tools/embed-fish.mjs — встраивает 37 SVG-портретов рыб (assets/fish/fish-<id>.svg)
|
|
// в index.html как СКРЫТЫЙ SVG-спрайт из <symbol> (id="fish-<id>", viewBox 0 0 160 100).
|
|
//
|
|
// Запуск: node tools/embed-fish.mjs (из каталога репо или откуда угодно — пути от репо)
|
|
// - идемпотентен: если маркеры FISH-SVG:BEGIN/END уже есть — заменяет ВСЁ между ними;
|
|
// если нет — вставляет блок сразу ПОСЛЕ открывающего тега <body> (в начале файла).
|
|
// - после записи — самопроверка: 37 вхождений id="fish-, оба маркера, затем node tools/check.mjs.
|
|
// Чистый Node, без npm-зависимостей. Детерминирован: одинаковый ввод → одинаковый index.html.
|
|
//
|
|
// ВАЖНО: это НЕ подключение UI. Дневник/карточка/JS-логику НЕ трогаем —
|
|
// <use href="#fish-<id>"> подключит следующий этап.
|
|
|
|
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 = '<!-- FISH-SVG:BEGIN (генерируется tools/embed-fish.mjs — не править руками) -->';
|
|
const END = '<!-- FISH-SVG:END -->';
|
|
const VIEWBOX = 'viewBox="0 0 160 100"';
|
|
|
|
function fail(msg) {
|
|
console.error(`embed-fish: ОШИБКА: ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Чтение и валидация одного fish-<id>.svg → ВНУТРЕННЕЕ содержимое (между '>' корневого
|
|
// <svg ...> и '</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);
|
|
// Перед <svg допустимы пробельные символы и XML-комментарии (в файлах — поясняющий комментарий вида).
|
|
let i = 0;
|
|
for (;;) {
|
|
const ws = /^[ \t\r\n]+/.exec(s.slice(i));
|
|
if (ws) { i += ws[0].length; continue; }
|
|
const cm = /^<!--[\s\S]*?-->/.exec(s.slice(i));
|
|
if (cm) { i += cm[0].length; continue; }
|
|
break;
|
|
}
|
|
// Корневой тег: <svg, за ним пробел (атрибуты) или '>'
|
|
if (!/^<svg([ >])/.test(s.slice(i))) {
|
|
fail(`${file} (вид ${id}): файл не начинается с <svg (после BOM/пробелов/комментариев)`);
|
|
}
|
|
if (!s.includes(VIEWBOX)) {
|
|
fail(`${file} (вид ${id}): не найдено ${VIEWBOX}`);
|
|
}
|
|
if (!s.trimEnd().endsWith('</svg>')) {
|
|
fail(`${file} (вид ${id}): файл не заканчивается на </svg>`);
|
|
}
|
|
const openEnd = s.indexOf('>', i); // закрывающая '>' корневого <svg ...>
|
|
const closeStart = s.lastIndexOf('</svg>'); // финальное </svg>
|
|
if (openEnd === -1 || closeStart === -1 || closeStart < openEnd) {
|
|
fail(`${file} (вид ${id}): нераспознана структура (корневой <svg> / </svg>)`);
|
|
}
|
|
return s.slice(openEnd + 1, closeStart);
|
|
}
|
|
|
|
// Сборка блока спрайта (строка на symbol; внутреннее содержимое — как в исходнике).
|
|
function buildBlock() {
|
|
const lines = [BEGIN, '<svg id="fishSprites" width="0" height="0" style="position:absolute" aria-hidden="true">'];
|
|
for (const id of IDS) {
|
|
lines.push(`<symbol id="fish-${id}" ${VIEWBOX}>${loadInner(id)}</symbol>`);
|
|
}
|
|
lines.push('</svg>', 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 {
|
|
// Маркеров нет — вставляем сразу ПОСЛЕ открывающего тега <body> (с атрибутами, первый в файле).
|
|
const m = /<body(\s[^>]*)?>/.exec(html);
|
|
if (!m) fail('в index.html не найден открывающий тег <body>');
|
|
const pos = m.index + m[0].length;
|
|
html = html.slice(0, pos) + '\n' + block + html.slice(pos);
|
|
console.log(`embed-fish: блок вставлен после <body> (строка ${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 пройден`);
|