La biblioteca Java

dev.stxt:stxt-core es el parser de STXT para Java, publicado en Maven Central. Implementa las cinco especificaciones y devuelve los mismos códigos de error que las bibliotecas TypeScript y Python.

No incluye línea de comandos; la del ecosistema es stxt.

Instalación

Requiere Java 17 o superior y no tiene dependencias en tiempo de ejecución. Bajo JPMS es un módulo automático llamado dev.stxt.

<dependency>
    <groupId>dev.stxt</groupId>
    <artifactId>stxt-core</artifactId>
    <version>1.0.3</version>
</dependency>

O con Gradle:

implementation 'dev.stxt:stxt-core:1.0.3'

El javadoc está en javadoc.io.

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 List<Node>
parseStream(reader) No retiene nodos ni errores; ver Streaming y límites del parser

Las dos primeras tienen su pareja sobre fichero: parseResultFile(File) y parseFile(File). La fachada STXT da parsers ya configurados: STXT.rawParser() sin validación y STXT.parser(loader) con la validación de esquemas y plantillas registrada.

import java.util.List;

import dev.stxt.Node;
import dev.stxt.ParseResult;
import dev.stxt.Parser;
import dev.stxt.exceptions.ParseException;
import dev.stxt.runtime.STXT;

Parser parser = STXT.rawParser();
ParseResult result = parser.parseResult(text);

if (result.hasErrors()) {
    for (ParseException error : result.getErrors()) {
        System.err.printf("line %d [%s]: %s%n", error.getLine(), error.getCode(), error.getMessage());
    }
}
List<Node> roots = result.getNodes();   // los nodos raíz, en orden; puede haber varios

// La forma "lanzar al primer error"
try {
    List<Node> nodes = parser.parse(text);
} catch (ParseException e) {
    System.err.println(e.getLine() + " " + e.getCode() + " " + e.getMessage());
}

Cada error es una ParseException con getLine() (desde 1), getCode() y getMessage(). 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. La jerarquía de excepciones está en Errores.

El árbol

Node es una clase sealed con dos formas:

Clase Sintaxis Métodos propios
InlineNode Nombre: valor getValue()/setValue(), getChildren(), getChild(name), getChildren(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

Los hijos se buscan por nombre canónico. getChild devuelve null si no hay ninguno y lanza AMBIGUOUS_CHILD si hay más de uno; para los repetidos está getChildren(name). Las dos admiten un segundo argumento con el namespace.

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 dev.stxt.InlineNode;
import dev.stxt.Node;
import dev.stxt.TextNode;
import dev.stxt.runtime.STXT;

Node book = STXT.rawParser().parseResult(text).getNodes().get(0);

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

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

    InlineNode authors = (InlineNode) inline.getChild("Authors");
    authors.getChildren("Author").stream().map(Node::getText).toList();   // [María Pérez, Juan García]
    authors.getDeclaredNamespace();                    // "": no lo declara...
    authors.getNamespace();                            // "com.acme.book": ...lo hereda

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

// Un recorrido genérico
static void walk(Node node, int depth) {
    System.out.println("  ".repeat(depth) + node.getName());
    if (node instanceof InlineNode inline)
        for (Node child : inline.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. Para insertar en una posición, addChild(index, node).
  • 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 String, el segundo es el contenido (valor o texto); el namespace solo aparece en la forma de tres argumentos.
import dev.stxt.InlineNode;
import dev.stxt.TextNode;

InlineNode email = new InlineNode("Email", "com.example.mail", "Weekly report");
email.addInlineNode("From", "Ana García <[email protected]>");
InlineNode to = email.addInlineNode("To");
to.addInlineNode("Address", "[email protected]");
TextNode 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(0, to);

// 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

Un ResourcesLoader determina de dónde salen los esquemas y las plantillas. ResourcesLoaderDirectory los busca en disco como <dir>/@stxt.schema/<namespace>.stxt y <dir>/@stxt.template/<namespace>.stxt, la disposición que deja stxt install. STXT.parser(loader) devuelve un parser que valida cada definición contra su meta-esquema al cargarla, la cachea y valida cada nodo con namespace del documento 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 java.io.File;

import dev.stxt.ParseResult;
import dev.stxt.Parser;
import dev.stxt.exceptions.ParseException;
import dev.stxt.exceptions.ValidationException;
import dev.stxt.resources.ResourcesLoader;
import dev.stxt.resources.ResourcesLoaderDirectory;
import dev.stxt.runtime.STXT;

ResourcesLoader loader = new ResourcesLoaderDirectory(new File("/home/ana/libros/.stxt"));
Parser parser = STXT.parser(loader);

ParseResult result = parser.parseResult(documentText);
for (ParseException error : result.getErrors()) {
    String kind = (error instanceof ValidationException) ? "schema" : "syntax";
    System.out.printf("%s line %d [%s]: %s%n", kind, error.getLine(), error.getCode(), error.getMessage());
}

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

ResourcesLoader es una interfaz de un solo método, retrieve(namespace, resource), donde namespace es @stxt.schema o @stxt.template y resource el namespace buscado. Una definición en memoria, en el classpath o en una base de datos se sirve con una lambda que devuelve su texto, o lanza ResourceNotFoundException:

import dev.stxt.exceptions.ResourceNotFoundException;

ResourcesLoader loader = (namespace, resource) -> {
    if (namespace.equals("@stxt.template") && resource.equals("com.acme.book")) return templateText;
    throw new ResourceNotFoundException(namespace, resource);
};
Parser parser = STXT.parser(loader);
  • Un error de cardinalidad se señala en la línea del padre.
  • Un namespace que el cargador no conoce produce un SCHEMA_NOT_FOUND por nodo; el proveedor no lanza (getSchema() devuelve null).
  • Una definición que no valida contra su meta-esquema se reporta como hallazgo del documento (por ejemplo TYPE_NOT_VALID) y no como excepción.
  • Los nodos sin namespace no se validan (STXT-SCHEMA-SPEC §5). Una definición se valida siempre contra su meta-esquema.
  • Están implementados todos los tipos de STXT-SCHEMA-SPEC §9.

La fachada equivale a registrar un SchemaValidator sobre su proveedor:

import dev.stxt.schema.SchemaValidator;

Parser parser = new Parser();
parser.registerValidator(new SchemaValidator(STXT.schemaProvider(loader)));

El esquema compilado se puede inspeccionar: STXT.schemaProvider(loader) es el SchemaProvider que usa la fachada, y getSchema(ns) devuelve un Schema con getNamespace(), getNodes() y getNodeDefinition(name); cada NodeDefinition tiene getType(), getChildren() (un mapa de ChildDefinition por nombre cualificado namespace:nombre, con getMin() / getMax()), getValues() para un ENUM y getDescription().

Resolución de gramáticas

ResourcesLoaderDirectory consulta un único directorio. 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), igual que la CLI y la extensión.

El constructor sin argumentos usa NioDiscoveryFileSystem (sobre java.nio.file) y SystemDiscoveryEnvironment (STXT_PATH, directorio de usuario y de sistema); las dos se pueden sustituir en un test (DiscoveryFileSystem, DiscoveryEnvironment). 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 java.nio.file.Path;

import dev.stxt.Parser;
import dev.stxt.discovery.DiscoveryDefinition;
import dev.stxt.discovery.DiscoveryError;
import dev.stxt.discovery.DiscoveryResolver;
import dev.stxt.discovery.DiscoveryResult;
import dev.stxt.schema.SchemaValidator;

DiscoveryResolver resolver = new DiscoveryResolver();
DiscoveryResult discovery = resolver.resolve(Path.of("/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 (DiscoveryError error : discovery.getErrors()) {
    System.err.printf("[%s] %s: %s%n", error.getCode(), error.getFile(), error.getMessage());
}

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

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

DiscoveryDefinition definition = discovery.getDefinition("com.acme.book");
definition.getFile();       // /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt
definition.getLevelDir();   // /home/ana/libros/.stxt  (el nivel que ha ganado)
definition.getSchema();     // 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 (getCode(), getFile(), getMessage(), getNamespace()), con los códigos DISCOVERY_DUPLICATE_NAMESPACE, DISCOVERY_NOT_A_DEFINITION, DISCOVERY_NOT_PARSEABLE y DISCOVERY_INVALID_DEFINITION.

El árbol canónico en JSON

TreeJson.toCanonicalJson(nodes), o toCanonicalJson(node) para un solo raíz, devuelve como texto el JSON de STXT-TREE-SPEC, el mismo que emite stxt describe, con dos espacios de sangría. Emite solo los campos normativos, sin posiciones ni comentarios, y no depende de ninguna biblioteca JSON.

import dev.stxt.runtime.TreeJson;

String json = TreeJson.toCanonicalJson(result.getNodes());
System.out.print(json);

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 dev.stxt.runtime.NodeWriter;
import dev.stxt.runtime.NodeWriter.IndentStyle;

String one = NodeWriter.toSTXT(email);                                   // un nodo, con tabuladores
String all = NodeWriter.toSTXT(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. Aplica las mismas reglas que stxt format, la extensión y el playground.

import java.util.List;

import dev.stxt.exceptions.ParseException;
import dev.stxt.runtime.Formatter;
import dev.stxt.runtime.FormatResult;
import dev.stxt.runtime.NodeWriter.IndentStyle;

FormatResult formatted = Formatter.format(source, IndentStyle.TABS);
String text = formatted.text();
List<ParseException> errors = formatted.errors();

Observar y validar durante 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.

Un Validator se ejecuta sobre cada nodo al cerrarlo y devuelve una lista de ValidationException, sin lanzar. SchemaValidator es el que trae la biblioteca; Validator es una interfaz funcional y admite una lambda.

import java.util.List;

import dev.stxt.LineIndent;
import dev.stxt.Node;
import dev.stxt.Parser;
import dev.stxt.TextNode;
import dev.stxt.exceptions.ValidationException;
import dev.stxt.processors.Observer;

Parser parser = new Parser();
parser.registerObserver(new Observer() {
    @Override public void onCreate(Node node, String line) { System.out.println("open " + node.getQualifiedName()); }
    @Override public void onFinish(Node node) { System.out.println("close " + node.getQualifiedName()); }
    @Override public void onComment(int lineNumber, String line) { }
    @Override public void onTextLine(TextNode node, int lineNumber, String lineString, LineIndent line) { }
});
parser.registerValidator(node -> List.<ValidationException>of());   // un validador que no se queja de nada
parser.parseResult(text);

Streaming y límites del parser

parseStream(reader) lee de un Reader 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 java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;

import dev.stxt.Node;
import dev.stxt.Parser;
import dev.stxt.exceptions.ParseException;
import dev.stxt.processors.StreamObserver;

Parser parser = new Parser();
parser.registerStreamObserver(new StreamObserver() {
    @Override public void onRootNode(Node node) { System.out.println(node.getQualifiedName()); }
    @Override public void onError(ParseException error) { System.err.println(error.getLine() + " " + error.getCode()); }
});
try (Reader reader = Files.newBufferedReader(Path.of("registro.stxt"))) {
    parser.parseStream(reader);
}

El parser aplica los límites de STXT-SPEC §11.2, configurables con setMaxNesting (100 niveles), setMaxLineLength (10 000 caracteres) y setMaxInputSize (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.

Parser parser = new Parser();
parser.setMaxNesting(50);
parser.setMaxInputSize(-1);

Errores

Todas las excepciones son unchecked y heredan de dev.stxt.exceptions.STXTException, con su código en getCode():

Excepción Cuándo
ParseException La sintaxis está mal; añade getLine()
ValidationException El documento incumple su gramática (tipo, cardinalidad, hijo no declarado); subclase de ParseException
LimitException Se ha superado un límite del parser (LIMIT_*); subclase de ParseException
SchemaException El esquema o la plantilla están mal formados
ResourceNotFoundException Un ResourcesLoader no tiene el recurso (los proveedores la convierten en un hallazgo SCHEMA_NOT_FOUND)
STXTIOException No se ha podido leer un fichero
STXTException (base) Integridad del árbol (NODE_ALREADY_ATTACHED, NODE_CYCLE, LINE_BREAK_NOT_ALLOWED), búsqueda ambigua (AMBIGUOUS_CHILD) y demás fallos en tiempo de ejecución

Las de parseResult no se lanzan: se acumulan en getErrors(). Las de integridad del árbol sí se lanzan.

La superficie de la API

Paquete Qué hay
dev.stxt Parser, ParseResult, Node, InlineNode, TextNode, Constants, LineIndent, LineIndentParser, NameNamespace, NameNamespaceParser, NamespaceValidator
dev.stxt.exceptions STXTException, ParseException, ValidationException, LimitException, SchemaException, ResourceNotFoundException, STXTIOException
dev.stxt.processors Observer, StreamObserver, Validator
dev.stxt.schema Schema, NodeDefinition, ChildDefinition, SchemaProvider, SchemaValidator, SchemaProviderResources, SchemaProviderCache, SchemaProviderMemory, SchemaProviderMeta, SchemaParser, DefinitionCompiler, Type, TypeRegistry, y los tipos en dev.stxt.schema.type
dev.stxt.template TemplateParser, TemplateSchemaProvider, TemplateSchemaProviderMemory, MetaTemplateSchemaProvider
dev.stxt.resources ResourcesLoader, ResourcesLoaderDirectory
dev.stxt.runtime STXT (la fachada), UnifiedSchemaProvider, NodeWriter e IndentStyle, Formatter y FormatResult, TreeJson
dev.stxt.discovery DiscoveryResolver, DiscoveryResult, DiscoveryDefinition, DiscoveryLevel, DiscoveryError, DiscoveryEnvironment, SystemDiscoveryEnvironment, DiscoveryFileSystem, NioDiscoveryFileSystem

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 CHANGELOG.md del repositorio, donde se reportan también los errores.