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:
+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
|
||||
}
|
||||
Reference in New Issue
Block a user