# Архитектура [English](ARCHITECTURE.md) Приложение состоит из двух частей: **движок вычислений на TypeScript + UI** в WebView2 и тонкая **оболочка на Rust** (Tauri 2) для всего, что касается ОС. ``` src/ engine/ сам калькулятор (чистый TS, без DOM) lexer.ts строка → сырые лексемы (числа, слова, символы) vocab-data.ts таблицы слов en+ru: единицы, валюты, операторы vocab.ts объединённый поиск по таблицам unitdata.ts коэффициенты конвертации (базовые единицы на измерение) extraunits.ts скорость/давление/энергия/мощность/частота/топливо + крипто registry.ts фраза → токен; строится один раз при запуске tokenizer.ts лексемы + реестр → семантические токены parser.ts токены → AST (Pratt-парсер, терпимый к «мусору») evaluator.ts AST + контекст документа → значения с единицами formatter.ts значения → строки для отображения datetime.ts карта часовых поясов, календарная арифметика index.ts фасад SumEngine + API расширений + итоги листа ui/ editor.ts обвязка CodeMirror 6: подсветка, оверлей результатов, копирование app.css темы (светлая/тёмная через CSS-переменные) main.ts запуск приложения: документы, настройки, строка статуса, dnd storage.ts команды Tauri / запасной вариант на localStorage extensions.ts запускает пользовательские .js-файлы через движок i18n.ts строки интерфейса (en/ru) updater.ts проверка tauri-plugin-updater, установка + перезапуск src-tauri/ src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов (open.er-api.com + CoinGecko) с кэшем, резервные копии, миграция папки данных, открытие файлов, регистрация плагинов (autostart, global-shortcut, single-instance, opener, dialog, updater, process) ``` ## Как строка превращается в результат 1. **Lexer** разбивает `"5 дюймов в см"` на лексемы: `5`, `дюймов`, `в`, `см`. Тот же lexer сегментирует фразы из словаря, чтобы совпадение было последовательным. 2. **Registry** хранит все фразы, которые понимает движок, по первой лексеме. Побеждает наиболее длинное совпадение; регистрозависимые записи (`m` vs `M`, `kB`) проверяются раньше регистронезависимых, а неоднозначная нижняя форма (`mm` vs `Mm`) просто не получает нечувствительный к регистру запасной вариант. 3. **Tokenizer** проходит по лексемам, спрашивает реестр и выдаёт типизированные токены. Несколько контекстных хаков: например, `in` сразу после числа в конце строки означает дюймы (`6 ft 3 in`). 4. **Parser** — Pratt-парсер, терпимый к шуму: неизвестные слова отбрасываются до разбора (поэтому `потратил 20 на пиццу плюс 5` работает) — кроме слов сразу после оператора конвертации, которые могут называть часовой пояс. Приоритет от наименьшего к наибольшему: конвертации на уровне строки → поразрядные → аддитивные → мультипликативные (включая `20% от x`) → унарные → постфиксные (единицы, `%`, `!`, `^`) → первичные. 5. **Evaluator** вычисляет тройки `{ value: Decimal, unit }`. Операнды одного измерения конвертируются через базовые единицы (`toBase`/`fromBase`); температуры несут смещение, единицы расхода топлива могут быть обратными (mpg ↔ л/100км). Контекст документа предоставляет переменные и результаты предыдущих строк для `сумма`/`avg`/`prev`. 6. **Formatter** отображает значение в формате единицы, с точностью и группировкой из настроек, или в hex/binary/дроби/римских цифрах и т.д. Числа — `decimal.js` везде, никаких артефактов двоичной арифметики. ## Как добавлять вещи **Единицу** — добавь строку в `UNIT_DATA` в `unitdata.ts` (id, измерение, коэффициент к базовой единице измерения) и её названия в `vocab-data.ts` в соответствующей категории (`"myunit.variants": "name,names,имя"`, `"myunit.format": "sym"`). Для совершенно нового измерения расширь объединение `Dimension` в `types.ts` и используй `extraunits.ts` — фразы живут прямо там. **Валюту** — фиатные коды берутся из таблицы `Currency` в `vocab-data.ts`; крипто идёт в `CRYPTO` в `extraunits.ts` (код, id CoinGecko, фразы, снимок цены) плюс зеркальный список в `src-tauri/src/main.rs`. **Оператор/функцию** — зарегистрируй фразу в `buildRegistry()` (`registry.ts`), добавь токен/узел AST если нужен новый синтаксис, реализуй в `evaluator.ts`. Большинство слов-операторов — это данные, а не код. **Город часового пояса** — одна строка в карте `Z` в `datetime.ts`. ## Подводные камни движка - Конфликты фраз реальны: одиночные буквы регистрозависимы по замыслу (`m` метр / `M` миллион, `k` тысяча / `K` кельвин). Генерация префиксов фильтруется там, где пересекается со словами — например, секунды получают только ms/µs/ns/ps, потому что символ «аттосекунды» затенил бы слово `as`. - `и`/`или` двойного назначения: `и` складывает (`5 и 3` = 8), `или` — побитовое. `от/без/на` работают только с процентом в левой части; иначе это просто текст. - Курсы валют — «единиц за 1 USD»; соотношение единицы валюты вычисляется при токенизации из текущей карты курсов, поэтому обновление курсов пересчитывает всё при следующей оценке. - Реестр не пересобирается никогда; расширения мутируют его (добавляют фразы/функции) при запуске. ## Особенности UI - CodeMirror встраивает свою базовую тему во время выполнения *после* `app.css`, поэтому всё, что должно побеждать (отступ контента для колонки результатов, цвет курсора), живёт в расширении `EditorView.theme(...)`, а не в стилях. - Колонка результатов — абсолютно позиционированный оверлей; каждый результат размещается по координатам `view.lineBlockAt(...)` и перерисовывается при прокрутке/изменении геометрии. Клик для копирования читает `data-value`. - Оценка движка синхронная на каждое нажатие клавиши — полный разбор листа занимает менее миллисекунды, поэтому нет дебаунса и воркеров. - Поиск по всем листам (`Ctrl+F`) — простое построчное сканирование `data.contents` в `main.ts`, а не расширение поиска CodeMirror — ему нужно пересекать документы, поэтому он не трогает активный редактор, пока не кликнут результат (`editor.goToLine`). ## Хранилище - `settings.json` всегда находится в `%APPDATA%/app.summarum.calc`. - `documents.json` (все листы) + `backups/` живут либо там, либо в папке данных, выбранной пользователем (путь хранится в настройках). - Ежедневный снимок при запуске (`backups/documents-YYYY-MM-DD.json`, хранится 14); удалённые листы становятся `.numi`-файлами в `backups/deleted/` и удаляются по возрасту. Сохранение откладывается на 400 мс. ## Автообновление - `updater.ts` вызывает `check()` из `tauri-plugin-updater` при запуске; если подписанное обновление доступно, пользователю предлагают установить, затем `downloadAndInstall()` + `relaunch()` из `tauri-plugin-process`. - Эндпоинт обновлений — `latest.json`, публикуемый вместе с каждым GitHub Release. `release.yml` подписывает обе архитектуры парой ключей minisign: публичная половина зашита в `tauri.conf.json`; приватная половина + пароль живут только как секреты репозитория `TAURI_SIGNING_PRIVATE_KEY(_PASSWORD)` (write-only — в репозитории нет постоянной копии). Потеря этого ключа означает, что уже установленные копии никогда больше не смогут доверять будущему подписанному релизу. ## Тесты `npm test` запускает ~96 vitest-кейсов по движку (`src/engine/*.test.ts`): все классы выражений, оба языка, детерминированные инжектированные курсы, плюс набор регрессий по всем известным исправленным багам. UI проверяется вручную; движок — там где живёт вся сложность.