# Парсер протокола Lino для JavaScript

Реализация парсера протокола Lino для JavaScript с использованием Bun и
генератора парсеров Peggy.js.

## Документы с отступами

`parseIndentedDocument()` и `formatIndentedDocument()` преобразуют текстовые
родительские строки и их дочерние строки в `Map<string, string | string[]>`:

```javascript
import { parseIndentedDocument, formatIndentedDocument } from 'links-notation';

const answers = parseIndentedDocument(`Ваш опыт?
  JavaScript
Навыки
  JavaScript
  Python
`);
console.log(answers.get('Ваш опыт?')); // 'JavaScript'
console.log(answers.get('Навыки')); // ['JavaScript', 'Python']
console.log(formatIndentedDocument(answers));
```

Без дочерних строк значение равно `[]`, с одной строкой — строке, с несколькими
— массиву. Повторяющиеся родительские строки объединяют дочерние значения в
порядке документа. Опция `{ multipleValues: 'join' }` объединяет несколько
значений через перевод строки. Поддерживаются опции `Parser`: `comments`,
`maxInputSize` и `maxDepth`. Вложенность глубже одного дочернего уровня и
именованные ссылки со значениями отклоняются.

Форматирование использует два пробела для отступа и завершающий перевод строки.
Обычный текст с одиночными пробелами остаётся без кавычек; специальные символы,
значимые пробелы и зарезервированные префиксы литералов заключаются в кавычки.
Кавычки экранируются удвоением. Пустые строки и управляющие символы используют
версионированные литералы, введённые в 0.24.0. Массив из одного элемента при
чтении становится строкой. Многострочная строка сохраняется одной ссылкой с
точными переводами строк, а элементы массива записываются отдельными дочерними
строками.

`Parser.parseGroups()` возвращает дерево существующих объектов `LinksGroup`:
`element` содержит ссылку текущей строки, а `children` — группы непосредственно
вложенных строк. Поведение `Parser.parse()` сохраняется.

Функции `escapeReference()` и `unescapeReference()` экспортируются из пакета и
доступны как статические методы `Link`. Опция `{ minimal: true }` разрешает
обычные пробелы при экранировании целой текстовой строки. По умолчанию пробелы
по-прежнему заключаются в кавычки. `unescapeReference()` принимает целую
сериализованную ссылку, включая версионированные литералы; значения `Link.id`
уже декодированы.

См. [пример базы вопросов и ответов](../examples/js_indented_document.js).

## Установка

### Использование Bun (рекомендуется)

```bash
cd js
bun install
```

### Использование npm

```bash
cd js
npm install
```

## Сборка

Компиляция грамматики Peggy.js:

```bash
bun run build:grammar
```

Сборка проекта:

```bash
bun run build
```

## Тестирование

Запуск тестов:

```bash
bun test
```

Режим наблюдения:

```bash
bun test --watch
```

## Использование

### Базовый парсинг

```javascript
import { Parser, Link } from 'links-notation';

// Создание парсера
const parser = new Parser();

// Парсинг строки в формате Lino
const input = `папа (любитМаму: любит маму)
сын любитМаму
дочь любитМаму
все (любят маму)`;

const result = parser.parse(input);
console.log(result);

// Обращение к распарсенной структуре
result.forEach((link) => {
  console.log(link.toString());
});
```

### Работа со связями

```javascript
import { Link } from 'links-notation';

// Создание связей программно
const link = new Link('родитель', [new Link('ребенок1'), new Link('ребенок2')]);

console.log(link.toString()); // (родитель: ребенок1 ребенок2)

// Доступ к свойствам связи
console.log('ID:', link.id);
console.log('Значения:', link.values);
```

### Расширенное использование

```javascript
// Обработка вложенных структур
const input = `родитель
  ребенок1
  ребенок2
    внук1
    внук2`;

const parsed = await parser.parse(input);

// Работа с группами
import { LinksGroup } from 'links-notation';
const group = new LinksGroup(parsed);
console.log(group.format());
```

### Потоковый парсер

`StreamParser` принимает произвольные порции данных и отправляет события только
для завершённых связей верхнего уровня. Отключите накопление для обработки с
ограниченной памятью.

```javascript
import { StreamParser } from 'links-notation';

const stream = new StreamParser({ collect: false, maxBufferSize: 1024 * 1024 });
stream.on('link', (link) => console.log(link.toString()));
stream.on('error', (error) => console.error(error));
stream.write('профиль:\n  имя Ада\n');
stream.end('следующая связь');
```

`StreamParser.parse(chunks)` и `StreamParser.parseAsync(chunks)` предоставляют
нативные синхронные и асинхронные итераторы. Также доступны `position()`,
`drain()`, `reset()` и ограничение размера незавершённой записи. См.
[исполняемый пример](../examples/js_streaming_parser.js).

Пакет не импортирует встроенные модули Node.js, поэтому сборщикам для браузера
не нужны полифилы: например, `esbuild entry.js --bundle --platform=browser`.
Встроенный в `StreamParser` эмиттер поддерживает методы подписки
`EventEmitter` из Node.js (`on`, `once`, `off`, `removeListener`, `emit`,
`listenerCount` и другие). До версии 0.27.1 `StreamParser` импортировал модуль
`events`, и для браузерной сборки требовался пакет-полифил `events`.

## Примеры синтаксиса

### Дуплеты (2-кортежи)

```lino
папа (любитМаму: любит маму)
сын любитМаму
дочь любитМаму
все (любят маму)
```

### Триплеты (3-кортежи)

```lino
папа имеет машину
мама имеет дом
(папа и мама) счастливы
```

### N-кортежи со ссылками

