tomledit

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 12 Imported by: 0

README

go-toml-edit

Zero-dep TOML editing library for Go with comment preservation

Why

Every Go TOML library either discards comments during parsing or provides read-only access to them. If you parse a config file, change one value, and write it back, the comments are gone. Python has tomlkit; Go had nothing. go-toml-edit fills this gap with a lossless AST that preserves every comment, blank line, and formatting detail through arbitrary edits.

What it preserves is content — comments, values, structure. Whitespace is formatting, not content: this library also reformats TOML, and Format() is strict about how output looks.

Before / After

package main

import (
	"fmt"
	"log"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`# Server config
[server]
host = "localhost"  # primary host
port = 8080
`))
	if err != nil {
		log.Fatal(err)
	}

	if err := doc.Set("server.port", 9090); err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(doc.Bytes()))
}

Output -- comments survive the edit:

# Server config
[server]
host = "localhost"  # primary host
port = 9090

Installation

go get github.com/stricttools/go-toml-edit

No runtime dependencies. The package imports the standard library and nothing else; the module's BurntSushi/toml and toml-test requirements are test-only (the benchmark comparison and the compliance corpus). A test asserts this over the non-test sources, and another asserts that the tomledit package touches no operating system: file reads and writes are the tomlfile subpackage's.

Two surfaces, two questions

A parsed document is readable through two surfaces, and which one to use follows from the question being asked.

  • The syntactic surface is the AST: Walk, Resolve and the concrete node types. It answers what the file contains, in the form it was written -- spellings, quoting styles, integer bases, comments, blank lines, and the source span of every construct.
  • The logical surface is the read-layer: doc.Root() returns a Record whose Entry values are the document's keys in first-appearance order. It answers what the document means, with the spellings folded away. A dotted key, a [header] table and an inline table are indistinguishable through it.

Reads are spelling-blind; writes are structurally conservative. A value write touches value fragments and nothing else, and a structural construct changes only through a structural operation or an explicit Delete.

Feature Comparison

Feature go-toml-edit BurntSushi/toml pelletier/go-toml/v2
Comment preservation Yes No Read-only (unstable)
Round-trip editing Yes No No
Set/Delete/RenameKey API Yes No No
Unmarshal to struct Yes (strict-only) Yes Yes
Marshal from Go values No Yes Yes
Position spans on AST nodes Yes No Unstable parser only
TOML 1.0 compliance Full Full Full
Formatter Yes No No
Document diffing Yes No No
Deep merge Yes No No

Quick Start

Parse and Read
doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 8080
`))
host, err := doc.GetString("server.host") // "localhost", nil
port, err := doc.GetInt("server.port")    // 8080, nil

Every getter returns (T, error). A missing path, a bad path, a wrong type and a value the target cannot hold exactly are distinct diagnostics -- see Diagnostics.

Read the logical structure
for entry := range doc.Root().Entries() {
	switch entry.Kind() {
	case tomledit.EntryValue:
		node, _ := entry.Node()
		fmt.Printf("%s = %v\n", entry.Key(), node.(tomledit.Scalar).Value())
	case tomledit.EntryRecord:
		rec, _ := entry.Record()
		fmt.Printf("%s is a table of %d keys\n", entry.Key(), rec.Len())
	case tomledit.EntryRecords:
		recs, _ := entry.Records()
		fmt.Printf("%s is an array of %d tables\n", entry.Key(), len(recs))
	}
}
Edit Values
doc.Set("server.port", 9090)               // update existing key
doc.SetCreate("database.host", "db.local") // create key + intermediate table
doc.Delete("server.debug")                 // remove a key (idempotent)
doc.RenameKey("server.host", "address")    // rename the binding, every spelling

Set is a no-op if and only if the bytes it would write are the bytes already there -- so a tool that writes back what it read touches nothing. When they differ, the value is written in the library's canonical spelling, and a container value (a map, a slice, a []Pair) is replaced wholesale.

Structural editing
doc.NewTable("logging")
doc.NewArrayTable("products")
doc.AppendToArray("server.ports", 8082)
doc.RemoveFromArray("server.ports", 0)
doc.PermuteChildren("server", []int{2, 0, 1}) // order[i] = index moving to i
doc.EnsureDefaults([]tomledit.Default{
	{Path: "server.port", Value: 8080},
	{Path: "logging.level", Value: "info"},
})
Comment Management
doc.SetComment("server.port", "default: 8080")
doc.SetLeadingComments("server", []string{
	"Server configuration",
	"See docs for all options",
})

inline, err := doc.GetComment("server.port")           // "default: 8080"
leading, err := doc.GetLeadingComments("server")       // the two lines above

The getters resolve a path to the same node the setters write to, so what one writes the other reads back, and they refuse the same paths with the same error kinds. Both answer normalized text -- the content without the # and the whitespace around it -- which is exactly what the setters take. A node with no inline comment answers the empty string, one with no leading comments answers nil, and neither is an error.

Fluent Cursor
host, err := doc.Key("database").Key("host").String()
port, err := doc.Key("database").Key("port").Int()
Strict decoding
type Config struct {
	Title  string `toml:"title"`
	Server struct {
		Host string `toml:"host"`
		Port int    `toml:"port"`
	} `toml:"server"`
}
cfg, err := tomledit.Unmarshal[Config](data)

Decoding is strict and strictness is the only mode: an unknown key, an unknown table, a value of a refused kind, a value the target cannot hold exactly, and a missing toml:"...,required" key are all errors. Key matching is exact -- a document key differing only in case matches nothing. Every independent violation is collected and reported together, in document order -- with one placement rule source order cannot give: a record's missing required keys are reported after that record's own entries, and among themselves in lexicographic key order.

The entry points return the value they build -- a *T, pointing at memory they allocated -- so a failed decode leaves nothing to inspect: Unmarshal[T](data), Decode[T](doc), DecodeNode[T](node), and DecodeOver[T](doc, seed) -- the last overlaying a document on a seed the factory builds, and reporting which paths the document supplied.

Descriptor validation

For a schema known only at runtime, describe it as data instead of as a struct:

spec := &tomledit.Spec{Fields: map[string]tomledit.Field{
	"host": {Kind: tomledit.FieldKindString, Required: true},
	"port": {Kind: tomledit.FieldKindInteger},
}}
err := doc.Validate(spec)                 // diagnostics only
values, err := doc.DecodeSpec(spec)       // validated, then native Go data

DecodeSpec is atomic: it returns a map only when the document has no violations at all.

Reading by hand

A file whose rules go beyond its shape -- a key whose meaning depends on another, entries that must not repeat, refusals that say what to write instead -- is read key by key through a Reader, which reports to a Collector:

var c tomledit.Collector
if r := c.Read("tool.toml", data); r != nil {
	name, _ := r.Str("name", true, `name = "<name>"`)
	fmt.Println(name)
	for _, cmd := range r.Tables("command") {
		cmd.Str("help", true, `help = "<one sentence>"`)
		if cmd.Flag("verbose") { /* ... */ }
		cmd.Done()
	}
	r.Done()
}
if err := c.Err(); err != nil {
	return err
}

Every key read through a Reader becomes known, and Done reports every key nobody read as unknown (KindUnknownKey, or KindUnknownTable for a table), the same kinds a strict decode reports. A required key that is absent is reported at its table's line, a value of the wrong type at its own, and each diagnostic carries a Fix saying what to write: tool.toml:4: command[0].help: is missing; write help = "<one sentence>". Collector.Read reports a syntax error as a diagnostic with its line, and Collector.Add takes a decode's diagnostics into the same list. Err sorts everything by file, line, path, and message, so problems from several files read in any order come back as one list in one order.

Format
formatted := doc.Format(tomledit.WithIndentWidth(2))

Format re-renders everything in canonical spellings. It preserves the writer's blank-line grouping at document and table-body level -- a run of blank lines becomes one. One gap it opens itself, unconditionally: a table header written flush against what precedes it always gets a blank line above it. What the library preserves is content -- the comments -- while whitespace is formatting, and Format is where the library is strict about output looking good, so the separation is not a caller's option. The insertion never removes a blank line and never doubles one already there.

Walk

Walk is the syntactic traversal: it hands the visitor the concrete nodes, with the spellings and trivia they were written with.

doc.Walk(func(path string, node tomledit.Node) error {
	fmt.Printf("%s = %v\n", path, node.(tomledit.Scalar).Value())
	return nil
}, tomledit.WalkLeaves)

Value() is on Scalar, the sub-interface the value-carrying node kinds implement; Node itself carries Type(), Span(), Raw(), Comment() and LeadingComments().

Position Spans

Every AST node carries its source position from the most recent parse, enabling custom semantic validation with precise line/column diagnostics:

doc.Walk(func(path string, node tomledit.Node) error {
	sp := node.Span() // 1-based line/column, plus a byte offset
	fmt.Printf("%s at %d:%d-%d:%d\n", path,
		sp.Start.Line, sp.Start.Column, sp.End.Line, sp.End.Column)
	return nil
}, tomledit.WalkLeaves)

Span() is available on all nodes: tables and array-tables (covering only the [header]), key-value pairs (key through value), keys, all scalars, arrays, inline tables, and comments. Span.End is exclusive -- the position immediately after the node's last byte.

Span policy: spans reflect the last Parse. Edit operations do not recompute them: nodes created programmatically (via Set, Merge, ...) return the zero span (Span.IsValid() reports false), and edited nodes keep their parse-time spans. Re-parse the output of Bytes() when fresh positions are needed after editing.

Files
import "github.com/stricttools/go-toml-edit/tomlfile"

doc, err := tomlfile.ParseFile("config.toml") // remembers the filename
err = tomlfile.WriteFile(doc, "config.toml")  // atomic, round-trip checked

The tomledit package reads and writes no file: it computes only over the bytes and documents it is handed. File access is the tomlfile subpackage's. A document loaded with tomlfile.ParseFile names the file in every diagnostic it later produces; tomledit.ParseWithFileName does the same for bytes the caller read. tomlfile.WriteFile writes the bytes Document.RoundTripVerifiedBytes returns -- bytes proved to re-parse to themselves -- and replaces the destination in one step.

Diagnostics
_, err := doc.GetInt("server.port")

if errors.Is(err, tomledit.ErrTypeMismatch) { /* ... */ }

var diag *tomledit.Error
if errors.As(err, &diag) {
	fmt.Println(diag.Kind, diag.Path, diag.Pos.Line, diag.Expected, diag.Got)
}

Every failure that depends on the document -- parse, decode, edit, access -- is an *Error with a kind, a path and a position, rendered in the compiler convention (config.toml:12:7: server.port: expected integer, got string). A decode reports its violations together as an *Errors aggregate, reachable with errors.As; its Diagnostics method lists them. A diagnostic reported through a Reader names a line without a column and a fix after the message (config.toml:4: server.port: is missing; write port = <integer>).

Diff
changes := tomledit.Diff(a, b)
for _, c := range changes {
	fmt.Printf("%s: %s\n", c.Kind, c.Path)
}
// removed: age
// added: email
// modified: name

Diff reads values, never spellings: two documents writing one value differently report no difference, so a document always compares equal to itself. Types are not bridged -- 1 and 1.0 are a modification.

Merge
base, _ := tomledit.Parse([]byte(`[server]
host = "localhost"
`))
defaults, _ := tomledit.Parse([]byte(`[server]
host = "0.0.0.0"
port = 8080
`))
base.Merge(defaults)
// host keeps "localhost" (already set); port added from defaults.

Performance

go-toml-edit retains the full AST -- source positions, trivia, comment nodes, and the lexeme every value was written as -- because that is what comment-preserving round-trip editing needs. A decode-only library discards all of it, so it parses faster and allocates less. Reach for a decode-only library when you only need Go values out of a TOML file; reach for this one when the comments have to survive the write-back.

Run go test -bench . to measure on your own machine. benchmarks.txt holds a captured run beside BurntSushi/toml.

API Reference

pkg.go.dev/github.com/stricttools/go-toml-edit

Deferred work

A struct-to-TOML Marshal, the remaining structural-manipulation suite (node moves, positional insertion, table/inline conversion), external node construction with spelling control, and a streaming parser are deferred rather than planned. TOML 1.1 is not supported: this library targets TOML 1.0 only. See todo/ for details.

License

MIT

Documentation

Overview

Package tomledit: Zero-dep TOML editing library for Go with comment preservation.

The tree preserves comments, whitespace and formatting. Values can be read, set, deleted and renamed without disturbing unrelated parts of the file. A document is serialized back with Document.Bytes (round-trip fidelity) or reformatted with Document.Format.

Two surfaces, two questions

A document is readable through two surfaces, and which one to use follows from the question being asked.

The SYNTACTIC surface is the AST: Document.Walk, Document.Resolve and the concrete node types. It answers what the file CONTAINS, in the form it was written -- spellings, quoting styles, integer bases, comments, blank lines and the source span of every construct. A value written as an inline table arrives as an *InlineTableNode; one written under a header arrives as that header's children.

The LOGICAL surface is the read-layer: Document.Root returns a Record, whose Entry values are the document's keys in first-appearance order. It answers what the document MEANS -- values, structure and order with the spellings folded away. A dotted key, a [header] table and an inline table are indistinguishable through it, and so is a table only a longer header implies.

Read values through the read-layer or the typed accessors; use the AST to edit, or to inspect how something was written. Reads are spelling-blind; writes are structurally conservative -- a value write touches value fragments and nothing else, and a structural construct changes only through a structural operation or an explicit Document.Delete.

Strictness and diagnostics

Decoding is strict and strictness is the only mode: an unknown key, an unknown table, a value of a refused kind, a value the target cannot hold exactly, and a missing required key are all errors. There is no lenient mode and no option to skip a check.

Every failure that depends on the document -- from parsing, decoding, editing or access alike -- is an *Error carrying a kind, a document path and a position, matchable with errors.Is against the ErrSyntax family of kind sentinels and readable with errors.As. A decode reports every independent violation together as an *Errors aggregate in document order; parsing reports its first failure alone, because a parse cannot continue past a syntax error.

Reading by hand

A file whose rules go beyond its shape is read key by key through a Reader over one table, which records every problem in a Collector: Collector.Read parses a file's bytes into a Reader over its top-level table, the accessors (Reader.Str, Reader.Integer, Reader.Flag, Reader.Tables, Reader.Sub, and the rest) read keys and report a missing key or a value of the wrong type with a fix saying what to write, and Reader.Done reports every key nobody read as unknown, with the kinds a strict decode uses. Collector.Err returns everything recorded, from every file, sorted by file, line, path, and message.

Capabilities

  • Lossless round-trip: parse and re-serialize without losing comments or formatting.
  • Path-based access: read and write values through paths such as "server.host", "items[0]" and "items[-1]".
  • Structural editing: create tables and array-of-tables, reorder children, append to and remove from arrays, rename and delete keys, seed defaults.
  • Strict decoding: into Go structs through Unmarshal, Decode, DecodeNode and DecodeOver, or against a hand-built descriptor through Document.Validate and Document.DecodeSpec.
  • Diff and merge: compare two documents, or merge one into another.
  • File names and writes: ParseWithFileName records a filename for later diagnostics, and Document.RoundTripVerifiedBytes renders only bytes that survive a round trip. The package reads and writes no file itself: the tomlfile subpackage reads a file and parses it, and writes a document atomically through that check.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrSyntax         error = kindError(KindSyntax)         // a lexing or parsing failure
	ErrUnknownKey     error = kindError(KindUnknownKey)     // a key matching no field of the target
	ErrUnknownTable   error = kindError(KindUnknownTable)   // a table matching no field of the target
	ErrMissingKey     error = kindError(KindMissingKey)     // a required key the document does not carry
	ErrTypeMismatch   error = kindError(KindTypeMismatch)   // a value whose kind the target refuses
	ErrInexact        error = kindError(KindInexact)        // a value the target cannot hold exactly
	ErrNotFound       error = kindError(KindNotFound)       // a path naming nothing
	ErrBadPath        error = kindError(KindBadPath)        // a syntactically invalid path
	ErrWrongContainer error = kindError(KindWrongContainer) // a structurally inapplicable path step
	ErrBadInput       error = kindError(KindBadInput)       // an invalid input to an editing operation
	ErrConflict       error = kindError(KindConflict)       // an edit that would produce an invalid document
	ErrRoundTrip      error = kindError(KindRoundTrip)      // rendered bytes that did not survive a re-parse
	ErrInvalidValue   error = kindError(KindInvalidValue)   // a value a hand-reading caller refused
)

The kind sentinels. Match a diagnostic against one with errors.Is:

if errors.Is(err, tomledit.ErrNotFound) { ... }

Every ErrorKind has exactly one sentinel and every sentinel has exactly one kind; TestErrorKindSentinelDrift fails when one is added without the other.

View Source
var ErrSkipTable = errors.New("skip table")

ErrSkipTable is a sentinel error returned from a Walk visitor function to skip the current table's children (or inline table's children). Returning ErrSkipTable on a scalar node is a no-op.

Functions

func Decode

func Decode[T any](d *Document) (*T, error)

Decode decodes the document into a freshly allocated T -- a struct, a map with string keys, any, or any other type the conversion table accepts.

The type argument is explicit, because Go infers nothing from a return type:

cfg, err := tomledit.Decode[Config](doc)

Decode writes to no memory the caller can observe. It allocates the value it returns, and on any failure it returns (nil, err): there is no partially written target, and nothing to inspect after an error.

Struct fields are matched by their "toml" tag, or, with no tag, by their exact field name. Matching is exact: a document key that differs only in case from a field's name is an unknown key, not a match. A tag of "-" excludes the field, and an excluded name is not part of the document's universe at all: a document key naming one is as unknown as any other. The only tag option the package reads is "required" (`toml:"port,required"`), which makes the key's absence an error; any other option is refused, because an option nothing reads is a silent no-op. Embedded structs promote their fields and pointer fields are allocated as they are reached.

Every value goes through the conversion table: no target decodes itself, so a field of type time.Time accepts the date-time kinds the table lists for it and refuses a string with KindTypeMismatch, exactly as Scalar.AsTime and Document.GetTime do. To read a string as a time, decode it as a string and parse it.

Decoding is strict and strictness is the only mode: an unknown key, an unknown table, a value of a refused kind, a value the target cannot hold exactly, and a missing required key are all errors. A map-typed or any-typed target matches every key by construction and so reports no unknown keys.

Every independent violation is collected and reported together as an *Errors aggregate, in document order; it renders as its first diagnostic and exposes the rest through errors.As.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	type Server struct {
		Host string `toml:"host"`
		Port int    `toml:"port"`
	}
	type Config struct {
		Title  string `toml:"title"`
		Server Server `toml:"server"`
	}

	doc, err := tomledit.Parse([]byte(`title = "My App"

