Saltar al contenido principal

Salida y formato

Una vez que los datos están declarados como secuencias, el bloque de salida los convierte en texto, con la forma que usted quiera: CSV, JSON, SQL, una línea de log, un archivo de configuración. Son tres etiquetas anidadas —<block><line><data>— más la interpolación y algunas piezas opcionales (fixtures, filtros, condiciones).

nota

Las salidas de ejemplo de abajo son ilustrativas: los valores exactos pueden variar según la versión del núcleo y la semilla. Lo que importa es la forma de cada transformación, no los nombres o números concretos.

La salida tal como se imprime, de arriba abajo, para una ejecución de dos tarjetas.
  • Ase imprime una vez, al mero principio
  • Bantes de cada tarjeta
  • Cuna línea de la tarjeta
  • Dentre las líneas de una tarjeta — nunca después de la última línea
  • Edespués de cada tarjeta
  • Fentre tarjetas — nunca después de la última tarjeta
  • Gse imprime una vez, al mero final
  • Htodo lo que está dentro del corchete se repite una vez por tarjeta

El bloque de salida: <block><line><data>

  • <block> describe el diseño de un registro. El renderizador lo repite count veces —una por registro— sustituyendo en cada pasada valores frescos de las secuencias.
  • <line> es una línea de salida dentro de un registro. Después de cada <line> va un salto de línea.
  • <data> es un contenedor de texto crudo: todo lo que está entre <data> y </data> se imprime tal cual, salvo la interpolación ${{…}}, que se reemplaza por los valores de las secuencias.

Un registro puede ocupar varias líneas; usted las describe una vez y el bloque se repite:

<tdc>
<env count="2" seed="demo">
<sequence name="Person">
<gen name="Name" type="text" value="Ana,Carlos,Elena"/>
<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=Carlos
age=30
name=Ana
age=28

El bloque de dos líneas se renderizó dos veces (count="2"): dos registros de dos líneas cada uno, cada uno con su propio Name/Age.

<block> — el diseño de un registro

<block> es el único hijo obligatorio de <tdc>. No tiene atributos. Su única tarea es contener las líneas de un solo registro, que el renderizador repite count veces.

Sus únicos hijos permitidos son <line>. Una etiqueta ajena directamente dentro de <block> es un error, no un salto silencioso: así se atrapa un <gen> mal puesto o una etiqueta con errata antes de que corrompa la salida:

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

Si <before_block> / <after_block> / <delimiter_block> están declarados en <env>, envuelven cada registro renderizado — vea Fixtures más abajo.

<line> — una línea de un registro

<line> describe una sola línea dentro de un registro. Se emite una vez por pasada, con un salto de línea al final. Contiene cualquier cantidad de hijos <data>, que se imprimen uno tras otro de izquierda a derecha.

if — mostrar toda la línea solo bajo una condición

Problema. Quiere un separador --- entre registros, pero no colgando después del último. Proteja toda la línea con if: cuando la expresión es falsa, en esa pasada no se emite ni la línea ni su delimitador entre líneas.

<tdc>
<env count="3" seed="demo">
<sequence name="Name">
<gen type="text" value="Ana,Carlos,Elena" 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=Ana
---
name=Carlos
---
name=Elena

La segunda línea se imprime en cada registro salvo el último (_last es la bandera integrada que dice «este es el registro final»): exactamente el comportamiento «entre, pero no al final» que también da la fixture <delimiter_block>. La expresión de if es el mismo lenguajito que usa <data if>: comparaciones, !, &&, || y los nombres integrados.

comment — una nota que nunca se renderiza

<line comment="…"> lleva una nota libre para el autor de la configuración. Se ignora al renderizar y nunca aparece en la salida — es cómoda para anotar un diseño complejo.

Los generadores no viven en <line>

El bloque de salida es solo para formatear. No se puede poner un <gen> ni un <mix> directamente en un <line>: declare una <sequence> nombrada (o un <mix>) en <env> y refiéralo con ${{Name}}. Así se mantiene separado qué son los datos de cómo se acomodan.

Un límite relacionado: un generador advanced_regex usado desde la salida no puede emplear la forma de selección ponderada (?%{...}), porque los porcentajes exactos necesitan un contexto de secuencia con un count conocido. Declárelo como una secuencia nombrada en <env>.

<data> — texto crudo

<data> es un contenedor de texto crudo. Todo lo que está entre el <data> de apertura y su </data> se imprime literalmente, con una sola excepción: la interpolación ${{…}} se reemplaza por los valores de las secuencias.

<data> es el único lugar donde se puede escribir libremente <, >, {, }, comillas y comas — los caracteres que son especiales para XML. Eso es lo que permite armar JSON, CSV, SQL o cualquier formato construido con esos caracteres:

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

