# Ktav (כְּתָב)

**Languages:** [English](README.md) · **Русский** · [简体中文](README.zh.md)

> Простой формат конфигурации. Форма JSON — скаляры, массивы, объекты,
> `null`, `true`, `false` — без пунктуации JSON. Никаких кавычек
> вокруг строк, никаких запятых, никакой таблицы escape-последовательностей.
> Точечные ключи для вложенности, видимые явные маркеры для литеральных
> и многострочных строк.

Этот репозиторий — **каноническая спецификация** формата Ktav.
Реализации на любом языке программирования должны соответствовать
той версии, на которую они ориентированы.

## На вкус

Один пример, задействующий все основные формы формата — обычный `:`
(String), ключевое слово Bool, типовые маркеры `:i` (Integer) и `:f`
(Float), сырой `::` (литеральная String), точечные ключи, вложенные
составные значения, многострочная строка.

```text
# A config for a SOCKS5 rotator.
port:i 20082
log_level: info
debug: true

banned_patterns: [
    .*\.onion:\d+
    .*\.local
]

upstreams: [
    {
        host: a.example
        port:i 1080
        weight:f 0.7
        timeouts: {
            read:i 30
            write:i 10
        }
    }
    {
        host: b.example
        port:i 1080
        weight:f 0.3
    }
]

# Dotted keys — flat alternative to nesting.
node.host: a.example
node.port:i 1080
# `::` делает строку литеральной — двоеточие внутри пароля сохраняется.
node.auth:: p@ss:word

motd: (
    Welcome to the node.
    Please behave.
)
```

Разбирается в следующее значение (показано в виде JSON5 — с
комментариями и без кавычек в ключах для читаемости). Обратите
внимание на соответствие маркеров:

- `:` — обычная String, на уровне Value остаётся строкой даже для
  цифрового содержимого (`log_level: "info"`,
  `banned_patterns[0]: "…"`).
- `: true` / `: false` / `: null` — ключевые Bool / Null.
- `:i` — Integer как нативное JSON-число.
- `:f` — Float как нативное JSON-число (с десятичной точкой).
- `::` — сырая String, классификация не применяется.

```json5
{
  port: 20082,
  log_level: "info",
  debug: true,

  banned_patterns: [
    ".*\\.onion:\\d+",
    ".*\\.local",
  ],

  upstreams: [
    {
      host: "a.example",
      port: 1080,
      weight: 0.7,
      timeouts: { read: 30, write: 10 },
    },
    {
      host: "b.example",
      port: 1080,
      weight: 0.3,
    },
  ],

  node: {
    host: "a.example",
    port: 1080,
    auth: "p@ss:word",
  },

  motd: "Welcome to the node.\nPlease behave.",
}
```

### Без `:i` / `:f` — числа остаются строками

Формат никогда не угадывает типы по виду содержимого; типизация
делается явно, по желанию. Без маркера каждый скаляр на уровне Value
— это String. Потребители, которым нужны нативные числа, либо
помечают значение `:i` / `:f`, либо приводят типы на своей границе
(Rust + serde делает это автоматически через `FromStr`).

```text
retries: 3
version: 1.2
ratio:f 0.5
count:i 42
```

```json5
{
  retries: "3",      // обычный `:` — String
  version: "1.2",    // обычный `:` — String
  ratio: 0.5,        // :f — нативное JSON-число
  count: 42,         // :i — нативное JSON-число
}
```

### С `::` — ключевые слова и скобки как обычные строки

Body, который иначе был бы классифицирован как ключевое слово
(`null`, `true`, `false`), пустое составное (`{}`, `[]`) или начало
составного (`{`, `[`), требует сырого маркера `::`, чтобы стать
обычной String.

