Перейти к основному содержимому

Вывод и форматирование

После того как данные объявлены в виде последовательностей, блок вывода превращает их в текст — в любой форме, какая нужна: CSV, JSON, SQL, строка лога, конфиг. Это три вложенных тега — <block><line><data> — плюс интерполяция и несколько необязательных частей (фикстуры, фильтры, условия).

примечание

Примеры вывода ниже — иллюстративные: точные значения зависят от версии ядра и сида. Важна форма каждого преобразования, а не конкретные имена или числа.

Вывод в том порядке, в каком он печатается, сверху вниз, для прогона из двух карточек.
  • Aпечатается один раз, в самом начале
  • Bперед каждой карточкой
  • Cстрока карточки
  • Dмежду строками одной карточки — и никогда после последней
  • Eпосле каждой карточки
  • Fмежду карточками — и никогда после последней
  • Gпечатается один раз, в самом конце
  • Hвсё, что внутри скобки, повторяется на каждой карточке

Блок вывода: <block><line><data>

  • <block> описывает макет одной карточки. Рендерер повторяет его count раз — по одному разу на карточку, — подставляя на каждом проходе свежие значения последовательностей.
  • <line> — одна строка вывода внутри карточки. После каждого <line> идёт перевод строки.
  • <data> — контейнер сырого текста: всё между <data> и </data> выводится как есть, кроме интерполяции ${{…}}, которая заменяется значениями последовательностей.

Карточка может занимать несколько строк; вы описываете их один раз, а блок повторяется:

<tdc>
<env count="2" seed="demo">
<sequence name="Person">
<gen name="Name" type="text" value="Анна,Пётр,Ольга"/>
<gen name="Age" type="number" value="20..40"/>
</sequence>
</env>
<block>
<line><data>name=${{Person.Name}}</data></line>
<line><data>age=${{Person.Age}}</data></line>
</block>
</tdc>
./run demo.tdc (count=2)
name=Пётр
age=30
name=Анна
age=28

Двухстрочный блок отрендерился дважды (count="2") — две карточки по две строки, у каждой свои Name/Age.

<block> — макет карточки

<block> — единственный обязательный ребёнок <tdc>. Атрибутов у него нет. Его единственная задача — хранить строки одной карточки, которые рендерер повторяет count раз.

Единственные допустимые дети — <line>. Посторонний тег прямо внутри <block> — это ошибка, а не молчаливый пропуск: так отлавливается неуместный <gen> или тег с опечаткой, прежде чем он испортит вывод:

./run demo.tdc
error[TDC013]: <foo> is not allowed directly inside <block>

Если в <env> объявлены <before_block> / <after_block> / <delimiter_block>, они обрамляют каждую отрендеренную карточку — см. Фикстуры ниже.

<line> — одна строка карточки

<line> описывает одну строку внутри карточки. Она выводится один раз за проход, с завершающим переводом строки. Внутри — любое количество <data>, которые печатаются подряд слева направо.

if — показать строку целиком по условию

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

<tdc>
<env count="3" seed="demo">
<sequence name="Name">
<gen type="text" value="Анна,Пётр,Ольга" order="sequential"/>
</sequence>
</env>
<block>
<line><data>name=${{Name}}</data></line>
<line if="!_last"><data>---</data></line>
</block>
</tdc>
./run demo.tdc (count=3)
name=Анна
---
name=Пётр
---
name=Ольга

Вторая строка печатается на каждой карточке, кроме последней (_last — встроенный флаг «это последняя карточка»), — ровно то поведение «между, но не в конце», которое даёт и фикстура <delimiter_block>. Выражение в if — тот же маленький язык, что и в <data if>: сравнения, !, &&, || и встроенные имена.

comment — заметка, которая не выводится

<line comment="…"> несёт произвольную заметку для автора конфига. При рендере она игнорируется и никогда не попадает в вывод — удобно комментировать сложный макет.

Генераторы не живут в <line>

Блок вывода — только для форматирования. Нельзя поставить <gen> или <mix> прямо в <line> — объявите именованную <sequence> (или <mix>) в <env> и сошлитесь на неё через ${{Name}}. Так что за данные отделяется от как они разложены.

Связанное ограничение: генератор advanced_regex, вызванный из блока вывода, не может использовать форму взвешенного выбора (?%{...}), потому что точные проценты требуют контекста последовательности с известным count. Объявите его именованной последовательностью в <env>.

<data> — сырой текст

<data> — контейнер сырого текста. Всё между открывающим <data> и его </data> выводится дословно, с единственным исключением: интерполяция ${{…}} заменяется значениями последовательностей.

<data>единственное место, где можно свободно писать <, >, {, }, кавычки и запятые — символы, особые для XML. Именно это позволяет собирать JSON, CSV, SQL или любой формат из этих символов:

<line><data><tag> = "value", {key: 42}</data></line>
./run demo.tdc
<tag> = "value", {key: 42}

