mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 16:51:59 +03:00
120 lines
10 KiB
Markdown
120 lines
10 KiB
Markdown
# Архитектура
|
||
|
||
[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 проверяется вручную; движок — там где живёт вся сложность.
|