La biblioteca TypeScript

@stxt-lang/core es el parser de STXT para TypeScript y JavaScript. Implementa las cinco especificaciones y devuelve los mismos códigos de error que las bibliotecas Java y Python.

Es la biblioteca que usan la línea de comandos, la extensión de VS Code y el playground.

Instalación

npm install @stxt-lang/core

Se distribuye como CommonJS con declaraciones de tipos, y vale desde TypeScript, desde require y desde módulos ES. Todo se importa del paquete raíz. No tiene dependencias en tiempo de ejecución ni accede al sistema de ficheros o al entorno (la resolución los recibe inyectados), por lo que funciona igual en Node y en el navegador.

Parsear

Entrada Comportamiento
parseResult(text) Acumula todos los errores y devuelve también los nodos que ha podido construir
parse(text) Lanza una ParseException en el primer error; sin errores, devuelve Node[]
parseStream(lines) No retiene nodos ni errores; ver Streaming y límites del parser
import { Parser, ParseResult, ParseException } from '@stxt-lang/core';

const parser = new Parser();
const result: ParseResult = parser.parseResult(text);

if (result.hasErrors()) {
    for (const error of result.getErrors()) {
        console.error(`line ${error.line} [${error.code}]: ${error.message}`);
    }
}
const roots = result.getNodes();   // los nodos raíz, en orden; puede haber varios

// La forma "lanzar al primer error"
try {
    const nodes = parser.parse(text);
} catch (e) {
    if (e instanceof ParseException) console.error(e.line, e.code, e.message);
}

Cada error es una ParseException con line (desde 1), code y message. Los errores de gramática son ValidationException, una subclase: un solo bucle recorre los dos tipos e instanceof los distingue. Tras un error de sintaxis el parser se recupera y sigue, y el resultado puede incluir nodos.

El árbol

Node es una clase abstracta con dos formas:

Clase Sintaxis Métodos propios
InlineNode Nombre: valor getValue()/setValue(), getChildren(), getChild(name), getChildrenByName(name), addChild(), removeChild(), addInlineNode(), addTextNode()
TextNode Nombre >> getTextLines(), setText(), setTextLines(), addTextLine(), clearText()

Métodos comunes, en Node:

Método Descripción
getName() El nombre tal como se escribió
getCanonicalName() El nombre canónico (STXT-SPEC §4.3)
getDeclaredNamespace() El namespace escrito entre paréntesis, o ""
getNamespace() El namespace efectivo, heredado de los padres
getLine() La línea del documento
getLevel() El nivel, derivado de la profundidad
getParent() El InlineNode padre, o null en la raíz
getText() El valor de un inline, o las líneas unidas de un bloque
detach() Separa el nodo de su padre

La forma de un nodo se distingue con instanceof. Con el documento del tutorial:

Book (com.acme.book):
	Title: Arquitectura de software moderna
	Authors:
		Author: María Pérez
		Author: Juan García
	ISBN: 978-84-123456-7-8
	Published: 2025-10-01
	Chapter: Introducción
		Content >>
			Conceptos básicos y objetivos del libro.
import { Parser, InlineNode, TextNode, Node } from '@stxt-lang/core';

const book = new Parser().parseResult(text).getNodes()[0];

book.getName();          // "Book"
book.getCanonicalName(); // "book"
book.getNamespace();     // "com.acme.book"
book.getLine();          // 1

if (book instanceof InlineNode) {
    book.getChild('Title')?.getText();                 // "Arquitectura de software moderna"
    book.getChild('title')?.getName();                 // "Title": la búsqueda es por nombre canónico
    book.getChild('Publisher');                        // null: no existe

    const authors = book.getChild('Authors') as InlineNode;
    authors.getChildrenByName('Author').map(a => a.getText());   // ["María Pérez", "Juan García"]
    authors.getDeclaredNamespace();                    // "": no lo declara...
    authors.getNamespace();                            // "com.acme.book": ...lo hereda

    const chapter = book.getChild('Chapter') as InlineNode;
    const content = chapter.getChild('Content');
    if (content instanceof TextNode) {
        content.getTextLines();   // ["Conceptos básicos y objetivos del libro."]
        content.getLevel();       // 2
        content.getParent() === chapter;   // true
    }
}

// Un recorrido genérico
function walk(node: Node, depth = 0): void {
    console.log('  '.repeat(depth) + node.getName());
    if (node instanceof InlineNode) {
        for (const child of node.getChildren()) walk(child, depth + 1);
    }
}

Construir y modificar

