# Ktav — Формат Записанной Конфигурации

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

> **Informative translation.** The canonical, normative version of
> this specification is the English [`spec.md`](spec.md) in the
> same directory. If this Russian translation disagrees with the
> English original, the English original is authoritative.

**Версия:** 0.1.0
**Дата:** 2026-04-22

## Аннотация

Ktav — это текстовый формат конфигурации, разработанный так, чтобы
каждая строка была либо самодостаточной, либо зависела только от явных,
видимых скобок. Он предоставляет форму JSON (скаляры, массивы, объекты,
`null`, логические значения) без свойственной JSON пунктуации: без
кавычек вокруг строк, без запятых, без escape-последовательностей.
Вложенные ключи записываются точечным путём; многострочные строки и
литералы используют небольшие, видимые opt-in-маркеры.

Настоящий документ специфицирует синтаксис и семантику формата на
версии 0.1.0. Реализации на любом языке программирования могут заявлять о
«Ktav 0.1.0 compliance» тогда и только тогда, когда они удовлетворяют
каждому нормативному утверждению ниже.

## 1. Введение

Документ Ktav — это последовательность строк, совместно описывающих
иерархический объект. Типичное применение — конфигурация приложений, где
документ пишется людьми, читается программами и сравнивается в системе
контроля версий.

Руководящий принцип формата:

> **Каждое правило локально. Смысл каждой строки либо самоочевиден,
> либо зависит только от видимых скобок выше.**

Это исключает значимые пробелы (YAML), арифметику запятых (JSON), якоря
и алиасы (YAML), директивы схемы и heredoc-маркеры, растянутые на
множество строк.

## 2. Соглашения

Ключевые слова **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT** и
**MAY** в настоящем документе следует интерпретировать согласно RFC 2119,
когда они встречаются заглавными буквами. Использование этих слов в
строчном виде относится к обычному английскому языку.

## 3. Лексическая Структура

### 3.1 Набор Символов

Документ Ktav — это последовательность кодовых точек Unicode, закодированных
в UTF-8. Реализации MUST отклонять документы, не являющиеся корректным
UTF-8.

### 3.2 Строки

Строки разделяются либо LF (`\n`), либо CRLF (`\r\n`). Разделитель строк
не является частью содержимого строки. За завершающим содержимым последней
строки разделителя может не быть.

### 3.3 Пробельные Символы

«ASCII-пробел» в настоящем документе означает любой из байтов,
соответствующих `U+0009` (tab), `U+000A` (LF), `U+000B` (VT), `U+000C` (FF),
`U+000D` (CR), `U+0020` (space). Значимы только эти; прочие пробельные
символы Unicode трактуются как обычное содержимое.

«Trim» означает удаление ведущих и завершающих ASCII-пробелов.

### 3.4 Комментарии

Строка, обрезанное содержимое которой начинается с символа `#` (U+0023),
является комментарием. Остаток строки игнорируется.

```text
# this is a comment
    # leading whitespace is allowed before '#'
```

Встроенные комментарии — `#`, следующий за иным содержимым в той же
строке, — **NOT** распознаются. Символ `#`, не находящийся в начале
обрезанной строки, является обычным содержимым (как правило, частью
значения).

### 3.5 Пустые Строки

Строка, обрезанное содержимое которой пусто, является пустой строкой.
Пустые строки не несут структурного смысла, за исключением случаев
внутри многострочной строки (§ 5.6), где они появляются в содержимом
дословно.

## 4. Грамматика

Грамматика представлена в полуформальной нотации, по одному правилу на
строку. Терминалы заключены в двойные кавычки; `<name>` обозначает
нетерминал; `*` — ноль или более, `?` — опционально, `|` — альтернатива.

