STXT Tree
1. Introduction
This document is STXT-TREE-SPEC; the other specifications cite it by that name. It defines the canonical JSON representation of the logical tree produced when a valid STXT document is parsed, and the two writing operations: the canonical text form of a tree (section 11) and the reformatting of a document (section 12).
Its purpose is for two conforming implementations to display, exchange and compare
the same parsing result without relying on a platform-specific class, getters or
automatic serialization. It is, in particular, the reference format for conformance
corpora and tools such as stxt describe.
This specification represents the logical tree, not the source file. It does not attempt to retain comments, blank lines outside blocks, indentation style, the exact position of nodes, or other presentation choices. Rewriting a file while retaining those elements requires a source-analysis layer separate from the tree.
STXT-SPEC defines document syntax and the meaning of names, values, blocks, namespaces and indentation. Schemas, templates and definition discovery do not alter this tree.
1.1 Version of this specification
This specification carries its own date and its own status in the Last modif and
Status fields of its Metadata, independent of the other STXT specifications' and with
the meaning fixed by STXT-SPEC §1.1. It is in Zenith.
It depends entirely on STXT-SPEC: it describes the tree its syntax produces.
2. Terminology
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174: they carry that meaning only when they appear in uppercase, as here.
The terms node, INLINE, BLOCK, canonical name, effective namespace and document retain the meaning defined by STXT-SPEC.
In this document, a tree representation is a JSON value that meets sections 3 to 6. An emitter is an implementation that transforms parsed STXT nodes into that representation.
3. Document value
The representation of an STXT document MUST be a JSON array. Each element of the array is a root node, in the same order in which it appears in the document.
The outer array is required because STXT permits multiple root nodes. A document
made solely of comments or blank lines is valid and is represented by the empty array
[].
The representation does not wrap the array in an object and does not include a document name, file path, implementation version or validation result. Those facts belong to the application invoking the parser, not to the STXT document.
4. Node representation
Each node MUST be represented as a JSON object with the common members name,
canonicalName, namespace and form, plus value and children for INLINE, or
lines for BLOCK.
| Member | JSON type | Meaning |
|---|---|---|
name |
string | Logical node name after the trimming and space compaction of STXT-SPEC section 4.1. It retains case, diacritics and separators from the logical name. |
canonicalName |
string | Canonical name calculated under STXT-SPEC section 4.3: NFC, Unicode lower case, and separators compacted to -. |
namespace |
string | Node's effective namespace, already inherited and lower-cased. It is "" when the node has no effective namespace. |
form |
string | Exactly "inline" for a : node or "block" for a >> node. |
children |
array | Only for inline nodes: direct-child representations, in order of appearance. It always exists for this form, even when empty. |
value |
string | Only for inline nodes: their inline value after trimming. It always exists for this form, including when it is "". |
lines |
array of strings | Only for block nodes: the logical lines of the block, in order. It always exists for this form, including when empty. |
An inline node MUST have exactly the members name, canonicalName,
namespace, form, value and children. A block node MUST have exactly
the members name, canonicalName, namespace, form and lines. Additional
members are not permitted in the canonical representation.
A block produced by the base parser has no structural children and therefore MUST
NOT have a children member. Its indented content is already represented as literal
text in lines.
5. Values and block lines
The value member holds the INLINE value after left and right trimming under
STXT-SPEC section 10.1. No further conversion is applied: it remains text even
when a schema may validate it as a date, number or another type.
The lines member retains the logical block lines exactly as defined by
STXT-SPEC sections 10.2 and 10.3. Every element is a string, including a leading
or intermediate empty line represented by "". Since parsing discards the final empty
lines of a block (STXT-SPEC §10.3), lines never ends in "": a value whose
last lines element were "" is not the representation of any document. The array is
not replaced with a string joined by \n: the lines are the logical unit of the block
and the array keeps them without reconstruction.
Comments and blank lines outside a block create neither nodes nor lines. Inside a
block, a line beginning with # appears in lines only when its indentation makes
it literal content, as defined by STXT-SPEC section 9.1.
6. Identity and excluded data
A node's structural identity is made up of canonicalName and namespace.
Therefore, no qualifiedName member is emitted: it is derived as canonicalName
when namespace is empty, and as namespace + ":" + canonicalName otherwise.
Neither is a text member emitted: for INLINE it equals value, and for BLOCK it
is the derived join of lines with newline characters. Both duplicate information,
and the latter loses distinctions needed for conformance.
Line numbers, indentation levels, file paths, indentation styles, comments and a namespace's original spelling MUST NOT appear. They are either metadata of one particular input or information discarded during parsing, and would make semantically equivalent sources produce different trees.
7. JSON and canonicality
The format uses standard JSON. An emitter MUST produce valid JSON and SHOULD encode it as UTF-8 without a BOM. Canonicality is defined over the JSON value and its members, not its bytes: object-member order, indentation, whitespace and equivalent choices of JSON escaping do not change the representation.
Conformance corpora MUST compare the JSON value after parsing it, not a character string. An application MAY choose a deterministic presentation for people, such as JSON indented with two spaces and a final line break.
A future specification may define a byte serialization profile for signatures, hashes or caches. That profile is not part of STXT-TREE-SPEC.
8. Errors and validation
STXT-TREE-SPEC represents only documents that completed syntactic parsing. It does not define serialization of exceptions, diagnostics, warnings, discovery errors or partial trees after an error. In particular, a parser's recovery after multiple errors is not part of this contract.
Tests for invalid input continue to check the stable codes and line numbers defined by the relevant specifications. If a normalized diagnostic representation is needed, it will be a separate specification.
Schema or template validation happens over the tree already produced. Its result does not modify the tree representation; a tool may display it through another channel.
9. Conformance
An STXT tree emitter is conforming if, for every valid document:
- It emits an array containing every root node, preserving their order.
- It emits all and only the members required by section 4.
- It applies the normalization and namespace inheritance of STXT-SPEC before emitting.
- It retains every logical block line, including leading and intermediate empty lines; final empty lines never reach the tree: parsing discards them (STXT-SPEC §10.3).
- It does not emit source metadata or derived values excluded by section 6.
The conformance/tree/ directory in the specification source contains normative pairs
of .stxt documents and .json representations. Every implementation claiming
STXT-TREE-SPEC support MUST produce a JSON value equal to the expected file for
each pair.
10. Normative example
Input STXT document:
# Discarded comment
Document (COM.EXAMPLE.DOCS):
Title: Report
Body >>
First line
# This is text
Appendix:
Note: no lateral namespace inheritanceIts representation is the following JSON value. Notice that the initial comment is
absent, the namespace is lower-cased, and Appendix has an empty namespace.
[
{
"name": "Document",
"canonicalName": "document",
"namespace": "com.example.docs",
"form": "inline",
"value": "",
"children": [
{
"name": "Title",
"canonicalName": "title",
"namespace": "com.example.docs",
"form": "inline",
"value": "Report",
"children": []
},
{
"name": "Body",
"canonicalName": "body",
"namespace": "com.example.docs",
"form": "block",
"lines": ["First line", "", "# This is text"]
}
]
},
{
"name": "Appendix",
"canonicalName": "appendix",
"namespace": "",
"form": "inline",
"value": "",
"children": [
{
"name": "Note",
"canonicalName": "note",
"namespace": "",
"form": "inline",
"value": "no lateral namespace inheritance",
"children": []
}
]
}
]
11. Canonical text form
This section defines the inverse of the operation of the previous sections: from
the logical tree to STXT text. Two implementations writing the same tree MUST
produce exactly the same text. It is what lets a tool rewrite a file with no noise in
the diffs, stxt export and the libraries serialize alike, and the conformance kit
compare the written text and not only the tree.
The canonical form is a function of the tree of section 4 and of an
indentation style, a parameter of the operation: TABS (one tab per level) or
SPACES_4 (four spaces per level). There are therefore two canonical forms of each
tree, one per style; a tool MUST offer both and SHOULD use TABS when told
nothing else, following STXT-SPEC §4.4.
The operation is defined for trees that satisfy the invariants of
sections 4 and 5 — the ones parsing produces: valid, already normalized
names, inline values with no leading or trailing blanks, block lines with no trailing
blanks. For a programmatically built tree that does not satisfy them (a value with
leading or trailing blanks, a line with trailing blanks), the written text would not
reparse to the same tree and the result is undefined; the case of a lines ending in
"" additionally has the explicit rule of section 11.1, point 6.
11.1 Rules
For an array of root nodes, the text is the concatenation of the writing of each root, with one empty line between a root and the next. An empty array produces the empty string. Each node is written as follows, with level 0 for the roots and the parent's plus one for the children:
- The indentation of the level, in the chosen style: level tabs or level groups of four spaces. Level 0 has no indentation.
- The name (
name) as it is in the tree: the logical name, already normalized per STXT-SPEC §4.1, never the canonical one. - The namespace in parentheses, preceded by a space, only where it needs
declaring: in a root, if its
namespaceis not the empty string; in a child, if itsnamespacediffers from its parent's. A child whose namespace equals its parent's does not write it, even if the original source repeated it: the tree does not record where it was declared, only which one applies (section 6), and re-parsing the text yields the same result through inheritance. It is written in lowercase, as it is in the tree. - The form. An
inlinenode writes:and, ifvalueis not the empty string, a space and the value; with no value the line ends in:with no space after it. Ablocknode writes a space and>>. - The line ending
LF. Every line, the last one of the document included, ends inLF;CRLFis never written. - The content: the children of an
inlinenode, in order, each written with this same rule one level deeper; or the lines of ablocknode, each one with the indentation of level + 1 followed by the text of the line andLF. An empty line of the block ("") is written with that indentation and nothing else: the block reads as one piece. If a programmatically built tree endedlinesin""—which parsing never produces (section 5)— the writer MUST NOT emit those final empty lines: they would not survive re-parsing (STXT-SPEC §10.3) and would break the round-trip guarantee.
Nothing else is written: no BOM, no comments, no empty lines outside the blocks, no trailing blanks. The resulting text is a valid STXT document and, re-parsed, MUST produce a tree equal to the one it came from: that is the round-trip guarantee. A canonical text is also a fixed point: writing the tree of a canonical text gives that same text back.
11.2 Example
The tree of the example of section 10 has this canonical form in the TABS
style. The comment of the source is gone, the namespace comes out in lowercase and
only on the root that declares it, and the empty line of the block carries the
indentation of the block:
Documento (com.example.docs):
Título: Informe
Cuerpo >>
Primera línea
# Esto es texto
Anexo:
Nota: sin namespace heredado lateralmenteWith SPACES_4 the text is the same with every tab replaced by four spaces.
11.3 Conformance
An STXT writer is conforming if, for every tree and in both styles, it produces
exactly the text of section 11.1. The conformance/ directory of the
specification source holds normative cases of tree and expected text; every
implementation claiming this section MUST produce the expected text of each one,
byte for byte.
12. Reformatting a document
The canonical form loses what the tree does not hold: comments, empty lines outside
the blocks, the original indentation style. Reformatting a document is the other
writing operation: rewriting it line by line, over the original text, so that the
lines the tree describes end up in canonical form and every other line is kept as its
author wrote it. It is what stxt format, the formatter of the VS Code extension and
the playground do, and two conforming tools MUST produce the same result.
Reformatting takes the document and an indentation style, and returns the reformatted document together with the syntax errors found. It does not repair a document with errors nor hide them; whether a document with errors should be reformatted at all is the calling tool's decision.
12.1 Rules
The result has the same lines as the source, in the same order, with the same line
ending (if the source uses CRLF on any line, every line comes out with CRLF;
otherwise with LF) and with a final line break only if the source had one. An
initial BOM MUST NOT be kept. Each line is transformed according to what it is in
the parse of the document with no schema at all (formatting has nothing to do with
validation):
- A line that opens a node is written in canonical form
(section 11.1, rules 1 to 4): the indentation of its level in the
requested style, the logical name,
: valuewith exactly one space —or a bare:with no value—, or>>for a block. The namespace is written if, and only if, the source wrote it on that line: a child repeating its parent's namespace is redundant but legal, and dropping it would be an edit, not a reformat. It is written in lowercase. - A text line of a block gets the indentation of the block (the level of the
>>node plus one) in the requested style, followed by its content as it is: any indentation the line had beyond the block's is content (STXT-SPEC §10.2) and is kept exactly. An empty line that precedes more block text is""in the content whatever it looks like in the source (STXT-SPEC §10.3) and is written with the indentation of the block, as in the canonical form. The final empty lines of the block are not content (STXT-SPEC §10.3): they are kept under rule 3, like any empty line outside a block. - Every other line —a comment, an empty line outside a block, or a line the tree does not describe because of a syntax error— is kept as it is, with only two touches: its trailing blanks are removed, and the whole indentation units at its start are converted one for one to the requested style. A unit is a tab or four spaces, in either style; whatever follows the last whole unit, including a remainder that is not a whole unit, is kept as it is. As STXT-SPEC §9 validates the indentation of a comment like a node's, in a document that parses every comment has a whole number of units and comes out fully in the new style; the remainder only survives in documents with errors, which this conversion neither repairs nor hides.
Three properties follow from these rules, and an implementation MUST honor them: the result is idempotent (reformatting it again in the same style leaves it unchanged); reformatting to the other style and back returns the starting text if it was already formatted; and the reformatted document produces the same canonical tree as the source.
12.2 Example
This document, indented with spaces, with a comment, an empty line between roots and spare blanks:
# Proyecto de ejemplo
Documento (COM.EXAMPLE.DOCS):
Título:Informe
Cuerpo >>
con dos espacios de más
última línea
Anexo (com.example.docs):
Nota: aquí no hay nadareformatted with TABS comes out as follows. The comment, the empty line —which,
being final in the block, is not content and comes out unindented (rule 3)— and the
redundant namespace of Anexo are kept; the spacing of Título, the >> of
Cuerpo, the trailing blanks and the indentation are normalized; and the first line
of the block keeps its two extra spaces, which are content:
# Proyecto de ejemplo
Documento (com.example.docs):
Título: Informe
Cuerpo >>
con dos espacios de más
última línea
Anexo (com.example.docs):
Nota: aquí no hay nada12.3 Conformance
An STXT formatter is conforming if, for every document and in both styles, it
produces exactly the text of section 12.1 and the same syntax errors as the
parser. The normative cases of document and expected reformatted text live in the
conformance/ directory of the specification source as well.