feat: add the Document edit pipeline with comment-preserving write
Test / test (push) Successful in 1m35s
Test / test (push) Successful in 1m35s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+8
-2
@@ -32,8 +32,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
values together with the key order, whether a table was written as an inline
|
values together with the key order, whether a table was written as an inline
|
||||||
table or under a header, and the comments, with `Keys`, `Entries`, `Get`,
|
table or under a header, and the comments, with `Keys`, `Entries`, `Get`,
|
||||||
`Comments` and `SetComments` to read and write them. `ParseMap` returns the
|
`Comments` and `SetComments` to read and write them. `ParseMap` returns the
|
||||||
plain `map[string]any` tree, the shape `Parse` used to give. `Marshal` does
|
plain `map[string]any` tree, the shape `Parse` used to give.
|
||||||
not accept a `Document`; it writes values, so `doc.Map()` is the way through.
|
- The edit pipeline on a `Document`: typed getters on `Document` and `Table`
|
||||||
|
(`GetString`, `GetInt`, `GetFloat`, `GetBool`, `GetArray`, `GetTable`),
|
||||||
|
`Set` and `Delete` that keep the surviving keys' positions and comments,
|
||||||
|
`UnmarshalDocument`, which decodes the document into a typed destination
|
||||||
|
without parsing again, and `Marshal` of a `Document`, which writes the keys
|
||||||
|
in written order, the comments above the lines and headers they belonged
|
||||||
|
to, and the inline tables inline.
|
||||||
- `OffsetDateTime`, the Go type of the offset date-time kind, so that all four
|
- `OffsetDateTime`, the Go type of the offset date-time kind, so that all four
|
||||||
TOML date-time kinds have one of their own. `Parse` and `Unmarshal` hand it
|
TOML date-time kinds have one of their own. `Parse` and `Unmarshal` hand it
|
||||||
back where they produced a bare `time.Time` before, and `Marshal` accepts it.
|
back where they produced a bare `time.Time` before, and `Marshal` accepts it.
|
||||||
|
|||||||
+35
-2
@@ -150,6 +150,37 @@ document and from `doc.Map()` is the same value.
|
|||||||
array of tables, and the inline tables inside a value array, with `nil` for the
|
array of tables, and the inline tables inside a value array, with `nil` for the
|
||||||
elements that are not tables.
|
elements that are not tables.
|
||||||
|
|
||||||
|
### Editing a document
|
||||||
|
|
||||||
|
The document is writable, which makes the read-change-write loop a round trip
|
||||||
|
through one value. The typed getters read with one call:
|
||||||
|
|
||||||
|
| Getter | Returns |
|
||||||
|
|---|---|
|
||||||
|
| `GetString(key)` | `(string, bool)` |
|
||||||
|
| `GetInt(key)` | `(int64, bool)` |
|
||||||
|
| `GetFloat(key)` | `(float64, bool)` |
|
||||||
|
| `GetBool(key)` | `(bool, bool)` |
|
||||||
|
| `GetArray(key)` | `([]any, bool)` |
|
||||||
|
| `GetTable(key)` | `(*Table, bool)` |
|
||||||
|
|
||||||
|
`Set(key, value)` stores a value, keeping an existing key's position and
|
||||||
|
comments and appending a new key to the end; a `map[string]any` value becomes
|
||||||
|
a table of its own under a header, its keys in sorted order. `Delete(key)`
|
||||||
|
removes a key and everything it holds. Every method exists on `Document` for
|
||||||
|
the root table and on `Table` for the table itself.
|
||||||
|
|
||||||
|
`Marshal` writes the document back as it stands: keys in written order, the
|
||||||
|
comments above the lines and headers they belonged to, tables that were
|
||||||
|
written inline written inline again. `UnmarshalDocument(doc, v)` decodes the
|
||||||
|
edited document into a typed destination without parsing again.
|
||||||
|
|
||||||
|
```go
|
||||||
|
doc, err := interpres.Parse(data)
|
||||||
|
doc.Set("port", 9090)
|
||||||
|
out, err := interpres.Marshal(doc)
|
||||||
|
```
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
A comment belongs to the line it precedes or follows, and to the node that line
|
A comment belongs to the line it precedes or follows, and to the node that line
|
||||||
@@ -868,8 +899,10 @@ long as no setter races with a call.
|
|||||||
|
|
||||||
### `type Document`, `type Table`, `type Entry`
|
### `type Document`, `type Table`, `type Entry`
|
||||||
|
|
||||||
See [Documents](#documents). A `Document` is what `Parse` returns, and it is
|
See [Documents](#documents). A `Document` is what `Parse` returns, and
|
||||||
not a value `Marshal` accepts.
|
`Marshal` writes it back: the keys in written order, the comments in place,
|
||||||
|
the inline tables inline. `UnmarshalDocument(doc, v)` decodes it without
|
||||||
|
parsing again.
|
||||||
|
|
||||||
### `type Marshaler interface{ MarshalTOML() (any, error) }`
|
### `type Marshaler interface{ MarshalTOML() (any, error) }`
|
||||||
|
|
||||||
|
|||||||
+151
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
package interpres
|
package interpres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
// A Document is a parsed TOML document: the values, plus what the map shape
|
// A Document is a parsed TOML document: the values, plus what the map shape
|
||||||
// cannot carry, which is the order the keys were written in, whether a table
|
// cannot carry, which is the order the keys were written in, whether a table
|
||||||
// was written inline or under a header, and the comments.
|
// was written inline or under a header, and the comments.
|
||||||
@@ -39,6 +44,38 @@ func (d *Document) Footer() []string { 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) { d.footer = lines }
|
||||||
|
|
||||||
|
// The document-level convenience forms of the Table edit API; they act on
|
||||||
|
// the root table.
|
||||||
|
|
||||||
|
// Get returns the root table's entry for key, and whether the document has
|
||||||
|
// one. See Table.Get.
|
||||||
|
func (d *Document) Get(key string) (*Entry, bool) { return d.Root().Get(key) }
|
||||||
|
|
||||||
|
// GetString returns the string the key holds, and whether it holds one.
|
||||||
|
func (d *Document) GetString(key string) (string, bool) { return d.Root().GetString(key) }
|
||||||
|
|
||||||
|
// GetInt returns the integer the key holds, and whether it holds one.
|
||||||
|
func (d *Document) GetInt(key string) (int64, bool) { return d.Root().GetInt(key) }
|
||||||
|
|
||||||
|
// GetFloat returns the float the key holds, and whether it holds one.
|
||||||
|
func (d *Document) GetFloat(key string) (float64, bool) { return d.Root().GetFloat(key) }
|
||||||
|
|
||||||
|
// GetBool returns the boolean the key holds, and whether it holds one.
|
||||||
|
func (d *Document) GetBool(key string) (bool, bool) { return d.Root().GetBool(key) }
|
||||||
|
|
||||||
|
// GetArray returns the value array the key holds, and whether it holds one.
|
||||||
|
func (d *Document) GetArray(key string) ([]any, bool) { return d.Root().GetArray(key) }
|
||||||
|
|
||||||
|
// GetTable returns the node of the table the key holds, and whether it holds
|
||||||
|
// one.
|
||||||
|
func (d *Document) GetTable(key string) (*Table, bool) { return d.Root().GetTable(key) }
|
||||||
|
|
||||||
|
// Set stores value under the key in the root table. See Table.Set.
|
||||||
|
func (d *Document) Set(key string, value any) { d.Root().Set(key, value) }
|
||||||
|
|
||||||
|
// Delete removes the key from the root table. See Table.Delete.
|
||||||
|
func (d *Document) Delete(key string) { d.Root().Delete(key) }
|
||||||
|
|
||||||
// A Table is one TOML table: its values, its keys in written order, and the
|
// A Table is one TOML table: its values, its keys in written order, and the
|
||||||
// comments around the header or the key that introduced it.
|
// comments around the header or the key that introduced it.
|
||||||
type Table struct {
|
type Table struct {
|
||||||
@@ -199,3 +236,117 @@ func (e *Entry) Trailing() string { return e.trailing }
|
|||||||
|
|
||||||
// SetTrailing replaces that comment.
|
// SetTrailing replaces that comment.
|
||||||
func (e *Entry) SetTrailing(line string) { e.trailing = line }
|
func (e *Entry) SetTrailing(line string) { e.trailing = line }
|
||||||
|
|
||||||
|
// GetString returns the string the key holds, and whether it holds one.
|
||||||
|
func (t *Table) GetString(key string) (string, bool) {
|
||||||
|
v, ok := t.values[key]
|
||||||
|
s, ok := v.(string)
|
||||||
|
return s, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt returns the integer the key holds, and whether it holds one.
|
||||||
|
func (t *Table) GetInt(key string) (int64, bool) {
|
||||||
|
v, ok := t.values[key]
|
||||||
|
i, ok := v.(int64)
|
||||||
|
return i, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat returns the float the key holds, and whether it holds one.
|
||||||
|
func (t *Table) GetFloat(key string) (float64, bool) {
|
||||||
|
v, ok := t.values[key]
|
||||||
|
f, ok := v.(float64)
|
||||||
|
return f, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool returns the boolean the key holds, and whether it holds one.
|
||||||
|
func (t *Table) GetBool(key string) (bool, bool) {
|
||||||
|
v, ok := t.values[key]
|
||||||
|
b, ok := v.(bool)
|
||||||
|
return b, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArray returns the value array the key holds, and whether it holds one.
|
||||||
|
func (t *Table) GetArray(key string) ([]any, bool) {
|
||||||
|
v, ok := t.values[key]
|
||||||
|
a, ok := v.([]any)
|
||||||
|
return a, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTable returns the node of the table the key holds, and whether it holds
|
||||||
|
// one, whichever way the document wrote the table.
|
||||||
|
func (t *Table) GetTable(key string) (*Table, bool) {
|
||||||
|
c := t.child(key)
|
||||||
|
return c, c != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// of []map[string]any becomes an array-of-tables node.
|
||||||
|
func (t *Table) Set(key string, value any) {
|
||||||
|
e, ok := t.index[key]
|
||||||
|
if !ok {
|
||||||
|
t.values[key] = value
|
||||||
|
e = t.addValue(key, value, false)
|
||||||
|
if m, isMap := value.(map[string]any); isMap {
|
||||||
|
e.child = newOrderedTable(m)
|
||||||
|
}
|
||||||
|
if items, isArray := value.([]map[string]any); isArray {
|
||||||
|
e.elements = make([]*Table, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
e.elements[i] = newOrderedTable(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.values[key] = value
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
if e.child == nil {
|
||||||
|
e.child = newOrderedTable(v)
|
||||||
|
} else {
|
||||||
|
e.child.values = v
|
||||||
|
}
|
||||||
|
e.elements = nil
|
||||||
|
case []map[string]any:
|
||||||
|
e.child = nil
|
||||||
|
e.elements = make([]*Table, len(v))
|
||||||
|
for i, item := range v {
|
||||||
|
e.elements[i] = newOrderedTable(item)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
e.child = nil
|
||||||
|
e.elements = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func newOrderedTable(m map[string]any) *Table {
|
||||||
|
t := newTable(m)
|
||||||
|
for _, k := range slices.Sorted(maps.Keys(m)) {
|
||||||
|
v := m[k]
|
||||||
|
_, isMap := v.(map[string]any)
|
||||||
|
e := t.addValue(k, v, false)
|
||||||
|
if isMap {
|
||||||
|
e.child = newOrderedTable(v.(map[string]any))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes key and everything it holds.
|
||||||
|
func (t *Table) Delete(key string) {
|
||||||
|
if _, ok := t.values[key]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(t.values, key)
|
||||||
|
delete(t.index, key)
|
||||||
|
for i, e := range t.entries {
|
||||||
|
if e.key == key {
|
||||||
|
t.entries = append(t.entries[:i], t.entries[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+121
-17
@@ -288,27 +288,131 @@ func TestParseMapIsTheValueTree(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalRejectsDocument(t *testing.T) {
|
func TestMarshalDocument(t *testing.T) {
|
||||||
// A Document is not a value to marshal: its order and comments would be
|
// A Document writes back: the keys in written order, the comments above
|
||||||
// dropped, and a struct walk would silently write nothing at all.
|
// the lines and headers they belonged to, and inline tables inline again.
|
||||||
doc, err := Parse([]byte("a = 1\n"))
|
doc, err := Parse([]byte("# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\n\ninline = { n = 1 }\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := Marshal(doc); err == nil {
|
out, err := Marshal(doc)
|
||||||
t.Fatal("expected an error for a Document")
|
|
||||||
} else if !strings.Contains(err.Error(), "Map()") {
|
|
||||||
t.Errorf("err = %v, want it to point at Map()", err)
|
|
||||||
}
|
|
||||||
if _, err := Marshal(*doc); err == nil {
|
|
||||||
t.Fatal("expected an error for a Document value")
|
|
||||||
}
|
|
||||||
// The tree marshals, which is the way through.
|
|
||||||
out, err := Marshal(doc.Map())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal of the tree: %v", err)
|
t.Fatalf("marshal of a Document: %v", err)
|
||||||
}
|
}
|
||||||
if want := "a = 1\n"; string(out) != want {
|
want := "# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\ninline = {n = 1}\n"
|
||||||
t.Errorf("output = %q, want %q", out, want)
|
if string(out) != want {
|
||||||
|
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
||||||
|
}
|
||||||
|
// The written document parses back to the same values.
|
||||||
|
re, err := Parse(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("re-parse: %v", err)
|
||||||
|
}
|
||||||
|
if got := re.Map()["a"]; got != int64(1) {
|
||||||
|
t.Errorf("a = %#v", got)
|
||||||
|
}
|
||||||
|
if _, err := Marshal(*doc); err != nil {
|
||||||
|
t.Errorf("marshal of a Document value: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDocumentEditPipeline(t *testing.T) {
|
||||||
|
doc, err := Parse([]byte("host = \"db\"\nport = 5432\n\n# The cache section\ntimeout = 1.5\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Run("typed getters", func(t *testing.T) {
|
||||||
|
if s, ok := doc.GetString("host"); !ok || s != "db" {
|
||||||
|
t.Errorf("host = %q, %v", s, ok)
|
||||||
|
}
|
||||||
|
if i, ok := doc.GetInt("port"); !ok || i != 5432 {
|
||||||
|
t.Errorf("port = %d, %v", i, ok)
|
||||||
|
}
|
||||||
|
if f, ok := doc.GetFloat("timeout"); !ok || f != 1.5 {
|
||||||
|
t.Errorf("timeout = %g, %v", f, ok)
|
||||||
|
}
|
||||||
|
if _, ok := doc.GetBool("host"); ok {
|
||||||
|
t.Error("host claimed as bool")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("set keeps the position and the comments", func(t *testing.T) {
|
||||||
|
doc.Set("port", int64(9090))
|
||||||
|
if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout"}) {
|
||||||
|
t.Fatalf("keys = %v", got)
|
||||||
|
}
|
||||||
|
out, err := Marshal(doc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "port = 9090") {
|
||||||
|
t.Errorf("output missing the new value:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("a new key joins the end", func(t *testing.T) {
|
||||||
|
doc.Set("lang", "cs")
|
||||||
|
if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout", "lang"}) {
|
||||||
|
t.Fatalf("keys = %v", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("a set table keeps an order of its own", func(t *testing.T) {
|
||||||
|
sub := map[string]any{"z": int64(1), "a": int64(2)}
|
||||||
|
doc.Set("cache", sub)
|
||||||
|
out, err := Marshal(doc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "[cache]\na = 2\nz = 1\n") {
|
||||||
|
t.Errorf("output missing the new table in order:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("delete removes the key", func(t *testing.T) {
|
||||||
|
doc.Delete("lang")
|
||||||
|
if _, ok := doc.Get("lang"); ok {
|
||||||
|
t.Fatal("lang survived Delete")
|
||||||
|
}
|
||||||
|
out, err := Marshal(doc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(out), "lang") {
|
||||||
|
t.Errorf("output still names lang:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("UnmarshalDocument decodes without reparsing", func(t *testing.T) {
|
||||||
|
type Cfg struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
}
|
||||||
|
var cfg Cfg
|
||||||
|
if err := UnmarshalDocument(doc, &cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cfg.Host != "db" || cfg.Port != 9090 {
|
||||||
|
t.Errorf("decoded %+v", cfg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("comments survive the round trip", func(t *testing.T) {
|
||||||
|
src := "# header comment\n[a]\n# key comment\nb = 2\n"
|
||||||
|
doc, err := Parse([]byte(src))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := Marshal(doc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"# header comment", "[a]", "# key comment", "b = 2"} {
|
||||||
|
if !strings.Contains(string(out), want) {
|
||||||
|
t.Errorf("output missing %q:\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("a nil document refuses to decode", func(t *testing.T) {
|
||||||
|
var cfg struct {
|
||||||
|
A int `toml:"a"`
|
||||||
|
}
|
||||||
|
if err := UnmarshalDocument(nil, &cfg); err == nil {
|
||||||
|
t.Error("UnmarshalDocument(nil) succeeded, want an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package interpres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmarshalDocument decodes a parsed Document into v without parsing again,
|
||||||
|
// the shape an edit pipeline wants: read the document, change the values it
|
||||||
|
// holds, decode the result into a typed destination. The key order and the
|
||||||
|
// comments the document carries are untouched; the decode reads the value
|
||||||
|
// tree the document shares with its nodes.
|
||||||
|
//
|
||||||
|
// UnmarshalDocument accepts the same destinations Unmarshal does.
|
||||||
|
func UnmarshalDocument(doc *Document, v any) error {
|
||||||
|
if doc == nil {
|
||||||
|
return fmt.Errorf("interpres: cannot decode a nil Document")
|
||||||
|
}
|
||||||
|
dec := newDecoder()
|
||||||
|
dec.nodes = indexNodes(doc.Root())
|
||||||
|
return dec.decode(doc.Map(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDocument renders a Document back to TOML: the keys in written order,
|
||||||
|
// the comments above the lines and headers they belonged to, tables that
|
||||||
|
// were written inline written inline again, and an array of tables in its
|
||||||
|
// header form. It is the write side of the edit pipeline: read with Parse,
|
||||||
|
// change with the Table and Document mutators, write with Marshal.
|
||||||
|
func (e *encoder) writeDocument(doc *Document) error {
|
||||||
|
if err := e.checkCtx(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if doc == nil || doc.root == nil {
|
||||||
|
return fmt.Errorf("interpres: cannot marshal a nil Document")
|
||||||
|
}
|
||||||
|
if err := e.writeTableEntries(doc.root, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.writeDocumentFooter(doc.footer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDocumentFooter writes the comment lines that follow the last
|
||||||
|
// statement, each separated from it by a blank line, the shape the parser
|
||||||
|
// reads them back from.
|
||||||
|
func (e *encoder) writeDocumentFooter(footer []string) {
|
||||||
|
for _, line := range footer {
|
||||||
|
e.buf.WriteString("# ")
|
||||||
|
e.buf.WriteString(line)
|
||||||
|
e.buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTableEntries writes one table's entries in written order at the given
|
||||||
|
// header path, nil for the document root, whose keys need no header.
|
||||||
|
func (e *encoder) writeTableEntries(t *Table, path []string) error {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if path != nil {
|
||||||
|
e.writeBlankLine()
|
||||||
|
e.writeComments(t.Comments())
|
||||||
|
e.buf.WriteString("[")
|
||||||
|
if err := e.writeKeyPath(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString("]\n")
|
||||||
|
if tr := t.Trailing(); tr != "" {
|
||||||
|
e.buf.WriteString(" # ")
|
||||||
|
e.buf.WriteString(tr)
|
||||||
|
e.buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range t.Entries() {
|
||||||
|
if err := e.checkCtx(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, isTables := entry.Value().([]map[string]any); isTables && len(entry.Elements()) > 0 {
|
||||||
|
for i, el := range entry.Elements() {
|
||||||
|
elemPath := append(append([]string{}, path...), entry.Key())
|
||||||
|
e.writeBlankLine()
|
||||||
|
if i == 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := e.writeDocumentEntry(entry, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDocumentEntry writes one "key = value" line of a document, with the
|
||||||
|
// comments the key carried. A value that is itself an inline table renders
|
||||||
|
// inline from its node, in the written order.
|
||||||
|
func (e *encoder) writeDocumentEntry(entry *Entry, path []string) error {
|
||||||
|
e.writeComments(entry.Comments())
|
||||||
|
if err := e.writeKey(entry.Key()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(" = ")
|
||||||
|
if child := entry.Table(); child != nil {
|
||||||
|
if err := e.writeInlineTableNode(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tr := entry.Trailing(); tr != "" {
|
||||||
|
e.buf.WriteString(" # ")
|
||||||
|
e.buf.WriteString(tr)
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('\n')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := e.writeValue(entry.Value()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tr := entry.Trailing(); tr != "" {
|
||||||
|
e.buf.WriteString(" # ")
|
||||||
|
e.buf.WriteString(tr)
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('\n')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineTableNode renders a table node as an inline table, its keys in
|
||||||
|
// written order, values that are tables inline in turn.
|
||||||
|
func (e *encoder) writeInlineTableNode(t *Table) error {
|
||||||
|
e.buf.WriteByte('{')
|
||||||
|
for i, key := range t.Keys() {
|
||||||
|
if i > 0 {
|
||||||
|
e.buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
if err := e.writeKey(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(" = ")
|
||||||
|
entry, _ := t.Get(key)
|
||||||
|
if child := entry.Table(); child != nil {
|
||||||
|
if err := e.writeInlineTableNode(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := e.writeValue(t.Values()[key]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('}')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -181,9 +181,12 @@ func (e *encoder) encode(v any) error {
|
|||||||
if x == nil {
|
if x == nil {
|
||||||
return fmt.Errorf("interpres: cannot marshal nil value")
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("interpres: cannot marshal a Document; marshal its Map() to write the values")
|
return e.writeDocument(x)
|
||||||
case Document:
|
case Document:
|
||||||
return fmt.Errorf("interpres: cannot marshal a Document; marshal its Map() to write the values")
|
if x.root == nil {
|
||||||
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
||||||
|
}
|
||||||
|
return e.writeDocument(&x)
|
||||||
case OrderedMap:
|
case OrderedMap:
|
||||||
return e.encodeOrderedMap(&x)
|
return e.encodeOrderedMap(&x)
|
||||||
case *OrderedMap:
|
case *OrderedMap:
|
||||||
|
|||||||
Reference in New Issue
Block a user