```
<document>      ::= <line>*
<line>          ::= <comment> | <blank> | <header-line> | <pair-line>
                  | <array-item-line> | <multiline-content-line>

<comment>       ::= (ws) "#" (any-chars until line-end)
<blank>         ::= (ws)

<header-line>   ::= (ws) "{" (ws) eol                ; object open
                  | (ws) "}" (ws) eol                ; object close
                  | (ws) "[" (ws) eol                ; array open
                  | (ws) "]" (ws) eol                ; array close
                  | (ws) ")" (ws) eol                ; multiline close (stripped)
                  | (ws) "))" (ws) eol               ; multiline close (verbatim)

<pair-line>     ::= <key> ":"  <sep-end> <value-part-opt> eol    ; default, String
                  | <key> "::" <sep-end> <value-part-opt> eol    ; literal String
                  | <key> ":i" <sep-end> <integer-body>   eol    ; typed Integer
                  | <key> ":f" <sep-end> <float-body>     eol    ; typed Float
<key>           ::= <segment> ("." <segment>)*
<segment>       ::= <key-char>+
<key-char>      ::= any character except ASCII whitespace (§ 3.3),
                    "[", "]", "{", "}", ":", "#"

<sep-end>       ::= 1*ws | &eol                    ; ≥1 whitespace byte, or the line end
<value-part-opt> ::= <value-start> | ""             ; value-part is optional; "" ⇒ empty String
<value-start>   ::= "{" (ws) "}" (ws)                ; empty inline object
                  | "[" (ws) "]" (ws)                ; empty inline array
                  | "{" (ws)                         ; open object
                  | "[" (ws)                         ; open array
                  | "(" (ws)                         ; open multiline (stripped)
                  | "((" (ws)                        ; open multiline (verbatim)
                  | "()" (ws)                        ; empty inline (yields "")
                  | "(())" (ws)                      ; empty inline (yields "")
                  | <scalar-body>                    ; scalar value

<scalar-body>   ::= (ws) any-chars-until-eol
                    ; trimmed; interpreted per §5.2

<integer-body>  ::= ("+" | "-")? 1*DIGIT
<float-body>    ::= ("+" | "-")? 1*DIGIT "." 1*DIGIT
                    ( ("e" | "E") ("+" | "-")? 1*DIGIT )?

<array-item-line> ::= <item-literal> | <item-typed-int>
                    | <item-typed-float> | <item-value>
<item-literal>  ::= (ws) "::" <sep-end> <any-chars>? eol   ; raw string item
<item-typed-int>   ::= (ws) ":i" <sep-end> <integer-body> eol ; typed Integer item
<item-typed-float> ::= (ws) ":f" <sep-end> <float-body>   eol ; typed Float item
<item-value>    ::= <value-start> eol                  ; value at array position

<multiline-content-line> ::= any line within an open <multiline>;
                             the terminator (")" or "))") ends the block
```

Замечания о нотации:

- Внутри строки `(ws)` означает ноль или более байтов ASCII-пробела.
- `1*ws` означает **один или более** байтов ASCII-пробела (та же
  конвенция, что и `1*DIGIT`).
- `<sep-end>` означает «хотя бы один байт ASCII-пробела, либо конец
  строки». Используется после каждого разделителя пар и маркера
  элементов массива (`:`, `::`, `:i`, `:f`). Опция EOL даёт автору
  возможность оставить значение пустым в форме `key:` (строка
  кончается сразу за разделителем); опция `1*ws` покрывает обычные
  формы `key: value`, `key:: literal`, `port:i 8080`. Запись
  `key:value` / `port:i42` (без пробела и без EOL — после разделителя
  сразу идёт содержимое) — синтаксическая ошибка, см. § 6.10.
- `&eol` — это zero-width positive lookahead: совпадает с концом
  строки, не потребляя её, так что EOL остаётся завершителем строки.
- Реализация MAY допускать дополнительные завершающие пробелы в любой
  строке перед разделителем строк.

## 5. Семантика

Соответствующий парсер производит **Value** для документа, согласующегося
со спецификацией. Value — одно из следующих: **Null, Bool, Integer,
Float, String, Array, Object**.

- **Null** — единственное выделенное значение.
- **Bool** — `true` или `false`.
- **Integer** — числовая строка вида `[-]?[0-9]+`. Value сохраняет
  текстовую форму в точности (никакого преобразования в машинное
  целое на уровне Value); сужение до целочисленного типа,
  необходимого приложению, — ответственность потребителя.
- **Float** — числовая строка вида
  `[-]?[0-9]+\.[0-9]+([eE][-+]?[0-9]+)?`. Value сохраняет текстовую
  форму в точности.
- **String** — (возможно, пустая) строка в UTF-8.
- **Array** — упорядоченная последовательность Value.
- **Object** — упорядоченная последовательность пар (имя, Value), где
  имена суть строки. Уникальность имён требуется внутри Object (§ 5.5).

Integer и Float никогда не возникают неявно из обычной `:`-пары —
они появляются только при использовании типовых маркеров `:i` или
`:f` (§ 5.3, § 5.4). Обычный скалярный body вида `8080` остаётся
String, сохраняя гарантию 0.1.0 «всё есть String, пока не помечено
иначе».

Корневое Value, получаемое в результате разбора документа, всегда
является Object (возможно, пустым).

### 5.1 Диспетчеризация по Строкам

Парсер MUST классифицировать каждую строку после trim, применяя
правила в точности в следующем порядке:

1. Если обрезанная строка пуста → пустая строка; без эффекта, кроме
   как где оговорено (§ 5.6, многострочная).
