Вывод и форматирование
После того как данные объявлены в виде последовательностей,
блок вывода превращает их в текст — в любой форме, какая нужна: 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>
name=Пётр age=30 name=Анна age=28
Двухстрочный блок отрендерился дважды (count="2") — две карточки по две строки, у
каждой свои Name/Age.
<block> — макет карточки
<block> — единственный обязательный ребёнок <tdc>.
Атрибутов у него нет. Его единственная задача — хранить строки одной карточки, которые
рендерер повторяет count раз.
Единственные допустимые дети — <line>. Посторонний тег прямо внутри <block> — это
ошибка, а не молчаливый пропуск: так отлавливается неуместный
<gen> или тег с опечаткой, прежде чем он испортит вывод:
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>
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>
<tag> = "value", {key: 42}Про сырой текст важно знать две вещи:
- XML-сущности не раскрываются.
<остаётся буквальным<и не превращается в<. Пишите нужный символ напрямую. - В разметке
<data>стоит внутри<line>— строк блока или строк фикстуры — и больше нигде. Здесь у него два необязательных атрибута:if(подавить кусок по условию) иpair(для литерального</data>в тексте). У того же тега есть вторая, никак не связанная работа внутри<env>: как ребёнок<sequence>он даёт литеральный текст, вклеенный в составное значение, а с атрибутомname— постоянное поле. Разметка или значение — решает окружающий тег.
Пробелы — тоже текст, и отступы получаются даром
«Дословно» включает пробелы. Ведущие, хвостовые и любые пачки внутри доходят до вывода ровно так, как набраны:
<line><data> hello </data><data>|end</data></line>
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>
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>
Имя: Дмитрий, возраст: 72 Имя: Мария, возраст: 18 Имя: Андрей, возраст: 64 Имя: Анна, возраст: 26
Как разрешается имя
Для каждого ${{Name}}:
- Объявленная последовательность со значением на этой строке → это значение.
- Объявленная последовательность без значения на этой строке (отфильтрована
parentна этой итерации) → пустая строка. - Необъявленное имя (опечатка, забытое объявление) → ошибка
TDC193с подсказкой:
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>
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>
[
{"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>
[ 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>:
Имя: Дмитрий Имя: Мария Имя: Андрей Имя: Анна
Меняется только синтаксис подстановки, но не данные — значения те же, что и в форме
${{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 защищает теги, а не маркеры подстановки.)
Смотрите также
- Маски и регистр — полный набор фильтров и масок.
- Детерминизм и пропорции —
seed,countиpercent. - Иерархические зависимости — как
parentфильтрует последовательность, давая случай пустой строки выше. - Встроенные имена —
_count,_first,_last,_total. - Справочник тегов —
<block>,<line>,<data>и фикстуры вкратце.