fix(document): rebuild set nodes and write documents back round-trip
Assisted-by: GLM 5.3
This commit is contained in:
+122
-26
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user