Зуд

Я ковырял pet-проект — ротатор SOCKS5-прокси — и в очередной раз редактировал конфиг апстримов руками. Открыл файл, посмотрел на JSON:

Кавычки вокруг каждого ключа. Кавычки вокруг каждой строки. Невозможно просто вставить список кредов от провайдера. Запятые после каждой строчки. Забыл запятую — ошибка парсинга указывает не на ту строку. Я тратил больше времени на пунктуацию, чем на сами значения.

Переписал на YAML — через двадцать минут вырезал случайно сдвинутый на пробел блок. Ощущения — как в комсмосе. Не понятно в каком блоке я нахожусь, страшно править. Переписал на TOML — [[upstreams]] для массива объектов оказался неудобным ровно в тот момент, когда объекты нужно было вложить ещё на уровень.

Дальше случилось то, что я обычно советую не делать: я не выбрал один из существующих 14 форматов, а сделал 15-й.

22 апреля 2026 года появился первый коммит спеки — 0.1.0. Через полтора месяца, 5 июня, экосистема из десяти репозиториев была на 0.6.1, полностью опубликована в семь пакетных реестров. Этот пост — не столько про сам формат, сколько про то, что оказалось по-настоящему сложным: как раскатать один парсер на семь языков так, чтобы его поведение нигде не разъехалось.

Что такое Ktav

Название — כְּתָב, «письмо» на иврите. Идея простая: взять модель данных JSON (скаляры, массивы, объекты, null, булевы) и снять с неё пунктуацию, которая мешает писать руками.

Ни кавычек, ни запятых, осттупы не влияют на структуру данных. Голое число, похожее на целое, становится Integer; похожее на дробное — Float;true/false/null — ключевые слова-значения, всё остальное — строка .

Массивы и объекты необязательно растягивать на несколько строк — если запись помещается в одну, можно (и часто удобнее) написать её однострочно, через запятую:

Здесь запятые — необходимость: в блочной, многострочной форме роль разделителя элементов играет перенос строки, и она же убирает нужду в запятых. Но когда всё на одной строке, переноса нет — запятая берёт эту роль на себя. Оба стиля дают одинаковое дерево значений, но и их можно свободно смешивать.

Следующие решения здесь — сознательные компромиссы:

  • ## вместо # для комментариев. Одиночная решётка слишком часто встречается внутри значений — hex-цвета, номера issue, имена каналов. color: #ff5577 парсится без экранирования именно поэтому.

  • :: — «форсировать строку». Когда типизация по форме ошибается (например, я хочу, чтобы "true" осталась строкой, а не булевым, или чтобы 00544 не превратилось в 544), :: — явный флаг «бери как есть».

  • Многострочные строки через ( … ) с авто-отбивкой отступа — вместо |/>. А так же (( … )) - для сохранения отступов.

  • Точечные ключи (node.host: a.example) — синтаксический сахар для вложенности. Единственное место в формате, где есть «два способа сделать одно и то же»; я долго колебался, оставлять ли, мне опказалось это очень удобным.