2. Если обрезанная строка начинается с `#` → комментарий; игнорируется.
3. **Если парсер находится внутри открытой многострочной строки** (§ 5.6):
   если обрезанная строка равна терминатору блока, многострочная строка
   закрывается; иначе сырая (необрезанная) строка добавляется к
   содержимому многострочной строки.
4. Если обрезанная строка в точности `}` → закрыть самый внутренний
   открытый Object, иначе ошибка (§ 6.1).
5. Если обрезанная строка в точности `]` → закрыть самый внутренний
   открытый Array, иначе ошибка (§ 6.1).
6. Если самый внутренний открытый составной элемент — Array:
   трактовать строку как **array-item line** (§ 5.4).
7. Иначе (самый внутренний открытый составной элемент — Object, либо
   верхний уровень): трактовать строку как **pair line** (§ 5.3).

### 5.2 Интерпретация Скалярного Значения

Дан скалярный body — обрезанный текст body pair-строки после обычного
разделителя `:`, либо строки-элемента массива, не использующей маркер, —
парсер классифицирует его следующим образом. Правила применяются по
порядку; выигрывает первое совпавшее правило. Body после `::`, `:i`
или `:f` через § 5.2 **не** проходят: они обрабатываются § 5.2.1
(типовые маркеры) и raw-string-клаузулами § 5.3 и § 5.4.

1. Если body в точности `{` → открыть новый Object scope.
2. Если body в точности `[` → открыть новый Array scope.
3. Если body начинается с `{`: если он также заканчивается на `}` и
   текст между ними пуст (либо пробельный), значением является пустой
   Object; иначе это ошибка (inline-непустые объекты НЕ валидны на
   версии 0.1.0).
4. Если body начинается с `[`: аналогично, пустой Array, если body —
   это `[]` (или `[`, за которым следуют пробелы и `]`); иначе ошибка.
5. Если body в точности `(` → открыть многострочную строку (stripped-форма,
   § 5.6).
6. Если body в точности `((` → открыть многострочную строку (verbatim-форма,
   § 5.6).
7. Если body — это `()` или `(())` → пустая String.
8. Если body в точности `null` → Null.
9. Если body в точности `true` → Bool `true`.
10. Если body в точности `false` → Bool `false`.
11. Иначе → String, содержимое которой — body как есть (уже обрезанный).

Ключевые формы `null`, `true`, `false` сопоставляются
**с учётом регистра**. Body вида `True`, `NULL`, `False` и т. п.
является String.

#### 5.2.1 Интерпретация Типового Скаляра

Когда строка использует типовые маркеры `:i` или `:f` (в pair-форме,
§ 5.3, или в array-item-форме, § 5.4), body **не** направляется
через классификацию § 5.2. Body типовых скаляров интерпретируются
следующим образом.

**Body типового целого** (маркер `:i`):

- Body после trim MUST совпадать с `integer-body` из § 4: один
  необязательный знак (`+` или `-`), затем один или более десятичных
  разрядов. Никакие иные символы не допускаются — в частности, нет
  десятичной точки, нет экспоненты, нет пробельных символов внутри
  разрядов, нет разделителей тысяч.
- При совпадении парсер производит Value типа **Integer**, текстовая
  форма которого есть body с удалённым ведущим `+`. Ведущий `-`
  сохраняется. Body `0` или `-0` даёт `"0"` и `"-0"` соответственно
  (парсер не нормализует знак нуля).
- При несовпадении — включая пустой body, body с не-цифровыми
  символами, body с десятичной точкой, или body, пытающийся открыть
  составной элемент (`{`, `[`) или многострочную строку (`(`, `((`),
  — парсер MUST эмитировать ошибку **InvalidTypedScalar** (§ 6.9).

**Body типового float** (маркер `:f`):

- Body после trim MUST совпадать с `float-body` из § 4: один
  необязательный знак мантиссы (`+` или `-`), один или более разрядов,
  обязательная `.`, один или более разрядов и необязательная
  экспонента (`e` или `E`, необязательный знак, один или более
  разрядов).
- При совпадении парсер производит Value типа **Float**, текстовая
  форма которого есть body с удалённым ведущим `+` на мантиссе.
  Знак внутри экспоненты (если есть) сохраняется как написан; буква
  экспоненты (`e` vs `E`) сохраняется как написана.
- При несовпадении — включая пустой body, body без десятичной точки
  (например, `42`), body с не-цифровыми символами, или body,
  пытающийся открыть составной элемент или многострочную строку, —
  парсер MUST эмитировать ошибку **InvalidTypedScalar** (§ 6.9).

Типовые маркеры **NOT** открывают составные значения или
многострочные строки. Body `{`, `[`, `(`, `((` — это ошибка для
любой типовой пары или элемента, даже несмотря на то, что эти body
осмысленны после обычного разделителя `:`.