Про сырой текст важно знать две вещи:

  • XML-сущности не раскрываются. &lt; остаётся буквальным &lt; и не превращается в <. Пишите нужный символ напрямую.
  • В разметке <data> стоит внутри <line> — строк блока или строк фикстуры — и больше нигде. Здесь у него два необязательных атрибута: if (подавить кусок по условию) и pair (для литерального </data> в тексте). У того же тега есть вторая, никак не связанная работа внутри <env>: как ребёнок <sequence> он даёт литеральный текст, вклеенный в составное значение, а с атрибутом name — постоянное поле. Разметка или значение — решает окружающий тег.

Пробелы — тоже текст, и отступы получаются даром

«Дословно» включает пробелы. Ведущие, хвостовые и любые пачки внутри доходят до вывода ровно так, как набраны:

<line><data> hello </data><data>|end</data></line>
./run demo.tdc
      hello   |end

Ничего не срезается и не схлопывается, поэтому отступ вывода — это отступ текста внутри <data>. В этом весь приём получения структурных, читаемых файлов: схема SQL с отступами, JSON с вложенностью, YAML, дерево.

Разница, которую важно уловить: отступ самих тегов (<line>, сдвинутый на два пробела) — это оформление вашего конфига, оно не попадает никуда. Текстом является только то, что стоит между <data> и </data>. В схеме ниже два пробела перед id находятся внутри данных и выживают, а четыре перед <line> — снаружи и исчезают:

<env count="2" seed="indent">
<before>
<line><data>CREATE TABLE customers (</data></line>
<line><data> id INTEGER PRIMARY KEY,</data></line>
<line><data> name TEXT NOT NULL,</data></line>
<line><data> city TEXT NOT NULL</data></line>
<line><data>);</data></line>
</before>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Name"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="City"><gen type="text" value="Paris,Berlin"/></sequence>
</env>
<block>
<line><data>INSERT INTO customers VALUES (${{Id}}, '${{Name}}', '${{City}}');</data></line>
</block>
./run demo.tdc
CREATE TABLE customers (
id    INTEGER PRIMARY KEY,
name  TEXT NOT NULL,
city  TEXT NOT NULL
);
INSERT INTO customers VALUES (1, 'James', 'Berlin');
INSERT INTO customers VALUES (2, 'John', 'Paris');

