fix(document): rebuild set nodes and write documents back round-trip

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-22 21:15:00 +02:00
parent 0ded34da3c
commit b7f39435e1
3 changed files with 446 additions and 63 deletions
+122 -26
View File
@@ -34,15 +34,31 @@ func (d *Document) Root() *Table {
} }
// Map returns the value tree, the shape ParseMap gives. It is the tree the // Map returns the value tree, the shape ParseMap gives. It is the tree the
// document was parsed into, not a copy. // document was parsed into, not a copy. A nil document or one with no root
func (d *Document) Map() map[string]any { return d.root.values } // holds no values.
func (d *Document) Map() map[string]any {
if d == nil || d.root == nil {
return nil
}
return d.root.values
}
// Footer returns the comment lines that follow the last statement, and every // Footer returns the comment lines that follow the last statement, and every
// line of a document that holds no statement at all. // line of a document that holds no statement at all.
func (d *Document) Footer() []string { return d.footer } func (d *Document) Footer() []string {
if d == nil {
return nil
}
return d.footer
}
// SetFooter replaces those lines. // SetFooter replaces those lines.
func (d *Document) SetFooter(lines []string) { d.footer = lines } func (d *Document) SetFooter(lines []string) {
if d == nil {
return
}
d.footer = lines
}
// The document-level convenience forms of the Table edit API; they act on // The document-level convenience forms of the Table edit API; they act on
// the root table. // the root table.
@@ -87,6 +103,11 @@ type Table struct {
// rather than under a header or as a dotted key. // rather than under a header or as a dotted key.
inline bool inline bool
// dotted records that a dotted key introduced the table, `a.b = 1`
// building the a around the leaf: the write side gives such a table back
// as dotted key lines, the form that holds the position of a line.
dotted bool
// comments are the lines above the table's header, trailing is the comment // comments are the lines above the table's header, trailing is the comment
// on the header's own line. Both are empty for a table a dotted key // on the header's own line. Both are empty for a table a dotted key
// introduced, which has no line of its own. // introduced, which has no line of its own.
@@ -98,8 +119,12 @@ func newTable(values map[string]any) *Table {
return &Table{values: values, index: map[string]*Entry{}} return &Table{values: values, index: map[string]*Entry{}}
} }
// Keys returns the table's keys in the order they were written. // Keys returns the table's keys in the order they were written. A nil table
// holds none, the answer a document without a root gives through Root.
func (t *Table) Keys() []string { func (t *Table) Keys() []string {
if t == nil {
return nil
}
keys := make([]string, len(t.entries)) keys := make([]string, len(t.entries))
for i, e := range t.entries { for i, e := range t.entries {
keys[i] = e.key keys[i] = e.key
@@ -109,43 +134,75 @@ func (t *Table) Keys() []string {
// Values returns the table's values, which is the map the value tree holds for // Values returns the table's values, which is the map the value tree holds for
// it. // it.
func (t *Table) Values() map[string]any { return t.values } func (t *Table) Values() map[string]any {
if t == nil {
return nil
}
return t.values
}
// Entries returns the table's entries in written order. // Entries returns the table's entries in written order.
func (t *Table) Entries() []*Entry { return t.entries } func (t *Table) Entries() []*Entry {
if t == nil {
return nil
}
return t.entries
}
// Get returns the entry for key, and whether the table has one. // Get returns the entry for key, and whether the table has one.
func (t *Table) Get(key string) (*Entry, bool) { func (t *Table) Get(key string) (*Entry, bool) {
if t == nil {
return nil, false
}
e, ok := t.index[key] e, ok := t.index[key]
return e, ok return e, ok
} }
// Inline reports whether the table was written as an inline table, `{…}`, // Inline reports whether the table was written as an inline table, `{…}`,
// rather than under a header or introduced by a dotted key. // rather than under a header or introduced by a dotted key.
func (t *Table) Inline() bool { return t.inline } func (t *Table) Inline() bool { return t != nil && t.inline }
// Comments returns the comment lines above the table's header, or above the // Comments returns the comment lines above the table's header, or above the
// key that introduced it. Lines carry no leading '#' and no surrounding space. // key that introduced it. Lines carry no leading '#' and no surrounding space.
func (t *Table) Comments() []string { return t.comments } func (t *Table) Comments() []string {
if t == nil {
return nil
}
return t.comments
}
// SetComments replaces those lines. Each line is written back with a "# " in // SetComments replaces those lines. Each line is written back with a "# " in
// front of it, so a line should not carry one. // front of it, so a line should not carry one.
func (t *Table) SetComments(lines []string) { t.comments = lines } func (t *Table) SetComments(lines []string) {
if t == nil {
return
}
t.comments = lines
}
// Trailing returns the comment on the header's own line, without the '#'. // Trailing returns the comment on the header's own line, without the '#'.
func (t *Table) Trailing() string { return t.trailing } func (t *Table) Trailing() string {
if t == nil {
return ""
}
return t.trailing
}
// SetTrailing replaces that comment. // SetTrailing replaces that comment.
func (t *Table) SetTrailing(line string) { t.trailing = line } func (t *Table) SetTrailing(line string) {
if t == nil {
return
}
t.trailing = line
}
// addValue records a key of the table, in written order. // addValue records a key of the table, in written order. The caller gives
// the entry a table node or element nodes when the value has that shape; a
// map value left without a node writes as an inline table.
func (t *Table) addValue(key string, val any, inline bool) *Entry { func (t *Table) addValue(key string, val any, inline bool) *Entry {
e := &Entry{table: t, key: key, inline: inline} e := &Entry{table: t, key: key, inline: inline}
t.entries = append(t.entries, e) t.entries = append(t.entries, e)
t.index[key] = e t.index[key] = e
if node, ok := val.(map[string]any); ok {
e.child = newTable(node)
}
return e return e
} }
@@ -178,6 +235,9 @@ func (t *Table) addElement(key string, values map[string]any) *Table {
// child returns the node of a table-valued key, or nil. // child returns the node of a table-valued key, or nil.
func (t *Table) child(key string) *Table { func (t *Table) child(key string) *Table {
if t == nil {
return nil
}
if e, ok := t.index[key]; ok { if e, ok := t.index[key]; ok {
return e.child return e.child
} }
@@ -239,6 +299,9 @@ func (e *Entry) SetTrailing(line string) { e.trailing = line }
// GetString returns the string the key holds, and whether it holds one. // GetString returns the string the key holds, and whether it holds one.
func (t *Table) GetString(key string) (string, bool) { func (t *Table) GetString(key string) (string, bool) {
if t == nil {
return "", false
}
v, ok := t.values[key] v, ok := t.values[key]
s, ok := v.(string) s, ok := v.(string)
return s, ok return s, ok
@@ -246,6 +309,9 @@ func (t *Table) GetString(key string) (string, bool) {
// GetInt returns the integer the key holds, and whether it holds one. // GetInt returns the integer the key holds, and whether it holds one.
func (t *Table) GetInt(key string) (int64, bool) { func (t *Table) GetInt(key string) (int64, bool) {
if t == nil {
return 0, false
}
v, ok := t.values[key] v, ok := t.values[key]
i, ok := v.(int64) i, ok := v.(int64)
return i, ok return i, ok
@@ -253,6 +319,9 @@ func (t *Table) GetInt(key string) (int64, bool) {
// GetFloat returns the float the key holds, and whether it holds one. // GetFloat returns the float the key holds, and whether it holds one.
func (t *Table) GetFloat(key string) (float64, bool) { func (t *Table) GetFloat(key string) (float64, bool) {
if t == nil {
return 0, false
}
v, ok := t.values[key] v, ok := t.values[key]
f, ok := v.(float64) f, ok := v.(float64)
return f, ok return f, ok
@@ -260,6 +329,9 @@ func (t *Table) GetFloat(key string) (float64, bool) {
// GetBool returns the boolean the key holds, and whether it holds one. // GetBool returns the boolean the key holds, and whether it holds one.
func (t *Table) GetBool(key string) (bool, bool) { func (t *Table) GetBool(key string) (bool, bool) {
if t == nil {
return false, false
}
v, ok := t.values[key] v, ok := t.values[key]
b, ok := v.(bool) b, ok := v.(bool)
return b, ok return b, ok
@@ -267,6 +339,9 @@ func (t *Table) GetBool(key string) (bool, bool) {
// GetArray returns the value array the key holds, and whether it holds one. // GetArray returns the value array the key holds, and whether it holds one.
func (t *Table) GetArray(key string) ([]any, bool) { func (t *Table) GetArray(key string) ([]any, bool) {
if t == nil {
return nil, false
}
v, ok := t.values[key] v, ok := t.values[key]
a, ok := v.([]any) a, ok := v.([]any)
return a, ok return a, ok
@@ -282,9 +357,13 @@ func (t *Table) GetTable(key string) (*Table, bool) {
// Set stores value under key. A key the table already has keeps its position // Set stores value under key. A key the table already has keeps its position
// and its comments; a new one joins the end. A value of map[string]any // and its comments; a new one joins the end. A value of map[string]any
// becomes a table node of its own, written under a header like any other // becomes a table node of its own, written under a header like any other
// table; a Go map carries no order, so its keys take sorted order. A value // table, and replaces the node the key held, which belonged to the value the
// key held; a Go map carries no order, so its keys take sorted order. A value
// of []map[string]any becomes an array-of-tables node. // of []map[string]any becomes an array-of-tables node.
func (t *Table) Set(key string, value any) { func (t *Table) Set(key string, value any) {
if t == nil {
return
}
e, ok := t.index[key] e, ok := t.index[key]
if !ok { if !ok {
t.values[key] = value t.values[key] = value
@@ -303,11 +382,10 @@ func (t *Table) Set(key string, value any) {
t.values[key] = value t.values[key] = value
switch v := value.(type) { switch v := value.(type) {
case map[string]any: case map[string]any:
if e.child == nil { // The node is rebuilt rather than patched: the entries and the index
e.child = newOrderedTable(v) // belong to the table the key held, and writing the new value
} else { // through them would leave the old table's keys in the output.
e.child.values = v e.child = newOrderedTable(v)
}
e.elements = nil e.elements = nil
case []map[string]any: case []map[string]any:
e.child = nil e.child = nil
@@ -322,15 +400,30 @@ func (t *Table) Set(key string, value any) {
} }
// newOrderedTable builds a table node for a value the caller set, its keys // newOrderedTable builds a table node for a value the caller set, its keys
// entered as entries in sorted order, the order Marshal writes maps in. // entered in sorted order, the order Marshal writes maps in.
func newOrderedTable(m map[string]any) *Table { func newOrderedTable(m map[string]any) *Table {
return orderedTable(m, 0)
}
// orderedTable is newOrderedTable's recursion. The depth bound is the value
// encoder's: a cyclic map stopped here is written by the value writer, which
// reports it instead of running the stack out.
func orderedTable(m map[string]any, depth int) *Table {
t := newTable(m) t := newTable(m)
for _, k := range slices.Sorted(maps.Keys(m)) { for _, k := range slices.Sorted(maps.Keys(m)) {
v := m[k] v := m[k]
_, isMap := v.(map[string]any)
e := t.addValue(k, v, false) e := t.addValue(k, v, false)
if isMap { if depth >= maxEncodeDepth {
e.child = newOrderedTable(v.(map[string]any)) continue
}
switch val := v.(type) {
case map[string]any:
e.child = orderedTable(val, depth+1)
case []map[string]any:
e.elements = make([]*Table, len(val))
for i, item := range val {
e.elements[i] = orderedTable(item, depth+1)
}
} }
} }
return t return t
@@ -338,6 +431,9 @@ func newOrderedTable(m map[string]any) *Table {
// Delete removes key and everything it holds. // Delete removes key and everything it holds.
func (t *Table) Delete(key string) { func (t *Table) Delete(key string) {
if t == nil {
return
}
if _, ok := t.values[key]; !ok { if _, ok := t.values[key]; !ok {
return return
} }
+141
View File
@@ -4,6 +4,7 @@
package interpres package interpres
import ( import (
"reflect"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -416,3 +417,143 @@ func TestDocumentEditPipeline(t *testing.T) {
} }
}) })
} }
// TestMarshalDocumentRoundTrips pins that a parsed document written back
// re-parses to the same tree: arrays of tables keep exactly one header per
// element, dotted keys hold their line position without swallowing the keys
// after them, inline tables inside value arrays keep their written order,
// and comments travel with their statements.
func TestMarshalDocumentRoundTrips(t *testing.T) {
tests := []struct {
name string
src string
}{
{"array of tables", "[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n"},
{"array of tables with comments", "# about items\n[[items]] # first\nname = \"a\"\n"},
{"dotted key before a later key", "a.b = 1\nc = 2\n"},
{"dotted keys grouped", "a.b = 1\na.c = 2\nd = 3\n"},
{"dotted key with a nested leaf", "a.b.c = 1\nz = 2\n"},
{"header section after a dotted key", "a.b = 1\n\n[a.x]\ny = 2\n"},
{"inline tables in a value array keep order", "arr = [{y = 1, x = 2}, {second = true, first = false}]\n"},
{"nested array of tables", "[[items]]\nn = 1\n\n[items.sub]\nk = \"v\"\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doc, err := Parse([]byte(tt.src))
if err != nil {
t.Fatalf("Parse: %v", err)
}
out, err := Marshal(doc)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
reparsed, err := Parse(out)
if err != nil {
t.Fatalf("re-parse of %q: %v", out, err)
}
if !reflect.DeepEqual(doc.Map(), reparsed.Map()) {
t.Errorf("round trip changed the tree:\nin: %#v\nout: %#v", doc.Map(), reparsed.Map())
}
if got, want := reparsed.Root().Keys(), doc.Root().Keys(); !slices.Equal(got, want) {
t.Errorf("root keys = %v, want %v", got, want)
}
})
}
}
// TestMarshalDocumentArrayComments pins where the comments of an array of
// tables land: above and beside the [[header]] itself.
func TestMarshalDocumentArrayComments(t *testing.T) {
doc, err := Parse([]byte("# element one\n[[items]] # trailing\nname = \"a\"\n"))
if err != nil {
t.Fatalf("Parse: %v", err)
}
out, err := Marshal(doc)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
want := "# element one\n[[items]] # trailing\nname = \"a\"\n"
if string(out) != want {
t.Errorf("output = %q, want %q", out, want)
}
}
// TestTableSetReplacesTableNode pins that Set over a key holding a table
// rebuilds the node, so the new map's keys are the ones written.
func TestTableSetReplacesTableNode(t *testing.T) {
doc, err := Parse([]byte("[cache]\nz = 1\n"))
if err != nil {
t.Fatalf("Parse: %v", err)
}
doc.Set("cache", map[string]any{"a": int64(2)})
out, err := Marshal(doc)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
want := "[cache]\na = 2\n"
if string(out) != want {
t.Errorf("output = %q, want %q", out, want)
}
}
// TestTableSetNestedArraysOfTables pins that a value set through the edit API
// carries its arrays of tables into the header form.
func TestTableSetNestedArraysOfTables(t *testing.T) {
doc, err := Parse([]byte("x = 1\n"))
if err != nil {
t.Fatalf("Parse: %v", err)
}
doc.Set("t", map[string]any{"items": []map[string]any{{"n": int64(1)}, {"n": int64(2)}}})
out, err := Marshal(doc)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if !strings.Contains(string(out), "[[t.items]]") {
t.Errorf("output = %q, want the array of tables under a header", out)
}
}
// TestTableSetCyclicMapErrors pins that a cyclic map set through the edit API
// reaches the depth limit instead of the stack.
func TestTableSetCyclicMapErrors(t *testing.T) {
doc, err := Parse([]byte("x = 1\n"))
if err != nil {
t.Fatalf("Parse: %v", err)
}
m := map[string]any{}
m["self"] = m
doc.Set("cyclic", m)
if _, err := Marshal(doc); err == nil || !strings.Contains(err.Error(), "nests deeper") {
t.Errorf("err = %v, want the depth-limit complaint", err)
}
}
// TestDocumentNilSafety pins that the nil document answers its readers
// instead of panicking, the contract Root already carries.
func TestDocumentNilSafety(t *testing.T) {
var doc *Document
if doc.Map() != nil {
t.Errorf("Map = %v", doc.Map())
}
if doc.Footer() != nil {
t.Errorf("Footer = %v", doc.Footer())
}
doc.SetFooter([]string{"x"})
if e, ok := doc.Get("k"); e != nil || ok {
t.Errorf("Get = %v, %v", e, ok)
}
if _, ok := doc.GetString("k"); ok {
t.Error("GetString on a nil document reports a value")
}
if _, ok := doc.GetTable("k"); ok {
t.Error("GetTable on a nil document reports a value")
}
doc.Set("k", 1)
doc.Delete("k")
if keys := doc.Root().Keys(); keys != nil {
t.Errorf("Keys = %v", keys)
}
if doc.Root().Entries() != nil {
t.Errorf("Entries = %v", doc.Root().Entries())
}
}
+183 -37
View File
@@ -43,8 +43,8 @@ func (e *encoder) writeDocument(doc *Document) error {
} }
// writeDocumentFooter writes the comment lines that follow the last // writeDocumentFooter writes the comment lines that follow the last
// statement, each separated from it by a blank line, the shape the parser // statement. The parser collects them wherever they sit after it, so the
// reads them back from. // writer needs no blank line of its own to have them read back.
func (e *encoder) writeDocumentFooter(footer []string) { func (e *encoder) writeDocumentFooter(footer []string) {
for _, line := range footer { for _, line := range footer {
e.buf.WriteString("# ") e.buf.WriteString("# ")
@@ -53,8 +53,9 @@ func (e *encoder) writeDocumentFooter(footer []string) {
} }
} }
// writeTableEntries writes one table's entries in written order at the given // writeTableEntries writes one table at the given header path, nil for the
// header path, nil for the document root, whose keys need no header. // document root, whose keys need no header: the blank line, the comments,
// the header line with its trailing comment, then the body.
func (e *encoder) writeTableEntries(t *Table, path []string) error { func (e *encoder) writeTableEntries(t *Table, path []string) error {
if t == nil { if t == nil {
return nil return nil
@@ -66,77 +67,222 @@ func (e *encoder) writeTableEntries(t *Table, path []string) error {
if err := e.writeKeyPath(path); err != nil { if err := e.writeKeyPath(path); err != nil {
return err return err
} }
e.buf.WriteString("]\n") e.buf.WriteString("]")
if tr := t.Trailing(); tr != "" { if tr := t.Trailing(); tr != "" {
e.buf.WriteString(" # ") e.buf.WriteString(" # ")
e.buf.WriteString(tr) e.buf.WriteString(tr)
e.buf.WriteByte('\n') }
e.buf.WriteByte('\n')
}
return e.writeTableBody(t, path)
}
// writeTableBody writes one table's entries: the value lines first, in
// written order, then the header sections. In a valid document every line at
// one level precedes the headers below it, so the split reorders nothing;
// what it prevents is a table a dotted key introduced, which the parse nests
// as a sub-table at the position of a line, from swallowing the lines that
// follow it into its header.
func (e *encoder) writeTableBody(t *Table, path []string) error {
for _, entry := range t.Entries() {
if err := e.checkCtx(); err != nil {
return err
}
if !e.isLineEntry(entry) {
continue
}
if err := e.writeLineEntry(entry, path); err != nil {
return err
} }
} }
for _, entry := range t.Entries() { for _, entry := range t.Entries() {
if err := e.checkCtx(); err != nil { if err := e.checkCtx(); err != nil {
return err return err
} }
if _, isTables := entry.Value().([]map[string]any); isTables && len(entry.Elements()) > 0 { if child := entry.Table(); child != nil && child.dotted && !entry.Inline() {
for i, el := range entry.Elements() { // A dotted table writes as lines above; its own header-form
elemPath := append(append([]string{}, path...), entry.Key()) // sub-tables are sections the document placed after those lines,
e.writeBlankLine() // so the section pass reaches through the dotted entry.
if i == 0 { if err := e.writeDottedSections(child, append(append([]string{}, path...), entry.Key())); err != nil {
e.writeComments(entry.Comments())
}
e.buf.WriteString("[[")
if err := e.writeKeyPath(elemPath); err != nil {
return err
}
e.buf.WriteString("]]\n")
if err := e.writeTableEntries(el, elemPath); err != nil {
return err
}
}
continue
}
if child := entry.Table(); child != nil && !entry.Inline() {
headerPath := append(append([]string{}, path...), entry.Key())
if err := e.writeTableEntries(child, headerPath); err != nil {
return err return err
} }
continue continue
} }
if err := e.writeDocumentEntry(entry, path); err != nil { if e.isLineEntry(entry) {
continue
}
if err := e.writeSectionEntry(entry, path); err != nil {
return err return err
} }
} }
return nil return nil
} }
// writeDottedSections writes the header-form sub-tables of a dotted table:
// the sections the document placed after the dotted lines, reached through
// the dotted entry itself.
func (e *encoder) writeDottedSections(t *Table, path []string) error {
for _, entry := range t.Entries() {
if err := e.checkCtx(); err != nil {
return err
}
if child := entry.Table(); child != nil && child.dotted && !entry.Inline() {
if err := e.writeDottedSections(child, append(append([]string{}, path...), entry.Key())); err != nil {
return err
}
continue
}
if e.isLineEntry(entry) {
continue
}
if err := e.writeSectionEntry(entry, path); err != nil {
return err
}
}
return nil
}
// writeSectionEntry writes one entry the line pass left behind: a table or
// an array of tables under its header, at the path this level carries.
func (e *encoder) writeSectionEntry(entry *Entry, path []string) error {
if _, isTables := entry.Value().([]map[string]any); isTables {
// An array of tables keeps its header form, one element per header
// with the element's own comments above it; the body that follows is
// the element's, with no header of its own to repeat.
elemPath := append(append([]string{}, path...), entry.Key())
for i, el := range entry.Elements() {
e.writeBlankLine()
if i == 0 {
e.writeComments(entry.Comments())
}
e.writeComments(el.Comments())
e.buf.WriteString("[[")
if err := e.writeKeyPath(elemPath); err != nil {
return err
}
e.buf.WriteString("]]")
if tr := el.Trailing(); tr != "" {
e.buf.WriteString(" # ")
e.buf.WriteString(tr)
}
e.buf.WriteByte('\n')
if err := e.writeTableBody(el, elemPath); err != nil {
return err
}
}
return nil
}
headerPath := append(append([]string{}, path...), entry.Key())
return e.writeTableEntries(entry.Table(), headerPath)
}
// isLineEntry reports whether an entry writes as one or more "key = value"
// lines at its own level: a value, an inline table, or a table a dotted key
// introduced, which goes back as dotted keys. An emptied array of tables
// counts as one only so the line pass can drop it, the omission the value
// encoder applies to an empty array of tables too.
func (e *encoder) isLineEntry(entry *Entry) bool {
if child := entry.Table(); child != nil {
return entry.Inline() || child.dotted
}
if _, isTables := entry.Value().([]map[string]any); isTables {
return len(entry.Elements()) == 0
}
return true
}
// writeLineEntry writes one entry as lines at this level, and drops an
// emptied array of tables, which has no TOML form.
func (e *encoder) writeLineEntry(entry *Entry, path []string) error {
if child := entry.Table(); child != nil && !entry.Inline() {
return e.writeDottedTable(child, append(append([]string{}, path...), entry.Key()))
}
if _, isTables := entry.Value().([]map[string]any); isTables {
return nil
}
return e.writeDocumentEntry(entry)
}
// writeDottedTable writes a table a dotted key introduced as one dotted line
// per leaf, in written order: `a.b = 1`. A sub-table the document added
// under a header stays a section and is left to the section pass.
func (e *encoder) writeDottedTable(t *Table, path []string) error {
for _, entry := range t.Entries() {
if err := e.checkCtx(); err != nil {
return err
}
if child := entry.Table(); child != nil && !entry.Inline() && !child.dotted {
continue
}
leafPath := append(append([]string{}, path...), entry.Key())
if child := entry.Table(); child != nil && !entry.Inline() {
if err := e.writeDottedTable(child, leafPath); err != nil {
return err
}
continue
}
e.writeComments(entry.Comments())
if err := e.writeKeyPath(leafPath); err != nil {
return err
}
e.buf.WriteString(" = ")
if err := e.writeEntryValueNodes(entry); err != nil {
return err
}
e.buf.WriteByte('\n')
}
return nil
}
// writeDocumentEntry writes one "key = value" line of a document, with the // writeDocumentEntry writes one "key = value" line of a document, with the
// comments the key carried. A value that is itself an inline table renders // comments the key carried. A value that is itself an inline table renders
// inline from its node, in the written order. // inline from its node, in the written order.
func (e *encoder) writeDocumentEntry(entry *Entry, path []string) error { func (e *encoder) writeDocumentEntry(entry *Entry) error {
e.writeComments(entry.Comments()) e.writeComments(entry.Comments())
if err := e.writeKey(entry.Key()); err != nil { if err := e.writeKey(entry.Key()); err != nil {
return err return err
} }
e.buf.WriteString(" = ") e.buf.WriteString(" = ")
if err := e.writeEntryValueNodes(entry); err != nil {
return err
}
e.buf.WriteByte('\n')
return nil
}
// writeEntryValueNodes writes the value of a document entry. An inline table
// node keeps the written key order even inside a value array, where the
// ordinary value writer would sort the keys.
func (e *encoder) writeEntryValueNodes(entry *Entry) error {
if child := entry.Table(); child != nil { if child := entry.Table(); child != nil {
if err := e.writeInlineTableNode(child); err != nil { if err := e.writeInlineTableNode(child); err != nil {
return err return err
} }
if tr := entry.Trailing(); tr != "" { } else if arr, ok := entry.Value().([]any); ok {
e.buf.WriteString(" # ") elems := entry.Elements()
e.buf.WriteString(tr) e.buf.WriteByte('[')
for i, item := range arr {
if i > 0 {
e.buf.WriteString(", ")
}
if i < len(elems) && elems[i] != nil {
if err := e.writeInlineTableNode(elems[i]); err != nil {
return err
}
continue
}
if err := e.writeValue(item); err != nil {
return err
}
} }
e.buf.WriteByte('\n') e.buf.WriteByte(']')
return nil } else if err := e.writeValue(entry.Value()); err != nil {
}
if err := e.writeValue(entry.Value()); err != nil {
return err return err
} }
if tr := entry.Trailing(); tr != "" { if tr := entry.Trailing(); tr != "" {
e.buf.WriteString(" # ") e.buf.WriteString(" # ")
e.buf.WriteString(tr) e.buf.WriteString(tr)
} }
e.buf.WriteByte('\n')
return nil return nil
} }