Toml.Ast
The syntax of a TOML document, with every character of it kept.
This is the layer an editing API works on. It is a list of expressions in the order they appear in the file, and it holds the whitespace and the comments between them as text rather than as positions, so that writing an untouched document back out produces the same bytes it came from. That is a property of the shape of these types and not of anyone's care: there is nowhere for a character to go missing.
The other layer is the table tree, which is what a program that only wants to read a config file should use. It is built from this one, it is where the rules the grammar cannot express are checked, and it does not remember any of this.
Positions are not here, and that is deliberate
A row number goes stale the moment a line is inserted above it. A column number
survives that but cannot tell a tab from eight spaces, and cannot represent the
alignment an author chose between two tokens. So instead of a position, each
node holds the actual string that sat there -- wsBeforeEq, indent,
trailing. Rows and columns still exist in parse errors, where they are
about a file that is not going to change.
Raw text is kept next to every value
BigDecimal reads 1.50 and 1.5 as the same number, which is correct and is
exactly why a value alone cannot be written back out: the file said 1.50. The
same goes for 0x1F, 1_000_000, +7, the T or space in a date-time, and
Z against z. Every scalar therefore carries both its source text and its
meaning. The writer uses the text when the node was not touched and formats
from the value when it was.
Comment ownership is not here either
Comments are expressions in the list, where the file put them. Which key a comment belongs to is a convention rather than a fact, so it is computed from this structure rather than baked into it -- otherwise changing the convention would mean changing the parser.
A file, and what it becomes
The example from https://toml.io:
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
[servers]
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"
parses to a Document of twenty expressions -- one per line, the blank ones
included, and the twentieth being the empty expression after the final newline
that says the file ended with one:
Toml.parse source
|> Result.map (\doc -> Array.length doc.expressions)
--> Ok 20
Line three is a Pair, and it shows the two things this module is shaped
around. The whitespace is text, and the value carries the spelling it was
written with next to what it means:
{ indent = ""
, content =
Pair
{ keyval =
{ key = { first = { raw = "dob", value = "dob" }, rest = [] }
, wsBeforeEq = " "
, wsAfterEq = " "
, value =
OffsetDateTimeVal
{ raw = "1979-05-27T07:32:00-08:00"
, value = -- a Civil.DateTime
}
}
, trailing = ""
, comment = Nothing
}
, newline = Just Lf
}
ports = [ 8000, 8001, 8002 ] is where the trivia earns its place. The author
put a space inside each bracket and none before the commas, and every one of
those spaces is in the tree:
ArrayVal
{ elements =
[ { before = [ Spaces " " ], value = IntegerVal { raw = "8000", .. }, after = [], comma = True }
, { before = [ Spaces " " ], value = IntegerVal { raw = "8001", .. }, after = [], comma = True }
, { before = [ Spaces " " ], value = IntegerVal { raw = "8002", .. }, after = [ Spaces " " ], comma = False }
]
, trailing = []
}
[servers.alpha] is a Header whose key has a rest, and the whitespace that
could have sat around the dot is kept even though there is none here:
Header
{ header =
{ kind = Std
, wsBefore = ""
, key =
{ first = { raw = "servers", value = "servers" }
, rest = [ { wsBefore = "", wsAfter = "", key = { raw = "alpha", value = "alpha" } } ]
}
, wsAfter = ""
}
, trailing = ""
, comment = Nothing
}
Note what is not here: [servers] on its own line is one expression and
[servers.alpha] is another, with no link between them. That alpha lives
inside servers is a fact about what the file means, not about how it is
written, so it belongs to Toml.Table and is worked out there.
For reaching a value by path rather than by walking this list, see
Toml.Edit.get. For what the file says, see
Toml.Table.
A whole TOML file.
hasBom records whether the source began with a byte order mark. It has to be
stored rather than derived, because decoding UTF-8 removes exactly one leading
BOM without saying so, and a document that had one must get it back.
One expression, which is one line of the file plus the newline that ended
it -- except that a value can be an array, an inline table or a multi-line
string, any of which may run on for many physical lines. indent is the
whitespace before the content began.
newline is Nothing only on the last expression of a file that does not end
with one.
What an expression turned out to be. The grammar allows exactly three things on a line: nothing, a key and a value, or a table header. All three may carry a comment, and the two that are not empty may have whitespace after them.
Comments and line endings
The text of a comment, without the # that started it and without the
newline that ended it. It is kept exactly, including any leading space, because
# note and #note are different files.
Which of the two line endings the grammar allows. TOML has no others, and a lone carriage return is not a line ending but an error.
Keys and values
A key, an equals sign and a value, with the whitespace around the equals sign kept so that a column of aligned assignments stays aligned.
A key, which is one simple key or several joined by dots.
The dots carry whitespace on both sides, because a . b = 1 is legal and means
what a.b = 1 means. rest is empty for an undotted key, which is why the
first part is separate: there is always at least one.
One part of a key.
raw is the source text, quotes and escapes included; value is the string
the key actually is. For a bare key the two are equal. For "a\tb" they are
not, and it is value that decides whether two keys collide.
Table headers
A [table] or [[array of tables]] line, with the whitespace inside the
brackets kept.
Which kind of bracket the header used.
Values
A value, and next to it the text it was written as.
The raw of a composite value is not stored, because an array and an inline
table already hold every character of themselves in their own fields.
A TOML float.
Most of them are exact decimals, which is what BigDecimal is for: 0.1 there
is 0.1 and not 0.1000000000000000055511151231257827. The other two are not
numbers at all, and cannot be BigDecimal values, so they sit beside it.
TOML writes +nan and -nan and says nothing about what the sign means. It is
kept so that the file can be written back as it was.
Which sign a non-finite float was written with. nan with no sign is
Positive, since that is how it is written back.
Arrays and inline tables
An array literal.
Everything between the brackets is here. trailing is what sat between the
last element -- or the opening bracket, in an empty array -- and the closing
one.
One element of an array, with the trivia on each side of it and whether a comma followed.
Only the last element may have comma = False; TOML 1.1 allows the trailing
comma but not a missing interior one.
An inline table.
Since TOML 1.1 this is the same shape as an array: newlines, comments and a trailing comma are all allowed inside the braces.
One entry of an inline table. The same shape as an array element, with a key and a value instead of a value.
Trivia
Whitespace, comments and newlines, in the order they appeared.
This is the grammar's ws-comment-newline, which is what may sit between the
parts of an array or an inline table. Concatenating the pieces gives back the
source text exactly.
One piece of trivia: a run of spaces and tabs, or the end of a line with an optional comment before it.
The two are separate because the grammar is: a comment inside an array has to
be followed by a newline, so [1 # note ] is not a TOML array and [1 # note
followed by ] on the next line is.