```lino
(нотацияСвязей: нотация связей)
(Это тоже нотацияСвязей)
(нотацияСвязей поддерживает (неограниченное количество (ссылок) в каждой связи))
```

### Структура с отступами

```lino
родитель
  ребенок1
  ребенок2
    внук1
    внук2
```

### Многострочные группы

Скобочная группа открывает _вложенный контекст_: её тело начинается заново с
нулевого уровня отступа и подчиняется тем же правилам, что и корень документа,
поэтому перенос строки внутри скобок — это структура, а не оформление.

```lino
value (
  id "1"
  label "one"
)
```

Документ выше разбирается в `(value ((id 1) (label one)))` — два потомка,
каждый из которых сам является связью, — а не в один плоский список, в котором
граница между `id` и `label` была бы потеряна. Тело, умещающееся в одну строку,
по-прежнему сворачивается в одну связь, так что `(a b c)` не меняется.

```javascript
import { Parser, formatLinks } from 'links-notation';

const links = new Parser().parse(`value (
  id "1"
  label "one"
)`);

console.log(formatLinks(links)); // (value ((id 1) (label one)))
```

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

`#` скрывает остаток строки, на которой стоит, поэтому документ может нести
пояснения о самом себе:

```lino
# машины, на которые идёт выкладка
deploy: staging # пока только staging
```

К моменту чтения документа обоих комментариев уже нет, остаётся одна связь
`(deploy: staging)`. `#` открывает комментарий только там, где могла бы
начаться ссылка, поэтому `#` внутри токена (`issue#1047`) и `#` внутри ссылки
в кавычках (`"#"`) остаются обычными символами.

Форматтер соблюдает то же правило с другой стороны: ссылка, начинающаяся с `#`,
записывается в кавычках (`'#tag'`), поэтому написанный им документ читается
обратно как он сам.

Комментарии включены по умолчанию, а парсеру можно велеть снова читать `#` как
обычный символ - для документов, написанных до появления комментариев:

```javascript
import { Parser, formatLinks } from 'links-notation';

const document =
  '# машины, на которые идёт выкладка\ndeploy: staging # пока только staging\n';
console.log(formatLinks(new Parser().parse(document))); // (deploy: staging)

const plain = new Parser({ comments: false });
console.log(formatLinks(plain.parse('# a b\n'))); // (# a b)
```

## Справочник API

### Классы

#### `Parser`

Основной класс парсера для преобразования строк в связи.

- `constructor(options)` - Создать парсер; `options.comments` со значением
  `false` читает `#` как обычный символ, а не как начало комментария
  - `options.maxInputSize` - Максимальный размер входных данных в байтах
    (по умолчанию: 10MB)
  - `options.maxDepth` - Насколько глубоко могут быть вложены связи
    (по умолчанию: `DEFAULT_MAX_DEPTH`, 64). Каждая группа в скобках и каждый
    уровень отступа - это один уровень; документ с более глубокой вложенностью
    отклоняется с `ParseError`, а не разбирается рекурсивно, пока не кончится стек
- `initialize()` - Инициализация парсера (асинхронно)
- `parse(input)` - Парсинг строки Lino и возвращение связей

#### `Link`

Представляет одну связь с ID и значениями.

- `constructor(id, values = [])` - Создание новой связи
- `toString()` - Преобразование связи в строковый формат
- `id` - Идентификатор связи
- `values` - Массив дочерних значений/связей

#### `LinksGroup`

Контейнер для группировки связанных связей.

- `constructor(links)` - Создание новой группы
- `format()` - Форматирование группы в строку

#### `ParseError`

Выбрасывается методом `parse(input)`, когда документ не разбирается. Сообщение
говорит, где документ перестал быть понятным, и цитирует сломанную строку:

```js
import { Parser, ParseError } from 'links-notation';

try {
  new Parser().parse('ci_gate x\nstage: rust: nextest\n');
} catch (error) {
  console.error(error.message);
  if (error instanceof ParseError) {
    console.error(`${error.line}:${error.column} (смещение ${error.offset})`);
  }
}
```

```text
Syntax error at line 2, column 12: Expected "(", [ \t], [\r\n], or [^ \t\n\r(:)] but ":" found.
2 | stage: rust: nextest
  |            ^
```

- `offset` - Смещение сломанной позиции от начала документа
- `line`, `column` - Где документ перестал разбираться, счет с 1
- `found` - Найденный символ или `null` в конце документа
- `lineText` - Сломанная строка, как она написана
- `snippet` - Сломанная строка с указателем под нужным столбцом
- `location` - Позиция в том виде, в каком ее сообщает сгенерированный парсер
- `cause` - Ошибка, которую выбросил сгенерированный парсер
- `maxDepth` - Наибольшая допустимая вложенность, если документ вложен глубже;
  `null` для любой другой ошибки

Документ, вложенный глубже `maxDepth`, отклоняется на той группе или строке,
которая на один уровень глубже допустимого:

```text
Nesting too deep at line 1, column 4: nesting depth exceeds the maximum of 3
1 | ((((a))))
  |    ^
```

## Структура проекта

- `src/grammar.pegjs` - Определение грамматики Peggy.js
- `src/Link.js` - Структура данных связи
- `src/LinksGroup.js` - Контейнер групп связей
- `src/ParseError.js` - Ошибка разбора с позицией дефекта
- `src/Parser.js` - Обертка парсера
- `src/index.js` - Главная точка входа
- `tests/` - Файлы тестов

## Зависимости

- Peggy.js (5.0.6) - Генератор парсеров
- Среда выполнения Bun (разработка)

## Информация о пакете

- Пакет: `links-notation`
- Лицензия: Unlicense (см. [LICENSE](../LICENSE))
