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:
addChildrechaza un nodo que ya tenga padre (NODE_ALREADY_ATTACHED) o que sea un ancestro (NODE_CYCLE);removeChildydetach()lo separan.setValueyaddTextLinerechazan 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 editorialesimport {
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_FOUNDpor nodo; el proveedor no lanza (getSchema()devuelveundefined). - Los nodos sin namespace no se validan (STXT-SCHEMA-SPEC §5). Una definición se valida siempre contra su meta-esquema.
addFileacepta ficheros con varias definiciones;getAllSchemas()las enumera yclear()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.