[server]
host = "0.0.0.0"
port = 443
`))
	if err != nil {
		panic(err)
	}

	// Decode returns the value it built; the document stays available for
	// comment-preserving edits.
	cfg, err := tomledit.Decode[Config](doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(cfg.Title, cfg.Server.Host, cfg.Server.Port)

	if err := doc.Set("server.port", cfg.Server.Port+1); err != nil {
		panic(err)
	}
	port, err := doc.GetInt("server.port")
	if err != nil {
		panic(err)
	}
	fmt.Println(port)
}
Output:
My App 0.0.0.0 443
444
Example (Strict)
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	type Config struct {
		Title string `toml:"title"`
	}
	doc, err := tomledit.Parse([]byte("title = \"ok\"\ntilte = \"typo\"\n"))
	if err != nil {
		panic(err)
	}

	// Strictness is the only mode: an unknown key is an error, and a failed
	// decode returns no value to inspect.
	cfg, err := tomledit.Decode[Config](doc)
	fmt.Println(cfg == nil)
	fmt.Println(errors.Is(err, tomledit.ErrUnknownKey))
	fmt.Println(err)
}
Output:
true
true
2:1: tilte: unknown key "tilte"

func DecodeNode

func DecodeNode[T any](n Node) (*T, error)

DecodeNode decodes a single node into a freshly allocated T. It is Decode scoped to one construct: a table, an array-table entry, an inline table or a document decodes like a table, an array decodes like an array, and a scalar node decodes into a scalar target. A key, a key-value pair and a comment carry no value of their own and are refused.

The rules are Decode's, including strictness and the unobservability of a failure: on any failure DecodeNode returns (nil, err). A diagnostic names the file the node's document was read from, which the node reaches through the parent links its container maintains; a node not attached to a document names none.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	type Server struct {
		Host string `toml:"host"`
		Port int    `toml:"port"`
	}
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 5432
`))
	if err != nil {
		panic(err)
	}

	// One construct decoded on its own.
	node, err := doc.Resolve("server")
	if err != nil {
		panic(err)
	}
	server, err := tomledit.DecodeNode[Server](node)
	if err != nil {
		panic(err)
	}
	fmt.Println(server.Host, server.Port)
}
Output:
localhost 5432

func DecodeOver

func DecodeOver[T any](d *Document, seed func() T) (*T, []string, error)

DecodeOver decodes the document over a seed: the value the factory builds receives whatever the document supplies, and every key the document does not carry keeps what the seed put there. It is how a set of defaults expressed in Go is overlaid by a configuration file.

The seed is a FACTORY, not a value, so that what the decode fills is an allocation of DecodeOver's own and never memory the caller still holds:

cfg, written, err := tomledit.DecodeOver(doc, func() Config { return defaultConfig() })

The factory must build its seed afresh on each call. A seed that shares a map, a slice or a pointer with the caller lets the decode write through it, which is exactly what the factory form exists to prevent.

The second result names the paths the decode wrote, in document order and in the library's path syntax, so a caller can tell a value the document supplied from one the seed left behind. A value written whole -- an array, an any-typed table -- is one path, not one per element inside it.

The rules are Decode's, including strictness and the unobservability of a failure: on any failure DecodeOver returns (nil, nil, err) and the seed it built is unreachable.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	type Config struct {
		Host    string `toml:"host"`
		Port    int    `toml:"port"`
		Verbose bool   `toml:"verbose"`
	}
	defaults := func() Config {
		return Config{Host: "localhost", Port: 8080, Verbose: false}
	}

	doc, err := tomledit.Parse([]byte("port = 9090\n"))
	if err != nil {
		panic(err)
	}

	// The factory builds the seed, the document overlays it, and the second
	// result names the paths the document supplied.
	cfg, written, err := tomledit.DecodeOver(doc, defaults)
	if err != nil {
		panic(err)
	}
	fmt.Println(cfg.Host, cfg.Port, cfg.Verbose)
	fmt.Println(written)
}
Output:
localhost 9090 false
[port]

func FormatFloat

func FormatFloat(f float64) string

FormatFloat returns f as a TOML float. It is TOTAL: every float64 has an output, including the ones TOML has no numeric spelling for.

A finite value is written in the shortest form that reads back as the same float64, with a float marker always present -- a ".0" is appended when the shortest form carries neither a fractional part nor an exponent, so the result never reads back as an integer. Negative zero keeps its sign.

The three non-finite values are written "nan", "inf" and "-inf". A NaN with its sign bit set is written "nan" like any other: the library never writes "+nan", "-nan" or "+inf", which no reader needs and which say nothing about the value. (The value-writing operations refuse a sign-bit NaN as input rather than silently dropping the sign; this renderer, having no input to refuse, renders it.)

Example
package main

import (
	"fmt"
	"math"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	fmt.Println(tomledit.FormatFloat(1))
	fmt.Println(tomledit.FormatFloat(0.1))
	fmt.Println(tomledit.FormatFloat(1e21))
	fmt.Println(tomledit.FormatFloat(math.Inf(-1)))
	fmt.Println(tomledit.FormatFloat(math.NaN()))
}
Output:
1.0
0.1
1e+21
-inf
nan

func JoinPath

func JoinPath(segs []PathSegment) string

JoinPath renders segments as path text, and is the single quoting authority for paths: a key segment is written bare when every byte of it is legal in a bare key, and quoted (with quotes and backslashes escaped) otherwise, so that ParsePath reads back exactly the segments JoinPath was given. An index segment is written in brackets, attached to whatever precedes it.

The result of joining no segments is the empty string, which ParsePath refuses: a path names at least one step.

func PathWithoutIndices added in v0.6.0

func PathWithoutIndices(path string) string

PathWithoutIndices returns a key path without its array indices, the way a file's headers spell it: "command[0].flag[1]" gives "command.flag".

func QuoteKey

func QuoteKey(s string) string

QuoteKey returns s as a TOML key: bare when TOML's bare-key rule allows it (ASCII letters, digits, hyphens and underscores, and at least one character), and the basic-string form of QuoteString otherwise. The empty key is therefore written as a pair of quotes.

It quotes ONE key. A dotted path is a sequence of keys, each quoted on its own and joined with dots; JoinPath does that for the library's path syntax.

It is total on the same terms as QuoteString, and inherits its treatment of a key that is not valid UTF-8: the invalid bytes render as U+FFFD, and the key-writing operations refuse such a key before it ever reaches here.

func QuoteString

func QuoteString(s string) string

QuoteString returns s as a TOML basic string: double-quoted, with the backslash, the double quote and the control characters escaped and everything else written verbatim.

The escapes are exactly TOML's own set. Backspace, tab, newline, form feed and carriage return take their short forms; every other control character, U+007F included, takes the four-digit "\u" form with LOWERCASE hex digits. Non-ASCII text is written as itself: a basic string carries it directly, and escaping it would only make the result harder to read.

It is TOTAL: every Go string has an output. For a string that is valid UTF-8 -- which is every string a write can carry, since the value-writing operations refuse anything else -- the result parses back to s, so QuoteString is the inverse of reading a string value. A string that is NOT valid UTF-8 is rendered with each invalid byte as U+FFFD, and the inverse property does not hold for it: no TOML spelling carries such a byte, and this renderer has no input to refuse.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	fmt.Println(tomledit.QuoteString(`say "hi"`))
	fmt.Println(tomledit.QuoteString("tab\there"))
	fmt.Println(tomledit.QuoteKey("bare_key"))
	fmt.Println(tomledit.QuoteKey("not a bare key"))
}
Output:
"say \"hi\""
"tab\there"
bare_key
"not a bare key"

func Unmarshal

func Unmarshal[T any](data []byte) (*T, error)

Unmarshal parses TOML data and decodes it into a freshly allocated T.

Decoding is strict: see Decode, whose rules Unmarshal is exactly. A parse failure is reported alone (parsing cannot continue past a syntax error); decode failures are collected and reported together as an *Errors aggregate.

On any failure Unmarshal returns a nil result: there is no state to inspect after an error.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	type Config struct {
		Title  string `toml:"title"`
		Server struct {
			Host string `toml:"host"`
			Port int    `toml:"port"`
		} `toml:"server"`
	}
	cfg, err := tomledit.Unmarshal[Config]([]byte(`title = "My App"

[server]
host = "0.0.0.0"
port = 443
`))
	if err != nil {
		panic(err)
	}
	fmt.Println(cfg.Title)
	fmt.Println(cfg.Server.Host)
	fmt.Println(cfg.Server.Port)
}
Output:
My App
0.0.0.0
443
Example (IntoAMap)
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	m, err := tomledit.Unmarshal[map[string]any]([]byte(`title = "My App"

[server]
port = 443
`))
	if err != nil {
		panic(err)
	}
	// The result is a pointer to the value Unmarshal built.
	fmt.Println((*m)["title"])
	fmt.Println((*m)["server"].(map[string]any)["port"])
}
Output:
My App
443

Types

type ArrayNode

type ArrayNode struct {
	// contains filtered or unexported fields
}

ArrayNode represents an array value.

func (*ArrayNode) Comment

func (n *ArrayNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*ArrayNode) Elements

func (n *ArrayNode) Elements() []Node

Elements returns the array's elements in order, as a copy.

func (*ArrayNode) LeadingComments

func (n *ArrayNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*ArrayNode) Raw

func (n *ArrayNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*ArrayNode) Span

func (n *ArrayNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*ArrayNode) Type

func (n *ArrayNode) Type() NodeType

Type returns NodeArray.

type ArrayTableNode

type ArrayTableNode struct {
	// contains filtered or unexported fields
}

ArrayTableNode represents an [[array-table]] header and its children.

func (*ArrayTableNode) Children

func (n *ArrayTableNode) Children() []Node

Children returns the constructs written under this header, in document order, as a copy.

func (*ArrayTableNode) Comment

func (n *ArrayTableNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*ArrayTableNode) KeyPath

func (n *ArrayTableNode) KeyPath() []string

KeyPath returns the parts of the header's key, decoded, as a copy.

func (*ArrayTableNode) LeadingComments

func (n *ArrayTableNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*ArrayTableNode) Raw

func (n *ArrayTableNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*ArrayTableNode) Span

func (n *ArrayTableNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*ArrayTableNode) Type

func (n *ArrayTableNode) Type() NodeType

Type returns NodeArrayTable.

type BooleanNode

type BooleanNode struct {
	// contains filtered or unexported fields
}

BooleanNode represents a boolean value.

func (*BooleanNode) AsBool

func (n *BooleanNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*BooleanNode) AsFloat

func (n *BooleanNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*BooleanNode) AsInt

func (n *BooleanNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*BooleanNode) AsLocalDate

func (n *BooleanNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*BooleanNode) AsLocalDateTime

func (n *BooleanNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*BooleanNode) AsLocalTime

func (n *BooleanNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*BooleanNode) AsString

func (n *BooleanNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*BooleanNode) AsTime

func (n *BooleanNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*BooleanNode) Comment

func (n *BooleanNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*BooleanNode) LeadingComments

func (n *BooleanNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*BooleanNode) Raw

func (n *BooleanNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*BooleanNode) Span

func (n *BooleanNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*BooleanNode) Type

func (n *BooleanNode) Type() NodeType

Type returns NodeBoolean.

func (*BooleanNode) Value

func (n *BooleanNode) Value() any

Value returns the boolean value.

type Change

type Change struct {
	Kind     ChangeKind
	Path     string
	OldValue any // nil for Added
	NewValue any // nil for Removed
}