Лишние пробелы после id и name работают на выравнивание — никто не выровняет за вас, вы их набираете. Тот же приём даёт JSON с отступами: { на одной строке, "id": ${{Id}}, на следующей — и вложенность есть, это просто написанные пробелы.

С атрибутами всё наоборот

Тело <data> не подрезается никогда, а список в атрибуте — подрезается. В value="a, b" второй элемент это b, а не " b": пробел вокруг запятой — набивка разделителя, а не данные. То есть внутри <data> пробелы ваши, а внутри списка value= их уберут за вас. Обратная сторона «никогда не подрезается»: хвостовой пробел, которого вы не видите, всё равно попадёт в файл.

Интерполяция — ${{Name}}

Внутри <data> ${{Name}} заменяется значением этой последовательности на текущей строке.

<block>
<line><data>Имя: ${{Name}}, возраст: ${{Age}}</data></line>
</block>
./run demo.tdc (Name — имя, Age — 18..80)
Имя: Дмитрий, возраст: 72
Имя: Мария, возраст: 18
Имя: Андрей, возраст: 64
Имя: Анна, возраст: 26

Как разрешается имя

Для каждого ${{Name}}:

  1. Объявленная последовательность со значением на этой строке → это значение.
  2. Объявленная последовательность без значения на этой строке (отфильтрована parent на этой итерации) → пустая строка.
  3. Необъявленное имя (опечатка, забытое объявление) → ошибка TDC193 с подсказкой:
./run typo.tdc
error[TDC193]: "Nmae" is not a declared sequence — it would be
printed literally
suggestion: did you mean "Name"?

Эта проверка окупается в типизированном выводе: в CSV случайный ${{Nmae}} ещё можно заметить глазами, а в Parquet он ляжет в типизированную строковую колонку и будет выглядеть настоящим значением — заметить невозможно. Если литеральный ${{...}} в выводе нужен намеренно (генерируете конфиг, файл GitHub Actions, шаблон Handlebars), смените маркер через inject — и проверка отключится.

Встроенные имена

Всегда доступны без объявления: ${{_count}} (номер карточки, начиная с 1), ${{_first}} / ${{_last}} ("true" / "false") и ${{_total}} (всего карточек). См. Встроенные имена.

Фильтры

Значение можно преобразовать на месте «трубой» | — маска, смена регистра, срез и так далее. Это форматирование на выходе: значение генерируется как обычно, а фильтр меняет его вид. Здесь одиннадцатизначный СНИЛС (сырьё) получает отображающую маску:

<data>${{Snils}} -> ${{Snils | mask:xxx-xxx-xxx xx}}</data>
./run demo.tdc
37898432363  ->  378-984-323 63
88973572402  ->  889-735-724 02

Фильтры сцепляются слева направо (${{Name | mask:w:w | upper}} — сначала маска, потом верхний регистр). Полный набор — mask, upper/lower/capitalize/title, slice, replace, trim, group, compact и экранирования csv/sql — в Маски и регистр.

Где интерполяция не выполняется

  • Не в фикстурах. ${{…}} остаётся нетронутым внутри <before> / <after> / <before_block> и прочих фикстур — они логически вне поитерационного прохода.
  • Не в атрибутах. Интерполяция работает только в тексте <data>, но никогда — в значении атрибута тега.
  • Без вложенности. Интерполяция одноуровневая. Если значение последовательности само содержит ${{Something}}, этот внутренний маркер повторно не обрабатывается — вставляется как есть.

Условный текст через if

<data if="…"> печатается, только когда выражение истинно. Если условие ложно, этот один <data> подавляется для строки — любой другой <data> в той же <line> выводится нормально. Классика — «JSON без висящей запятой»: второй <data if="!_last">, печатает запятую на каждой карточке, кроме последней.

<tdc>
<env count="3" seed="demo">
<before><line><data>[</data></line></before>
<after><line><data>]</data></line></after>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
</env>
<block>
<line><data> {"id": ${{Id}}}</data><data if="!_last">,</data></line>
</block>
</tdc>
./run demo.tdc (count=3)
[
{"id": 1},
{"id": 2},
{"id": 3}
]

Два <data> в одной <line> печатаются встык: сначала объект, потом запятая. На последней карточке !_last ложно, запятая пропускается, и JSON остаётся валидным. Язык выражений поддерживает сравнения (==, !=, <, >), булевы !/&&/|| и встроенные имена — тот же, что использует <line if>.

Фикстуры — текст вокруг карточек

Фикстуры печатают фиксированный текст по краям прогона, чтобы поитерационный <block> оставался чистым:

  • <before> / <after> печатаются один раз — перед первой карточкой и после последней. Типично: открывающая [ и закрывающая ] JSON-массива, строка-заголовок CSV, префикс INSERT INTO … VALUES, завершающий ;.
  • <before_block> / <after_block> / <delimiter_block> обрамляют каждую карточку — последний из них печатается между карточками, но не после финальной.

Все фикстуры живут внутри <env> и, как и блок, содержат <line><data>. [ и ] из примера JSON выше — это <before> и <after>:

<tdc>
<env count="3" seed="demo">
<before><line><data>[</data></line></before>
<after><line><data>]</data></line></after>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
</env>
<block>
<line><data> ${{Id}}</data></line>
</block>
</tdc>
./run demo.tdc (count=3)
[
1
2
3
]

[ напечаталась ровно один раз сверху, а ] — ровно один раз снизу, сколько бы ни было карточек: 3 или 3000.

Две оговорки, обе принудительные:

  • Интерполяция в фикстурах не работает (они вне итерации) — ${{…}} там остаётся литеральным текстом.
  • Генераторы в фикстурах тоже не работают. <gen>, <mix> или <switch> внутри фикстуры — ошибка TDC131. Раньше это молча падало: генератор value="500..999" в фикстуре всегда выдавал 500 (нижнюю границу), что выглядело настоящим значением.

Свой маркер интерполяции

inject на <env> меняет маркеры интерполяции. В строке должен быть ровно один % — на его место встаёт имя:

<env inject="${{%}}"></env> <!-- по умолчанию -->
<env inject="[%]"></env> <!-- тогда в <data> пишем [Name] -->
<env inject="%{%}%"></env> <!-- тогда в <data> пишем %{Name}% -->

С inject="[%]" и <data>Имя: [Name]</data>:

./run demo.tdc (inject=[%])
Имя: Дмитрий
Имя: Мария
Имя: Андрей
Имя: Анна

Меняется только синтаксис подстановки, но не данные — значения те же, что и в форме ${{Name}} по умолчанию. Префикс и суффикс экранируются автоматически, даже если содержат спецсимволы регулярных выражений. Главная причина сменить маркер — вывести литеральный ${{...}} (в конфиге, шаблоне), не задев проверку необъявленных имён.

Сырой текст и литеральный </data>

<data> сырой, но обычный <data> закрывается на первом встреченном </data>. Чтобы вывести литеральный </data> в тексте — например, фрагмент TDC-синтаксиса в генерируемой документации — дайте тегу маркер pair; один и тот же маркер должен стоять и на открывающем, и на закрывающем теге. Здесь и ${{...}} должен попасть в вывод буквально, поэтому пример заодно уводит маркер подстановки в сторону через inject:

<env count="1" seed="demo" inject="[[%]]">
<sequence name="Name"><gen type="text" value="a,b"/></sequence>
</env>
<block>
<line><data pair="doc-example">Чтобы вывести имя, напишите: <data>${{Name}}</data></data pair="doc-example"></line>
</block>
Чтобы вывести имя, напишите: <data>${{Name}}</data>

Значение pair должно быть уникальным в пределах файла. Теперь парсер знает, что блок закрывает только </data pair="doc-example">, поэтому внутренний </data> считается обычным текстом. (Без inject="[[%]]" ${{Name}} всё равно подставился бы значением — pair защищает теги, а не маркеры подстановки.)

Смотрите также