### 5.3 Pair-Строки

Pair-строка имеет форму `<key> <sep> <body>`, где `<sep>` — один из
`:`, `::`, `:i` или `:f`.

- Часть до первого `:` — это **ключ**.
- **Разделитель `::`** — значение является **сырой строкой**: body,
  следующий за двумя двоеточиями и обрезанный, берётся дословно как
  String. Скалярная классификация (§ 5.2) не применяется.
- **Разделитель `:i`** — **типовая целочисленная пара** (§ 5.2.1).
  Body MUST совпадать с `integer-body` из § 4; при совпадении Value
  — Integer; при несовпадении парсер MUST эмитировать ошибку
  **InvalidTypedScalar** (§ 6.9).
- **Разделитель `:f`** — **типовая float-пара** (§ 5.2.1). Body
  MUST совпадать с `float-body` из § 4; при совпадении Value —
  Float; при несовпадении парсер MUST эмитировать ошибку
  **InvalidTypedScalar** (§ 6.9).
- Иначе (разделитель `:`) body классифицируется согласно § 5.2.

Разделитель сопоставляется жадно и с учётом регистра: `:i`, `:f` и
`::` распознаются как точные двухсимвольные токены. `:I`, `:F`,
`:int`, `:integer`, `:float` — NOT распознаются как типовые
маркеры; ключ, за которым следует любое из этого, либо образует
обычную `:`-пару (где остаток разделителя становится частью body),
либо — для последовательностей, оставляющих пустой ключ или иначе
ломающих структуру pair-строки, — является синтаксической ошибкой.

**Обязательный пробел после разделителя.** Каждый разделитель пары —
`:`, `::`, `:i`, `:f` — MUST быть за ним хотя бы один байт
ASCII-пробела, **либо** конец строки. Запись `key:value`,
`key::value`, `port:i42` или `ratio:f0.5` — разделитель, склеенный с
body без пробела между ними, — это ошибка **MissingSeparatorSpace**
(§ 6.10). Опция EOL существует, чтобы пустое значение можно было
записать как `key:` / `key::` / `key:i` / `key:f` без замыкающего
пробела (редакторы часто незаметно удаляют его); впрочем, `:i` / `:f`
с пустым body всё равно дают **InvalidTypedScalar** по § 5.2.1 —
маркер требует цифр. Правило существует, чтобы сохранять визуальную
регулярность строки: значение всегда стартует на фиксированном
расстоянии от разделителя.

Пример:

```text
# key: Bool true
key:  true
# key: String "true"
key:: true
# regex: String "[a-z]+"
regex:: [a-z]+
# addr: String "[::1]:8080"
addr:: [::1]:8080
# port: Integer "8080"
port:i 8080
# ratio: Float "0.5"
ratio:f 0.5
# offset: Integer "-100"
offset:i -100
# count: Integer "5" (ведущий '+' удалён)
count:i +5
# eps: Float "1.5e-10"
eps:f  1.5e-10
```

#### 5.3.1 Валидация Ключа

Ключ MUST быть непустой последовательностью **сегментов**, соединённых
`.`. Каждый сегмент MUST быть непустым и MUST не содержать ASCII-пробелов
и символов `[`, `]`, `{`, `}`, `:` или `#`. Ключ, нарушающий эти
ограничения, является синтаксической ошибкой.

Реализации MAY дополнительно ограничивать сегменты ключей (например,
ASCII-идентификаторами), но MUST NOT принимать ключи, отвергнутые выше.

#### 5.3.2 Разворачивание Точечного Ключа

Ключ `a.b.c` обозначает путь через вложенные Object. Запись
`a.b.c: v` семантически эквивалентна:

```text
a: {
    b: {
        c: v
    }
}
```

Несколько точечных пар с общим префиксом объединяются в один Object по
этому префиксу:

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

производит Object `{server: {host: "127.0.0.1", port: "8080"}}`.

Если две пары противоречат друг другу в общем префиксе (одна ожидает,
что он будет скаляром, другая — что он будет Object), это ошибка
**path conflict** (§ 6.3).

### 5.4 Array-Item Строки

Внутри открытого Array не-закрывающие строки являются элементами
массива.

- Если обрезанная строка начинается с `::`, маркер MUST сопровождаться
  либо хотя бы одним байтом ASCII-пробела, либо концом строки (то же
  правило `<sep-end>`, что и в § 5.3); остаток (после пропуска пробелов
  за `::`) является элементом String дословно. Классификация (§ 5.2)
  НЕ применяется. Запись `::value` (маркер склеен с body) — ошибка
  **MissingSeparatorSpace** (§ 6.10).
