alchemistry

Сайт

Статический сайт из markdown-файлов репозитория. Генерируется, не пишется руками.

python tools/site/build.py

Открывать site/index.html — работает файлом, без сервера.

Правь только .md. Файлы в site/ не редактируются никогда: они
перезаписываются при каждой сборке, и правка html руками теряется молча.
CI проверяет, что site/ соответствует markdown, — забыл пересобрать,
сборка упадёт.

Что внутри

tools/site/
├── build.py генератор
├── markdown.py конвертер markdown → HTML
├── bundle.py сборка в один html-файл
├── check_site.py проверка собранного сайта
├── verify_build.py побайтовая воспроизводимость
└── assets/
    ├── style.css
    ├── app.js тема, меню, поиск
    ├── alchemy-map.js интерактивная карта
    ├── scaler.js пересчёт порций
    └── diagrams/ схемы mermaid, готовые SVG

Вывод зеркалит структуру репозитория: docs/base/alchemy.md →
site/docs/base/alchemy.html, README.md → site/index.html.

Ключевые решения

Данные отдаются как .js, а не .json. На file:// браузер блокирует
fetch локальных файлов, а <script src> — нет. Ради этого сайт открывается
без сервера, в Electron и на телефоне.

Схемы mermaid пре-рендерятся в SVG при сборке. Вендорить mermaid в клиент
значило 5,2 МБ на каждую страницу. Если SVG нет, а рендерер доступен:

bash
npx -y @mermaid-js/mermaid-cli -i файл.mmd -o tools/site/assets/diagrams/имя.svg -t neutral -b transparent

Рендерера нет — в сборку попадёт исходник схемы, страница останется читаемой.

Свой конвертер markdown вместо библиотеки. Зависимостей в проекте нет.
markdown.py покрывает используемое подмножество. Принцип: сначала
экранируем всё, потом размечаем — так текст не может случайно стать разметкой.

Эмодзи запрещены, иконки Lucide встроены в разметку. Подмены эмодзи на
иконки нет: правило репозитория запрещает эмодзи совсем, проверяет
tools/check_no_emoji.py. Иконки Lucide живут только в интерфейсе — лупы
масштаба, шеврон скрытия панели — и вставлены прямо в HTML генератора.

Возможности

ЧтоКак работает
ПоискИндекс при сборке, по названию, заголовкам и тексту. Все слова запроса должны найтись. Ctrl+K
Тёмная темаCSS-переменные, в localStorage, изначально по системной
Меню в шапкеЧетыре раздела вместо списка страниц, описаны в nav.sections в ../../data/site.json. Аккаунт — значок справа, а не раздел
ОглавлениеСлева список ## и ### текущей страницы, раздел подсвечивается при прокрутке
Полоска прогрессаДва пикселя под шапкой
Карта превращенийОтдельная страница на весь экран, подробности в ../../map.md
Переключатель вариантовВарианты одного блюда на одной странице, кнопки сверху
Пересчёт порцийJSON рецепта вшит в страницу, множитель произвольный; галочка в строке убирает отключаемый ингредиент по полю give
Работа офлайнНи одного сетевого запроса

Меню отвечает на вопрос «куда идти» и должно помещаться в одну строку,
поэтому это категории. Оглавление отвечает на «где я в этой статье» и
принадлежит конкретной странице. Оно собирается из уже отрендеренного
HTML, а не из markdown: тогда не может разойтись со страницей, даже если
заголовок добавила сборка.

Раздел набирает страницы по префиксу (docs/base/), а exclude отсеивает
и точную страницу, и целый каталог (docs/chocolate/recipes). Рецепты в
меню не перечисляются: на них ведёт страница «Рецепты». На телефоне меню —
короткий список под кнопкой, категории раскрывает человек, а не скрипт.

Карта превращений

site/map.html — не статья, а приложение на весь экран, поэтому своя
оболочка рисуется в build.py функцией render_map_page.

ЧтоГде
Данные: узлы, рёбра, роли, варианты../../data/alchemy.json
Поведение: раскладка, подсветка, масштаб, управление../../map.md
Кодassets/alchemy-map.js

Правила данных, которые проверяет код: у ребра с флагом alt должен быть
узел с вариантами, а pick у варианта — указывать на узел с пунктирной
стрелкой. Иначе на схеме будет утверждение, которому нет соответствия в
статье.

Проверка

bash
python tools/site/build.py # собрать
python tools/site/check_site.py # проверить

check_site.py ловит битые внутренние ссылки и несуществующие ассеты,
остатки markdown (**, #, |), ссылки на .md, которые забыли переписать
в .html, несуществующие docUrl и recipeUrl в данных карты, страницы без
поиска, темы, карты или пересчёта там, где они обязательны, и подозрительно
короткие страницы.

Как пользоваться

Просто открыть: python tools/site/build.py --clean, дальше
site/index.html. Без сервера и без сети.

Один файл — для телефона и для сервера:

python tools/site/build.py --single-file

dist/alchemistry.html: все страницы, стили, скрипты и схемы внутри одного
файла около 820 КБ. Внутри роутер по хешу: #/docs/base/alchemy.html. Файл
можно перебросить в мессенджер, открыть в браузере на телефоне или положить
на свой сервер как есть.

В Electron:

js
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

app.whenReady().then(() => {
  const win = new BrowserWindow({ width: 1100, height: 800 });
  win.loadFile(path.join(__dirname, 'dist', 'alchemistry.html'));
});

app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
bash
npm init -y
npm i -D electron electron-builder
npx electron .

На телефон: dist/alchemistry.html открывается в браузере без установки
чего-либо. Нужно приложение, а не страница — подойдёт Capacitor, он тоже
умеет отдавать локальные файлы и собирается под Android и iOS.

Публикация: сборка от неё не зависит — на входе markdown, на выходе
папка файлов или один html. Нужен сайт с адресами вида
/docs/base/alchemy.html → папка site/; нужен один файл и нет возни с
путями → dist/alchemistry.html. GitHub Pages берёт папку site/ и работает,
пока репозиторий публичный.

Свой сервер: site/ лежит в /var/www/html/alchemistry, рядом служба
server/server.mjs, а nginx отдаёт статику и проксирует /api/ на неё.
Подробности — в server/README.md, раздел «Как это стоит на alchemistry.su».