Change represents a single difference between two documents. OldValue is nil for Added changes; NewValue is nil for Removed changes.

func Diff

func Diff(a, b *Document) []Change

Diff returns all differences between documents a and b.

It walks both documents to collect all leaf (scalar) values, then compares them. Container nodes (inline tables, arrays) are not compared directly; instead their individual elements are compared. Changes are sorted by path (alphabetical), then by kind (Removed, Modified, Added).

The comparison reads values, never spellings. Two documents writing one value differently -- 0x2A against 42, 1_000 against 1000, one instant in two zone offsets, a literal string against a basic one, an array-of-tables against an inline array of inline tables -- report no difference. A document therefore always compares equal to itself.

Types are not bridged: an integer and a float never compare equal, so 1 and 1.0 are a modification.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	a, _ := tomledit.Parse([]byte(`name = "Alice"
age = 30
`))
	b, _ := tomledit.Parse([]byte(`name = "Bob"
email = "bob@example.com"
`))
	changes := tomledit.Diff(a, b)
	for _, c := range changes {
		fmt.Printf("%s: %s\n", c.Kind, c.Path)
	}
}
Output:
removed: age
added: email
modified: name

type ChangeKind

type ChangeKind int

ChangeKind identifies the type of difference between two documents.

const (
	Added    ChangeKind = iota // Added means the key exists in b but not in a.
	Removed                    // Removed means the key exists in a but not in b.
	Modified                   // Modified means the key exists in both but with different values.
)

func (ChangeKind) String

func (k ChangeKind) String() string

String returns the human-readable name of the change kind.

type Collector added in v0.6.0

type Collector struct {
	// contains filtered or unexported fields
}

Collector accumulates diagnostics, from one file or several, and reports them together as one error sorted by file, line, path, and message, so a tool reading a set of files reports every problem in all of them at once, in an order that does not depend on the order the files were read in:

var c tomledit.Collector
if r := c.Read("a.toml", a); r != nil { readA(r) }
if r := c.Read("b.toml", b); r != nil { readB(r) }
if err := c.Err(); err != nil { return err }

The zero Collector is empty and ready to use.

func (*Collector) Add added in v0.6.0

func (c *Collector) Add(err error)

Add records every diagnostic err carries: each one of an *Errors aggregate (a decode's result, say), or the one *Error err is or wraps. An error carrying no diagnostic is recorded as a KindConflict diagnostic wrapping it. A nil err records nothing.

func (*Collector) Err added in v0.6.0

func (c *Collector) Err() error

Err returns every diagnostic recorded so far, sorted by file, line, path, and message, as an *Errors aggregate, or nil when there are none. The sort is stable, so two diagnostics equal in all four keep the order they were recorded in. Recording more after Err leaves the returned error unchanged.

func (*Collector) Errorf added in v0.6.0

func (c *Collector) Errorf(file string, line int, path, format string, args ...any)

Errorf records a KindInvalidValue diagnostic at a line of a file, naming the key path it concerns (in this package's path syntax, such as "command[0].flag[1].name") and a formatted message. It is the form for a problem found across tables or files, which no single Reader owns; within one table, Reader.Errorf names the file, line, and path itself.

func (*Collector) Len added in v0.6.0

func (c *Collector) Len() int

Len returns how many diagnostics the collector holds.

func (*Collector) Read added in v0.6.0

func (c *Collector) Read(file string, data []byte) *Reader

Read parses one file's bytes and returns a Reader over its top-level table. A syntax error is recorded as a KindSyntax diagnostic naming the file, at the position Parse reports, with the fix "write valid TOML", and Read returns nil.

type CommentNode

type CommentNode struct {
	// contains filtered or unexported fields
}

CommentNode represents a standalone comment line.

func (*CommentNode) Comment

func (n *CommentNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*CommentNode) LeadingComments

func (n *CommentNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*CommentNode) Raw

func (n *CommentNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*CommentNode) Span

func (n *CommentNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*CommentNode) Text

func (n *CommentNode) Text() string

Text returns the comment line as it was written, including its "#" and its trailing newline. A comment node standing for a run of blank lines has no text of its own; Raw carries the blank bytes.

func (*CommentNode) Type

func (n *CommentNode) Type() NodeType

Type returns NodeComment.

type Cursor

type Cursor struct {
	// contains filtered or unexported fields
}

Cursor provides a fluent, nil-safe API for navigating a TOML document. A Cursor is never nil. If navigation fails at any point, the cursor captures the error and all subsequent operations (Key, At, String, etc.) become no-ops that propagate the original error. Check Err after a chain of calls to see whether the traversal succeeded.

A cursor navigates the read-layer, so Key and At step through compound tables and array-of-tables the same way a path does: Key crosses into a table however the document spells it, and At addresses an entry of an array-of-tables or an element of an array.

func (*Cursor) At

func (c *Cursor) At(index int) *Cursor

At navigates to an array index. Supports negative indices.

func (*Cursor) Bool

func (c *Cursor) Bool() (bool, error)

Bool reads the value at the cursor as a bool.

The error is the navigation failure that ended the chain, or KindTypeMismatch when the value is not a boolean.

func (*Cursor) Err

func (c *Cursor) Err() error

Err returns the first error encountered during navigation, as an *Error naming the document's file when it has one.

func (*Cursor) Float

func (c *Cursor) Float() (float64, error)

Float reads the value at the cursor as a float64: a float verbatim, and an integer the target holds exactly.

The error is the navigation failure that ended the chain, KindTypeMismatch when the value is neither a float nor an integer, or KindInexact for an integer no float64 holds exactly.

func (*Cursor) Int

func (c *Cursor) Int() (int64, error)

Int reads the value at the cursor as an int64.

The error is the navigation failure that ended the chain, or KindTypeMismatch when the value is not an integer.

func (*Cursor) Items

func (c *Cursor) Items() iter.Seq2[int, Node]

Items returns a range-over-func iterator over elements of the current node. Works with ArrayNode elements and array-of-tables entries. An inert cursor (with error) yields nothing.

func (*Cursor) Key

func (c *Cursor) Key(name string) *Cursor

Key navigates to a named child within the current scope.

func (*Cursor) Len

func (c *Cursor) Len() int

Len returns the number of elements at the current cursor position. Returns -1 if the cursor has an error or the node isn't an array/array-of-tables.

func (*Cursor) Node

func (c *Cursor) Node() Node

Node returns the node at the cursor's position, or nil if the cursor has an error. A position no single node stands for -- an array-of-tables, or a table implied by a longer header or a dotted key -- has no node to return: Node reports that through Err as a KindWrongContainer diagnostic and returns nil.

func (*Cursor) String

func (c *Cursor) String() (string, error)

String reads the value at the cursor as a string. It is deliberately not a fmt.Stringer: a cursor is a position, not a rendering of one.

The error is the navigation failure that ended the chain, or KindTypeMismatch when the value is not a string.

func (*Cursor) Time

func (c *Cursor) Time() (time.Time, error)

Time reads the value at the cursor as a time.Time: an offset date-time verbatim, and a local date-time or local date read as UTC.

The error is the navigation failure that ended the chain, or KindTypeMismatch when the value is not one of those three flavors -- a string among them, even one spelling a valid RFC 3339 timestamp, which a time.Time decode target refuses too.

type DateTimeNode

type DateTimeNode struct {
	// contains filtered or unexported fields
}

DateTimeNode represents an offset date-time value.

func (*DateTimeNode) AsBool

func (n *DateTimeNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*DateTimeNode) AsFloat

func (n *DateTimeNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*DateTimeNode) AsInt

func (n *DateTimeNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*DateTimeNode) AsLocalDate

func (n *DateTimeNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*DateTimeNode) AsLocalDateTime

func (n *DateTimeNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*DateTimeNode) AsLocalTime

func (n *DateTimeNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*DateTimeNode) AsString

func (n *DateTimeNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*DateTimeNode) AsTime

func (n *DateTimeNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*DateTimeNode) Comment

func (n *DateTimeNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*DateTimeNode) LeadingComments

func (n *DateTimeNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*DateTimeNode) Raw

func (n *DateTimeNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*DateTimeNode) Span

func (n *DateTimeNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*DateTimeNode) Type

func (n *DateTimeNode) Type() NodeType

Type returns NodeDateTime.

func (*DateTimeNode) Value

func (n *DateTimeNode) Value() any

Value returns the time.Time value.

type Default

type Default struct {
	Path  string
	Value any
}

Default is one entry of the EnsureDefaults input: a full PATH in this package's path syntax, and the value to seed there when the document does not carry it.

Path is a path, not a key: "server.host" reaches the "host" of the "server" table, and a key that carries a dot of its own is quoted, `server."host.name"`. Pair is the other way round -- one key, taken verbatim -- and the two are separate types so that neither grammar can be handed to the other by accident.

type Document

type Document struct {
	// contains filtered or unexported fields
}

Document is the root node of a TOML document.

func Parse

func Parse(src []byte) (*Document, error)

Parse lexes and parses TOML source bytes into a Document AST.

The returned Document preserves all whitespace, comments, and formatting from the original source. Serializing it back with Bytes produces the exact original bytes (round-trip fidelity).

Every lexing or parsing failure -- including duplicate key detection and invalid TOML syntax -- is reported as an *Error of kind KindSyntax, carrying the position, span and source line of the offending construct. Parsing stops at the first failure.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 8080
`))
	if err != nil {
		panic(err)
	}
	host, err := doc.GetString("server.host")
	if err != nil {
		panic(err)
	}
	port, err := doc.GetInt("server.port")
	if err != nil {
		panic(err)
	}
	fmt.Println(host)
	fmt.Println(port)
}
Output:
localhost
8080

func ParseWithFileName added in v0.7.0

func ParseWithFileName(src []byte, file string) (*Document, error)

ParseWithFileName parses src as Parse does, and records file as the document's origin, so every diagnostic it later produces -- from parsing, access or editing -- names that file. It reads nothing: file is only the name the diagnostics carry. The tomlfile subpackage reads a file and parses it through this function.

Example
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.ParseWithFileName([]byte("[server]\nport = \"8080\"\n"), "config.toml")
	if err != nil {
		panic(err)
	}
	// The document remembers the name it was parsed under, so a later
	// diagnostic names the file, the line and the column.
	_, err = doc.GetInt("server.port")
	var diag *tomledit.Error
	if errors.As(err, &diag) {
		fmt.Println(diag.File, diag.Pos.Line, diag.Pos.Column, diag.Path, diag.Expected, diag.Got)
	}
}
Output:
config.toml 2 8 server.port integer string

func (*Document) AppendToArray

func (d *Document) AppendToArray(path string, value any) error

AppendToArray appends a value to the array the path names.

The value is converted the way Set converts one: the Go types listed there, including []Pair for an ordered inline table. An array-of-tables is not an array value -- add an entry to one with NewArrayTable.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte("ports = [8080, 8081]\n"))
	if err != nil {
		panic(err)
	}
	if err := doc.AppendToArray("ports", 8082); err != nil {
		panic(err)
	}
	if err := doc.RemoveFromArray("ports", 0); err != nil {
		panic(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
ports = [8081, 8082]

func (*Document) Bytes

func (d *Document) Bytes() []byte

Bytes serializes the document back to TOML bytes.

A document parsed and never edited renders as the exact bytes it was parsed from. An edited one renders per FRAGMENT: every byte range an edit did not touch is written back as it was read, and only the ranges the edits invalidated are rendered anew, in the library's canonical spellings.

So a value write leaves the line's spacing, its inline comment and the quoting of its key exactly as they were; a comment write leaves the value's base and quoting; a write inside an array or an inline table leaves every sibling, separator and interior comment; and a rename leaves everything but the renamed key part. What the library writes -- and only that -- is written in the forms of QuoteString, QuoteKey and FormatFloat.

func (*Document) Children

func (n *Document) Children() []Node

Children returns the document's top-level constructs in document order, as a copy.

func (*Document) Comment

func (n *Document) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*Document) DecodeSpec

func (d *Document) DecodeSpec(spec *Spec) (map[string]any, error)

DecodeSpec checks the document against the descriptor and, when it satisfies it, returns the document's values as native Go data: map[string]any for every table spelling, []any for arrays and arrays of tables, and string, int64, float64, bool, time.Time, LocalDateTime, LocalDate or LocalTime for scalars.

It is the descriptor path's decode: the same engine Validate runs, followed by a value built out of the read-layer. No reflection is involved and no consumer code runs, so the result is exactly what the document says.

DecodeSpec is ATOMIC, without exception: it returns a map only when the document has no violations at all, and (nil, err) otherwise. There is no partial map, no half-populated table, and nothing to inspect after an error -- the diagnostics are the whole answer. This is a stronger promise than the typed entry points make, and it holds because nothing outside this package participates in building the map.

The errors are Validate's: an *Errors aggregate of every independent violation in document order, or a plain error for a descriptor that describes nothing.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 8080
`))
	if err != nil {
		panic(err)
	}

	spec := &tomledit.Spec{Fields: map[string]tomledit.Field{
		"server": {Kind: tomledit.FieldKindTable, Table: &tomledit.Spec{
			Fields: map[string]tomledit.Field{
				"host": {Kind: tomledit.FieldKindString},
				"port": {Kind: tomledit.FieldKindInteger},
			},
		}},
	}}

	// A descriptor decode runs no consumer code: the result is exactly what
	// the document says, and it is all-or-nothing.
	values, err := doc.DecodeSpec(spec)
	if err != nil {
		panic(err)
	}
	server := values["server"].(map[string]any)
	fmt.Println(server["host"], server["port"])
}
Output:
localhost 8080

func (*Document) Delete

func (d *Document) Delete(path string) error

Delete removes the node at the given path from the document. It handles key-value pairs, tables, array-of-tables, and array elements.

Removal is idempotent: a path the document does not carry is a silent no-op, so an ensure-absent loop can call it unconditionally. A path that cannot be parsed, and a document the read-layer cannot fold at all, are still reported.

The spelling of the table the key sits in changes nothing about that. In a table no single node stands for -- one a dotted key spelled out, or one only a longer header implies -- the removal reaches the dotted pair that binds a value, or the headers that spell a table out, those of the tables nested inside it included.