- Если обрезанная строка начинается с `:i`, маркер MUST сопровождаться
  хотя бы одним байтом ASCII-пробела; остаток является **типовым
  целочисленным элементом**: его body интерпретируется согласно
  § 5.2.1 как Integer (или отвергается с **InvalidTypedScalar**).
  `:i42` (склеено) — ошибка **MissingSeparatorSpace**.
- Если обрезанная строка начинается с `:f`, правило аналогичное —
  обязательный пробел, body — **типовой float-элемент** по § 5.2.1.
- Иначе обрезанная строка классифицируется согласно § 5.2, и
  полученное Value становится следующим элементом Array.

Пример:

```text
items: [
    # String "ok"
    ok
    # String "[literal]"
    :: [literal]
    # String "true"
    :: true
    # Bool true
    true
    # Null
    null
    # Integer "42"
    :i 42
    # Integer "-7"
    :i -7
    # Float "3.14"
    :f 3.14
    # открывает вложенный Object
    {
        name: inner
    }
]
```

### 5.5 Дублирующиеся Имена

Внутри любого единственного Object каждое имя MUST быть уникальным.
Дубликат — будь то от двух простых pair-строк с одинаковым ключом или от
разворачивания точечного ключа в уже присутствующее имя — является
синтаксической ошибкой (§ 6.2).

Конфликт между скаляром и Object по одному и тому же пути является
отдельной ошибкой (§ 6.3), а не ошибкой дублирующегося имени.

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

Многострочная строка открывается любым из двух value-start-токенов:

- `(` одиночно на своей строке (после trim) или после `:` в pair-строке —
  **stripped-форма**.
- `((` одиночно — **verbatim-форма**.

Блок закрывается строкой, обрезанное содержимое которой в точности:

- `)` для stripped-формы, либо
- `))` для verbatim-формы.

Пока многострочная строка открыта:

- Строки между открывающим и закрывающим токенами собираются как
  сырое содержимое (включая их пробелы, комментарии, скобки — ничто
  из этого не интерпретируется).
- Парсер MUST NOT применять иную классификацию к этим строкам.

При закрытии блока:

- **Verbatim-форма** (`((` ... `))`): результирующая String — это
  строки содержимого, соединённые одиночными символами `\n`, без
  каких-либо модификаций.
- **Stripped-форма** (`(` ... `)`): пусть *common* — это самый длинный
  пробельный префикс, являющийся префиксом каждой непустой строки
  содержимого. Результирующая String производится:

  1. Удалением *common* из начала каждой непустой строки.
  2. Заменой каждой пустой строки пустой строкой.
  3. Соединением полученной последовательности одиночными `\n`.

#### 5.6.1 Ограничение «Закрывающая скобка в содержимом»

Строка содержимого, обрезанная форма которой **в точности** равна
терминатору блока — `)` для stripped-блока, `))` для verbatim-блока —
ВСЕГДА закрывает блок. Это единственное правило, применяемое к
строкам внутри открытой многострочной строки; никакого escape-
механизма не существует.

Следствия и обходные пути:

- **Строка с trimmed-содержимым `)` — является содержимым** в
  verbatim-блоке (терминатор `))`, коллизии нет). Используйте
  verbatim, если в данных есть одиночные `)` на своей строке:

  ```text
  body: ((
  hello
  )
  world
  ))
  ```

  Результирующая String: `hello\n)\nworld`.

- **Строка с trimmed-содержимым `))` — является содержимым** в
  stripped-блоке (терминатор `)`). Используйте stripped, если в
  данных есть одиночные `))` на своей строке:

  ```text
  body: (
      hello
      ))
      world
  )
  ```

  Результирующая String (после dedent): `hello\n))\nworld`.

- **Оба** варианта `)` и `))` одновременно на собственных строках в
  одном содержимом — единственная комбинация, которую ни одна из форм
  не удержит. Документ с такими данными MUST либо разбить содержимое
  на несколько соседних многострочных значений, склеенных потребителем,
  либо загрузить значение из внешнего файла. Это намеренный компромисс
  версии 0.1.0 — формат не предоставляет escape-последовательностей
  внутри содержимого значения.

Дополнительные символы на строке предотвращают коллизию: строка с
trimmed-содержимым `) x`, `))suffix` или любой формой, не являющейся
**точным** терминатором, — это содержимое, а не закрытие.

### 5.7 Сокращения Пустых Составных Значений

Формы `{}` и `[]` в строке значения обозначают соответственно пустой
Object и пустой Array. Формы `()` и `(())` обозначают пустую String.
Пробелы между скобками допускаются только для `{}` и `[]`. Никакое иное inline-непустое
составное значение не валидно на версии 0.1.0.