```text
# Без `::` было бы Bool true — здесь это строка "true".
on_release:: true
# Начинается с `[` — `::` предотвращает "открытие массива".
regex::      [a-z]+
# IPv6-адрес — та же причина.
ipv6::       [::1]:8080
# Ключевое слово `null` как литеральная четырёхсимвольная строка.
placeholder:: null
```

```json5
{
  on_release: "true",
  regex: "[a-z]+",
  ipv6: "[::1]:8080",
  placeholder: "null",
}
```

## Девиз

> **Будь другом конфига, а не его экзаменатором. Конфиг неидеален —
> но он лучший из возможных.**

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

## Зачем Ktav

| Свойство                                             | JSON | YAML | TOML | Ktav |
|------------------------------------------------------|:----:|:----:|:----:|:----:|
| Строки без обязательных кавычек                      |  ✗   |  ~   |  ✗   |  ✓   |
| Списки без разделителей (никаких запятых)            |  ✗   |  ✓   |  ✓   |  ✓   |
| Нечувствительность к пробелам / отступам             |  ✓   |  ✗   |  ✓   |  ✓   |
| Удобные для человека многострочные строки            |  ✗   |  ~   |  ~   |  ✓   |
| Встроенные комментарии                               |  ✗   |  ✓   |  ✓   |  ✓   |
| Точечные ключи для плоских правок                    |  ✗   |  ✗   |  ✓   |  ✓   |
| Один парсер, небольшая спецификация                  |  ✓   |  ✗   |  ~   |  ✓   |

> ✓ = свойство есть · ✗ = нет · ~ = частично

Ktav сохраняет форму JSON (вы всегда знаете, что означает документ),
но отбрасывает синтаксис, делающий JSON враждебным для ручного
написания. Сохраняет точечные ключи TOML (удобно для плоских правок
и CLI-переопределений), но отказывается от двухмерного деления TOML
на таблицы и inline.

## Правила на одном экране

Документ Ktav — это неявный объект верхнего уровня. Внутри любого
объекта — пары; внутри любого массива — элементы.

```text
# comment              — any line starting with '#'
key: value             — scalar pair; value is a String (default)
key:: value            — scalar pair; value is ALWAYS a literal string
key:i value            — scalar pair; value is an Integer (digits only)
key:f value            — scalar pair; value is a Float (needs decimal)
key: { ... }           — multi-line object; `}` closes on its own line
key: [ ... ]           — multi-line array; `]` closes on its own line
key: {}   /   key: []  — empty compound, inline
key: ( ... )           — multi-line string; common indent stripped
key: (( ... ))         — multi-line string; verbatim (no stripping)
:: value               — inside an array: literal-string item
:i value               — inside an array: Integer item
:f value               — inside an array: Float item
```

Это весь язык. Никаких запятых, никаких кавычек, никакой таблицы
escape-последовательностей — единственный «escape» это маркер `::`,
и он живёт в разделителе (для пар) или в префиксе строки (для
элементов массива).

### Точечные ключи

Ключами могут быть точечные пути. Эти два документа *идентичны*:

```text
server.host: 127.0.0.1
server.port: 8080
```

```text
server: {
    host: 127.0.0.1
    port: 8080
}
```

Точечные ключи свободно сочетаются с вложенной формой. Парсер строит
одно и то же дерево в обоих случаях. Полезно для:

- CLI/env-переопределений (`--set server.port=9090`).
- Частичных правок в небольших конфигах без реструктуризации.
- Файлов с плоской основой, обрастающих вложенными секциями по мере
  необходимости.

### Строки напрямую

Значение по умолчанию — строка. То, что идёт после `:` (через один
пробел отступа), и есть строка, посимвольно, до конца строки.
Отсутствие кавычек означает отсутствие правил кавычек — пути, URL,
регулярки, токены с пунктуацией просто работают.

```text
pattern: .*\.onion:\d+
url: https://example.com:8080/path?x=1
key: s3cret/with:colons and-dashes
```

Когда строка конфликтовала бы с грамматикой (начинается с `{`, `[`,
`(` или равна ключевому слову вроде `true`), префиксуйте разделитель
через `::`:

```text
literal_bracket:: [
keyword_as_string:: true
```

### Числа, когда они нужны

По умолчанию значение, выглядящее как число, — строка: `port: 8080`
даёт вам `"8080"`. Потребители на типизированных языках (Rust +
serde, Go) сами кастуют это к настоящему числу на своей границе,
без содействия формата.

Для потребителей на динамических языках (JS, PHP, Python) выбирайте
типовые значения через `:i` (Integer) или `:f` (Float):

```text
port:i   8080
ratio:f  0.5
offset:i -100
eps:f    1.5e-10
```

Значения по-прежнему сохраняются в текстовой форме на уровне Value
— `Integer("8080")`, `Float("0.5")` — так что 40-значные целые
переживают round-trip, а `1.2` никогда не окажется случайно
приведённым к Number. Потребитель сужает до нативного типа, какой
ему нужен.

### Многострочные строки

Две формы, разные цели:

```text
stripped: (
    line 1
    line 2
        relative indent preserved
)

verbatim: ((
    line 1
        exact leading whitespace preserved
    line 3
))
```

`(` срезает общий ведущий отступ — пишите код/текст, который *читается*
в файле хорошо, а значение получается чистым. `((` сохраняет каждый
байт, так что документ round-trip'ится байт-в-байт.

### Ключевые слова

Только в нижнем регистре: `null`, `true`, `false`. Всё остальное —
`Null`, `TRUE`, `yes`, `on` — обычная строка. Никакого волшебного
приведения типов, никакого зависящего от версии списка ловушек.

```text
port: 8080
active: true
timeout: null
```

```json5
{
  port: "8080",   // обычный `:` — String, не число
  active: true,   // ключевое слово → нативный JSON bool
  timeout: null,  // ключевое слово → нативный JSON null
}
```

## Полная спецификация