Los árboles son mutables:

  • addChild rechaza un nodo que ya tenga padre (NODE_ALREADY_ATTACHED) o que sea un ancestro (NODE_CYCLE); removeChild y detach() lo separan.
  • setValue y addTextLine rechazan un salto de línea (LINE_BREAK_NOT_ALLOWED).
  • El nivel se deriva de la cadena de padres, y la línea solo la fija el parser.
  • En las fábricas con dos cadenas, la segunda es el contenido (valor o texto); el namespace solo aparece en la forma de tres argumentos.
import { InlineNode, TextNode } from '@stxt-lang/core';

const email = new InlineNode('Email', 'com.example.mail', 'Weekly report');
email.addInlineNode('From', 'Ana García <[email protected]>');
const to = email.addInlineNode('To');
to.addInlineNode('Address', '[email protected]');
const body = email.addTextNode('Body', 'Hi Bob,\n\nSee attached.');

body.getParent() === email;   // true
body.getLevel();              // 1
to.getNamespace();            // "com.example.mail", heredado

// Reordenar: "To" al principio
to.detach();
email.addChild(to, 0);

// Editar
email.setNamespace('com.example.docs');   // todo el subárbol que hereda le sigue
body.setText('Hi Bob,\n\nSee the new attachment.');

Validar contra un esquema o una plantilla

UnifiedSchemaProvider carga esquemas y plantillas con addFile(text): parsea la definición, la valida contra su meta-esquema y registra el esquema por namespace. SchemaValidator es un Validator que se registra en el Parser y valida cada nodo con namespace al cerrarlo.

Template (@stxt.template): com.acme.book
	Structure >>
		Book:
			Title: (1)
			Authors: (1)
				Author: (+)
			ISBN: (1)
			Publisher: (?)
			Published: (?) DATE
			Summary: (?) TEXT
			Chapter: (+)
				Content: (?) TEXT
	Description >>
		Book: Plantilla para fichas de libros editoriales
import {
    Parser, UnifiedSchemaProvider, SchemaValidator,
    ValidationException,
} from '@stxt-lang/core';

const provider = new UnifiedSchemaProvider();
provider.addFile(templateText);            // lanza si la plantilla no valida contra su meta-esquema

const parser = new Parser();
parser.registerValidator(new SchemaValidator(provider));

const result = parser.parseResult(documentText);
for (const error of result.getErrors()) {
    const kind = error instanceof ValidationException ? 'schema' : 'syntax';
    console.log(`${kind} line ${error.line} [${error.code}]: ${error.message}`);
}

Con un libro sin ISBN y con una fecha que no es YYYY-MM-DD, el bucle imprime:

schema line 6 [INVALID_VALUE]: Published: Invalid date (1 de octubre de 2025)
schema line 1 [TOO_FEW_CHILDREN]: 0 nodes of 'com.acme.book:isbn' and min is 1
  • Un error de cardinalidad se señala en la línea del padre.
  • Un namespace que el proveedor no conoce produce un SCHEMA_NOT_FOUND por nodo; el proveedor no lanza (getSchema() devuelve undefined).
  • Los nodos sin namespace no se validan (STXT-SCHEMA-SPEC §5). Una definición se valida siempre contra su meta-esquema.
  • addFile acepta ficheros con varias definiciones; getAllSchemas() las enumera y clear() vacía el proveedor.
  • Están implementados todos los tipos de STXT-SCHEMA-SPEC §9.

El esquema compilado se puede inspeccionar: provider.getSchema(ns) devuelve un Schema con getNamespace() y getNodeDefinition(name); cada NodeDefinition tiene getType(), getChildren() (un mapa de ChildDefinition con getMin() / getMax()), getValues() para un ENUM y getDescription().

Resolución de gramáticas

DiscoveryResolver implementa STXT-DISCOVERY-SPEC: dado el directorio de un documento, determina qué definiciones le aplican (los .stxt/ del documento y de sus ancestros, después ~/.stxt y /etc/stxt, con precedencia por namespace; STXT_PATH sustituye la cadena). Es el resolutor de la CLI y de la extensión.

No accede al sistema de ficheros ni al entorno: recibe un DiscoveryFileSystem y un DiscoveryEnvironment, que pueden ser el fs de Node, el sistema de ficheros virtual de un editor o un árbol en memoria en un test. Adaptadores para Node:

import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { DiscoveryEntry, DiscoveryEnvironment, DiscoveryFileSystem } from '@stxt-lang/core';