Dos cosas que hay que saber sobre el texto crudo:

  • Las entidades XML no se expanden. &lt; se queda como el literal &lt;, no se convierte en <. Escriba directamente el carácter que realmente quiere.
  • En el diseño, <data> vive dentro de un <line> —las líneas del bloque o las de una fixture— y en ningún otro sitio. Aquí lleva dos atributos opcionales: if (suprimir esta pieza según una condición) y pair (para un </data> literal en el texto). La misma etiqueta tiene un segundo trabajo, sin relación con el primero, dentro de <env>: como hijo de una <sequence> es texto literal pegado a un valor compuesto, y con un name es un campo constante. Diseño o valor — lo decide la etiqueta que la rodea.

Los espacios también son texto — la indentación sale gratis

«Literal» incluye los espacios. Los iniciales, los finales y cualquier racha interior llegan a la salida exactamente como se escribieron:

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

No se recorta ni se colapsa nada, así que la indentación de la salida es la indentación del texto dentro de <data>. En eso consiste toda la técnica para producir ficheros estructurados y legibles: un esquema SQL indentado, JSON anidado, YAML, un árbol.

La diferencia que conviene captar: la indentación de las etiquetas (<line> desplazado dos espacios) es la disposición de su propio config y no aparece en ninguna parte. Solo es texto lo que está entre <data> y </data>. En el esquema de abajo, los dos espacios delante de id están dentro de los datos y sobreviven; los cuatro delante de <line> están fuera y desaparecen:

<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');

Los espacios de más tras id y name hacen el trabajo de alineación: nadie los alinea por usted, los escribe usted. El mismo truco da JSON indentado: { en una línea, "id": ${{Id}}, en la siguiente, y el anidamiento son simplemente los espacios que escribió.

Con los atributos ocurre lo contrario

El cuerpo de <data> no se recorta nunca; una lista en un atributo . En value="a, b" el segundo elemento es b, no " b": el espacio junto a la coma es relleno del separador, no dato. Dentro de <data> los espacios son suyos; dentro de una lista value= se los limpian. La otra cara de «nunca se recorta»: un espacio final que no ve seguirá estando en el fichero.

Interpolación — ${{Name}}

Dentro de <data>, ${{Name}} se reemplaza por el valor de esa secuencia en la fila actual.

<block>
<line><data>Nombre: ${{Name}}, edad: ${{Age}}</data></line>
</block>
./run demo.tdc (Name = person name, Age = 18..80)
Nombre: Paulina, edad: 72
Nombre: Cruz, edad: 18
Nombre: Fabiola, edad: 64
Nombre: Santiago, edad: 26

Cómo se resuelve un nombre

Para cada ${{Name}}:

  1. Una secuencia declarada con valor en esta fila → ese valor.
  2. Una secuencia declarada sin valor en esta fila (filtrada por un parent en esta iteración) → la cadena vacía.
  3. Un nombre no declarado (una errata, una declaración olvidada) → error TDC193, con una sugerencia:
./run typo.tdc
error[TDC193]: "Nmae" is not a declared sequence — it would be printed literally
help: did you mean "Name"?

Esta verificación se gana su lugar en la salida tipada: en un CSV quizá alcance a detectar a simple vista un ${{Nmae}} perdido, pero en un archivo Parquet caería en una columna tipada de texto y parecería un valor real, imposible de notar. Si de veras quiere un ${{...}} literal en la salida (al generar una configuración, un archivo de GitHub Actions, una plantilla de Handlebars), cambie el marcador con inject y la verificación se apaga.

Nombres integrados

Siempre disponibles sin declarar nada: ${{_count}} (número de registro, empezando en 1), ${{_first}} / ${{_last}} ("true" / "false") y ${{_total}} (el total). Vea Nombres integrados.

Filtros

Un valor se puede transformar en el lugar con una «tubería» |: una máscara, un cambio de mayúsculas, un recorte, etcétera. Esto es formateo a la salida: el valor se genera como siempre y luego el filtro le cambia la forma. Aquí un SSN estadounidense de nueve dígitos (en crudo) recibe una máscara de presentación:

<data>${{Ssn}} -> ${{Ssn | mask:xxx-xx-xxxx}}</data>
./run demo.tdc
407963258  ->  407-96-3258
539027461  ->  539-02-7461

Los filtros se encadenan de izquierda a derecha (${{Name | mask:w:w | upper}} — primero la máscara, luego mayúsculas). El conjunto completo —mask, upper/lower/capitalize/title, slice, replace, trim, group, compact y los escapes csv/sql— está en Máscaras y mayúsculas.

Dónde no se ejecuta la interpolación

  • No en los atributos. La interpolación ocurre solo dentro del texto de <data>, nunca dentro del valor de un atributo.
  • Sin anidamiento. La interpolación es de una sola pasada. Si el valor de una secuencia contiene a su vez ${{Something}}, ese marcador interno no se vuelve a procesar: se inserta tal cual.

Texto condicional con if

<data if="…"> se imprime solo cuando la expresión es verdadera. Si la condición es falsa, ese <data> en particular se suprime para esa fila, y cualquier otro <data> de la misma <line> se renderiza con normalidad. El uso clásico es «JSON sin coma colgante»: un segundo <data if="!_last">, imprime una coma en cada registro menos el último.

<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}
]