Чего в формате нет: якорей и ссылок (&/* из YAML), тегов типов, выражений, интерполяции, схемы. Каждая отсутствующая фича — решение в пользуй минимализма.

Постановка настоящей задачи

Формат, который живёт на одном языке, — игрушка. Как только над одним конфигом работает полиглот-команда (сервис на Go, тулинг на Python, дашборд на JS), «формат конфига» обязан значить одно и то же везде, байт в байт. Есть три способа это провалить:

  1. Переписать парсер на каждом языке — N реализаций, N чуть разных диалектов. Именно так фрагментируется экосистема YAML: где-то 1.0 — строка, где-то float, где-то по-разному трактуются якоря.

  2. Один эталонный парсер, но склейка с языками настолько рыхлая, что каждый биндинг обрастает своими причудами.

  3. Один парсер, один источник правды, и тест-сьют, который доказывает, что все биндинги согласны.

Я выбрал третий вариант, и именно это оказалось интереснее самого формата.

Ядро на Rust, один C ABI и WASM в браузере

Эталонный парсер — крейт ktav на Rust: recursive descent, без генератора парсеров, zero-copy где возможно, нативная интеграция с serde.

Поверх ядра — тонкий C ABI (ktav_cabi): несколько extern "C" функций с явным контрактом владения памятью (парсер аллоцирует, вызывающая сторона освобождает через _free-функцию; строки пересекают границу как байтовые срезы с длиной, а не null-terminated C-строки). Все языковые биндинги — просто разные способы говорить с этим ABI:

Язык

Механизм FFI

Как ставится

JS/TS

N-API (нативно) + WASM (фолбэк)

npm i @ktav-lang/ktav

Python

PyO3 + abi3-wheels

pip install ktav

Go

purego, без cgo для потребителя

go get github.com/ktav-lang/golang

PHP

ext-ffi (PHP 7.4+)

composer require ktav-lang/ktav

Java

JNA, без JNI для потребителя

Maven Central

C#/.NET

P/Invoke

dotnet add package Ktav

Отдельно хочу выделить WASM — это же самое Rust-ядро, тот же самый код, что гоняет тесты и обслуживает продовые парсинги, компилируется в WebAssembly и крутится прямо в браузере на лендинге — без сервера, без бэкенда, без парсинга на js. Вставил в playground YAML или TOML — получил Ktav.

Самыми неочевидными оказались не сами обёртки, а протаскивание ошибок через границу: Result в Rust на ABI-уровне становится размеченным объединением (код + позиция + сообщение), а дальше каждый язык заворачивает это в свою идиому — исключение в Python/Java/C#, error в Go, отклонённый промис в JS.

Как держим биндинги: тест-спека

C ABI даёт одну реализацию, но у каждого биндинга остаётся собственная поверхность: кодировка строк, маппинг ошибок, поведение на границах чисел. Поэтому спека — это отдельная папка tests/ с фикстурами прямо в репозитории, версионированная вместе с грамматикой (versions/0.6/tests/). В актуальной версии там 374 файла, разложенных по valid/ (массивы, объекты, точечные ключи, комментарии, многострочные строки, инлайн-компаунды, экранирование ключей, числа…) и invalid/ (что обязано падать: дубли ключей, незакрытые скобки, битое экранирование, конфликт ключевого пути).

Каждый тестовый кейс — это тройка файлов: .ktav (вход), .json (ожидаемое дерево значений) и *.canonical.ktav (эталонный round-trip — во что должен превратиться этот вход, если распарсить и сериализовать обратно). То есть проверяется не только «правильно ли распарсили», но и «правильно ли записали canonical-форму назад» — это ловит асимметрии вроде «прочитали инлайн-объект, а на выходе почему-то развернули его в блочный».

Именно эту тройку файлов каждый из семи биндингов гоняет в своём собственном CI против своей собственной реализации. Биндинг у меня не считается готовым, когда он компилируется. Он готов, когда проходит тот же самый набор фикстур, что и эталон, на своём языке. «Все тесты спеки зелёные на 7 языках» — это доказательство, которое я предъявляю своему внутреннему скептику вместо слов «работает же».

Тулинг для редакторов

  • LSP-сервер (ktav-lsp, отдельный крейт на Rust) — диагностика, автодополнение, hover.

  • Плагин VS Code и плагин JetBrains (IntelliJ, RustRover, PyCharm, WebStorm, GoLand, PhpStorm, Rider) — оба бандлят LSP и подсветку.

  • Грамматика tree-sitter — для Neovim, Helix, Zed и любого другого редактора с поддержкой tree-sitter.

Плагины так и не опубликовал. На сайте MS заблудился и меня они забанили, похоже, за количество переходов по их ссылкам. JetBrains - устал модерацию проходить. Но плагины для них выложил на сайте (ссылка ниже).

Где можно потыкать

Всё написанное — open-source, dual-licensed MIT OR Apache-2.0. Попробовать без установки: ktav-lang.github.io (playground на WASM, конвертирует JSON/YAML/TOML/INI ⇄ Ktav прямо в браузере, ничего не уходит на сервер).

Код — github.com/ktav-lang.

Комментарии (27)


  1. ZurgInq
    19.08.2026 11:16

    Выглядит симпатично.

    Один парсер, один источник правды, и тест-сьют, который доказывает, что все биндинги согласны.

    А что, если, вместо биндингов через FFI сделать транслятор в JSON. А там пусть родные библиотеки парсят. Тестами покрыть все краевые случаи, что бы убедиться, что транслятор правильно работает. В итоге получаем не прям новый 15ый стандарт, а просто упрощённый способ описания json.


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо, приятно слышать!

      По сути это уже встроено - только как метод тестирования, не как продукт. Тест-спека - это пары “вход на Ktav → ожидаемый JSON”: https://github.com/ktav-lang/spec/tree/main/versions/0.6/tests/valid Берёте любой .ktav - рядом его .json-эквивалент. Транслятор, покрытый тестами на все краевые случаи, - это и есть наш эталонный Rust-парсер.

      Почему всё же выбрал FFI в 7 языков, а не “отдай JSON нативному парсеру” - довод один, про скорость. Цель была - парсинг вровень с serde_json; по факту медленнее раза в два, но это наносекунды, я считаю результат хорошим. А вот лишний проход через JSON-текст (сериализовать → распарсить второй раз) - уже отдельная стоимость поверх, которая на горячем пути (частые перечитывания, много мелких конфигов) будет заметна. На конфиге, который читается раз при старте, - без разницы.

      И обычно: “транслятор + родной парсер” - ровно то, что делают на старте нового формата, и это хорошая стратегия при ограниченности ресурсов. Я сделал наоборот - вложился в 7 FFI-биндингов за полтора месяца, до первого внешнего пользователя. Риск, чего уж там.

      Лёгкий транслятор в чистый JSON отдельно от биндингов - хорошая идея, завёл issue: https://github.com/ktav-lang/spec/issues/2. В следующих версиях рассмотрю подробнее как с сней быть.

      И да: без всего, что Ktav даёт сверх модели JSON, формулировка была бы именно такой - не 15-й стандарт, а просто приятный способ печатать JSON руками)


  1. Ru6aKa
    19.08.2026 11:16

    Вообще непонятно в чем отличие от JSON, точнее JSONC. Было бы неплохо увидеть таблицу сравнения хотя бы с JSON и проблем разного рода. Например надо вставить в конфиг дефолтный приватный ключ для тестового окружения, в JSON он выглядит так, в ktav он выглядит так.
    Или например, ktav для решения этой проблемы использует include с файлом с приватным ключом. Или проблема с установкой log_level при ручном запуске, и что-то типа такого в конфиге ENV('LOG_LEVEL', 'info'), тоесть при ручном запуске просто устанавливаем LOG_LEVEL в переменных окружения, если ничего не выставлено то дефолт info.


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо, что заглянули! Видимо в статье разница с JSONC не бросилась в глаза. Есть сравнительаня таблица на сайте - ktav-lang.github.io (в комментарий пока не разобрался как вставлять таблицы), опишу словами:

      JSONC - это JSON плюс комментарии, и всё, остальное как в обычном JSON: кавычки на ключах и строках обязательны, запятые обязательны.

      В Ktav иначе: кавычки на ключах и строках не нужны вообще (кроме ::, когда явно хочешь застолбить строку), запятые нужны только если пишешь массив или объект в одну строку, комментарии свои - ##, а для многострочного текста есть отдельный блок в скобках вместо \n внутри одной строки в кавычках.

      На примере с приватным ключом. В JSON/JSONC он склеен в одну строку через \n:

      {
            // тестовый ключ, не для прода
            "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKC...\n-----END RSA PRIVATE KEY-----"
      }
      

      В Ktav - отдельный блок, построчно как есть:

      ## тестовый ключ, не для прода
      private_key: (
         -----BEGIN RSA PRIVATE KEY-----
         MIIEowIBAAKC...
         -----END RSA PRIVATE KEY-----
      )
      

      Include с файлом секрета и ENV(‘LOG_LEVEL’, ‘info’) внутри конфига - сознательно не входит в формат. Ktav - только слой данных, без include/expressions/interpolation, как и JSON с TOML: добавишь интерполяцию - формат превращается в мини-язык со своей семантикой выполнения.

      Стремился к минимализму - “Ktav простой и хороший, он не совершенный, но лучший для конфигов”. Получилось сделать его таким или нет - покажет время.

      Возможно параллельно будет какой-нибудь ktav_plus - со ссылками, переменными и прочим, но основной формат останется в минимализме, как json


      1. Ru6aKa
        19.08.2026 11:16

        Попробуйте как-то создать конфигурацию эдак на 3-4 тыс ключей (а лучше больше), и для 3-х окружений: dev, prod, test, причем для dev/test должны быть какие-то дефолтные креды для внешних сервисов и настройки (dev и test должны тоже отличаться, dev это локальная разработка, test это какой-то поднятый сервер для qa), а для prod такие креды которые не должны попасть в git. И сразу будет понятно все недостатки и ограничения Вашего решения. Пока из приятного только многострочные значения, кавычки и запятые вкусовщин. Да можно сказать что этого всего нету и в JSON/YAML/TOML и include/expressions/interpolation и логику merge надо будет городить руками, но при этом всем среди простых форматов у JSON явное преимущество из-за наличия схем.


        1. CraftDream Автор
          19.08.2026 11:16

          Спасибо за подробный контр-аргумент - это полезнее общей критики, потому что чётко очерчивает границу применимости.

          И да, вы правы: на 3-4 тысячах ключей с тремя окружениями и разделением секретов Ktav вам ничего не даст сверх того, что дают JSON/YAML/TOML. Формат этого не решает и не пытается. Он про другое - про конфиг, который человек пишет и читает руками: сервис, CLI, небольшое приложение. Там, где конфигурация становится системой со своим наследованием, оверлеями и секрет-менеджментом, нужен не формат, а инструмент: Helm/Kustomize, CUE, Consul, Vault. Выбор формата на таком масштабе - вопрос десятый.

          Про include / interpolation / merge - это сознательно вне формата, а не недоделка. Как только внутрь синтаксиса заходит include или подстановка переменных, файл перестаёт читаться сам по себе, и появляется порядок вычисления, который надо описывать и реализовывать одинаково во всех семи биндингах. Мы решили, что этот слой живёт в приложении: конфиг из файла, env поверх, секреты инжектятся отдельно. Скучно, зато предсказуемо.

          Про схемы вы попали в реальный пробел - своего языка описания схем у Ktav нет, и это честное ограничение.

          Но одно уточнение: модель данных у Ktav ровно JSON-овская - те же скаляры, массивы, объекты, null, bool, без тегов и якорей. Поэтому JSON Schema применима к результату разбора: распарсили .ktav в структуру и валидируйте тем же валидатором, что и для JSON. Отдельная схема под формат не нужна - преимущество JSON здесь наследуется, а не теряется.

          Про вкусовщину спорить не буду. Кавычки и запятые действительно вопрос привычки, и если они вам не мешают - формат вам не нужен, это нормальный исход. Он вырос из личного раздражения, а не из уверенности, что так лучше всем, и 20 лет за клавитурой накопили разражение в бесконечных танцах пальцев с шифтами)


          1. Ru6aKa
            19.08.2026 11:16

            Опишу свою позицию в плане конфигурации. В конфигурации есть две крайности, или переменные окружения (.env, .env.production) или инструменты.
            А между этими крайностями есть много форматов конфигурации и каждый имеет свои недостатки.

            Как для меня выглядит нормальная конфигурация.

            • Нативная поддержка схемы данных и валидации, конвертировать yaml/toml/etc в json и потом валидировать не вариант, не видно ошибок при редактировании конфига;

            • Поддержка окружения, например dev/prod/test/e2e.

            • Вложение конфигов, например dev.xxx содержит в себе и сразу понятно что и в каком порядке загружаем

              include language.xxx
              include country.xxx
              include currency.xxx
            • Перекрытие значений или merge для окружений, например добавляем новый язык в общие настройки, и потом для prod окружения его выключаем, подменяя только нужное поле, что-то типа этого

              include language.xxx
              include country.xxx
              include currency.xxx
              
              language:
                ita:
                  enable: false
              
            • Разделение конфигураций на открытую и закрытую части, открытая то что можно положить в git, для всех окружений, закрытая то что надо взять откуда-то (из определенного файла, из переменной окружения).


            1. CraftDream Автор
              19.08.2026 11:16

              Очень ценные запросы, спасибо! И почти всё здесь я готов подписать.

              Про валидацию соглашусь. В другом комментарии я предлагал ровно то, что вы отвергаете - взять JSON Schema и проверять ей уже разобранный конфиг. Но тогда проверка идёт по готовым данным, в которых уже не осталось привязки к строкам файла. Подсветить ошибку в редакторе нечем.

              Про остальные четыре пункта скажу прямо: этого нет. Почему - мы этим просто не занимались и не планировали заниматься. Формат отвечает на вопрос, как выглядит файл, а окружения, include и merge - это уже про то, как конфигурация собирается из нескольких файлов. Другой слой.

              JSON и TOML останавливаются там же. Вы сами это отмечаете - и ваш тезис именно в том, что между форматом и полноценным инструментом остаётся дыра. С этим спорить нечего, дыра есть.

              Мне кажется правильным закрывать её не изменением формата, а отдельной необязательной спекой поверх него: include как соглашение над обычными Ktav-документами, семантика глубокого merge (включая удаление ключа, а не только замену), выбор окружения, соглашение о ссылках на внешние секреты. Тогда файл остаётся обычным Ktav для тех, кому обвязка не нужна.

              Завёл под это issue, расписал ваши пять пунктов как отправную точку: https://github.com/ktav-lang/spec/issues/5

              Там же вынес открытые вопросы: своя схема или привязка к JSON Schema, отдельный документ или репозиторий, и стоит ли браться до 1.0 самого формата.


              1. Ru6aKa
                19.08.2026 11:16

                В YAML есть что-то типа merge, через якоря, коряво, косо, но есть.

                Не соглашусь с тем что все кроме формата данных надо кидать в обвязку.
                Тем же merge хотелось бы управлять на уровне файла, что-то типа

                include language.xxx
                include country.xxx, merge_left: true
                include currency.xxx, merge_right: true

                Но с include проблема в другом, в редакторе не видно финального конфига, и собственно он не может быть провалидирован через схему. Тут только надежда на то что конфиг из валидных кусков будет валидным(и это работает с простыми типами и вариацией только типов). Но опять таки это все ломается, если у схемы и валидации есть условия, например одно поле зависит от другого (тут только валидация при старте поможет).

                Поэтому и валидацию идеально было бы разделить на две части. Валидация через схему, только типов и чтобы было в редакторе. И валидация при старте с условиями и возможно кастомными валидаторами. Выдуманный пример, есть у нас в конфиге допустим адрес какого-то биткоин кошелька, с точки зрения данных это строка и ее вариация ничего не даст, но вот для prod/test кошельки должны быть разные и валидные, и для этого надо вызвать кастомный валидатор.


                1. CraftDream Автор
                  19.08.2026 11:16

                  Это будет масштабной работой. Не уверен, что возмусь за это, но мне нравятся ваши слова. В любом случае, это будет либо ktavPlus - вторым форматом (как TS над JS), либо обвязкой.

                  Оригинальный ktav очень хочу сохранить в простом виде - мне он тоже нравится именно своей простотой.

                  Если вы готовы быть поставщиком идей (в случае, если возьмусь) - создайте issue (https://github.com/ktav-lang/spec/issues). Если в будущем возьмусь, то я отпишусь под ним.


  1. chappihappymeal
    19.08.2026 11:16

    Крутой мини продукт. Выглядит очень приятно. И тут, наверное, главный плюс. Вы не пытаетесь сказать, что это удобно и полезно везде и всегда.

    Конкретная боль - кросс-языковые команды. В этом случае все рассуждения о том, что для того же Go решение наверное не самое идиоматичное, надо доставлять .so рядом, думать про musl/alpine в докере, про комбинации OS/arch и.т.д. это осознанная цена за консистентность.

    Единственное, чего мне не хватило бы, доп. синтаксиса для явного указания типа. У вас уже есть :: для форс-строки, и напрашивается то же самое для остальных типов, условные ::int и ::float. Защита от дурака для критичных переменных, где типизация по форме может поехать, плюс ревью конфигов становится проще: тип виден глазами, а не выводится в голове.

    Пробежался по репо и заметил одну деталь, тихую канонизацию числовых скаляров при выводе типов (1.10 -> 1.1, 01234 -> 1234). Я с подобным сталкивался в работе, поэтому решил помочь и подготовил issue и PR. Ознакомьтесь, может будет полезным.
    Issue: https://github.com/ktav-lang/rust/issues/1
    PR: https://github.com/ktav-lang/rust/pull/2


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо - и отдельное спасибо за ПР, это редкий жанр комментария.

      Про кросс-языковые команды верно, цена именно такая, как вы описали: .so рядом и прочее. Платим её сознательно, ради одинакового поведения везде и производительности Rust.

      PR влит, релиз для Rust опубликован - 0.6.2: https://crates.io/crates/ktav

      Теперь про ::int / ::float. Идея понятна и ложится на существующий синтаксис, но в 0.5.0 мы убрали ровно такие маркеры - были :i и :f.

      Главная причина - типизированные языки всё равно сами решают, какого типа получать значение. Целевой тип задаёт структура, в которую десериализуют, а не документ: serde в Rust, struct в Go, класс в C# приведут port к u16 независимо от того, как парсер классифицировал скаляр. Маркер в конфиге дублировал то, что уже объявлено в коде, и добавлял вопрос, что делать, когда он противоречит форме.

      Плюс ощущение, что для конфигов небольшого ПО текущих возможностей хватает - формат задумывался именно для удобных конфигураций, а не для передачи данных.

      Отчасти ту же боль закрывает как раз ваш parse_strict, только с другой стороны: он не даёт форме молча поехать.

      Благодраю за участие! Очень рад первому ПР.


  1. LyuMih
    19.08.2026 11:16

    У вас на сайте в INI ошибка - так и задумано?


    1. CraftDream Автор
      19.08.2026 11:16

      Оказывается еще и баг в темной теме со списоком. Исправил. Спасибо!

      По INI - не каждый конфиг может сконвертироваться в INI. Это не баг. Ниже на скрине показывается корректная ошибка об этом.


  1. DmitrySolomennikov
    19.08.2026 11:16

    Посмотрите plist из Common Lisp или Clojure, где ключи - keywords.

    Не совсем то, что у вас, но семантически близко.


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо, мысль хорошая - keywords действительно про то же самое: ключ самоозначающий, кавычки ему не нужны.

      Если продолжить её и поставить форматы рядом, то ближайшим соседом по внешнему виду окажется, наверное, всё-таки JSON5 - там тот же порядок “ключ двоеточие значение” и ключи тоже без кавычек:

      { host: 'a.example', port: 1080 }
      

      В plist и EDN двоеточие переезжает влево и превращает сам ключ в keyword, а пары разделяются пробелами внутри s-выражения:

      (:host "a.example" :port 1080)
      

      Но любопытно, что plist и JSON5 при этом роднит ровно то же самое, что вы заметили: ключ не нуждается в кавычках. Похоже, до этой мысли рано или поздно доходит каждый, кто делает формат для чтения человеком, - просто доводят её по-разному.

      Ktav дотянул её на шаг дальше и снял кавычки и со строковых значений тоже, а структуру оставил построчной, без объемлющих скобок:

      host: a.example
      port: 1080
      

      Так что, да - “не совсем то, но семантически близко”)


      1. DmitrySolomennikov
        19.08.2026 11:16

        Что произойдет с парсером, если строка будет содержать двоеточие?

        host: https://example.com
        port: *:1080


        1. CraftDream Автор
          19.08.2026 11:16

          Это корректная форма ktav, спарсится в объект, который эквивалентен JSON:

          {
            "host": "https://example.com",
            "port": "*:1080"
          }
          

          Это можно в живую проверить на сайте ktav-lang.github.io

          (на сложные случаи есть экранирование, но в малом ПО для конфигов это скорее всего не понадобится)


  1. LeshaRB
    19.08.2026 11:16

    Заинтересовало и возник вопрос

    У вас на сайте есть демо пример. Я немного изменил его

    {
      "log_level": "info",
      "log_level1": "true",
      "log_level2": true,
      "debug": true
    }
    

    на выходе

    log_level: info
    log_level1:: true
    log_level2: true
    debug: true
    

    log_level2 и debug оба boolean, но записались по разному

    PS Все понял :: это наоброт переводить в строку


  1. dsrk_dev
    19.08.2026 11:16

    Получился JSON5 без запятых)


    1. CraftDream Автор
      19.08.2026 11:16

      Почти) Только запятые были довеском - по-настоящему меня довели кавычки.

      В JSON5 голым можно написать ключ, а значение всё равно в кавычках:

      { host: 'a.example', port: 1080 }
      

      В Ktav кавычек нет ни в ключе, ни в значении, и корень не обёрнут в скобки - файл читается построчно:

      host: a.example
      port: 1080
      

      Запятые ушли уже за компанию: раз пары и так разделены переносом строки, разделять их ещё и запятой стало не нужно.


  1. vvshard
    19.08.2026 11:16

    Понравилось. Есть небольшое замечание: По песочнице ktav-lang.github.io бросается в глаза недоведенность по начальным и конечным пробелам строковых значений. ktav -> json обрабатывает невидимые пробелы как мне и хотелось: их игнорирует, а в ((…)) - сохраняет. Хотелось бы, чтобы json -> ktav начальные и конечные пробелы строковых значений превращал в соответствующую 3-строчную конструкцию ((…)), ну или придумайте для этого, хоть и редкого, но значимого случая специальные кавычки.


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо - точное и ценное замечание, круто, что вы сразу нашли реальуню вещь, которая стоит доработки!

      Это не пробел в спеке - она уже требует именно того, чего вам хочется. § 5.9.5(b) и § 5.9.7 прямо запрещают однострочную форму для строки с краевыми пробелами и обязывают многострочный блок ((…)) вместо неё. Отдельные кавычки не нужны - механизм для этого случая в формате уже есть, просто writer его не соблюдает.

      Направление вы нащупали точно: ktav -> json у вас работает правильно, потому что там парсер, а он спеке следует. json -> ktav - это запись, и вот там баг: строка с пробелом по краям сейчас молча теряет байты вместо того, чтобы уйти в блок.

      Этот же баг на этой неделе нашёл и прислал с PR @chappihappymeal - независимо от вас, через код.

      Issue: https://github.com/ktav-lang/rust/issues/3

      PR: https://github.com/ktav-lang/rust/pull/4

      И раз тема всплыла - параллельно копится пачка для отдельного релиза 0.7. Там уже прилично набралось: и спеку, и все семь парсеров гонять волной ради одной мелкой правки слишком дорого, поэтому такие изменения собираем до критической массы


  1. andyblaster
    19.08.2026 11:16


    1. CraftDream Автор
      19.08.2026 11:16

      Спасибо!

      Это не пробел в спеке - она уже требует именно того, чего вам хочется. § 5.9.5(b) и § 5.9.7 прямо запрещают однострочную форму для строки с краевыми пробелами и обязывают многострочный блок ((…)) вместо неё.

      Направление вы нащупали точно: ktav -> json у вас работает правильно, потому что там парсер, а он спеке следует. json -> ktav - это запись, и вот там баг: строка с пробелом по краям сейчас молча теряет байты вместо того, чтобы уйти в блок. Проверил на самом остром случае - строка “\ttrue”: вместо блока сейчас пишется однострочное field1:: true, а при обратном чтении ведущий таб пропадает вообще, значение схлопывается в чистое “true”. То есть это не просто некрасивая форма записи, а реальная потеря байта.

      Этот же класс бага @chappihappymeal нашёл и прислал с PR, независимо от вас:

      Issue: https://github.com/ktav-lang/rust/issues/3

      PR: https://github.com/ktav-lang/rust/pull/4

      Я прогнал ваш случай на ветке этого PR отдельно - чинит и его: та же строка с ведущим табом уходит в ((…)) и возвращается обратно байт в байт. Фикс идёт в ближайший патч-релиз 0.6.3, ничего не ломает - плейграунд подтянет его следующим обновлением вендоренного WASM.

      И раз тема всплыла - параллельно копится пачка для отдельного релиза 0.7. Там уже прилично набралось: и спеку, и все семь парсеров гонять волной ради одной мелкой правки слишком дорого, поэтому такие изменения собираем до критической массы, а не выкатываем по одному.


      1. andyblaster
        19.08.2026 11:16

        Почему в первом случае одинарные скобочки? В целом, понятно, что можно в обеих формах записывать любой текст, просто интересно, как конвертер выбирает способ.


        1. CraftDream Автор
          19.08.2026 11:16

          Одинарные скобки делают красивый триминг отступов когда можно сохранить целостность данных (в обратном конвертировании они значат, что отступы не нужны). Двойные - сохраняют строку как есть. Такой была цель.