Example
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 8080
debug = true
`))
	if err != nil {
		panic(err)
	}
	err = doc.Delete("server.debug")
	if err != nil {
		panic(err)
	}
	// The key is gone, so reading it is a not-found diagnostic.
	_, err = doc.GetBool("server.debug")
	fmt.Println(errors.Is(err, tomledit.ErrNotFound))
}
Output:
true

func (*Document) EnsureDefaults

func (d *Document) EnsureDefaults(defaults []Default) (added []string, err error)

EnsureDefaults seeds the paths the document does not already carry, in the order given, and returns the paths it added.

A path the document carries in ANY spelling is left alone: a key written as a dotted key, inside a [header] table, or inside an inline table all count as present, and so does a table that only exists because a longer header implies it. Nothing is ever overwritten and no existing value, comment or spelling is touched.

A missing intermediate table is created as a standard [header] table, never an inline one, so what the document ends up spelling depends only on the list and not on the order in which the intermediates happened to be needed: running the same list against the same document twice writes the same bytes.

Partial application: the seeding stops at the first error, everything written before it stays written, and added names exactly those paths -- so a caller that must know what it changed reads them from the return value rather than from a diff. The default that failed wrote nothing at all, not even the tables leading to it, so added is exact rather than approximate.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
`))
	if err != nil {
		panic(err)
	}

	added, err := doc.EnsureDefaults([]tomledit.Default{
		{Path: "server.host", Value: "0.0.0.0"}, // already there, left alone
		{Path: "server.port", Value: 8080},
		{Path: "logging.level", Value: "info"},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(added)
	fmt.Print(string(doc.Bytes()))
}
Output:
[server.port logging.level]
[server]
host = "localhost"
port = 8080
[logging]
level = "info"

func (*Document) Format

func (d *Document) Format(opts ...FormatOption) []byte

Format returns normalized TOML bytes. It does NOT mutate the document -- it produces a new byte slice by walking the AST and re-rendering every node with consistent formatting, ignoring all raw bytes. This is useful for enforcing a canonical style. Pass zero or more FormatOption values (e.g. WithIndentWidth, WithLineWidth) to customize the output.

The writer's blank-line grouping survives at document and table-body level: a run of blank lines becomes exactly one. The output never begins with a blank line and always ends with exactly one newline, so blank lines at either end of the document are dropped. Arrays are restructured wholesale (inline or multi-line from the configured line width), so blank lines between array elements do not survive formatting; Bytes preserves them.

The one gap the formatter opens itself is before a [table] or [[array-table]] header written flush against what precedes it: Format always inserts the missing blank line. What this library preserves is CONTENT -- the comments -- while whitespace is formatting, and Format is where the library is strict about output looking good, so the separation is not a caller's option. The insertion never removes anything and never doubles a blank line already there, so a document that separates its own tables is left as it stands.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`name="Alice"
[server]
host="localhost"
port=8080
`))
	if err != nil {
		panic(err)
	}
	formatted := doc.Format(tomledit.WithIndentWidth(2))
	fmt.Print(string(formatted))
}
Output:
name = "Alice"

[server]
  host = "localhost"
  port = 8080

func (*Document) GetBool

func (d *Document) GetBool(path string) (bool, error)

GetBool resolves the path and reads the value it names as a bool.

The error is the unified *Error: KindBadPath, KindNotFound or KindWrongContainer from the navigation, and KindTypeMismatch when the value is not a boolean.

func (*Document) GetComment

func (d *Document) GetComment(path string) (string, error)

GetComment returns the inline comment on the node at the given path, as text: without the "#" and the whitespace around it, so a line written `x = 1 # note` answers "note". A node with no inline comment answers the empty string.

The path resolves to the same node SetComment writes to -- for a key-value path the pair, not the unwrapped value; for a table path the header line -- so the two round-trip: what SetComment writes, GetComment reads back. It answers the NORMALIZED text, which is what SetComment takes, and a caller that needs the bytes as written reads Raw on the node instead.

The navigation errors are SetComment's: a path naming nothing is KindNotFound, a path naming a member of an inline table is KindWrongContainer (TOML gives an inline table nowhere to put a comment, so nothing there can carry one to read), and a malformed path is KindBadPath.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(
		"# The port to bind to.\n" +
			"# Any value above 1024 needs no privileges.\n" +
			"port = 8080  # default HTTP port\n"))
	if err != nil {
		panic(err)
	}

	inline, err := doc.GetComment("port")
	if err != nil {
		panic(err)
	}
	leading, err := doc.GetLeadingComments("port")
	if err != nil {
		panic(err)
	}

	fmt.Printf("inline: %q\n", inline)
	for _, line := range leading {
		fmt.Printf("leading: %q\n", line)
	}

}
Output:
inline: "default HTTP port"
leading: "The port to bind to."
leading: "Any value above 1024 needs no privileges."

func (*Document) GetFloat

func (d *Document) GetFloat(path string) (float64, error)

GetFloat resolves the path and reads the value it names as a float64: a float verbatim, and an integer the target holds exactly.

The error is the unified *Error: KindBadPath, KindNotFound or KindWrongContainer from the navigation, KindTypeMismatch when the value is neither a float nor an integer, and KindInexact for an integer no float64 holds exactly.

func (*Document) GetInt

func (d *Document) GetInt(path string) (int64, error)

GetInt resolves the path and reads the value it names as an int64.

The error is the unified *Error: KindBadPath, KindNotFound or KindWrongContainer from the navigation, and KindTypeMismatch when the value is not an integer. A float is never an integer, however whole it is written.

func (*Document) GetLeadingComments

func (d *Document) GetLeadingComments(path string) ([]string, error)

GetLeadingComments returns the comment lines written above the node at the given path, in order, each as text: without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil.

The path resolves to the same node SetLeadingComments writes to, so the two round-trip, and the navigation errors are the same ones -- see GetComment.

func (*Document) GetString

func (d *Document) GetString(path string) (string, error)

GetString resolves the path and reads the value it names as a string.

The error is the unified *Error: KindBadPath, KindNotFound or KindWrongContainer from the navigation, and KindTypeMismatch when the value is not a string.

func (*Document) GetTime

func (d *Document) GetTime(path string) (time.Time, error)

GetTime resolves the path and reads the value it names as a time.Time: an offset date-time verbatim, and a local date-time or local date read as UTC, because a time.Time target declares that intent. A local time carries no date and is refused.

A STRING is refused with KindTypeMismatch, even one spelling a valid RFC 3339 timestamp; a struct field of type time.Time refuses one too. To read a string as a time, use GetString and parse it.

The error is the unified *Error: KindBadPath, KindNotFound or KindWrongContainer from the navigation, and KindTypeMismatch when the value is not one of the three date-time flavors above.

Example
package main

import (
	"errors"
	"fmt"
	"time"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`stamp = 1979-05-27T07:32:00Z
written = "1979-05-27T07:32:00Z"
`))
	if err != nil {
		panic(err)
	}
	stamp, err := doc.GetTime("stamp")
	if err != nil {
		panic(err)
	}
	fmt.Println(stamp.Format(time.RFC3339))

	// A string is not a date-time, even one spelling a valid timestamp. A
	// time.Time DECODE target answers the same way: both surfaces read the one
	// conversion table.
	_, err = doc.GetTime("written")
	fmt.Println(errors.Is(err, tomledit.ErrTypeMismatch))

	type Config struct {
		Written time.Time `toml:"written"`
		Stamp   time.Time `toml:"stamp"`
	}
	_, err = tomledit.Decode[Config](doc)
	fmt.Println(errors.Is(err, tomledit.ErrTypeMismatch))
}
Output:
1979-05-27T07:32:00Z
true
true

func (*Document) Has

func (d *Document) Has(path string) bool

Has reports whether the path names a concrete node, on the same terms as Lookup.

func (*Document) Key

func (d *Document) Key(name string) *Cursor

Key returns a Cursor navigated to the named child of the document root. This is the entry point for the fluent cursor API. Chain additional Key or At calls to traverse deeper, then extract the value with String, Int, etc.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[database]
host = "localhost"
port = 5432
`))
	if err != nil {
		panic(err)
	}
	host, err := doc.Key("database").Key("host").String()
	if err != nil {
		panic(err)
	}
	fmt.Println(host)

	port, err := doc.Key("database").Key("port").Int()
	if err != nil {
		panic(err)
	}
	fmt.Println(port)
}
Output:
localhost
5432

func (*Document) LeadingComments

func (n *Document) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*Document) Lookup

func (d *Document) Lookup(path string) (Node, bool)

Lookup returns the node the path names, and whether it names one. It is the comma-ok form of Resolve, and answers about CONCRETE nodes: a path naming something no single node stands for -- an array-of-tables, or a table implied by a longer header or a dotted key -- reports false, even though the read-layer carries it. Use Root to read those.

func (*Document) Merge

func (d *Document) Merge(other *Document) error

Merge merges all values from the other document into d. Only keys that do not exist in d are set; existing values are never overwritten. Seeding a document from a list of defaults rather than from another document is EnsureDefaults.

The other document is read through the read-layer, so what merges is what it MEANS, not how it is written: a key bound by a dotted key, by a header table or by an inline table arrives the same way, and a table the source only implies is merged as the table it implies. The target is asked the same question -- a key it spells with a longer header counts as present just as much as one with a header of its own.

Comment handling:

  • For existing keys: other's leading comments are appended to d's leading comments; if d has no inline comment and other does, it is copied.
  • For new keys: the comments of the line that binds the key travel with it -- the comments written above it and the one written after it on the same line. Comments written INSIDE a container value do not. An array or an inline table that is new in the target is written from its values, so a comment between its elements is dropped. A table merged key by key keeps the comments of every key it brings, since each of those is a binding line with comments of its own.

Two consequences of the rules above, stated so they are read as the contract and not as an accident:

  • Merging one source twice doubles its leading comments. The second merge finds every key already present, and the existing-key rule appends.
  • The tables above a new nested table are written as empty headers: a source whose only content is [deep.nest] leaves an empty [deep] header in the target above it. Merge writes through SetCreate, which spells every intermediate table the path names as a header of its own.

Array-of-tables are treated atomically: if d already has anything at a given path, all of other's entries for that path are skipped.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	base, _ := tomledit.Parse([]byte(`[server]
host = "localhost"
`))
	defaults, _ := tomledit.Parse([]byte(`[server]
host = "0.0.0.0"
port = 8080
`))
	err := base.Merge(defaults)
	if err != nil {
		panic(err)
	}
	// host was already set, so it keeps "localhost".
	host, err := base.GetString("server.host")
	if err != nil {
		panic(err)
	}
	// port was missing, so it was added from defaults.
	port, err := base.GetInt("server.port")
	if err != nil {
		panic(err)
	}
	fmt.Println(host)
	fmt.Println(port)
}
Output:
localhost
8080

func (*Document) NewArrayTable

func (d *Document) NewArrayTable(path string) error

NewArrayTable appends a new [[array-table]] entry at the given path. Multiple entries with the same path are valid in TOML and represent successive elements of the array. The path must consist of key segments only (no array indices).

A name already bound to an array-of-tables is exactly what a new entry extends. Any other binding of the name -- a value, an inline table, a table with a header of its own or one another construct implied -- is refused with KindConflict, as is a prefix of the path that holds a value.

func (*Document) NewTable

func (d *Document) NewTable(path string) error

NewTable creates a new [table] header at the given path and appends it to the document. The path must consist of key segments only (no array indices).

The header must be able to bind its name. It is refused with KindConflict when anything else in the document already does: a value, an inline table, a table with its own header, an array-of-tables, or a table a dotted key implied (TOML does not allow giving one of those a header of its own). A table implied only by a LONGER header -- the "a" of an earlier [a.b] -- has no header of its own yet, and this is how it gets one. A prefix of the path that holds a value is refused on the same terms.

The refusal is about the path's FINAL key only. A table a dotted key implied may not be redefined by a header, and it may still hold sub-tables of its own: with "apple.color" written under [fruit], creating [fruit.apple] is refused and [fruit.apple.texture] is not, exactly as TOML has it.

func (*Document) PermuteChildren

func (d *Document) PermuteChildren(path string, order []int) error

PermuteChildren reorders the children of the container the path names. The empty path addresses the document's own children.

The order is a GATHER: order[i] is the index of the child that moves to position i, so children [A, B] with order [1, 0] end up [B, A]. It must be total -- a permutation of every index the container has, each exactly once. A wrong length, an out-of-range index and a repeated one are each refused with KindBadInput, and nothing is reordered. The two index violations name the offending index; a length mismatch is about the order as a whole and names both counts instead.

The indices address the container's children as they stand right now. Read them, compute the order and permute in one editing sequence: an edit in between that adds or removes a child shifts every index after it, and the permutation would then move the wrong nodes.

A child's own trivia travels with it -- its leading comments, its inline comment and the blank lines before it are part of the child, not of the position it used to hold.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`# about b
b = 2
a = 1
c = 3
`))
	if err != nil {
		panic(err)
	}
	// order[i] is the index of the child moving to position i, so this puts
	// the second child first. A child's comments travel with it.
	if err := doc.PermuteChildren("", []int{1, 0, 2}); err != nil {
		panic(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
a = 1
# about b
b = 2
c = 3

func (*Document) Raw

func (n *Document) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*Document) RemoveFromArray

func (d *Document) RemoveFromArray(path string, index int) error

RemoveFromArray removes the element at index from the array the path names. A negative index counts from the end, so -1 removes the last element.

Unlike Delete, which is idempotent by contract, this operation names a position that must exist: an index outside the array is refused with KindNotFound.

func (*Document) RenameKey

func (d *Document) RenameKey(path string, newKey string) error

RenameKey changes the key name of the node at the given path to newKey.

It renames the BINDING, whatever constructs spell it out. A name bound by a value is renamed in the pair that writes it; a name bound by a table is renamed in every header that names that table, the headers of the tables nested inside it included, and in every dotted pair written under it. A name bound by an array-of-tables is renamed in every entry's header. The renamed key part is the only fragment invalidated: the brackets, the other parts, the whitespace between them and the line's comment all splice as written.

It reports KindNotFound when the path names nothing, KindWrongContainer when the last path segment is an array index (an element has no key to rename) or when the parent names an array-of-tables rather than one of its entries, KindConflict when anything in the parent already binds newKey -- a value, a table in any spelling, or an array-of-tables -- and KindBadInput when newKey is not valid UTF-8 and so is not a key TOML can carry. A refused rename changes nothing.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"  # bind address

[server.tls]
cert = "/etc/cert.pem"
`))
	if err != nil {
		panic(err)
	}
	// Renaming a table renames the binding: every header that names it,
	// nested ones included.
	if err := doc.RenameKey("server", "listener"); err != nil {
		panic(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
[listener]
host = "localhost"  # bind address

[listener.tls]
cert = "/etc/cert.pem"

func (*Document) Resolve

func (d *Document) Resolve(path string) (Node, error)

Resolve resolves the path against the document and returns the node it names. For a key-value pair the value node is returned, not the pair.

Path syntax is ParsePath's: dots between keys ("server.host"), brackets for indices ("items[0]", "items[-1]" for the last), quotes around a key that would otherwise read as more than one segment.

The returned *Error carries KindBadPath for a syntactically invalid path, KindNotFound for a path naming nothing, and KindWrongContainer for a step that does not apply to what it addresses -- including a path that names something no single node stands for: an array-of-tables (index it, or read its entries through Root) or a table implied by a longer header or a dotted key. Lookup and Has answer the same question without an error.

func (*Document) Root

func (d *Document) Root() *Record

Root returns the read-layer view of the document: the record holding every top-level key, in first-appearance order.

Parsing folds nothing; the layer is built the first time a logical question is asked and kept until the document is written to, so repeated reads share one fold and reads of a shared document stay safe under concurrency. A write drops it: the next call folds again and answers what the document says then. A record obtained BEFORE a write keeps answering what the document said before it -- layer handles are snapshots, and a stale one reports stale data rather than reporting an error. Mutating a document while iterating its entries is unspecified.

Root panics when the document cannot be folded -- when two constructs claim one key, for instance. A parsed document never can be: the parser refuses every such conflict. Only an editing sequence that built an invalid document can reach it.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`title = "example"
database.host = "localhost"
database.port = 5432

[[server]]
name = "alpha"

[[server]]
name = "beta"
`))
	if err != nil {
		panic(err)
	}

	// The read-layer folds the spellings away: a dotted key and an
	// array-of-tables answer the same questions as anything else.
	for entry := range doc.Root().Entries() {
		switch entry.Kind() {
		case tomledit.EntryValue:
			node, _ := entry.Node()
			fmt.Printf("%s = %v\n", entry.Key(), node.(tomledit.Scalar).Value())
		case tomledit.EntryRecord:
			rec, _ := entry.Record()
			fmt.Printf("%s is a table of %d keys\n", entry.Key(), rec.Len())
		case tomledit.EntryRecords:
			recs, _ := entry.Records()
			fmt.Printf("%s is an array of %d tables\n", entry.Key(), len(recs))
		}
	}
}
Output:
title = example
database is a table of 2 keys
server is an array of 2 tables