## 6. Ошибки

Соответствующий парсер MUST производить ясно идентифицированную ошибку,
останавливаясь перед тем, как выдать некорректное Value, во всех
следующих случаях. Сообщение об ошибке SHOULD включать номер
нарушающей строки.

### 6.1 Несбалансированные или Несовпадающие Скобки (UnbalancedBracket, MismatchedBracket)

Парсер эмитирует эту ошибку под одним из двух связанных кодов,
выбираемых по ситуации:

- **UnbalancedBracket** — закрывающий токен без парного открывающего,
  либо EOF, достигнутый при ещё открытом Object, Array или
  многострочной String.
- **MismatchedBracket** — закрывающий токен, не совпадающий по виду
  с самым внутренним открытым: `}` на открытом `[`-Array или `]` на
  открытом `{`-Object.

Оба — подкатегории этого раздела; conformance-набор указывает в
`<name>.json`-оракуле тот код, который соответствует ситуации.

### 6.2 Дублирующееся Имя (DuplicateName)

Две pair-строки, присваивающие одно и то же имя внутри одного и того же
Object (§ 5.5).

### 6.3 Конфликт Пути (PathConflict)

Две pair-строки расходятся в том, каким должен быть общий путь: одна
считает его не-Object-значением (скаляром, Array), другая ожидает Object
(через разворачивание точечного ключа или вложенные скобки). Пример:

```text
a: 1
a.b: 2
```

Вторая строка пытается спуститься в `"a"`, но `"a"` уже является скаляром.
Обратное направление — также конфликт пути:

```text
a.b: 1
a: 2
```

Вторая строка пытается перезаписать Object `{b: 1}` скаляром.

### 6.4 Некорректный Ключ (InvalidKey)

Ключ, сегменты которого не проходят валидацию из § 5.3.1.

### 6.5 Пустой Ключ (EmptyKey)

Pair-строка, ключевая часть которой пуста (до первого `:`) после trim.

### 6.6 Висячая Строка (OrphanLine)

Непустая, не-комментарная строка внутри Object, не содержащая `:` и не
являющаяся одиночным закрывающим токеном.

### 6.7 Inline-Непустой Составной Элемент (InlineNonEmptyCompound)

Body значения, начинающийся с `{` или `[`, не являющийся точным
открывающим токеном (`{` / `[`) или пустой формой (`{}` / `[]`).

### 6.8 Ошибки Ввода-Вывода (IO Errors)

Сбои чтения лежащего в основе потока байтов сообщаются как ошибки
ввода-вывода и отличаются от ошибок выше.

### 6.9 Некорректный Типовой Скаляр (InvalidTypedScalar)

Типовой маркер (`:i` или `:f`) был использован с body, не
совпадающим с грамматикой, требуемой маркером (§ 5.2.1). Body не
был синтаксически валидным integer или float соответственно, был
пуст или пытался открыть составной элемент либо многострочную
строку. Примеры:

- `count:i abc` — body не числовой.
- `ratio:f 42` — body не имеет десятичной точки (integer бы
  разобрался, но маркер требует float).
- `value:i 1.5` — body имеет десятичную точку, но маркер — integer.
- `x:i ` — пустой body после маркера.
- `x:i {` — типовой маркер не может открыть составной элемент.
- `x:f (` — типовой маркер не может открыть многострочную строку.
- `x:i -` — только знак, без цифр.

Парсер MUST отвергать такие документы.

### 6.10 Отсутствующий Пробел После Разделителя (MissingSeparatorSpace)

Разделитель пары (`:`, `::`, `:i`, `:f`) или маркер элемента массива
(`::`, `:i`, `:f` в начале обрезанной строки) приклеен к body без
байта ASCII-пробела между ними, и строка не завершается сразу за
разделителем. Парсер MUST отвергать такие документы (§ 5.3 и § 5.4,
правило обязательного пробела).

**Примеры ошибок — парсер MUST отвергнуть каждую из этих записей:**

- `key:value` — за `:` идёт `v`, нет пробела, нет EOL.
- `pattern::[a-z]+` — `::` склеен с `[a-z]+`.
- `port:i42` — `:i` склеен с `42`.
- `ratio:f0.5` — `:f` склеен с `0.5`.
- внутри Array: `::value`, `:i42`, `:f0.5` как строки-элементы.

**Корректные** формы тех же намерений: `key: value`,
`pattern:: [a-z]+`, `port:i 42`, `ratio:f 0.5`, `:: value` (в массиве),
`:i 42` (в массиве), `:f 0.5` (в массиве).