class NodeFileSystem implements DiscoveryFileSystem {
    async isDirectory(p: string): Promise<boolean> {
        try { return (await fs.stat(p)).isDirectory(); } catch { return false; }
    }
    async listDirectory(p: string): Promise<DiscoveryEntry[]> {
        const entries = await fs.readdir(p, { withFileTypes: true });
        return entries.map(e => ({ path: path.join(p, e.name), name: e.name, isDirectory: e.isDirectory() }));
    }
    readFile(p: string): Promise<string> { return fs.readFile(p, 'utf-8'); }
    parentOf(p: string): string | null {
        const parent = path.dirname(p);
        return parent === p ? null : parent;   // null en la raíz del sistema de ficheros
    }
    join(p: string, name: string): string { return path.join(p, name); }
}

class NodeEnvironment implements DiscoveryEnvironment {
    getStxtPath(): string[] | null {
        const value = process.env.STXT_PATH;
        // null (no definida) y [] (definida pero vacía) significan cosas distintas
        return value === undefined ? null : value.split(path.delimiter).filter(e => e !== '');
    }
    getUserLevelDir(): string | null { return path.join(os.homedir(), '.stxt'); }
    getSystemLevelDir(): string | null { return '/etc/stxt'; }
}

A resolve se le pasa el directorio del documento, o null para la entrada estándar o un búfer sin guardar; en ese caso la cadena empieza en el nivel de usuario. DiscoveryResult implementa SchemaProvider y se pasa directamente al validador:

import { Parser, SchemaValidator, DiscoveryResolver } from '@stxt-lang/core';

const resolver = new DiscoveryResolver(new NodeFileSystem(), new NodeEnvironment());
const discovery = await resolver.resolve('/home/ana/libros/docs');

discovery.getChain();        // ["/home/ana/libros/.stxt"]  (todos los ancestros, el más cercano primero)

// Los errores de resolución se acumulan, no se lanzan
for (const error of discovery.getErrors()) {
    console.error(`[${error.code}] ${error.file}: ${error.message}`);
}

const parser = new Parser();
parser.registerValidator(new SchemaValidator(discovery));
const result = parser.parseResult(documentText);

DiscoveryResult registra también el origen de cada definición:

const definition = discovery.getDefinition('com.acme.book');
definition?.file;       // "/home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt"
definition?.levelDir;   // "/home/ana/libros/.stxt"  (el nivel que ha ganado)
definition?.schema;     // el Schema compilado

discovery.getActiveDefinitions();   // una por namespace, con la precedencia aplicada
discovery.getAllSchemas();          // solo los esquemas de las anteriores

Los niveles se cachean por directorio; resolver.clearCache() los invalida cuando los ficheros de definición pueden haber cambiado. Los errores son DiscoveryError (code, file, message, namespace), con los códigos DISCOVERY_DUPLICATE_NAMESPACE, DISCOVERY_NOT_A_DEFINITION, DISCOVERY_NOT_PARSEABLE y DISCOVERY_INVALID_DEFINITION.

El árbol canónico en JSON

toCanonicalTree(nodes) devuelve el valor JSON de STXT-TREE-SPEC, el mismo que emite stxt describe, y toCanonicalJson(nodes) lo serializa con dos espacios de sangría. Emiten solo los campos normativos, sin posiciones ni comentarios. Los tipos CanonicalDocument, CanonicalNode, CanonicalInlineNode y CanonicalBlockNode describen el resultado.

import { Parser, toCanonicalTree, toCanonicalJson, CanonicalDocument } from '@stxt-lang/core';

const nodes = new Parser().parseResult(text).getNodes();
const tree: CanonicalDocument = toCanonicalTree(nodes);
tree[0].name;    // "Book"
tree[0].form;    // "inline"

process.stdout.write(toCanonicalJson(nodes));

Escribir STXT

NodeWriter serializa un nodo, o una lista de nodos raíz, en la forma canónica de STXT-TREE-SPEC §11, con IndentStyle.TABS (por defecto) o IndentStyle.SPACES_4. Escribe el namespace solo donde cambia respecto al padre. Como parte del árbol, la salida no lleva comentarios ni líneas en blanco fuera de los bloques; es lo que hace stxt format --clean.

import { NodeWriter, IndentStyle } from '@stxt-lang/core';

const one = NodeWriter.toSTXT(email);                                   // un nodo, con tabuladores
const all = NodeWriter.toSTXTDocs(result.getNodes(), IndentStyle.SPACES_4);   // un documento entero

El email construido más arriba, escrito con toSTXT:

Email (com.example.docs): Weekly report
	To:
		Address: [email protected]
	From: Ana García <[email protected]>
	Body >>
		Hi Bob,

		See the new attachment.

Formatter (STXT-TREE-SPEC §12) reformatea conservando comentarios y líneas en blanco: reescribe el texto original línea a línea y devuelve, junto al texto, los errores de sintaxis encontrados. Es el formateador de stxt format, de la extensión y del playground.

import { Formatter, IndentStyle } from '@stxt-lang/core';

const { text, errors } = Formatter.format(source, IndentStyle.TABS);

Observar el parseo

Un Observer registrado recibe llamadas durante el parseo: onCreate(node, line) al abrir un nodo (ya con padre, namespace efectivo y nivel), onFinish(node) al cerrarlo, onComment(lineNumber, line) por cada comentario y onTextLine(node, lineNumber, lineString, line) por cada línea de un bloque.

import { Parser, Observer, Node, TextNode, Line } from '@stxt-lang/core';

class LoggingObserver implements Observer {
    onCreate(node: Node, line: string): void { console.log('open', node.getQualifiedName()); }
    onFinish(node: Node): void { console.log('close', node.getQualifiedName()); }
    onComment(lineNumber: number, line: string): void { /* ... */ }
    onTextLine(node: TextNode, lineNumber: number, lineString: string, line: Line): void { /* ... */ }
}

const parser = new Parser();
parser.registerObserver(new LoggingObserver());
parser.parseResult(text);

Streaming y límites del parser

parseStream(lines) recibe un iterable de líneas, cada una sin su salto de línea, y no retiene nodos ni errores. Los resultados llegan por un StreamObserver: onRootNode(node) con cada nodo raíz completo y ya validado, que el parser libera tras la llamada, y onError(error) con cada error, de sintaxis o de validación. La memoria en uso es la de un árbol raíz, lo que permite procesar ficheros que no caben en memoria. Un StreamObserver registrado recibe las mismas llamadas con parse y parseResult.

import { Parser, StreamObserver, Node, ParseException } from '@stxt-lang/core';

class Counter implements StreamObserver {
    roots = 0;
    onRootNode(node: Node): void { this.roots++; }
    onError(error: ParseException): void { console.error(error.line, error.code, error.message); }
}

const parser = new Parser();
const counter = new Counter();
parser.registerStreamObserver(counter);
parser.parseStream(lines);   // Iterable<string>, una línea por elemento
counter.roots;

El parser aplica los límites de STXT-SPEC §11.2, configurables en el constructor con ParserOptions: maxNesting (100 niveles), maxLineLength (10 000 caracteres) y maxInputSize (10 000 000 caracteres); -1 desactiva uno. Superar un límite produce una LimitException (subclase de ParseException, con los códigos LIMIT_NESTING_EXCEEDED, LIMIT_LINE_LENGTH_EXCEEDED y LIMIT_INPUT_SIZE_EXCEEDED) y aborta el parseo.

const parser = new Parser({ maxNesting: 50, maxInputSize: -1 });

La superficie de la API

Exportaciones del paquete; lo demás no es API pública:

Grupo Exportaciones
Parseo Parser, ParserOptions, ParseResult, Node, InlineNode, TextNode, Line, parseLine, Constants, SPEC_VERSION, StringUtils
Errores ParseException, ValidationException, LimitException, RuntimeException
Puntos de extensión Observer, StreamObserver, Validator
Esquemas Schema, NodeDefinition, ChildDefinition, SchemaProvider, SchemaProviderMemory, SchemaProviderMeta, SchemaValidator, Type, TypeRegistry, transformNodeToSchema
Plantillas transformTemplateNodeToSchema, TemplateSchemaProviderMemory, MetaTemplateSchemaProvider, TEMPLATE_NAMESPACE
Runtime UnifiedSchemaProvider, NodeWriter, IndentStyle, Formatter, FormatResult, toCanonicalTree, toCanonicalJson y los tipos Canonical*
Resolución DiscoveryResolver, DiscoveryOptions, DiscoveryResult, DiscoveryDefinition, DiscoveryLevel, DiscoveryError, DiscoveryFileSystem, DiscoveryEntry, DiscoveryEnvironment

El paquete sigue el versionado semántico: la API no cambia de forma incompatible dentro de la línea 1.x. El estado de cada especificación está en Estabilidad y versiones. Los cambios de cada versión están en el repositorio, donde se reportan también los errores de parseo y de validación vistos desde la CLI, la extensión o el playground.