# `$.csv` — CSV/TSV и безопасный JSON

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

Заодно здесь живут JSON-помощники, которые **не бросают исключений**: битый
файл даёт запасное значение и понятную запись в журнал. Движок JSON умеет
(`JSON.parse`), но выразить «разобрать или вернуть запасное» из игры нечем.

```js
const text = $.fs.readText('data/weapons.csv');
const weapons = $.csv.parseTable(text);          // [{ name: 'меч', damage: '10' }, …]
$.csv.parse(text);                               // [[...], [...]] — как есть, с заголовком
$.csv.stringify(rows, { delimiter: '\t' });      // собрать TSV обратно

const save = $.csv.jsonParse($.fs.readText('save.json'), { level: 1, hp: 100 });
$.fs.write('save.json', $.csv.jsonStringify(save, true));
```

Проверка без движка:

```bash
build/_deps/quickjs-build/qjs tests/js/csv_test.mjs
```

---

## 1. Разбор

### `$.csv.parse(text, opts?) → string[][]`

Возвращает все строки, каждая строка — массив полей (всегда строки).

| Опция | По умолчанию | Смысл |
|---|---|---|
| `delimiter` | автоопределение | Один символ-разделитель: `,`, `;`, `\t`, `|` |
| `trim` | `false` | Обрезать пробелы у полей |
| `skipEmptyLines` | `false` | Выбросить полностью пустые строки |

Правила разбора:

* `"` в начале поля открывает кавычки; внутри кавычек разделители и переводы
  строк не действуют, а `""` превращается в одну кавычку;
* `\n`, `\r\n` и одиночный `\r` считаются одним переводом строки;
* последний перевод строки не создаёт лишнюю пустую запись, а настоящая
  пустая строка в середине (`a\n\nb`) — создаёт;
* пустой текст даёт `[]`;
* незакрытая кавычка не ошибка: поле берётся как есть — битый файл лучше
  показать целиком, чем потерять данные.

```js
$.csv.parse('a,"b,c"\n1,2');
// → [['a', 'b,c'], ['1', '2']]

$.csv.parse('первая,"строка\nвнутри"');
// → [['первая', 'строка\nвнутри']]      — одна запись, а не две
```

### `$.csv.detectDelimiter(text) → string`

Определяет разделитель по первой строке, **не считая вхождения внутри
кавычек**. Кандидаты: `,`, `;`, `\t`, `|` (при равенстве побеждает запятая).
Если разделителей нет — возвращает запятую.

```js
$.csv.detectDelimiter('a;b;c');       // ';'
$.csv.detectDelimiter('"a,b";c');     // ';' — запятая внутри кавычек не считается
```

### `$.csv.parseTable(text, opts?) → object[]`

Первая строка — имена полей, остальные — данные. Недостающие поля
добиваются пустой строкой, поэтому у всех объектов один набор ключей.
Опции те же, что у `parse`, плюс:

* `keys` — свой список имён: тогда первая строка тоже считается данными;
* пустой заголовок получает имя `col1`, `col2`…, повтор — суффикс `_2`.

```js
$.csv.parseTable('name,damage\nмеч,10\nщит,5');
// → [{ name: 'меч', damage: '10' }, { name: 'щит', damage: '5' }]

$.csv.parseTable('1,2\n3,4', { keys: ['x', 'y'] });
// → [{ x: '1', y: '2' }, { x: '3', y: '4' }]
```

## 2. Сборка

### `$.csv.stringify(rows, opts?) → string`

| Опция | По умолчанию | Смысл |
|---|---|---|
| `delimiter` | `','` | Разделитель (`'\t'` — для TSV) |
| `eol` | `'\n'` | Перевод строки (`'\r\n'` — для Excel) |
| `header` | — | Массив имён: печатается первой строкой |

Строки могут быть массивами или объектами. Если задан `header`, объекты
берут значения по этим именам (иначе — по своим ключам).

Кавычки ставятся только когда нужны: внутри разделитель, кавычка, перевод
строки или пробелы по краям. `null`/`undefined` → пустое поле, объект → JSON,
остальное → `String`. `parse(stringify(rows))` возвращает исходные строки.

```js
$.csv.stringify([['a,b', 'c"d']]);                     // '"a,b","c""d"'
$.csv.stringify([{ name: 'меч', damage: 10 }], { header: ['name', 'damage'] });
// 'name,damage\nмеч,10'
```

### `$.csv.quoteField(value, delimiter?) → string`

Экранирует одно поле — полезно, когда таблица собирается по частям вручную.

## 3. JSON без исключений

| Функция | Назначение |
|---|---|
| `$.csv.jsonParse(text, fallback?) → any` | Разобрать JSON; при ошибке вернуть `fallback` (по умолчанию `null`) |
| `$.csv.jsonStringify(value, pretty?) → string \| null` | Собрать JSON; при невозможности — `null` |

Оба пишут в журнал движка, что именно не так (с началом текста или причиной),
и никогда не роняют игру. `pretty` — `true` или число пробелов отступа.
`jsonStringify` возвращает `null`, если в данных ссылка на себя, `BigInt`,
функция или `undefined`.

```js
const cfg = $.csv.jsonParse($.fs.readText('config.json'), { volume: 1 });
$.fs.write('config.json', $.csv.jsonStringify(cfg, true));
```

## 4. Установка

```js
import { installCsv } from './csv.js';
installCsv($);       // $.csv = { parse, parseTable, stringify, … }
```

Чистые функции экспортируются наружу и проверяются qjs без движка: `parse`,
`parseTable`, `stringify`, `detectDelimiter`, `quoteField`, `jsonParse`,
`jsonStringify`.

## 5. Ограничения

| Чего нет | Почему / что делать |
|---|---|
| Чтения и записи файлов | Это `$.fs`; `$.csv` работает с текстом — так его можно проверить без диска |
| Комментариев (`#`) и произвольных кавычек (`'`) | RFC 4180 знает только `"`; комментарии отфильтруйте до разбора |
| Типизации значений | Все поля — строки: `'10'`, а не `10`. Преобразуйте сами (`Number(row.damage)`) или через `$.csv.jsonParse` |
| Вложенных структур в CSV | Для сложных данных берите JSON: `$.csv.jsonParse`/`jsonStringify` |
| Потокового разбора огромных файлов | Текст читается целиком; для мегабайтных таблиц лучше бинарный формат |
| Автоопределения кодировки | Только UTF-8, как везде в движке |
