summarum/ARCHITECTURE.ru.md

120 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Архитектура
[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)
src-tauri/
src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов
(open.er-api.com + CoinGecko) с кэшем, резервные копии,
миграция папки данных, открытие файлов
```
## Как строка превращается в результат
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`.
- Оценка движка синхронная на каждое нажатие клавиши — полный разбор листа
занимает менее миллисекунды, поэтому нет дебаунса и воркеров.
## Хранилище
- `settings.json` всегда находится в `%APPDATA%/app.summarum.calc`.
- `documents.json` (все листы) + `backups/` живут либо там, либо в папке данных,
выбранной пользователем (путь хранится в настройках).
- Ежедневный снимок при запуске (`backups/documents-YYYY-MM-DD.json`, хранится 14);
удалённые листы становятся `.numi`-файлами в `backups/deleted/` и удаляются по возрасту.
Сохранение откладывается на 400 мс.
## Тесты
`npm test` запускает ~94 vitest-кейса по движку (`src/engine/*.test.ts`):
все классы выражений, оба языка, детерминированные инжектированные курсы,
плюс набор регрессий по всем известным исправленным багам.
UI проверяется вручную; движок — там где живёт вся сложность.