# tab-plus Parser y generador para una variante segura del formato separado por pipe. [![npm-version](https://img.shields.io/npm/v/tab-plus.svg)](https://npmjs.org/package/tab-plus) [![downloads](https://img.shields.io/npm/dm/tab-plus.svg)](https://npmjs.org/package/tab-plus) [![build](https://github.com/ari-dc-uba-ar/tab-plus/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ari-dc-uba-ar/tab-plus/actions/workflows/build-and-test.yml) [![security](https://socket.dev/api/badge/npm/package/tab-plus)](https://socket.dev/npm/package/tab-plus) [![qa-control](https://github.com/ari-dc-uba-ar/tab-plus/actions/workflows/qa-control.yml/badge.svg)](https://github.com/ari-dc-uba-ar/tab-plus/actions/workflows/qa-control.yml) idioma: ![castellano](https://raw.githubusercontent.com/codenautas/multilang/master/img/lang-es.png) también disponible en: [![inglés](https://raw.githubusercontent.com/codenautas/multilang/master/img/lang-en.png)](README.md) ## Uso Supongamos que tenemos el siguiente archivo ```ts c2|c3|num|en_name|sp_name AF|AFG|004|Afghanistan|Afganistán AL|ALB|008|Albania|Albania DE|DEU|276|Germany|Alemania AD|AND|020|Andorra|Andorra AO|AGO|024|Angola|Angola AI|AIA|660|Anguila|Anguila AG|ATG|028|Antigua y Barbuda|Antigua and Barbuda SA|SAU|682|Saudi Arabia|Arabia Saudita DZ|DZA|012|Argelia|Algeria AR|ARG|032|Argentina|Argentina ``` Levantemos el archivo, hagamos algunos cambios y volvámoslo a guardar ```ts import { parseTab, generateTab } from "tab-plus"; import * as fs from "fs/promise"; var textContent = await fs.readFile('countries.tab', 'utf-8'); var info = parseTab(textContent) console.log(info); var orderedInfo = { fields: info.fields, rows: sortArrayOfArrays(info.rows) } var textToSave = generateTab(orderedInfo); await fs.writeFile(textToSave, 'utf-8'); ``` ## Por qué `.tab` `.tab` es el formato más seguro para intercambiar datos tabulares cuando no se sabe con certeza quién lo va a leer. CSV no tiene un estándar único, y sus variantes más comunes permiten, dentro de un campo entre comillas, saltos de línea literales. Eso significa que un parser de CSV no puede confiar en los saltos de línea físicos del archivo para saber dónde termina cada registro — tiene que llevar la cuenta de si está "dentro" o "fuera" de comillas, y hasta contar registros deja de ser trivial sin un parser completo. En `.tab`, en cambio, un salto de línea físico siempre separa registros y un `|` siempre separa campos —ninguno de los dos puede aparecer literal dentro de un valor, porque el generador siempre los reemplaza por un código (`\r\n`, `\n` o `\x7C` para el `|` que no es separador). No hace falta llevar ningún estado (como contar backslashes) para decidir si un `|` es separador: si aparece tal cual, lo es siempre. Esta garantía depende, claro, de que quien genera el archivo respete las reglas de escape —ningún formato es seguro si el productor no lo respeta. La ventaja de `.tab` está del lado de quien lo *lee* sin cuidado: el peor resultado posible de un parser ingenuo (que ni siquiera resuelva los escapes) es encontrar una secuencia como `\x7C` en el contenido de un campo en vez del carácter `|` —un defecto cosmético, local a ese campo. Nunca se cortan registros, ni se desalinean columnas, ni una fila se mezcla con la siguiente. Con CSV citado, en cambio, un parser descuidado ante una coma o un salto de línea dentro de comillas produce fallas mucho más graves y silenciosas: registros partidos, columnas corridas, conteos de filas incorrectos. Y precisamente porque CSV es un formato tan conocido, es común que se implemente "a mano" de forma ingenua, confiando en que alcanza con dividir por comas. ## El formato `tab-plus` * Los registros se separan por líneas (`\r\n` o `\n`). * Los campos dentro de un registro se separan con `|`. * El primer registro es el encabezado (nombres de campo); el resto son filas de datos. * `\` es el carácter de escape. Puede producir: * `\t`, `\r`, `\n`, `\s`, `\\` para tabulación, retorno de carro, salto de línea y espacio y la propia contrabarra. * `\xHH` para cualquier byte, dado como un código hexadecimal de 2 dígitos (el uso es obligatorio para el `|` al que le corresponde el código`\x7C` y recomendado para los caracteres especiales que no figuran en la lista de arriba, que no son utf8 estándar o no son visibles o imprimibles). * `\E` y `\N` solo pueden aparecer como valor completo de una columna y significan una cadena de longitud cero `''` o el valor `null` respectivamente. Dos separadores seguidos `||` producen los mismo que `\E` o `\N` según qué opción se use en `emptyField`. * `\` seguido de cualquier otro carácter tiene un comportamiento reservado que puede cambiar en futuras versiones (aunque el parser implementado respeta el caracter que sigue inlcuido el `|`). * Los espacios en blanco al final de un campo se recortan. Este recorte ocurre sobre el texto crudo (todavía escapado) antes de resolver las secuencias de escape, por lo que solo elimina espacios/tabs literales al final — un `\s` final (o cualquier otra secuencia de escape) no se ve afectado y se conserva en el valor resultante. * Se elimina un BOM UTF-8 inicial en el primer campo del encabezado. ### La opción `emptyField` `parseTab`, `generateTab`, `parseRow`, `generateRow`, `unescapeField` y `escapeField` aceptan todas un argumento `options` opcional con una propiedad `emptyField`: `'string'` (por defecto), `'null'`, o un `symbol`. * `'string'` (por defecto): un campo sin contenido alguno se parsea como `''`; un valor `null` se genera explícitamente como `\N` (ya que el vacío implícito ya significa `''`). * `'null'`: un campo sin contenido alguno se parsea como `null`; un valor de string vacío (`''`) se genera explícitamente como `\E` (ya que el vacío implícito ahora significa `null`). * un `symbol`: un campo sin contenido alguno se parsea como ese symbol exacto; tanto `''` como `null` se generan explícitamente (como `\E` y `\N`). Esto es útil cuando el significado de "acá no se escribió nada" necesita distinguirse de un `''` o `null` explícitos — por ejemplo, al cargar un archivo `.tab` en una tabla y querer que un campo dejado en blanco signifique "usar lo que diga la definición de la columna" (su valor por defecto del esquema, o dejarlo sin tocar), mientras que `\E` y `\N` siguen permitiendo forzar un string vacío o `NULL` sin importar esa definición. Generar un campo para cualquier otro symbol lanza una excepción. En todos los modos `\E` siempre se parsea como `''` y `\N` siempre se parsea como `null`, y `generateTab`/ `generateRow` nunca necesitan emitirlos para el valor "por defecto" del modo elegido — solo para los que no lo son — así que un round trip a través de `generateTab`/`parseTab` (o `generateRow`/`parseRow`) con las mismas opciones siempre preserva `''`, `null` y el symbol configurado como valores distintos. ```js tabPlus.parseRow('a||b', {emptyField: 'null'}); // => ['a', null, 'b'] tabPlus.generateRow(['a', null, 'b'], {emptyField: 'null'}); // => 'a||b' tabPlus.generateRow(['a', '', 'b'], {emptyField: 'null'}); // => 'a|\\E|b' var missing = Symbol('missing'); tabPlus.parseRow('a||b', {emptyField: missing}); // => ['a', missing, 'b'] tabPlus.generateRow(['a', null, ''], {emptyField: missing}); // => 'a|\\N|\\E' ``` ### La opción `objectRows` Por defecto `parseTab` devuelve `rows` como un array de arrays (un valor por columna, en el mismo orden que `fields`). Con `{objectRows: true}` devuelve en cambio un array de objetos, uno por fila, con los nombres de columna como atributos. `generateTab` acepta ambas formas indistintamente (sin necesidad de pasar la opción): detecta fila por fila si es un array o un objeto. ```js tabPlus.parseTab('a|b\r\n1|2\r\n', {objectRows: true}); // => {fields: ['a', 'b'], rows: [{a: '1', b: '2'}]} tabPlus.generateTab({fields: ['a', 'b'], rows: [{a: '1', b: '2'}]}); // => 'a|b\r\n1|2\r\n' ``` ## Instalación ``` npm install tab-plus ``` La librería está escrita en TypeScript e incluye sus propias declaraciones de tipos (`dist/tab-plus.d.ts`); no hace falta ningún paquete `@types`. ## API ```js var tabPlus = require('tab-plus'); ``` ```ts import * as tabPlus from 'tab-plus'; ``` ### `tabPlus.parseTab(text, options)` Parsea el contenido completo de un archivo `.tab`. Devuelve `{fields, rows}` donde `fields` es un array con los nombres de columna y `rows` es un array de arrays de valores de campo (strings, además de `null` o el symbol configurado donde corresponda — ver `emptyField` más arriba), o un array de objetos si se pasa `{objectRows: true}` (ver `objectRows` más arriba). `options` es opcional. ```js tabPlus.parseTab('a|b\r\n1|2\r\n'); // => {fields: ['a', 'b'], rows: [['1', '2']]} ``` ### `tabPlus.generateTab(tab, options)` Genera el contenido completo de un archivo `.tab` a partir de `{fields, rows}` (la inversa de `parseTab`), donde cada fila puede ser un array o un objeto (ver `objectRows` más arriba). `options` es opcional; ver la opción `emptyField` más arriba. ```js tabPlus.generateTab({fields: ['a', 'b'], rows: [['1', '2']]}); // => 'a|b\r\n1|2\r\n' ``` ### `tabPlus.parseRow(rawRow, options)` Parsea una única línea cruda (sin su salto de línea) a un array de valores de campo. ### `tabPlus.generateRow(row, options)` Genera una única línea cruda (sin salto de línea) a partir de un array de valores de campo. ### `tabPlus.escapeField(value, options)` / `tabPlus.unescapeField(rawValue, options)` Escapa/desescapa el valor de un único campo. `escapeField` trata `undefined` exactamente igual que `null` — no hay un comportamiento separado para una entrada faltante del array vs. un `null` explícito. ### Tipos El paquete exporta los tipos de TypeScript `FieldValue` (`string | null | symbol`), `Options` (`{emptyField?: 'string' | 'null' | symbol, objectRows?: boolean}`), `Tab` (`{fields: FieldValue[], rows: FieldValue[][]}`), `RowObject` (`{[field: string]: FieldValue}`) y `ObjectTab` (`{fields: FieldValue[], rows: RowObject[]}`). ## Licencia MIT