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:
addChildrechaza un nodo que ya tenga padre (NODE_ALREADY_ATTACHED) o que sea un ancestro (NODE_CYCLE);removeChildydetach()lo separan. Para insertar en una posición,addChild(index, node).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
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 editorialesimport 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_FOUNDpor nodo; el proveedor no lanza (getSchema()devuelvenull). - 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.