func (*Document) RoundTripVerifiedBytes added in v0.7.0

func (d *Document) RoundTripVerifiedBytes(file string) ([]byte, error)

RoundTripVerifiedBytes renders the document and returns the rendered bytes only after they have proved they survive a round trip: they must parse, and re-rendering that parse must reproduce them byte for byte. It computes over the document alone and touches no file; the tomlfile subpackage writes a document through it, so nothing that fails the check is ever written.

A failure is an *Error of kind KindRoundTrip whose Offset field carries the byte at which the two renders disagree -- or, when the rendered bytes do not parse at all, the offset at which they stopped being TOML, with the parse error wrapped so errors.Is still reaches it. The diagnostic names file, the destination the bytes are meant for; an empty file names none.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte("# kept\n[server]\nport = 8080\n"))
	if err != nil {
		panic(err)
	}
	if err := doc.Set("server.port", 9090); err != nil {
		panic(err)
	}
	// The bytes are handed out only after they survive a re-parse and a
	// re-render.
	out, err := doc.RoundTripVerifiedBytes("config.toml")
	if err != nil {
		panic(err)
	}
	fmt.Print(string(out))
}
Output:
# kept
[server]
port = 9090

func (*Document) Set

func (d *Document) Set(path string, value any) error

Set writes a value at the given path. A key the parent does not carry yet is created there; a path whose parent does not exist is an error, which is what SetCreate relaxes.

Supported value types: string, bool, int/int8-64, uint/uint8-64, float32/64, time.Time, LocalDateTime, LocalDate, LocalTime, []any and typed slices, map[string]any (written with its keys sorted), and []Pair (written in the order given). A value is a Go value: an AST node is not one, and passing a node -- including one resolved out of this or another document -- is refused as an unsupported type. Copying a value from one key to another is a read followed by a write of the value.

A value write touches value fragments and nothing else. Where the path names a key bound by a structural construct -- a [header] table, an array-of-tables, or a table another construct only implied, by a longer header or a dotted key -- the write is refused with KindWrongContainer: those change through a structural operation, or through an explicit Delete followed by the write. A key holding an inline table is a value, and setting it replaces that value wholesale, interior comments and spellings included.

The PARENT table's spelling decides only where the write goes, never whether it happens. A table no single node stands for is written where the document already spells it out: a key it already carries is replaced through the pair that binds it; a new key of a table a dotted key spelled out joins the same region as another dotted pair ("a.b = 1" gains "a.new = 5" beside it); and a new key of a table only a longer header implies arrives under the anchoring header the write gives that table -- the one TOML allows it.

Equality

Set is a no-op if and only if the bytes it would write for the value are exactly the bytes the value fragment already carries. Nothing else counts as equal, and nothing equal is written: the node stays, with the spelling, the lexeme and the span it was parsed with, and the document records no edit. A value the library wrote before carries no lexeme, and the comparison is against its canonical rendering. A container value -- a map, a []Pair or a slice -- is compared as a whole against the stored container's whole byte range, and replaced WHOLESALE when it differs: the interior comments and spellings of the old container do not survive, which is what setting a container value means.

One rule, no special cases: NaN spellings, infinities, signed zeros, integer-versus-float, date-time offsets, string quoting and integer bases alike. Two consequences worth stating plainly. An idempotent tool that writes values it read back normalises a non-canonical spelling the first time it touches it -- 0x2A set to 42 becomes 42 -- and is byte-stable from then on. And a same-content write over a literal-quoted string converts it to basic quoting. What you Set is what the file says.

Deciding not to write is not undoing a write: a no-op Set never clears dirtiness an earlier edit recorded.

A NaN whose sign bit is set is refused with KindBadInput, since TOML has one NaN spelling and writing it would drop the sign; the ordinary NaN is accepted and writes "nan".

Text that is not valid UTF-8 is refused with KindBadInput too, wherever it appears: a string value, a string inside a container value, a map or []Pair key, and a key the path itself names. A TOML document is UTF-8, so such bytes would be written as replacement characters and read back as something else.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
port = 8080
`))
	if err != nil {
		panic(err)
	}
	err = doc.Set("server.port", 9090)
	if err != nil {
		panic(err)
	}
	port, err := doc.GetInt("server.port")
	if err != nil {
		panic(err)
	}
	fmt.Println(port)
}
Output:
9090
Example (OrderedInlineTable)
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte("name = \"example\"\n"))
	if err != nil {
		panic(err)
	}
	// A map is written with its keys sorted; []Pair keeps the order given.
	if err := doc.Set("tls", []tomledit.Pair{
		{Key: "cert", Value: "/etc/cert.pem"},
		{Key: "key", Value: "/etc/key.pem"},
	}); err != nil {
		panic(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
name = "example"
tls = {cert = "/etc/cert.pem", key = "/etc/key.pem"}

func (*Document) SetBools added in v0.8.0

func (d *Document) SetBools(path string, values []bool) error

SetBools writes an array of booleans at the path, as Set does.

func (*Document) SetComment

func (d *Document) SetComment(path string, comment string) error

SetComment sets the inline comment on the node at the given path. The comment string should NOT include the "# " prefix -- it will be added automatically. An empty string removes the comment. For table paths, the comment is set on the table header line. Returns an error if the path does not exist or targets a member of an inline table (TOML forbids comments inside inline tables).

The text has to be something a comment can carry: valid UTF-8, and no control character other than a tab -- so a newline, a carriage return and U+0000 are each refused with KindBadInput, and the document is left as it was. These are the lexer's rules for reading a comment; text breaking them would render bytes the parser cannot read back.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte("[server]\nport = 8080\n"))
	if err != nil {
		panic(err)
	}
	if err := doc.SetComment("server.port", "default HTTP port"); err != nil {
		panic(err)
	}
	if err := doc.SetLeadingComments("server.port", []string{
		"The port to bind to.",
		"Any value above 1024 needs no privileges.",
	}); err != nil {
		panic(err)
	}
	fmt.Print(string(doc.Bytes()))
}
Output:
[server]
# The port to bind to.
# Any value above 1024 needs no privileges.
port = 8080 # default HTTP port

func (*Document) SetCreate

func (d *Document) SetCreate(path string, value any) error

SetCreate is like Set but creates the intermediate tables the path names and the document does not carry, as standard [header] tables appended to the document. It refuses exactly what Set refuses, and is a no-op on exactly the same terms -- see Set's equality rule.

A refused write creates nothing. The value is converted before a single table is made, so a value the library will not write -- and a key it will not spell -- leaves the document byte-for-byte as it was, rather than leaving the headers of the path behind.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`title = "example"
`))
	if err != nil {
		panic(err)
	}
	// SetCreate auto-creates intermediate [database] table.
	err = doc.SetCreate("database.host", "db.example.com")
	if err != nil {
		panic(err)
	}
	host, err := doc.GetString("database.host")
	if err != nil {
		panic(err)
	}
	fmt.Println(host)
}
Output:
db.example.com

func (*Document) SetCreateBools added in v0.8.0

func (d *Document) SetCreateBools(path string, values []bool) error

SetCreateBools writes an array of booleans at the path, as SetCreate does.

func (*Document) SetCreateFloats added in v0.8.0

func (d *Document) SetCreateFloats(path string, values []float64) error

SetCreateFloats writes an array of floats at the path, as SetCreate does.

func (*Document) SetCreateInlineTable added in v0.8.0

func (d *Document) SetCreateInlineTable(path string, table InlineTable) error

SetCreateInlineTable writes the inline table at the path, as SetCreate does, with its keys in the order they were added.

func (*Document) SetCreateInts added in v0.8.0

func (d *Document) SetCreateInts(path string, values []int64) error

SetCreateInts writes an array of integers at the path, as SetCreate does.

func (*Document) SetCreateStrings added in v0.8.0

func (d *Document) SetCreateStrings(path string, values []string) error

SetCreateStrings writes an array of strings at the path, as SetCreate does.

func (*Document) SetFloats added in v0.8.0

func (d *Document) SetFloats(path string, values []float64) error

SetFloats writes an array of floats at the path, as Set does. A NaN whose sign bit is set is refused with KindBadInput, as Set refuses one.

func (*Document) SetInlineTable added in v0.8.0

func (d *Document) SetInlineTable(path string, table InlineTable) error

SetInlineTable writes the inline table at the path, as Set does, with its keys in the order they were added.

func (*Document) SetInts added in v0.8.0

func (d *Document) SetInts(path string, values []int64) error

SetInts writes an array of integers at the path, as Set does.

func (*Document) SetLeadingComments

func (d *Document) SetLeadingComments(path string, comments []string) error

SetLeadingComments sets the leading comment lines on the node at the given path. Each string should NOT include the "# " prefix -- it will be added automatically. For table paths, the comments are set on the table header. Returns an error if the path does not exist.

Each element is ONE comment line, and each is held to what SetComment holds its text to: valid UTF-8, no control character other than a tab. An element carrying a newline would be a second line it does not get to open, so it is refused with KindBadInput -- and one refused element refuses the whole call, leaving the node's comments as they were.

A nil or empty slice removes the leading comments. An empty STRING element is not removal: it writes a "# " comment line with no content.

func (*Document) SetStrings added in v0.8.0

func (d *Document) SetStrings(path string, values []string) error

SetStrings writes an array of strings at the path, as Set does.

func (*Document) Span

func (n *Document) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*Document) Type

func (n *Document) Type() NodeType

Type returns NodeDocument.

func (*Document) Validate

func (d *Document) Validate(spec *Spec) error

Validate checks the document against the descriptor and reports every independent violation it finds, in document order, as one aggregate error (see Errors): an unknown key or table, a value of a kind the field refuses, a value the field cannot hold exactly, and a required key the document does not carry. Validation continues across siblings but never descends below a construct it has already refused, so one broken table cannot bury the document in diagnostics from its interior.

A descriptor that does not describe anything -- an array field with no Elem, a table field with no Table, a sub-descriptor on a kind that has no use for one -- is refused before the document is looked at.

Validate reports nil when the document satisfies the descriptor.

Example
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`[server]
host = "localhost"
prot = 8080
`))
	if err != nil {
		panic(err)
	}

	spec := &tomledit.Spec{Fields: map[string]tomledit.Field{
		"server": {Kind: tomledit.FieldKindTable, Required: true, Table: &tomledit.Spec{
			Fields: map[string]tomledit.Field{
				"host": {Kind: tomledit.FieldKindString, Required: true},
				"port": {Kind: tomledit.FieldKindInteger, Required: true},
			},
		}},
	}}

	err = doc.Validate(spec)
	fmt.Println(errors.Is(err, tomledit.ErrUnknownKey))
	fmt.Println(errors.Is(err, tomledit.ErrMissingKey))

	// Every independent violation is collected, in document order.
	var all *tomledit.Errors
	if errors.As(err, &all) {
		for _, e := range all.Unwrap() {
			var diag *tomledit.Error
			if errors.As(e, &diag) {
				fmt.Printf("%s: %s\n", diag.Kind, diag.Path)
			}
		}
	}
}
Output:
true
true
unknown key: server.prot
missing key: server.port

func (*Document) Walk

func (d *Document) Walk(fn func(path string, node Node) error, mode WalkMode) error

Walk is the SYNTACTIC traversal: it walks the syntax tree, in the order the file writes it, and hands the visitor the concrete nodes it finds -- each with the spelling, the trivia and the span it was written with. A value written as an inline table arrives as an *InlineTableNode and one written under a header arrives as the header's children, because that is what the two files contain.

The LOGICAL traversal is the read-layer: Root returns the document's records and entries with the spellings folded away, and a consumer walks it by recursing over Record.Entries. Use Walk to ask what the file contains and how it is written; use the read-layer to ask what the document means.

Walk visits every key-value pair in the document in order, calling fn with the dot-path and the value node. Tables and array-of-tables are walked into (their children are visited), not yielded as standalone entries. Inline tables and arrays are yielded first, then their children are recursed into. Comment nodes are not visited: a comment reaches the visitor as the trivia of the node it is attached to.

The mode parameter controls which nodes are visited:

  • WalkLeaves: only scalar values (containers are recursed but not yielded)
  • WalkAll: containers AND their children are yielded

The path uses dot-separated keys with bracket indices for array-of-tables entries (e.g. "servers[0].host"). Return ErrSkipTable from fn to skip the children of the current inline table or array. Return any other non-nil error to stop the walk immediately.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte(`name = "example"