Правило **не** запрещает пустой body: `key:`, `key::`, `key:i`,
`key:f` (строка завершается сразу после разделителя) — синтаксически
допустимы, см. § 5.3. Для `:i` / `:f` пустой body всё равно не
соответствует грамматике типового body и даёт **InvalidTypedScalar**
(§ 6.9), а не MissingSeparatorSpace.

## 7. Примеры

### 7.1 Минимальный

```text
port: 20082

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

Результирующее Value:

```
Object {
    "port":            String "20082",
    "banned_patterns": Array [
        String ".*\\.onion:\\d+"
    ]
}
```

### 7.2 Вложенные Объекты, Массивы, Ключевые Слова

```text
server.host: 127.0.0.1
server.port: 8080
app.debug:   true
app.label:   null

http.methods: [
    GET
    POST
    DELETE
]
```

### 7.3 Сырые Строки

```text
pattern::  [a-z]+
template:: {issue.id}.tpl
ipv6::     [::1]:8080

literals: [
    ok
    :: true
    :: null
    :: [::1]
]
```

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

```text
stripped: (
    {
      "qwe": 1
    }
)

verbatim: ((
  -----BEGIN-----
  CONTENT
  -----END-----
))
```

После разбора `stripped` — это трёхстрочная строка

```
{
  "qwe": 1
}
```

(четыре ведущих пробела, общие для каждой строки содержимого, удалены), а
`verbatim` — это трёхстрочная строка

```
  -----BEGIN-----
  CONTENT
  -----END-----