- **Текущая стабильная:** [Ktav 0.1.0](versions/0.1/spec.ru.md) — выпущена 2026-04-22.
- **Машиночитаемый индекс** всех версий: [`versions.ktav`](versions.ktav).
- **История версий:** [`CHANGELOG.md`](CHANGELOG.md).

## Набор тестов соответствия

Каждая версия поставляется с языконезависимым набором тестов в
[`versions/<v>/tests/`](versions/0.1/tests/). Пары
`<name>.ktav` + `<name>.json` — `.json` это ожидаемый `Value`,
отображённый 1:1 для обычных скаляров (`Null`→`null`,
`Bool`→`bool`, `String`→`string`, `Array`→`array`,
`Object`→`object`; числовые body обычных `:`-пар остаются
строками на уровне Value). Типовые скаляры (`:i` / `:f`)
кодируются как нативные JSON-числа — `8080` для Integer,
`0.5` для Float — и различаются наличием десятичной точки.
Полный оракул см. в
[`versions/0.1/tests/README.ru.md`](versions/0.1/tests/README.ru.md).
Порядок полей объекта значим.

Реализация соответствует версии, если проходит каждый тест из
набора этой версии. Подключайте директорию как git submodule (или
копию); см. [`versions/0.1/tests/README.ru.md`](versions/0.1/tests/README.ru.md).

## Схема версионирования

Версии спецификации используют `MAJOR.MINOR.PATCH`:

| Bump              | Смысл                                                                                              |
|-------------------|----------------------------------------------------------------------------------------------------|
| `x.y → x.y.(z+1)` | Редакторский — исправления опечаток, уточнения; соответствующие реализации не затрагиваются.       |
| `x.y → x.(y+1)`   | Обратно совместимое расширение (новое ключевое слово, новая примитивная форма).                    |
| `x.y → (x+1).0`   | Ломающее изменение грамматики или семантики.                                                       |

Внутри любого стабильного `MAJOR` реализация, ориентированная на
`x.0`, MUST разбирать каждый документ, валидный при любом более
позднем `x.y.z`, идентично — в пределах подмножества, которое она
поддерживает.

Директория каждой версии полностью самодостаточна: `spec.md`, набор
соответствия `tests/` и добавления по версиям. Реализации
привязываются к директории версии по пути.

## Структура

```
.
├── README.md              this file
├── versions.ktav          machine-readable index of versions
├── CHANGELOG.md           summary across versions
├── CONTRIBUTING.md        how to propose changes
├── LICENSE                MIT
└── versions/
    └── <version>/
        ├── spec.md        the specification document
        └── tests/         language-agnostic conformance suite
            ├── README.md
            ├── valid/
            └── invalid/
```

## Реализации

| Язык             | Репозиторий                                             | Установка                                            |
|------------------|---------------------------------------------------------|------------------------------------------------------|
| Rust (эталонная) | [`ktav-lang/rust`](https://github.com/ktav-lang/rust)   | `cargo add ktav`                                     |
| C# / .NET        | [`ktav-lang/csharp`](https://github.com/ktav-lang/csharp) | `dotnet add package Ktav`                          |
| Go               | [`ktav-lang/golang`](https://github.com/ktav-lang/golang) | `go get github.com/ktav-lang/golang`               |
| Java / JVM       | [`ktav-lang/java`](https://github.com/ktav-lang/java)   | `io.github.ktav-lang:ktav` на Maven Central          |
| JS / TS          | [`ktav-lang/js`](https://github.com/ktav-lang/js)       | `npm install @ktav-lang/ktav`                        |
| PHP              | [`ktav-lang/php`](https://github.com/ktav-lang/php)     | `composer require ktav-lang/ktav`                    |
| Python           | [`ktav-lang/python`](https://github.com/ktav-lang/python) | `pip install ktav`                                 |

Rust crate — эталонный парсер; каждый из остальных биндингов поставляет
сборку `ktav_cabi` (C-ABI обёртка) и предоставляет тот же Ktav 0.1
интерфейс — language-agnostic набор `tests/` ниже прогоняется на всех
из них при каждом релизе.

Строите новую реализацию? Начните со `spec.md` целевой версии
([`spec.ru.md`](versions/0.1/spec.ru.md), раздел 8 — Compliance)
и прогоните набор [`tests/`](versions/0.1/tests/) через свой парсер.

## Вклад

Редакторские правки в существующую версию — PR напрямую. Всё
остальное — сначала открывайте issue. См.
[`CONTRIBUTING.ru.md`](CONTRIBUTING.ru.md).

## Поддержите проект

У автора много идей, которые могут быть полезны IT во всём мире, — и
далеко не только для Ktav. Их реализация требует финансирования. Если
вы хотите помочь — пишите на **phpcraftdream@gmail.com**.

## Лицензия

MIT. См. [LICENSE](LICENSE).