[server]
host = "localhost"
port = 8080
`))
	if err != nil {
		panic(err)
	}
	err = doc.Walk(func(path string, node tomledit.Node) error {
		fmt.Printf("%s = %v\n", path, node.(tomledit.Scalar).Value())
		return nil
	}, tomledit.WalkLeaves)
	if err != nil {
		panic(err)
	}
}
Output:
name = example
server.host = localhost
server.port = 8080

type Entry

type Entry struct {
	// contains filtered or unexported fields
}

Entry is one key of a record: the key, where it was written, and what it holds. Entries are values; holding one does not keep the document alive in any special way, and like the records they come from they are snapshots.

func (Entry) Key

func (e Entry) Key() string

Key returns the entry's key, decoded (quotes and escapes resolved).

func (Entry) KeySpan

func (e Entry) KeySpan() Span

KeySpan returns the source range of the key itself -- the one part of the construct the entry is anchored to that names it.

func (Entry) Kind

func (e Entry) Kind() EntryKind

Kind reports what the entry holds.

func (Entry) Node

func (e Entry) Node() (Node, bool)

Node returns the value node the entry holds, and whether it holds one. It answers for entries of kind EntryValue -- a scalar or a plain array -- and reports false for every other kind: a record's own backing construct is Record.Node's answer, and an array-of-tables has no single node at all, only the entries Records returns.

func (Entry) Record

func (e Entry) Record() (*Record, bool)

Record returns the record this entry holds, for an entry of kind EntryRecord: a table in any spelling. It reports false for any other kind.

func (Entry) Records

func (e Entry) Records() ([]*Record, bool)

Records returns the entries of the array-of-tables this entry holds, for an entry of kind EntryRecords. The returned slice is a copy: appending to it or reordering it changes nothing in the document. It reports false for any other kind.

func (Entry) RecordsSpan

func (e Entry) RecordsSpan() Span

RecordsSpan returns the synthesized range of an array-of-tables: from the first entry's header to the end of the last entry's content, or to the end of its header when that entry has no content of its own. It is the zero Span for an entry of any other kind -- no concrete node covers a collection, so this is the only range that describes one.

type EntryKind

type EntryKind int

EntryKind classifies what a read-layer entry holds.

const (
	// EntryValue is a scalar or a plain array: one concrete value node.
	EntryValue EntryKind = iota
	// EntryRecord is a table in any spelling -- a header table, an inline
	// table, or a table implied by a longer header or a dotted key.
	EntryRecord
	// EntryRecords is an array-of-tables: the entries collected under one key,
	// in document order.
	EntryRecords
)

func (EntryKind) String

func (k EntryKind) String() string

String returns the human-readable name of the entry kind.

type Error

type Error struct {
	Kind     ErrorKind // what went wrong
	Path     string    // document path, in this package's path syntax
	Pos      Position  // primary source position; the zero Position when unknown
	Span     Span      // source range of the construct concerned, when known
	Message  string    // human-readable description, without position or file
	File     string    // source file, empty when no file is known
	Snippet  string    // source excerpt, parse-stage diagnostics
	Expected string    // type-mismatch detail: what the target accepts
	Got      string    // type-mismatch detail: what the document carries
	Value    any       // the offending value, for inexact and bad-input kinds
	Keys     []string  // the direct child keys, for unknown-table diagnostics
	Offset   int       // KindRoundTrip: first divergence in the rendered bytes
	Fix      string    // what to do about it ("write port = 8080", "remove it"); empty when unknown
	// contains filtered or unexported fields
}

Error is the one diagnostic type of this package. Parse, edit, and access failures are all reported as *Error, so a caller can match them structurally (errors.As) or by kind (errors.Is against the matching Err* sentinel) instead of by message text:

var diag *tomledit.Error
if errors.As(err, &diag) {
    fmt.Println(diag.Kind, diag.Pos.Line, diag.Pos.Column)
}

Which fields are populated depends on the kind and on what the reporting site knew: Pos (line, column and byte offset) and Snippet are filled for every parse-stage diagnostic; Span only where the reporting site knows the extent of the construct it concerns, which for the parse stage means the parser's token diagnostics -- the lexer's and the duplicate-definition tracker's carry the zero Span. Path is filled for path-addressed operations, File whenever the document's origin is known (see ParseWithFileName and Collector.Read), and the remaining fields by the kinds documented on ErrorKind. A diagnostic reported through a Reader carries a line without a column (Pos.Column is 0), and Fix when the reporting site knows what to write instead. An unpopulated field carries its zero value.

Example
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	doc, err := tomledit.Parse([]byte("[server]\nport = \"8080\"\n"))
	if err != nil {
		panic(err)
	}
	_, err = doc.GetInt("server.port")

	// Every document-dependent failure is an *Error: match a kind with
	// errors.Is, read the structure with errors.As.
	fmt.Println(errors.Is(err, tomledit.ErrTypeMismatch))
	var diag *tomledit.Error
	if errors.As(err, &diag) {
		fmt.Printf("%d:%d %s: expected %s, got %s\n",
			diag.Pos.Line, diag.Pos.Column, diag.Path, diag.Expected, diag.Got)
	}
	fmt.Println(err)
}
Output:
true
2:8 server.port: expected integer, got string
2:8: server.port: expected integer, got string

func (*Error) Error

func (e *Error) Error() string

Error renders the diagnostic in the compiler convention: the location, the path and the message, joined with ": ", with every absent part left out, and the fix, when there is one, after the message and "; ". The location is "file:line:column" when both the file and a position are known, "line:column" with a position alone, and the file alone when there is no position; a position without a column (Pos.Column 0) leaves the column out. A diagnostic reads as one of

config.toml:3:10: expected a value, got end of input
3:10: expected a value, got end of input
config.toml: server.port: key not found
config.toml:4: server.port: is missing; write port = <integer>
server.port: key not found
key not found

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether the diagnostic matches target, which is true for the kind sentinel of its own kind.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying error this diagnostic reports, or nil.

type ErrorKind

type ErrorKind int

ErrorKind classifies a diagnostic. The set is closed: every kind has exactly one matching sentinel (KindSyntax has ErrSyntax, and so on), and a diagnostic reports itself equal to the sentinel of its own kind through errors.Is.

const (
	// KindSyntax is a lexing or parsing failure.
	KindSyntax ErrorKind = iota
	// KindUnknownKey is a decoded key matching no field of the target.
	KindUnknownKey
	// KindUnknownTable is a decoded table or array-of-tables matching no
	// field of the target; the diagnostic's Keys field lists the direct
	// child keys of the offending construct.
	KindUnknownTable
	// KindMissingKey is a required key that the document does not carry.
	KindMissingKey
	// KindTypeMismatch is a value whose kind is not acceptable for the
	// target; the diagnostic's Expected and Got fields name both sides.
	KindTypeMismatch
	// KindInexact is a value the target cannot hold exactly: an out-of-range
	// integer, a lossy conversion, or a length mismatch. The diagnostic's
	// Value field carries the offending value.
	KindInexact
	// KindNotFound is a path naming nothing in the document.
	KindNotFound
	// KindBadPath is a syntactically invalid path.
	KindBadPath
	// KindWrongContainer is a path step that is structurally inapplicable:
	// a key on an array, an index on a scalar, a concrete-node operation on
	// a path that names no single node.
	KindWrongContainer
	// KindBadInput is an invalid input value to an editing operation.
	KindBadInput
	// KindConflict is an edit refused because it would produce an invalid
	// document.
	KindConflict
	// KindRoundTrip is a write whose rendered bytes did not survive a
	// re-parse; the diagnostic's Offset field carries the byte offset of the
	// first divergence.
	KindRoundTrip
	// KindInvalidValue is a value a hand-reading caller refused: present, and
	// usually of an accepted type, but not a value the caller allows (an empty
	// list, a false that only restates an absence, a name breaking a rule).
	// Reader.Errorf, Reader.ErrorfAt and Collector.Errorf report this kind.
	KindInvalidValue
)

func (ErrorKind) String

func (k ErrorKind) String() string

String returns the human-readable name of the kind.

type Errors

type Errors struct {
	// contains filtered or unexported fields
}

Errors is an aggregate of diagnostics reported together: in document order from a decode, and sorted by file, line, path, and message from a Collector. It renders as its first diagnostic, so a call site that prints the error reads like a single failure; the whole list is reachable through Diagnostics and through errors.Unwrap semantics:

var all *tomledit.Errors
if errors.As(err, &all) {
    for _, d := range all.Diagnostics() { ... }
}

errors.As with an *Error target yields the first diagnostic, and errors.Is against a kind sentinel matches when any contained diagnostic carries that kind. An empty aggregate is never returned: no diagnostics means a nil error.

func (*Errors) Diagnostics added in v0.6.0

func (e *Errors) Diagnostics() []*Error

Diagnostics returns every diagnostic of the aggregate, in its order. The slice is a copy; the diagnostics are the aggregate's own.

func (*Errors) Error

func (e *Errors) Error() string

Error renders the first diagnostic of the aggregate, or "no diagnostics" when it holds none. This package never produces an empty aggregate, but the type is exported, so rendering a zero value reports rather than panics.

func (*Errors) Unwrap

func (e *Errors) Unwrap() []error

Unwrap returns every diagnostic of the aggregate, in its order.

type Field

type Field struct {
	// Kind is what the value must be.
	Kind FieldKind

	// Required makes the absence of the key an error.
	Required bool

	// Elem describes the elements of an array. It is required for
	// FieldKindArray and refused for every other kind.
	Elem *Field

	// Table describes the keys of a table. It is required for
	// FieldKindTable and refused for every other kind.
	Table *Spec
}

Field is one expected value of a descriptor.

func FieldAny

func FieldAny() Field

FieldAny returns the descriptor of a value of any kind: the explicit spelling of "whatever is here, I am not describing it".

type FieldKind

type FieldKind int

FieldKind names what a descriptor field expects. There is one member per TOML value type -- the four date-time flavors kept apart, never unified -- plus the two containers and the total FieldKindAny.

const (
	// FieldKindString expects a string.
	FieldKindString FieldKind = iota
	// FieldKindInteger expects an integer.
	FieldKindInteger
	// FieldKindFloat expects a float, or an integer that a float holds
	// exactly.
	FieldKindFloat
	// FieldKindBoolean expects a boolean.
	FieldKindBoolean
	// FieldKindOffsetDateTime expects a date-time carrying an offset.
	FieldKindOffsetDateTime
	// FieldKindLocalDateTime expects a date-time without an offset.
	FieldKindLocalDateTime
	// FieldKindLocalDate expects a date.
	FieldKindLocalDate
	// FieldKindLocalTime expects a time of day.
	FieldKindLocalTime
	// FieldKindArray expects an array, or an array-of-tables. The element
	// descriptor is required.
	FieldKindArray
	// FieldKindTable expects a table in any spelling -- a header table, an
	// inline table, or one implied by a longer header or a dotted key. The
	// field set is required.
	FieldKindTable
	// FieldKindAny expects anything, and reports nothing about what it holds.
	FieldKindAny
)

func (FieldKind) String

func (k FieldKind) String() string

String returns the human-readable name of the field kind.

type FloatNode

type FloatNode struct {
	// contains filtered or unexported fields
}

FloatNode represents a float value.

func (*FloatNode) AsBool

func (n *FloatNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*FloatNode) AsFloat

func (n *FloatNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*FloatNode) AsInt

func (n *FloatNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*FloatNode) AsLocalDate

func (n *FloatNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*FloatNode) AsLocalDateTime

func (n *FloatNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*FloatNode) AsLocalTime

func (n *FloatNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*FloatNode) AsString

func (n *FloatNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*FloatNode) AsTime

func (n *FloatNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*FloatNode) Comment

func (n *FloatNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*FloatNode) LeadingComments

func (n *FloatNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*FloatNode) Raw

func (n *FloatNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*FloatNode) Span

func (n *FloatNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*FloatNode) Type

func (n *FloatNode) Type() NodeType

Type returns NodeFloat.

func (*FloatNode) Value

func (n *FloatNode) Value() any

Value returns the float value as float64.

type FormatConfig

type FormatConfig struct {
	IndentWidth int // spaces per indent level (default: 0, no indentation of values)
	LineWidth   int // max line width before arrays go multi-line (default: 80)
}

FormatConfig controls how the formatter normalizes TOML output. Use DefaultFormatConfig to get sensible defaults and WithIndentWidth or WithLineWidth to override specific settings.

func DefaultFormatConfig

func DefaultFormatConfig() FormatConfig

DefaultFormatConfig returns a FormatConfig with sensible defaults.

type FormatOption

type FormatOption func(*FormatConfig)

FormatOption is a functional option for configuring the formatter.

func WithIndentWidth

func WithIndentWidth(n int) FormatOption

WithIndentWidth sets the number of spaces per indent level for values under table headers.

func WithLineWidth

func WithLineWidth(n int) FormatOption

WithLineWidth sets the maximum line width before arrays are rendered in multi-line format.

type InlineTable added in v0.8.0

type InlineTable struct {
	// contains filtered or unexported fields
}

InlineTable is an ordered inline table built from typed values, the input of SetInlineTable and SetCreateInlineTable. The zero value is an empty table. Each method adds one key and returns the table, so additions chain.

A key is a SINGLE key, taken verbatim -- never a path, as a Pair's Key is not. A duplicate key and a key or string that is not valid UTF-8 are not refused when added: the write that takes the table refuses them with KindBadInput, and writes nothing.

func (*InlineTable) Bool added in v0.8.0

func (t *InlineTable) Bool(key string, value bool) *InlineTable

Bool adds a boolean value under the key.

func (*InlineTable) Bools added in v0.8.0

func (t *InlineTable) Bools(key string, values []bool) *InlineTable

Bools adds an array of booleans under the key.

func (*InlineTable) Float added in v0.8.0

func (t *InlineTable) Float(key string, value float64) *InlineTable

Float adds a float value under the key.

func (*InlineTable) Floats added in v0.8.0

func (t *InlineTable) Floats(key string, values []float64) *InlineTable

Floats adds an array of floats under the key.

func (*InlineTable) Int added in v0.8.0

func (t *InlineTable) Int(key string, value int64) *InlineTable

Int adds an integer value under the key.

func (*InlineTable) Ints added in v0.8.0

func (t *InlineTable) Ints(key string, values []int64) *InlineTable

Ints adds an array of integers under the key.

func (*InlineTable) String added in v0.8.0

func (t *InlineTable) String(key, value string) *InlineTable

String adds a string value under the key.

func (*InlineTable) Strings added in v0.8.0

func (t *InlineTable) Strings(key string, values []string) *InlineTable

Strings adds an array of strings under the key.

func (*InlineTable) Table added in v0.8.0

func (t *InlineTable) Table(key string, table InlineTable) *InlineTable

Table adds a nested inline table under the key. The table is copied: keys added to it afterwards do not reach this one.

type InlineTableNode

type InlineTableNode struct {
	// contains filtered or unexported fields
}

InlineTableNode represents an inline table value.

func (*InlineTableNode) Children

func (n *InlineTableNode) Children() []Node

Children returns the inline table's pairs in order, as a copy.

func (*InlineTableNode) Comment

func (n *InlineTableNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*InlineTableNode) LeadingComments

func (n *InlineTableNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*InlineTableNode) Raw

func (n *InlineTableNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*InlineTableNode) Span

func (n *InlineTableNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*InlineTableNode) Type

func (n *InlineTableNode) Type() NodeType

Type returns NodeInlineTable.

type IntegerBase

type IntegerBase int

IntegerBase indicates the numeric base for an integer node.

const (
	IntegerDecimal IntegerBase = iota // IntegerDecimal is base-10 (e.g. 42).
	IntegerHex                        // IntegerHex is base-16 (e.g. 0xFF).
	IntegerOctal                      // IntegerOctal is base-8 (e.g. 0o77).
	IntegerBinary                     // IntegerBinary is base-2 (e.g. 0b1010).
)

type IntegerNode

type IntegerNode struct {
	// contains filtered or unexported fields
}

IntegerNode represents an integer value.

func (*IntegerNode) AsBool

func (n *IntegerNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*IntegerNode) AsFloat

func (n *IntegerNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*IntegerNode) AsInt

func (n *IntegerNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*IntegerNode) AsLocalDate

func (n *IntegerNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*IntegerNode) AsLocalDateTime

func (n *IntegerNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*IntegerNode) AsLocalTime

func (n *IntegerNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*IntegerNode) AsString

func (n *IntegerNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*IntegerNode) AsTime

func (n *IntegerNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*IntegerNode) Base

func (n *IntegerNode) Base() IntegerBase

Base returns the numeric base the integer was written in.

func (*IntegerNode) Comment

func (n *IntegerNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*IntegerNode) LeadingComments

func (n *IntegerNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*IntegerNode) Raw

func (n *IntegerNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*IntegerNode) Span

func (n *IntegerNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*IntegerNode) Type

func (n *IntegerNode) Type() NodeType

Type returns NodeInteger.

func (*IntegerNode) Value

func (n *IntegerNode) Value() any

Value returns the integer value as int64.

type KeyNode

type KeyNode struct {
	// contains filtered or unexported fields
}

KeyNode represents a (possibly dotted) key.

func (*KeyNode) Comment

func (n *KeyNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*KeyNode) LeadingComments

func (n *KeyNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*KeyNode) Parts

func (n *KeyNode) Parts() []string

Parts returns the key's parts, decoded, as a copy: "a.b" reads as ["a", "b"], and a quoted part carries no quotes.

func (*KeyNode) Raw

func (n *KeyNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*KeyNode) RawParts

func (n *KeyNode) RawParts() [][]byte

RawParts returns the source bytes of each part, as written, as a copy: the outer slice and every part in it. It is empty for a key created programmatically.

func (*KeyNode) Span

func (n *KeyNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*KeyNode) Styles

func (n *KeyNode) Styles() []StringStyle

Styles returns the quoting style of each part, as a copy.

func (*KeyNode) Type

func (n *KeyNode) Type() NodeType

Type returns NodeKey.

type KeyValueNode

type KeyValueNode struct {
	// contains filtered or unexported fields
}

KeyValueNode represents a key = value pair.

func (*KeyValueNode) Comment

func (n *KeyValueNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*KeyValueNode) Key

func (n *KeyValueNode) Key() *KeyNode

Key returns the pair's key node.

func (*KeyValueNode) LeadingComments

func (n *KeyValueNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*KeyValueNode) Raw

func (n *KeyValueNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*KeyValueNode) Span

func (n *KeyValueNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*KeyValueNode) Type

func (n *KeyValueNode) Type() NodeType

Type returns NodeKeyValue.

func (*KeyValueNode) Val

func (n *KeyValueNode) Val() Node

Val returns the pair's value node.

type LocalDate

type LocalDate struct {
	Year, Month, Day int
}

LocalDate represents a TOML local date.

type LocalDateNode

type LocalDateNode struct {
	// contains filtered or unexported fields
}

LocalDateNode represents a local date value.

func (*LocalDateNode) AsBool

func (n *LocalDateNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*LocalDateNode) AsFloat

func (n *LocalDateNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*LocalDateNode) AsInt

func (n *LocalDateNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*LocalDateNode) AsLocalDate

func (n *LocalDateNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*LocalDateNode) AsLocalDateTime

func (n *LocalDateNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*LocalDateNode) AsLocalTime

func (n *LocalDateNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*LocalDateNode) AsString

func (n *LocalDateNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*LocalDateNode) AsTime

func (n *LocalDateNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*LocalDateNode) Comment

func (n *LocalDateNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalDateNode) LeadingComments

func (n *LocalDateNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalDateNode) Raw

func (n *LocalDateNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*LocalDateNode) Span

func (n *LocalDateNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*LocalDateNode) Type

func (n *LocalDateNode) Type() NodeType

Type returns NodeLocalDate.

func (*LocalDateNode) Value

func (n *LocalDateNode) Value() any

Value returns the LocalDate value.

type LocalDateTime

type LocalDateTime struct {
	Year, Month, Day     int
	Hour, Minute, Second int
	Nanosecond           int
}

LocalDateTime represents a TOML local date-time (no timezone).

type LocalDateTimeNode

type LocalDateTimeNode struct {
	// contains filtered or unexported fields
}

LocalDateTimeNode represents a local date-time value (no timezone).

func (*LocalDateTimeNode) AsBool

func (n *LocalDateTimeNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*LocalDateTimeNode) AsFloat

func (n *LocalDateTimeNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*LocalDateTimeNode) AsInt

func (n *LocalDateTimeNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*LocalDateTimeNode) AsLocalDate

func (n *LocalDateTimeNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*LocalDateTimeNode) AsLocalDateTime

func (n *LocalDateTimeNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*LocalDateTimeNode) AsLocalTime

func (n *LocalDateTimeNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*LocalDateTimeNode) AsString

func (n *LocalDateTimeNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*LocalDateTimeNode) AsTime

func (n *LocalDateTimeNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*LocalDateTimeNode) Comment

func (n *LocalDateTimeNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalDateTimeNode) LeadingComments

func (n *LocalDateTimeNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalDateTimeNode) Raw

func (n *LocalDateTimeNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*LocalDateTimeNode) Span

func (n *LocalDateTimeNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*LocalDateTimeNode) Type

func (n *LocalDateTimeNode) Type() NodeType

Type returns NodeLocalDateTime.

func (*LocalDateTimeNode) Value

func (n *LocalDateTimeNode) Value() any

Value returns the LocalDateTime value.

type LocalTime

type LocalTime struct {
	Hour, Minute, Second int
	Nanosecond           int
}

LocalTime represents a TOML local time.

type LocalTimeNode

type LocalTimeNode struct {
	// contains filtered or unexported fields
}

LocalTimeNode represents a local time value.

func (*LocalTimeNode) AsBool

func (n *LocalTimeNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*LocalTimeNode) AsFloat

func (n *LocalTimeNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*LocalTimeNode) AsInt

func (n *LocalTimeNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*LocalTimeNode) AsLocalDate

func (n *LocalTimeNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*LocalTimeNode) AsLocalDateTime

func (n *LocalTimeNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*LocalTimeNode) AsLocalTime

func (n *LocalTimeNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*LocalTimeNode) AsString

func (n *LocalTimeNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*LocalTimeNode) AsTime

func (n *LocalTimeNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*LocalTimeNode) Comment

func (n *LocalTimeNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalTimeNode) LeadingComments

func (n *LocalTimeNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*LocalTimeNode) Raw

func (n *LocalTimeNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*LocalTimeNode) Span

func (n *LocalTimeNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*LocalTimeNode) Type

func (n *LocalTimeNode) Type() NodeType

Type returns NodeLocalTime.

func (*LocalTimeNode) Value

func (n *LocalTimeNode) Value() any

Value returns the LocalTime value.

type Node

type Node interface {
	Type() NodeType

	// Comment returns the node's inline comment as text: without the "#" and
	// the whitespace around it, and empty when there is none.
	Comment() string

	// LeadingComments returns the comment lines written above the node as
	// text, each without its "#", its trailing newline and the whitespace
	// around them.
	LeadingComments() []string

	// Raw returns the node's bytes as written, comments and spacing included,
	// as a copy: writing into what it returns changes neither what a later
	// read answers nor what the document renders.
	Raw() []byte

	// Span returns the source range the node covered in the most recent
	// Parse. Nodes created programmatically by an edit return the
	// zero Span (IsValid reports false). Edits do not update spans; see
	// the Span type documentation for the exact policy.
	Span() Span
	// contains filtered or unexported methods
}

Node is the interface implemented by all AST nodes. Every node carries its original raw bytes, its trivia (whitespace and comments) and its source range. It carries no value: only the value-carrying kinds do, and those implement Scalar. Implementation is restricted to this package.

type NodeType

type NodeType int

NodeType identifies the kind of AST node.

const (
	NodeDocument      NodeType = iota // NodeDocument is the root document node.
	NodeTable                         // NodeTable is a [table] header node.
	NodeArrayTable                    // NodeArrayTable is an [[array-table]] header node.
	NodeKeyValue                      // NodeKeyValue is a key = value pair.
	NodeKey                           // NodeKey is a (possibly dotted) key.
	NodeString                        // NodeString is a string value.
	NodeInteger                       // NodeInteger is an integer value.
	NodeFloat                         // NodeFloat is a float value.
	NodeBoolean                       // NodeBoolean is a boolean value.
	NodeDateTime                      // NodeDateTime is an offset date-time value.
	NodeLocalDateTime                 // NodeLocalDateTime is a local date-time value (no timezone).
	NodeLocalDate                     // NodeLocalDate is a local date value.
	NodeLocalTime                     // NodeLocalTime is a local time value.
	NodeArray                         // NodeArray is an array value.
	NodeInlineTable                   // NodeInlineTable is an inline table value.
	NodeComment                       // NodeComment is a standalone comment line.
)

func (NodeType) String

func (n NodeType) String() string

String returns the human-readable name of the node type.

type Pair

type Pair struct {
	Key   string
	Value any
}

Pair is one key of an ordered inline table, the input any value-writing operation takes as a []Pair: Set, SetCreate, AppendToArray, EnsureDefaults, and the element and value positions inside them.

Key is a SINGLE key, taken verbatim -- never a path. "a.b" is one key spelled with a dot in it, written quoted, and not a table "a" holding a "b". Use a nested []Pair (or a Default's path) to reach into a table. A duplicate key and a key that is not valid UTF-8 are each refused with KindBadInput when the operation converts the value.

A map[string]any is the unordered alternative: its keys are written in sorted order, where a []Pair is written in the order given.

type PathSegment

type PathSegment struct {
	Kind  SegmentKind
	Key   string // the key to look up, for SegmentKey
	Index int    // the element position, for SegmentIndex; negative counts from the end
}

PathSegment is one step of a parsed path. Kind says which of the remaining fields carries the step: Key for SegmentKey, Index for SegmentIndex.

func ParsePath

func ParsePath(path string) ([]PathSegment, error)

ParsePath parses a path in this package's path syntax into its segments. Every path-addressed operation in the package -- Resolve, Lookup, Set, Delete, the comment setters -- reads its path with it, so a path that parses here is spelled the way they expect.

Syntax:

  • A dot separates key segments: "server.host".
  • Brackets hold an index, which may be negative: "items[0]", "items[-1]".
  • Brackets may follow each other for nested arrays: "matrix[0][1]".
  • A quoted segment carries a key verbatim, so a key with dots or spaces stays one segment: `server."host.name"`. The quotes belong to the path syntax, not to the key.
  • A backslash escapes the next byte: `host\.name` is the single key "host.name", and inside a quoted segment `\"` is a quote.