Los dos <data> de una misma <line> se imprimen pegados: primero el objeto, luego la coma. En el último registro !_last es falso, la coma se salta y el JSON sigue siendo válido. El lenguaje de expresiones admite comparaciones (==, !=, <, >), los booleanos !/&&/|| y los nombres integrados — el mismo que usa <line if>.

Fixtures — texto alrededor de los registros

Las fixtures imprimen texto fijo en los bordes de la ejecución, para que el <block> por registro se mantenga limpio:

  • <before> / <after> se imprimen una vez: antes del primer registro y después del último. Usos típicos: el [ de apertura y el ] de cierre de un arreglo JSON, una fila de encabezado de CSV, un prefijo INSERT INTO … VALUES, un ; final.
  • <before_block> / <after_block> / <delimiter_block> envuelven cada registro; el último de ellos se imprime entre registros pero no después del final.

Todas las fixtures viven dentro de <env> y, como el bloque, contienen <line><data>. El [ y el ] del ejemplo JSON de arriba son <before> y <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
]

El [ se imprimió exactamente una vez arriba y el ] exactamente una vez abajo, sin importar si count es 3 o 3 000.

Dos advertencias, ambas obligadas:

  • La interpolación SÍ se ejecuta en las fixtures. Un ${{Name}} lee la fila junto a la que está la fixture: <before> ve la primera, <after> la última, y las de registro y de línea la fila que envuelven. Eso es lo que permite que un registro ponga sus propios campos alrededor de una lista anidada. (Es uno de los pocos comportamientos que hubo que corregir en la implementación de referencia: imprimía las llaves literalmente mientras los cuatro ports las expandían.)
  • Los generadores tampoco corren en las fixtures. Un <gen>, <mix> o <switch> dentro de una fixture es el error TDC131. Antes esto fallaba en silencio: un generador value="500..999" en una fixture siempre emitía 500 (la cota inferior), lo que parecía un valor real.

Un marcador de interpolación personalizado

inject en <env> cambia los marcadores de interpolación. El % es donde va el nombre, y necesita una parte de apertura y otra de cierre a su alrededor — TDC corta el patrón por el % más a la derecha que deja ambos lados no vacíos, que es lo que permite que el propio % aparezca en la envoltura:

<env inject="${{%}}"></env> <!-- el valor por omisión -->
<env inject="[%]"></env> <!-- entonces en <data> se escribe [Name] -->
<env inject="%{%}%"></env> <!-- corta por el % central: se escribe %{Name}% -->

Con inject="[%]" y <data>Nombre: [Name]</data>:

./run demo.tdc (inject=[%])
Nombre: Paulina
Nombre: Cruz
Nombre: Fabiola
Nombre: Santiago

Solo cambia la sintaxis de la sustitución, no los datos: los valores son idénticos a los de la forma ${{Name}} por omisión. El prefijo y el sufijo se escapan automáticamente, incluso cuando contienen metacaracteres de expresiones regulares. La razón principal para cambiar el marcador es emitir un ${{...}} literal en la salida (una configuración, una plantilla) sin disparar la verificación de nombres no declarados.

Texto crudo y un </data> literal

<data> es crudo, pero un <data> común se cierra en el primer </data> que encuentra. Para emitir un </data> literal en el texto —por ejemplo, un fragmento de sintaxis TDC en documentación generada— dele a la etiqueta un marcador pair; tanto la etiqueta de apertura como la de cierre deben llevar el mismo marcador. Aquí el ${{...}} también debe aparecer literal, así que el ejemplo además aparta el marcador de interpolación con inject:

<env count="1" seed="demo" inject="[[%]]">
<sequence name="Name"><gen type="text" value="a,b"/></sequence>
</env>
<block>
<line><data pair="doc-example">Para imprimir un nombre, escriba: <data>${{Name}}</data></data pair="doc-example"></line>
</block>
Para imprimir un nombre, escriba: <data>${{Name}}</data>

El valor de pair debe ser único dentro del archivo. Ahora el parser sabe que solo </data pair="doc-example"> cierra el bloque, así que el </data> interno se trata como texto común. (Sin el inject="[[%]]", ${{Name}} se interpolaría igualmente a un valor — pair protege las etiquetas, no los marcadores de sustitución.)

Véase también