// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A document that remembers how it was written.
///
/// The other entry points read a TOML document into a tree and forget the page it
/// came from. This one keeps the page: the source text, the items in the order
/// they were read, each with the bytes it occupied, and the tree they denote.
/// Change one value in place and write the result back, and the rest of the
/// document is byte for byte as it was — the comments, the blank lines, the order,
/// and the spelling of every value but the one that moved.
///
/// This is the reading for a file a person edits by hand — `moon.mod`, a service
/// config, a manifest — where the diff should be the one line that changed.
#derive(Debug)
pub(all) struct Doc {
// The document as its bytes were read.
src : String
// The items, in the order the document holds them.
mut items : Array[Item]
// The tree the items point into.
root : Value
}
///|
pub extend Doc with Debug::{to_repr}
///|
/// Read a document into a `Doc`, which keeps it as it was written.
///
/// `flavor` names the edition, the same as the other entry points. A document the
/// edition refuses is refused the same way `decode` refuses it.
pub fn Doc::decode(
src : StringView,
flavor~ : Flavor = strict,
) -> Doc raise Refused {
check_text(src)
let reader = Reader::new(src, flavor)
reader.recording = true
let root = reader.document()
guard root is Table(_) else { abort("a document is a table") }
{ src: src.to_owned(), items: reader.rec_items, root, }
}
///|
/// The tree the document denotes — what `decode` answers for the same bytes.
pub fn Doc::value(self : Doc) -> Value {
self.root
}
///|
/// The value a path names, or nothing when the path does not name a value.
///
/// A path is the segments of a key as a person writes them: `["soil", "ph"]` is
/// the key under the table a `[soil]` header opens, and `["a", "b"]` is the key
/// written `a.b`.
pub fn Doc::get(self : Doc, path : Array[String]) -> Value? {
let mut here = self.root
for i = 0; i < path.length(); i = i + 1 {
match here {
Table(fields) =>
match fields.get(path[i]) {
Some(held) => here = held
None => return None
}
_ => return None
}
}
Some(here)
}
///|
/// Change the value a path names, in place.
///
/// The path must name a value already in the document; when it names nothing — a
/// missing key, or a table where a value should stand — the answer is nothing and
/// the document is left as it was. Otherwise the value is replaced, the answer is
/// whether it actually moved, and the line that held it is the only one written
/// fresh when the document is written back.
pub fn Doc::set(self : Doc, path : Array[String], value : Value) -> Bool? {
let key = path_key(path)
// The item whose key is the path.
let mut index = -1
for i = 0; i < self.items.length(); i = i + 1 {
let item = self.items[i]
guard item.kind is Pair else { continue }
if path_key(item.path) == key {
index = i
break
}
}
if index < 0 {
return None
}
// The table that holds the key, in the tree the items point into.
let mut here = self.root
for i = 0; i < path.length() - 1; i = i + 1 {
match here {
Table(fields) =>
match fields.get(path[i]) {
Some(held) => here = held
None => return None
}
_ => return None
}
}
guard here is Table(fields) else { return None }
let name = path[path.length() - 1]
guard fields.get(name) is Some(held) else { return None }
let moved = held != value
fields[name] = value
let item = self.items[index]
self.items[index] = { ..item, changed: true, }
Some(moved)
}
///|
/// The document as it was read, with its changed values written back.
///
/// Nothing changed and the answer is the source byte for byte. A value that moved
/// is written where it occupied, the way this module writes one; the key, the
/// separator and anything after the value on the line are the original text, and
/// so is everything between the lines — the comments, the blank lines, the order.
pub fn Doc::render(self : Doc) -> String {
let out = StringBuilder()
let mut prev = 0
for item in self.items {
out.write_string(self.src[prev:item.start].to_owned())
prev = item.end
if item.kind is Pair && item.changed {
out.write_string(self.src[item.start:item.vstart].to_owned())
let value = StringBuilder()
let held = item.value_of(self.root)
write_value(value, held)
out.write_string(value.to_string())
out.write_string(self.src[item.vend:item.end].to_owned())
} else {
out.write_string(self.src[item.start:prev].to_owned())
}
}
out.write_string(self.src[prev:].to_owned())
out.to_string()
}
///|
/// The value an item of a document holds, walking the tree to its path.
pub fn Item::value_of(self : Item, root : Value) -> Value {
let mut here = root
for name in self.path {
match here {
Table(fields) =>
match fields.get(name) {
Some(held) => here = held
None => return Value::string("")
}
_ => return Value::string("")
}
}
here
}