The empty path names nothing and is reported as a KindBadPath diagnostic; so are an unclosed bracket or quote, a non-numeric index, and a trailing dot.

Example
package main

import (
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	segs, err := tomledit.ParsePath(`servers[0]."host.name"`)
	if err != nil {
		panic(err)
	}
	for _, s := range segs {
		if s.Kind == tomledit.SegmentKey {
			fmt.Printf("key %q\n", s.Key)
		} else {
			fmt.Printf("index %d\n", s.Index)
		}
	}
	// JoinPath is the quoting authority: it writes back what ParsePath read.
	fmt.Println(tomledit.JoinPath(segs))
}
Output:
key "servers"
index 0
key "host.name"
servers[0]."host.name"

type Position

type Position struct {
	Line   int // 1-based line number
	Column int // 1-based byte column within the line
	Offset int // 0-based byte offset from the start of the source
}

Position is a location in TOML source: a 1-based line and column plus the 0-based byte offset of the same point. Columns count bytes (not runes), matching the convention used by diagnostics and tokens.

func (Position) IsValid

func (p Position) IsValid() bool

IsValid reports whether the position was populated from source (Line >= 1). The zero Position is invalid.

type Reader added in v0.6.0

type Reader struct {
	// contains filtered or unexported fields
}

Reader reads one table of a document by hand, recording every problem it finds in a Collector. Every key read through Get or an accessor built on it becomes known; Done reports the keys nobody read as unknown.

The accessors share one convention. A required key that is absent is a KindMissingKey diagnostic at the table's own line; a value of the wrong type is a KindTypeMismatch diagnostic at the value's line; and each diagnostic's Fix says what to write, from the write argument the caller passes ("port = <integer>", "at least one [[command]] entry"). An accessor that reports a problem returns the zero value and false.

Example
package main

import (
	"errors"
	"fmt"

	tomledit "github.com/stricttools/go-toml-edit"
)

func main() {
	// One collector gathers the problems of every file read through it.
	var c tomledit.Collector
	r := c.Read("tool.toml", []byte(`name = "fmt"
mode = "fast"

[[command]]
help = "Formats files."
retries = "three"
`))
	if r != nil {
		name, _ := r.Str("name", true, `name = "<name>"`)
		fmt.Println(name)
		for _, cmd := range r.Tables("command") {
			cmd.Str("name", true, `name = "<command name>"`)
			cmd.Str("help", true, `help = "<one sentence>"`)
			cmd.Integer("retries", false, "retries = <integer>")
			cmd.Done() // every key nobody read is unknown
		}
		r.Done()
	}

	// Err sorts by file, line, path, and message.
	var all *tomledit.Errors
	if errors.As(c.Err(), &all) {
		for _, d := range all.Diagnostics() {
			fmt.Println(d)
		}
	}
}
Output:
fmt
tool.toml:2: mode: is not a key of this table; remove it, or write one of command, name
tool.toml:4: command[0].name: is missing; write name = "<command name>"
tool.toml:6: command[0].retries: is a string, not an integer; write retries = <integer>

func (*Reader) Boolean added in v0.6.0

func (r *Reader) Boolean(key string, required bool) (bool, bool)

Boolean reads a key written as true or false; the fix it reports is "write <key> = true or <key> = false". It returns the value and whether the key was read without a problem.

func (*Reader) Child added in v0.6.0

func (r *Reader) Child(path string, v Value) *Reader

Child returns a Reader over a table value at the path the caller names, sharing this Reader's file and collector: the form for a table met inside a list the caller walks itself. It returns nil when v is not a table.

func (*Reader) Done added in v0.6.0

func (r *Reader) Done()

Done reports every key of the table nobody read, in the order the keys were written: "is not a key of this table", with the fix "remove it, or write one of <the keys read, sorted>", or "remove it" when nothing was read. A key holding a table or a list of [[header]] tables is a KindUnknownTable diagnostic listing its direct child keys, any other a KindUnknownKey one -- the kinds the decode engine reports for the same refusals.

func (*Reader) Errorf added in v0.6.0

func (r *Reader) Errorf(key, format string, args ...any)

