12 KiB
Архитектура
Приложение состоит из двух частей: движок вычислений на 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)
Как строка превращается в результат
- Lexer разбивает
"5 дюймов в см"на лексемы:5,дюймов,в,см. Тот же lexer сегментирует фразы из словаря, чтобы совпадение было последовательным. - Registry хранит все фразы, которые понимает движок, по первой лексеме.
Побеждает наиболее длинное совпадение; регистрозависимые записи (
mvsM,kB) проверяются раньше регистронезависимых, а неоднозначная нижняя форма (mmvsMm) просто не получает нечувствительный к регистру запасной вариант. - Tokenizer проходит по лексемам, спрашивает реестр и выдаёт типизированные токены.
Несколько контекстных хаков: например,
inсразу после числа в конце строки означает дюймы (6 ft 3 in). - Parser — Pratt-парсер, терпимый к шуму: неизвестные слова отбрасываются
до разбора (поэтому
потратил 20 на пиццу плюс 5работает) — кроме слов сразу после оператора конвертации, которые могут называть часовой пояс. Приоритет от наименьшего к наибольшему: конвертации на уровне строки → поразрядные → аддитивные → мультипликативные (включая20% от x) → унарные → постфиксные (единицы,%,!,^) → первичные. - Evaluator вычисляет тройки
{ value: Decimal, unit }. Операнды одного измерения конвертируются через базовые единицы (toBase/fromBase); температуры несут смещение, единицы расхода топлива могут быть обратными (mpg ↔ л/100км). Контекст документа предоставляет переменные и результаты предыдущих строк длясумма/avg/prev. - 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 проверяется вручную; движок — там где живёт вся сложность.