```

(сохранённая в точности как написана).

## 8. Соответствие

**Ktav 0.1.0 conformant parser** MUST:

- C-1. Принимать каждый синтаксически валидный документ, определённый в
  § 4, и производить Value, определённое в § 5.
- C-2. Отвергать каждый ввод, нарушающий правила §§ 4, 5 или 6,
  возвращая ошибку, идентифицирующую категорию (§ 6) и, где
  осмысленно, номер строки.
- C-3. Сопоставлять ключевые слова (`null`, `true`, `false`) с учётом
  регистра и только в точных позициях, определённых в § 5.2.
- C-4. Сохранять порядок вставки полей Object при операциях round-trip
  (парсинг, затем повторная эмиссия) внутри своей модели данных. (Сам
  текстовый вид явно не кодирует порядок — порядок берётся из
  последовательности pair-строк.)
- C-5. Производить идентичные Value для двух документов, отличающихся
  только ASCII-пробелами вне содержимого многострочных строк,
  комментариев и пустых строк.

**Ktav 0.1.0 conformant writer** (текстовый продюсер) MUST:

- W-1. Производить вывод, десериализуемый соответствующим парсером в
  Value, равное входному Value.
- W-2. Эмитировать маркер сырой строки `::` для любого значения String,
  body которого, будучи эмитированным в обычном виде, было бы
  классифицировано как не-String согласно § 5.2 — включая строки,
  равные `null`, `true`, `false`, `(`, `((`, `()`, `(())`, или
  начинающиеся с `{` или `[`.
- W-3. Эмитировать многострочные строки в форме, сохраняющей их
  содержимое байт в байт при последующем разборе (verbatim-форма либо
  stripped-форма, dedent которой воспроизводит оригинал).

## 9. Соображения Безопасности

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

- **Исчерпание ресурсов**: соответствующий парсер MAY налагать
  ограничения на глубину вложенных составных элементов, общий размер
  документа и максимальную длину одной строки, чтобы защищаться от
  атак типа «отказ в обслуживании». Эти ограничения, если они есть,
  MUST быть задокументированы реализацией.
- **Валидация UTF-8**: реализации MUST отвергать ввод, не являющийся
  UTF-8. Они MUST NOT молча подставлять символы-заменители.
- **Коллизии идентификаторов**: поскольку ключи Ktav не взяты в
  кавычки, специальные символы в ключах обнаруживаются § 5.3.1.
  Реализация MUST NOT ослаблять эти правила, позволяя, к примеру,
  ключи в кавычках с произвольным содержимым — такое послабление
  изменило бы семантику разделителя `:`.

Формат по замыслу не имеет механизмов include, import, reference или
макросов. Разбор одного документа не имеет побочных эффектов сверх
того, что парсер наблюдает в байтах самого документа.

## 10. Обоснование (Ненормативное)

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

**Почему без кавычек.** Кавычки — это визуальный шум, утраивающий
количество символов для простых строк. Большинство скалярных значений
(имена хостов, пути, числа, идентификаторы) не нуждаются в экранировании;
те, что нуждаются в пометке, обслуживаются сырым маркером `::`.

**Почему без запятых и отступов.** Запятые, как известно, легко
забыть. Значимые отступы, как известно, легко сосчитать неверно.
Переводы строк разделяют всё; цена «по одному полю на строку» —
перевод строки, который читатель всё равно собирался сделать.

**Почему только строчные ключевые слова.** Регистронезависимые булевы
значения YAML (`Yes`, `No`, `True`, `False` — печально известная «проблема
Норвегии») являются хорошо задокументированным источником ошибок.
Ограничение ключевых слов точным нижним регистром целиком устраняет
этот класс ошибок.

**Почему `::`.** Escape-символы внутри значений (`\n`, `\t` и т. д.)
вынуждают реализации выбирать и документировать полную escape-таблицу —
бессрочное обязательство. Размещение решения об экранировании в
разделителе (выбор между `:` и `::`) — это один бит информации, не
затрагивающий байты значения.

**Почему многострочные через `( ... )`.** Индикаторы блочных скаляров
YAML `|` / `>` имеют множество модификаторов (`|-`, `|+`, `>-`, `>+`,
явный индикатор отступа). Ktav ограничивает выбор двумя: «stripped
common indent» против «verbatim». Verbatim-форма гарантирует round-trip
без потерь; stripped-форма обслуживает частый случай вставки JSON-
или код-блока с отступом в конфигурацию.

**Почему точечные ключи.** Без них пользователи прибегают к отступам в
стиле YAML. Точечные ключи позволяют каждой строке локально заявлять
свой полный адрес — «что это за значение и куда оно относится?» —
без какого-либо контекстного учёта.

**Почему типовые маркеры и только два.** Строгий 0.1.0 оставляет все
скаляры строками на уровне Value, откладывая типизацию на
потребителя. Это хорошо работает для потребителей на типизированных
языках (Rust + serde, Go + marshal), которые могут преобразовать
`"8080"` в `u16` без содействия формата. В динамически типизированных
потребителях (JavaScript, PHP, Python) тот же механизм вынуждает
вручную кастовать каждое числовое поле — `Number(cfg.port)`,
`intval($cfg['port'])` — или приносить свой schema-слой. Типовые
маркеры `:i` (Integer) и `:f` (Float) закрывают этот разрыв.

Выбор ровно двух маркеров продуман:

- **Без битовой ширины** (`:i8`, `:u32`): PHP, Python и JavaScript
  имеют ровно по одному целочисленному типу (PHP `int` —
  платформенной ширины; Python `int` — произвольной точности;
  JavaScript `Number` покрывает и то и другое). Битовая ширина —
  забота потребителя на типизированном языке и уже решается там
  через serde / reflection.
- **Без знаковости** (`:si`, `:ui`): по той же причине. Ни в PHP,
  ни в Python, ни в JavaScript нет нативного беззнакового
  целочисленного типа.
- **Без автодетекта** (`port: 8080` молча становится Number): это
  путь JSON, и он молча портит `version: 1.2` (становится Number,
  теряется строковая интерпретация), `port: 0755` (ведущий ноль
  съедается) и `id: 99999999999999999` (потеря точности JavaScript
  Number). Явные маркеры — пишущиеся только там, где нужно число,
  — избегают всех трёх случаев.
- **Значения хранятся строками.** Value-типы Integer и Float
  хранят текстовую форму, а не машинное число. Это сохраняет
  произвольную точность (40-значное целое выдерживает round-trip)
  и сохраняет байт-в-байт round-trip-гарантию `((` verbatim и для
  типовых значений. Потребитель сужает на своей границе.

## 11. Литература

- RFC 2119, *Key words for use in RFCs to Indicate Requirement
  Levels*, S. Bradner, March 1997.
- [JSON5 specification](https://json5.org/). Ktav borrows its overall
  shape from JSON5 while dropping quotes and commas.
- [TOML](https://toml.io/) and
  [YAML](https://yaml.org/) — formats whose design choices informed
  Ktav by contrast.

## Приложение A. Изменения

Версия 0.1.0 является первоначальной спецификацией. Внутри 0.1.0
относительно предвыпускных черновиков применимы следующие изменения:

- **Добавлено**: типовые скалярные маркеры `:i` (Integer) и `:f`
  (Float) в позициях pair и array-item.
- **Добавлено**: Value-типы `Integer` и `Float` (§ 5).
- **Добавлено**: категория ошибки `InvalidTypedScalar` (§ 6.9).
- **Изменено**: § 4 Грамматика и § 5.2 Интерпретация скаляров — для
  новых маркеров; § 5.3 Pair-Строки и § 5.4 Array-Item Строки
  расширены типовыми формами.

Все последующие изменения — как между версиями, так и редакторские
правки внутри версии — фиксируются в
[`CHANGELOG.md`](../../CHANGELOG.md) на уровне репозитория.