Errorf records a KindInvalidValue diagnostic at one key of the table: its line (the table's own when the key is absent) and its path.

func (*Reader) ErrorfAt added in v0.6.0

func (r *Reader) ErrorfAt(line int, path, format string, args ...any)

ErrorfAt records a KindInvalidValue diagnostic in the Reader's file at a line and path the caller names: an element of a list, say, or a key the table derives from another.

func (*Reader) File added in v0.6.0

func (r *Reader) File() string

File returns the name of the file the table was read from.

func (*Reader) Flag added in v0.6.0

func (r *Reader) Flag(key string) bool

Flag reads an optional key whose only value is true. Its absence is its other meaning, so false is a second spelling of the absence and is refused as a KindInvalidValue diagnostic: "is false, and its absence already means that; write <key> = true, or remove the key".

func (*Reader) Get added in v0.6.0

func (r *Reader) Get(key string) (Value, bool)

Get reads a key: it becomes known, and when the table holds it its line is recorded in Lines. It returns the key's value and whether the table holds it.

func (*Reader) Has added in v0.6.0

func (r *Reader) Has(key string) bool

Has reads a key and reports whether the table holds it.

func (*Reader) Integer added in v0.6.0

func (r *Reader) Integer(key string, required bool, write string) (int64, bool)

Integer reads an integer key, as Str reads a string one.

func (*Reader) KeyPath added in v0.6.0

func (r *Reader) KeyPath(key string) string

KeyPath returns the path of one key of the table: "command[0].flag" for the key "flag" of the table "command[0]".

func (*Reader) Keys added in v0.6.0

func (r *Reader) Keys() []string

Keys returns the table's keys in the order they were first written. Listing them marks none known.

func (*Reader) Line added in v0.6.0

func (r *Reader) Line(key string) int

Line returns the line a key was written on, or the table's own line when the table does not hold the key.

func (*Reader) Lines added in v0.6.0

func (r *Reader) Lines() map[string]int

Lines returns the line of every key read so far. The map is the Reader's own and fills as keys are read, so a caller may keep it before reading.

func (*Reader) Missing added in v0.6.0

func (r *Reader) Missing(key, write string)

Missing records that a required key is absent: "is missing; write <write>", a KindMissingKey diagnostic at the table's own line.

func (*Reader) NonEmpty added in v0.6.0

func (r *Reader) NonEmpty(key, write string)

NonEmpty refuses a key holding an empty list, as a KindInvalidValue diagnostic: "is empty; write <write>". It neither reads the key nor reports its absence or its type.

func (*Reader) Path added in v0.6.0

func (r *Reader) Path() string

Path returns the table's own key path, empty for the top-level table.

func (*Reader) Peek added in v0.6.0

func (r *Reader) Peek(key string) (Value, bool)

Peek returns the value of a key without reading it: the key does not become known, and its line is not recorded.

func (*Reader) PeekTrue added in v0.6.0

func (r *Reader) PeekTrue(key string) bool

PeekTrue reports whether the table holds key written as true, without reading it.

func (*Reader) Str added in v0.6.0

func (r *Reader) Str(key string, required bool, write string) (string, bool)

Str reads a string key. A required key that is absent is reported missing; an optional one returns false without a diagnostic.

func (*Reader) StrList added in v0.6.0

func (r *Reader) StrList(key string, required bool, write string) ([]string, bool)

StrList reads a key holding a list of strings. An element that is not a string is reported at its own line and path (key[i]), and the read returns false.

func (*Reader) Sub added in v0.6.0

func (r *Reader) Sub(key string) *Reader

Sub reads a key holding one table and returns a Reader over it. An absent key returns nil without a diagnostic; a value that is not a table is reported, with the fix "write [<header>]", and returns nil.

func (*Reader) TableLine added in v0.6.0

func (r *Reader) TableLine() int

TableLine returns the line the table was written on: its own header, the inline table spelling it, or the construct that implied it; line 1 for the top-level table.

func (*Reader) Tables added in v0.6.0

func (r *Reader) Tables(key string) []*Reader

Tables reads a key holding a list of tables -- [[key]] entries, or an array of inline tables -- and returns one Reader per table, each at its path key[i]. An absent key returns nil without a diagnostic. An element that is not a table is reported and skipped.

func (*Reader) WrongKind added in v0.6.0

func (r *Reader) WrongKind(key string, v Value, want, write string)

WrongKind records that a key holds a value of the wrong type: "is <what v is>, not <want>; write <write>", a KindTypeMismatch diagnostic whose Expected is want and whose Got is what v is.

type Record

type Record struct {
	// contains filtered or unexported fields
}

Record is a table of the read-layer: an ordered set of entries, whatever the spelling that produced it. Records are read-only; edit through the document's path API.

A record is a snapshot of the document as it was when the layer was built. It stays valid until the next mutation of that document, and reading it afterwards reports what the document held before the change. Mutating a document while iterating its entries is unspecified.

func (*Record) Entries

func (r *Record) Entries() iter.Seq[Entry]

Entries iterates the record's entries in first-appearance order.

func (*Record) Get

func (r *Record) Get(key string) (Entry, bool)

Get returns the entry with the given key, and whether the record has one.

func (*Record) Len

func (r *Record) Len() int

Len returns the number of entries in the record.

func (*Record) Node

func (r *Record) Node() (Node, bool)

Node returns the concrete node backing the record, and whether one does. A table written as a header or as an inline table is backed by that construct, an array-of-tables entry by its own [[header]], and the root record by the document. A record no single node stands for -- one implied by a longer header or by a dotted key -- reports false; it exists in the layer, and nothing in the source stands for it alone.

func (*Record) Span

func (r *Record) Span() Span

Span returns the record's anchoring range: its own header where it has one, the inline table that spells it out, or the construct that implied it.

type Scalar

type Scalar interface {
	Node

	// Value returns the node's payload as the Go type the conversion table
	// gives that TOML type: string, int64, float64, bool, time.Time, or one of
	// LocalDateTime, LocalDate and LocalTime.
	Value() any

	// AsString reads the node as a string. Only a string is one.
	AsString() (string, error)

	// AsInt reads the node as an int64. Only an integer is one: a float is
	// never an integer, however whole it is written.
	AsInt() (int64, error)

	// AsFloat reads the node as a float64: a float verbatim, and an integer
	// the target holds exactly.
	AsFloat() (float64, error)

	// AsBool reads the node as a bool. Only a boolean is one.
	AsBool() (bool, error)

	// AsTime reads the node as a time.Time: an offset date-time verbatim, and
	// a local date-time or local date read as UTC, because a time.Time target
	// declares that intent. A local time carries no date and is refused.
	//
	// A STRING is refused too, with KindTypeMismatch, even one spelling a valid
	// RFC 3339 timestamp. A struct field of type time.Time answers the same
	// way: both surfaces read the one conversion table, and no target decodes
	// itself. To read a string as a time, read it with AsString and parse it.
	AsTime() (time.Time, error)

	// AsLocalDateTime reads the node as a LocalDateTime. Only a local
	// date-time is one.
	AsLocalDateTime() (LocalDateTime, error)

	// AsLocalDate reads the node as a LocalDate. Only a local date is one.
	AsLocalDate() (LocalDate, error)

	// AsLocalTime reads the node as a LocalTime. Only a local time is one.
	AsLocalTime() (LocalTime, error)
}

Scalar is the sub-interface of Node the value-carrying node kinds implement: strings, integers, floats, booleans and the four date-time flavors. Every other kind holds structure rather than a value -- a document, a table, an array-of-tables, an array, an inline table, a key, a key-value pair, a comment -- and is read through its own accessors instead.

type SegmentKind

type SegmentKind int

SegmentKind distinguishes the two kinds of path step: a lookup by key and a lookup by position.

const (
	// SegmentKey addresses a child by name.
	SegmentKey SegmentKind = iota
	// SegmentIndex addresses an element by position. A negative index counts
	// from the end, so -1 is the last element.
	SegmentIndex
)

type Span

type Span struct {
	Start Position
	End   Position
}

Span is the half-open source range [Start, End) covered by a node: Start is the position of the node's first byte, End is the position immediately after its last byte.

Spans reflect the most recent Parse. Edit operations (Set, Delete, RenameKey, Merge, ...) do not update spans: nodes created programmatically carry the zero Span (IsValid reports false), and nodes whose content was edited keep the span from the last parse. To obtain fresh spans after editing, serialize with Bytes and re-Parse the result.

func (Span) IsValid

func (s Span) IsValid() bool

IsValid reports whether the span was populated by Parse. The zero Span is invalid; it is returned by nodes created programmatically by an edit.

type Spec

type Spec struct {
	// Fields describes the keys the table may carry, by name.
	Fields map[string]Field

	// Dynamic describes every key Fields does not name: it is how a table of
	// arbitrary keys, all of one shape, is spelled. A nil Dynamic means no
	// other key is permitted, and is the one place in a descriptor where nil
	// carries a meaning of its own.
	Dynamic *Field
}

Spec is the descriptor of a table: the keys it may carry, and whether keys beyond them are permitted at all.

Field values are built whole and assigned into Fields -- map elements are not addressable, so mutating one in place is not a supported spelling:

spec := &tomledit.Spec{Fields: map[string]tomledit.Field{
    "host": {Kind: tomledit.FieldKindString, Required: true},
    "port": {Kind: tomledit.FieldKindInteger},
}}

A missing-key diagnostic is reported in lexicographic key order, because map iteration has no order to report in.

type StringNode

type StringNode struct {
	// contains filtered or unexported fields
}

StringNode represents a string value.

func (*StringNode) AsBool

func (n *StringNode) AsBool() (bool, error)

AsBool reads the node through the conversion table.

func (*StringNode) AsFloat

func (n *StringNode) AsFloat() (float64, error)

AsFloat reads the node through the conversion table.

func (*StringNode) AsInt

func (n *StringNode) AsInt() (int64, error)

AsInt reads the node through the conversion table.

func (*StringNode) AsLocalDate

func (n *StringNode) AsLocalDate() (LocalDate, error)

AsLocalDate reads the node through the conversion table.

func (*StringNode) AsLocalDateTime

func (n *StringNode) AsLocalDateTime() (LocalDateTime, error)

AsLocalDateTime reads the node through the conversion table.

func (*StringNode) AsLocalTime

func (n *StringNode) AsLocalTime() (LocalTime, error)

AsLocalTime reads the node through the conversion table.

func (*StringNode) AsString

func (n *StringNode) AsString() (string, error)

AsString reads the node through the conversion table.

func (*StringNode) AsTime

func (n *StringNode) AsTime() (time.Time, error)

AsTime reads the node through the conversion table.

func (*StringNode) Comment

func (n *StringNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*StringNode) LeadingComments

func (n *StringNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*StringNode) Raw

func (n *StringNode) Raw() []byte

Raw returns a copy of the bytes the value was written as -- its lexeme, with the spelling the document used. A value the library wrote carries the bytes it rendered.

func (*StringNode) Span

func (n *StringNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*StringNode) Style

func (n *StringNode) Style() StringStyle

Style returns the quoting style the string was written in.

func (*StringNode) Type

func (n *StringNode) Type() NodeType

Type returns NodeString.

func (*StringNode) Value

func (n *StringNode) Value() any

Value returns the decoded string.

type StringStyle

type StringStyle int

StringStyle indicates the quoting style for a string node.

const (
	StringBasic            StringStyle = iota // StringBasic is a double-quoted string ("...").
	StringLiteral                             // StringLiteral is a single-quoted string ('...').
	StringMultiLineBasic                      // StringMultiLineBasic is a triple-double-quoted string ("""...""").
	StringMultiLineLiteral                    // StringMultiLineLiteral is a triple-single-quoted string (”'...”').
)

type TableNode

type TableNode struct {
	// contains filtered or unexported fields
}

TableNode represents a [table] header and its children.

func (*TableNode) Children

func (n *TableNode) Children() []Node

Children returns the constructs written under this header, in document order, as a copy.

func (*TableNode) Comment

func (n *TableNode) Comment() string

Comment returns the text of the node's inline comment, without the "#" and the whitespace around it: a node written `x = 1 # note` answers "note". A node with no inline comment answers the empty string. Raw carries the bytes as written, for a caller that needs them exactly.

func (*TableNode) KeyPath

func (n *TableNode) KeyPath() []string

KeyPath returns the parts of the header's key, decoded, as a copy.

func (*TableNode) LeadingComments

func (n *TableNode) LeadingComments() []string

LeadingComments returns the text of each comment line written above the node, in order, each without its "#", its trailing newline and the whitespace around them. A node with no leading comments answers nil, the same spelling the path-based GetLeadingComments uses. Raw carries the bytes as written, for a caller that needs them exactly.

func (*TableNode) Raw

func (n *TableNode) Raw() []byte

Raw answers with a copy. The serializer splices through rawBytes instead, so the copy is paid for only by a caller that asked for the bytes.

func (*TableNode) Span

func (n *TableNode) Span() Span

Span returns the half-open source range the node occupied in the document it was parsed from, each end carrying a 1-based line and column and a 0-based byte offset. Spans reflect the most recent Parse: an edit does not recompute them, and a node the library built rather than parsed carries the zero span, for which Span.IsValid reports false.

func (*TableNode) Type

func (n *TableNode) Type() NodeType

Type returns NodeTable.

type Value added in v0.6.0

type Value struct {
	// contains filtered or unexported fields
}

Value is one value of a document as a Reader sees it: its type, the line it was written on, and what it holds. It is a view over the document's own nodes and records, not a copy of them.

func (Value) Bool added in v0.6.0

func (v Value) Bool() bool

Bool returns the boolean a ValueBoolean holds, and false for any other type.

func (Value) Describe added in v0.6.0

func (v Value) Describe() string

Describe writes the value for a diagnostic: a string quoted, any other value by what it is ("an integer", "a table").

func (Value) Float added in v0.6.0

func (v Value) Float() float64

Float returns the float a ValueFloat holds, and 0 for any other type.

func (Value) Int added in v0.6.0

func (v Value) Int() int64

Int returns the integer a ValueInteger holds, and 0 for any other type.

func (Value) Line added in v0.6.0

func (v Value) Line() int

Line returns the 1-based line the value was written on.

func (Value) List added in v0.6.0

func (v Value) List() []Value

List returns the elements a ValueList holds, in document order, and nil for any other type. The entries of an array of tables are ValueTable elements, each on the line of its own [[header]].

func (Value) Node added in v0.6.0

func (v Value) Node() (Node, bool)

Node returns the scalar or array node backing the value, and whether one does. A table and an array of tables report false: their contents are read through a Reader (Reader.Sub, Reader.Tables, Reader.Child).

func (Value) Text added in v0.6.0

func (v Value) Text() string

Text returns the string a ValueString holds, and "" for any other type.

func (Value) Type added in v0.6.0

func (v Value) Type() ValueType

Type returns what the value is.

type ValueType added in v0.6.0

type ValueType int

ValueType classifies a value a Reader hands back. A table in any spelling is ValueTable; a plain array and an array of tables -- whether written as [[header]] entries or as an array of inline tables -- are both ValueList.

const (
	ValueString   ValueType = iota // a string
	ValueInteger                   // an integer
	ValueBoolean                   // true or false
	ValueFloat                     // a float
	ValueTable                     // a table in any spelling
	ValueList                      // an array, or an array of tables
	ValueDateTime                  // an offset date-time, local date-time, local date, or local time
)

func (ValueType) String added in v0.6.0

func (t ValueType) String() string

String returns the phrase a diagnostic uses for a value of this type: "a string", "an integer", "a date or time".

type WalkMode

type WalkMode int

WalkMode controls which nodes the Walk visitor function is called for. Both modes traverse the syntax tree; they differ only in whether the container nodes standing between the keys and the scalars are handed to the visitor.

const (
	// WalkLeaves visits only scalar (leaf) values. Container nodes
	// (InlineTableNode, ArrayNode) are not passed to fn, but their
	// children are still recursed into.
	WalkLeaves WalkMode = iota

	// WalkAll visits containers (inline tables, arrays) AND their children.
	// The visitor is called for every node.
	WalkAll
)

Directories

Path Synopsis
Package tomlfile reads TOML files into tomledit documents and writes documents back to files.
Package tomlfile reads TOML files into tomledit documents and writes documents back to files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL