Skip to content
Changelog
Edit
On this page

#Changelog

#0.4.3

The tool describes itself with one sentence everywhere, and its documentation moved onto selfdoc's .stricttools/ layout.

#Fixes

  • The library describes itself with one sentence everywhere. The README, docs site and package metadata now carry the same line as the package doc comment.

#0.4.2

Documentation frontmatter converted to TOML for the current selfdoc; no user-facing change.

#Infrastructure

  • Documentation frontmatter converted to TOML for the current selfdoc; no user-facing change.

#0.4.1

The library describes itself consistently in its README and package documentation, and its documentation base is the unified site.

#Fixes

  • The project describes itself consistently in its README and package documentation. The README opened with a tagline instead of a definition, the package doc comment said something different again, and the project metadata carried no description at all.
  • Documentation links point at the unified site. The declared docs base was the retired per-project host; it is https://smmh.dev/go-toml-edit/ now, so generated sitemaps, feeds and llms.txt name the address that serves the pages.

#0.4.0

The strictness-and-fidelity redesign: a logical read-layer, strict-only decoding with value-returning entry points, a unified diagnostic contract, fragment-based rendering with canonical spellings, and a hardened edit surface that refuses to corrupt a document.

Context

This release is the whole redesign campaign shipped at once. The library's surface was rebuilt around what its consumers actually hand-rolled downstream: strict decoding (unknown keys, exact key matching, every independent violation collected), a logical read-layer that answers what a document means regardless of how its tables are spelled, one diagnostic type with a closed kind set and compiler-style rendering, and byte-precise fragment splicing so an edit touches only the bytes it names. Every edit sequence that used to corrupt a document or panic its readers now refuses with a classified diagnostic. The decode entry points return the value they build, so a failed decode leaves nothing to inspect; the custom-decoder hooks were removed outright, making the engine the only decode path. Format() reformats: whitespace is formatting, comments are content, and only content is preserved. Breaking throughout, as a pre-stable redesign should be; the changelog's breaking entries carry the migration for every removed or renamed surface.

#Breaking

  • Breaking. Two renames on the document API. The document type DocumentNode is now Document -- replace every *tomledit.DocumentNode with *tomledit.Document (Parse, Diff and Merge take and return the new spelling). The key-renaming method Rename is now RenameKey -- replace doc.Rename(path, newKey) with doc.RenameKey(path, newKey), signature unchanged.
  • Breaking. ParseError is replaced by Error, one diagnostic type for the whole library. Replace *tomledit.ParseError with *tomledit.Error, and read positions through the embedded Pos field (err.Pos.Line rather than err.Line), which also carries a byte Offset. Errors can now be matched by kind with errors.Is(err, tomledit.ErrSyntax) and structurally with errors.As, instead of by message text; they also render in the compiler convention -- config.toml:3:10: expected a value, got end of input, with the location, the addressed path and the message joined by : -- in place of the old line 3, column 10: prefix. Parse messages also name what they saw in the reader's vocabulary rather than the lexer's: a fixed piece of punctuation is quoted as its glyph and everything else is plain words, so expected RightBracket, got Newline now reads expected ']', got newline, expected value, got EOF reads expected a value, got end of input, and unexpected token Equals reads '=' is not valid here.
  • Breaking. Path, access and edit failures are now structured diagnostics instead of plain messages: Resolve, Set, SetCreate, Delete, RenameKey, NewTable, NewArrayTable, the comment setters, the Cursor and Merge all return *Error values matchable with errors.Is(err, tomledit.ErrNotFound) and friends. Code matching on error text must switch to the kind sentinels -- the "path syntax error: " and "resolution error: " prefixes are gone, and messages now carry the addressed path.
  • Breaking. Get is deleted, and a path that names no single node is now refused instead of resolving to an internal placeholder. Get swallowed every diagnostic and returned nil, so a typo and a type error looked alike; use Resolve for the node with a reason for failure. An array-of-tables (products) and a table nothing spells out on its own (a where only [a.b] or a.b.c = 1 appears) now make Resolve return a KindWrongContainer diagnostic, and Lookup/Has report false -- read those through doc.Root(), or address a concrete entry with an index (products[0]). Editing is unchanged: Set("products[0].x", ...) and Delete("products[0]") still address entries.
  • Cursor. A cursor chain crosses every table spelling the same way -- doc.Key("a").Key("b") reaches b whether a is a header table, an inline table, or a table only implied by [a.b] -- and Node() on a position no single node stands for (an array-of-tables, an implied table) returns nil and reports a KindWrongContainer diagnostic through Err() instead of handing back an internal placeholder.
  • Breaking: decoding is strict, and strictness is the only mode. Unmarshal, Decode and the new Validate refuse an unknown key, an unknown table, a value of a kind the target does not accept, a value the target cannot hold exactly, and a missing required key -- where the old decoder silently ignored every key it did not recognize. An excluded field (toml:"-", or an unexported one) is not part of the document universe either, so a key naming one is unknown rather than ignored -- a toml tag on an unexported field binds nothing and changes that in no way; required is the only toml tag option the package reads, and any other option (omitempty) is now refused. A fixed-size Go array requires exactly its own number of elements. Every violation is a positioned *Error, and independent violations arrive together as an *Errors aggregate in document order: the walk continues across the sibling keys instead of stopping at the first refusal.
  • Breaking: key matching is exact. A document key reaches a struct field only when it is spelled exactly as the field's toml tag -- or, with no tag, exactly as the field name. A key that differs only in case is an unknown key and is reported as one, where the decoder previously folded case (host reached an untagged field Host, HOST reached it too). An untagged field now needs the document to spell its Go name, capital and all; tag the field (toml:"host") to keep a lowercase spelling. Both schema sources agree on this: Unmarshal/Decode and a Spec handed to Validate match keys the same way and report a mis-cased key identically.
  • Breaking: creating a table refuses a name that is already taken. NewTable and NewArrayTable consult the document's logical tree before appending a header, and refuse with a KindConflict diagnostic when the name is already bound -- by a value, an inline table, a table with a header of its own, an array-of-tables, or a table a dotted key spelled out -- and when a prefix of the path holds a value. Such an edit used to be accepted and left behind a document that no longer had a valid TOML spelling. Appending another [[entry]] to an existing array-of-tables, and giving a header to a table only implied by a longer header, both still work.
  • Breaking: renaming onto a taken name is refused. RenameKey now consults the document's logical tree for the new name, so a [header] table, an array-of-tables or a table a dotted key implied refuses the rename with a KindConflict diagnostic. It previously looked only at the key-value pairs written literally beside the renamed key, and renaming onto any other construct silently produced a document with two constructs on one key.
  • Breaking: a value write refuses a name a structural construct binds. Set and SetCreate report KindWrongContainer when the target key is bound by a [header] table, by an array-of-tables, or by a table another construct only implied (a dotted key, or a longer header). Such a write used to be accepted and appended a second binding of the same name, leaving a document with no valid TOML spelling -- and, where a dotted key had spelled the name out, silently writing a duplicate key beside it. Delete the construct first, or use a structural operation. A key holding an inline table is a value and is still replaced wholesale.
  • **Breaking: a comment write inside an inline table reports KindWrongContainer.** SetComment and SetLeadingComments on a key of an inline table used to report KindConflict. TOML gives an inline table no place to put a comment, so the refusal is now the same kind as renaming through an array index -- a container that structurally cannot host the operation, rather than an edit that would produce an invalid document. Code matching ErrConflict on this refusal must match ErrWrongContainer instead.
  • **Breaking: MergeDefaults is replaced by EnsureDefaults.** The new operation takes an ordered []Default{Path, Value} of full paths -- the old sub-path-plus-nested-map input is gone -- and returns the list of paths it added. A path the document already carries in any spelling (a dotted key, a [header] table, an inline table, a table a longer header implies) is left alone, a missing intermediate table is created as a standard header rather than an inline table so the same list writes the same bytes every time, and seeding stops at the first error with everything written before it reported in the returned list.
  • Breaking. Node no longer has Value(); the value-carrying node kinds implement the new Scalar interface, which embeds Node and carries it. Read a scalar with node.(tomledit.Scalar).Value(), and read a container through its own accessors -- Value() on a document, table, array-of-tables, array or inline table returned that node's children as []Node, so a []interface{} assertion on an array's value always failed.
  • Node fields are unexported. A node's contents are read through named accessors instead of struct fields -- Children(), KeyPath(), Elements(), Parts(), RawParts(), Styles(), Key(), Val(), Text(), Style(), Base() -- and each slice-returning accessor answers with a copy, so writing into what a read handed you no longer edits the document.
  • Comments are read and written as text, through the document. Comment() and LeadingComments() now answer a comment's content -- no #, no surrounding whitespace, no trailing newline -- which is the same form (*Document).SetComment(path, text) and (*Document).SetLeadingComments(path, texts) take, so a comment read from one document writes into another unchanged. Raw() still answers the bytes as written. The per-node SetComment/SetLeadingComments methods are gone; comment writes go through the document, which is what can refuse a container with nowhere to put one.
  • A pre-built node is no longer accepted as a value. Set, SetCreate, AppendToArray and the other value-taking write entry points refuse anything implementing Node with ErrBadInput; values enter as Go values and the library renders them. Copy a value from one key to another by reading the value and writing it, not by handing the node over.
  • Written values now render canonically. A float the library writes uses the shortest round-trip form (1e+300, not 301 digits) and a written string escapes control characters with lowercase hex. Values the library only read still keep their original bytes.
  • **Set is a no-op when it would write the bytes already there.** Writing a value equal to what the file says leaves the node, its spelling and its span untouched and records no edit; writing a different one normalizes the spelling on first touch and is byte-stable afterwards. A NaN with its sign bit set is now refused with KindBadInput.
  • **Format() keeps the blank-line grouping you wrote.** A run of blank lines becomes exactly one, a place you left no gap keeps none, and the output no longer opens with a blank line. WithTableBlankLine is now insertion-only -- it adds a blank line before a table that has none and never removes the separation the document already carries, so WithTableBlankLine(false) no longer strips your blank lines.
  • The typed accessors report an error instead of a comma-ok boolean, and follow the decode engine's conversion table. GetString, GetInt, GetBool, GetFloat and GetTime on a document, and String(), Int(), Bool(), Float() and Time() on a cursor, now return (T, error): a positioned diagnostic naming what failed -- a bad path, a missing key, a container the step does not apply to, a type mismatch, or a value the target cannot hold exactly -- where the second return used to be an unexplained false. Scalar nodes gain the matching AsString, AsInt, AsFloat, AsBool, AsTime, AsLocalDateTime, AsLocalDate and AsLocalTime. All three surfaces read the same conversion table the decoder uses, so a float read now accepts an integer the target holds exactly, and a time.Time read accepts a local date-time or a local date.
  • Decode returns the value it decodes. Unmarshal, Decode and DecodeNode no longer take a destination pointer: they are generic, allocate the result themselves, and answer either a value or diagnostics -- so a failed decode has no partially written target to inspect. Migration: var cfg Config; err := tomledit.Unmarshal(data, &cfg) becomes cfg, err := tomledit.Unmarshal[Config](data); doc.Decode(&cfg) becomes tomledit.Decode[Config](doc) (a package function, because Go has no parameterized methods); tomledit.DecodeNode(node, &cfg) becomes tomledit.DecodeNode[Config](node). Each returns *T, so a struct target reads as before and a map or slice target is dereferenced once. Code that seeded a target with defaults and decoded over it uses the new DecodeOver.
  • **Removed: the document's path-based Items and Len.** (*Document).Items(path) and (*Document).Len(path) are gone. To iterate or count what a path names, walk to it with the Cursor and ask the position -- doc.Key("config").Key("tags").Items() and .Len() are unchanged. To read the logical structure, use the read-layer: Record.Entries enumerates a table's keys in first-appearance order, Entry.Records hands out an array-of-tables' entries (its length is the count), and an array node's Elements hands out a plain array's. The deleted Len reported -1 for a missing path, a scalar and a table alike; the replacements distinguish them.
  • **Removed: Marshal.** The map-only encoder is gone: it accepted map[string]any and refused structs, slices and primitives, so it was neither a general encoder nor a shape the library was going to grow into. There is no replacement in this release -- a real struct-to-TOML Marshal is deferred work. Build a document through the editing surface instead: SetCreate writes a map as a container value, and Bytes or Format renders it.
  • **Removed: the exported token vocabulary and the Trivia struct.** Token, TokenType and the 25 Token* constants, and the Trivia struct, are now unexported. Nothing could reach them: no exported function takes or returns a token, and no exported method hands out a node's trivia. Reading a node's comments and spacing is Comment(), LeadingComments() and Raw() on the Node interface, as it already was.
  • **Breaking: Format always inserts the blank line before a table header.** WithTableBlankLine and FormatConfig.TableBlankLine are removed. A [table] or [[array-table]] header written flush against what precedes it always gets a blank line above it, and there is no longer a way to turn that off. What the library preserves is content -- the comments -- while whitespace is formatting, and Format is where it is strict about output looking good. The insertion still only ever adds: a blank line already there is never doubled, a run still collapses to one, and none is ever removed. Callers passing WithTableBlankLine(true) drop the option; callers passing WithTableBlankLine(false) use Bytes instead if they need the writer's own spacing kept.
  • Breaking: a decode target never decodes itself. The Unmarshaler interface (UnmarshalTOML(node) error) is gone, and a target's encoding.TextUnmarshaler is no longer consulted for a string value. Every value a decode produces now goes through the one conversion table, so a type that used to parse itself decodes the plain value and converts it explicitly. The KindHookError kind and its ErrHookError sentinel are gone with them. One visible consequence: a struct field of type time.Time no longer accepts an RFC 3339 string -- it is now KindTypeMismatch, exactly as Scalar.AsTime, Document.GetTime and Cursor.Time have always answered. Decode such a field as a string and call time.Parse.

#Features

  • Full documentation: a usage guide, a design guide, and Go doc comments on the whole exported API. The package now reads as documented on pkg.go.dev, and the docs site carries prose guides alongside the generated reference.
  • Byte offsets on positions and spans. Position now carries an Offset field alongside line and column, so every node span reports the byte range of its construct in the source and callers no longer have to re-derive offsets by counting lines.
  • **ParseFile.** tomledit.ParseFile(path) reads and parses a file in one call, and the document remembers where it came from: every diagnostic it later produces -- parse, access, edit, comment, cursor -- names the file, so errors render as config.toml:2:1: duplicate key. A file that cannot be read comes back as the underlying read error.
  • Path helpers. tomledit.ParsePath and tomledit.JoinPath expose the library's path syntax as data ([]PathSegment, each a key or an index), so a consumer can build, inspect and render paths without string surgery. JoinPath is the quoting authority: a key that would not read back bare comes out quoted, and every path a diagnostic prints can be pasted straight back into Resolve, Set or Delete.
  • The read-layer. doc.Root() returns the document as a logical tree of Records and Entrys: keys in first-appearance order, dotted keys expanded, inline tables and [header] tables indistinguishable, and array-of-tables entries collected under one key. Each entry reports what it holds (Kind), where its key was written (KeySpan), and -- for a scalar or a plain array -- the value node behind it (Node); a record reports the construct that backs it (Record.Node): a [header] table, an inline table, an array-of-tables entry, the document itself for the root record, and nothing at all for a table only implied by a longer header or a dotted key. A collection reports the synthesized range covering all its entries (RecordsSpan). Reading values through the layer no longer means knowing which of TOML's spellings a document happened to use.
  • **Lookup and Has.** doc.Lookup(path) returns (Node, bool) and doc.Has(path) returns a bool: the comma-ok way to ask whether a path names a concrete node, without building a diagnostic for the answer "no".
  • **The descriptor: Spec, Field, Validate and DecodeNode.** A document shape can now be described as data -- tomledit.Spec{Fields: map[string]tomledit.Field{...}}, with Dynamic for tables of arbitrary keys and FieldAny() for values left undescribed -- and checked with doc.Validate(spec), which is the same engine Decode runs, so a consumer whose schema is only known at runtime gets the same diagnostics as one with a Go struct. tomledit.DecodeNode(node, &v) decodes one construct -- a table, an array-table entry, an inline table, an array or a scalar -- with those same rules.
  • Feature: structural edits. PermuteChildren(path, order) reorders the children of any concrete container -- the document (the empty path), a [header] table, an array-of-tables entry, an array, an inline table. The order is a gather: order[i] is the index of the child that moves to position i, and it must name every child exactly once, so a wrong length, an out-of-range index or a repeated one is refused with KindBadInput and nothing is reordered. Each child's comments and blank lines travel with it. AppendToArray(path, value) and RemoveFromArray(path, index) resize a plain array, the index counting from the end when negative.
  • Feature: ordered inline tables. Pair{Key, Value} is a new input type: pass a []Pair to Set, SetCreate, AppendToArray or EnsureDefaults and the inline table is written in the order given, where a map[string]any is written with its keys sorted. A Pair's key is a single key taken verbatim ("a.b" is one key with a dot in it, written quoted -- not a path), and a duplicate key or a key that is not valid UTF-8 is refused with KindBadInput.
  • Repeated reads no longer refold the document. The logical read-layer behind Root, the path getters, Resolve, the Cursor and Decode is now built once and reused until the document is written to, instead of being rebuilt on every access. Parsing alone builds nothing, concurrent reads of a shared document remain safe, and a record obtained before a write keeps answering what the document said before it.
  • Value renderers exported. QuoteString, QuoteKey and FormatFloat write the same TOML spellings the library writes; FormatFloat is total, rendering every float64 including NaN and the infinities.
  • **RenameKey renames tables.** A key bound by a [header] or [[array-table]] can now be renamed: every header naming that table, the nested ones included, and every dotted pair written under the name move together. Previously such a rename reported "not found", refused, or renamed only the dotted half.
  • **WriteFile writes a document to a file, atomically and only if it survives a round trip.** The rendered bytes must parse, and re-rendering that parse must reproduce them exactly, before anything reaches the filesystem -- otherwise the write is refused with a round-trip diagnostic naming the byte where the two disagree. The bytes then go to a temporary file in the destination's own directory and replace it in one rename, so a failure leaves the destination untouched and nothing behind. An existing file keeps its mode; a new one is created like any other file the process creates.
  • **DecodeOver decodes a document over a seed, and reports what the document wrote.** It takes a seed FACTORY (func() T), so the value it fills is an allocation of its own rather than memory the caller still holds, and returns (*T, []string, error): the decoded value, the document paths it wrote in document order, and the diagnostics. A key the document does not carry keeps the seed's value, which is the defaults-overlay pattern; the path list says which of the seed the file replaced. A value written whole -- an array, an any-typed table, a target that decodes itself -- is one path rather than one per element inside it.
  • **DecodeSpec returns a hand-built descriptor's values.** (*Document).DecodeSpec(spec) runs the same engine Validate runs and, when the document satisfies the descriptor, answers with the document as native Go data: map[string]any for every table spelling, []any for arrays and arrays of tables, and the native type of each scalar. It is atomic without exception -- either the whole map or no map at all -- because no reflection is involved and no consumer code runs while it is built.
  • **Walk documents itself as the syntactic traversal.** Its doc comment now states the contract the library's two read surfaces divide between them: Walk hands the visitor the concrete nodes in the order the file writes them, with their spelling, trivia and span, so a value written as an inline table arrives as an *InlineTableNode; Root and the read-layer answer the same questions with the spellings folded away. Behavior is unchanged.
  • Documentation: runnable examples for every entry point the redesign introduced. ParseFile, WriteFile, the read-layer (Root/Record/Entry), Validate, DecodeSpec, Decode, DecodeNode, DecodeOver, EnsureDefaults, AppendToArray, RemoveFromArray, PermuteChildren, RenameKey, the ordered []Pair write, the comment setters, QuoteString, QuoteKey, FormatFloat, ParsePath/JoinPath and the *Error contract each have a godoc example that runs in the suite.
  • **Path-based comment getters: GetComment and GetLeadingComments.** Reading a comment now takes the same path the setters take: doc.GetComment("server.host") answers the inline comment on the line that binds the key, and doc.GetLeadingComments("server.host") the comment lines above it. Both resolve exactly as SetComment and SetLeadingComments do, 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. A node with no inline comment answers the empty string, one with no leading comments answers nil, and neither is an error. The node-level Comment() and LeadingComments() remain the general mechanism for a traversal that holds nodes rather than paths.

#Fixes

  • Four decoding defects, from one cause. The decoder used to rebuild the document structure from raw headers rather than reading the document: a [items.a] table under a map-typed field decoded into the map itself instead of its a element (silently, for map[string]any); an array-of-tables nested under a plain table ([[a.b]] under [a]) was never decoded at all; a table reaching a map element that could not hold one panicked inside reflect instead of reporting a type mismatch; and a fixed-size Go array quietly accepted a shorter array, zero-padding values the document never carried. All four now decode, or refuse with a positioned diagnostic.
  • Fix. Two write-path defects are gone: setting an unsigned value larger than a TOML integer can hold (a uint/uint64 above 9223372036854775807) is refused with a KindBadInput diagnostic naming the value, where it used to wrap around and write a negative number; and deleting a key an inline table does not carry now leaves that table's bytes untouched, where it used to mark the table modified and let the next render rewrite its spacing and quoting.
  • Fix. Delete reports a document that cannot be folded instead of answering as though the path were simply absent. Deleting a path the document does not carry is still a silent no-op.
  • Fix. A new root-level key is written before the document's first table header instead of being appended to the end of the file. Appending it put the key after the last [header], so the written bytes read as a key of that table -- and, where that table already carried the name, as a duplicate key that no longer parsed.
  • Fix. NewTable and NewArrayTable no longer refuse a header whose path passes through a table a dotted key spelled out. TOML forbids only redefining such a table with a header of its own; a sub-table under it is valid, and the two spellings the compliance corpus carries -- [fruit.apple.texture] and [[fruit.apple.seeds]] beside an apple.color dotted key -- were both refused, with a message claiming a rule TOML does not have.
  • Fix. NewTable and NewArrayTable refuse a path whose prefix is an inline table instead of writing a header the parser then rejects: TOML gives an inline table no way to be added to, so [a.c] beside a = { b = 1 } is not a document that re-parses.
  • Fix. Set, SetCreate and EnsureDefaults write into a table no single node stands for instead of refusing: a key an existing dotted pair binds is replaced through that pair, a new key beside a dotted key joins the same region as another dotted pair, and a new key of a table only a longer header implies arrives under the anchoring header the write gives it. Seeding server.log.file into a [server] table that spells its options as log.level = "info" used to fail outright.
  • Fix. Delete removes a key whose parent table no single node stands for, instead of reporting success and leaving the document untouched: Delete("a.b") on a.b = 1 removes the pair, and deleting a table under one only a longer header implies takes the nested headers with it. The silent no-op stays what it was documented to be -- a path the document does not carry.
  • Fix. SetComment and SetLeadingComments write onto the pair that binds a key inside a table no single node stands for, instead of reporting success and leaving the line unchanged. A comment aimed at a dotted key inside an inline table is now refused with KindWrongContainer rather than accepted and dropped.
  • Fix. SetCreate no longer writes a [table] header for an intermediate under an inline table, which produced bytes the parser rejects; TOML gives an inline table no way to be added to, so the write is refused as it was before.
  • Fix. Delete removes every construct the key binds instead of the first one it finds: the other pairs of a dotted region, a table nested inside a dotted key, sub-table headers written further down the file, an array-of-tables under the key, and a table written under an array-of-tables entry. A deleted name no longer comes back through a construct the removal left behind.
  • Fix. A blank line no longer disappears. Parse(x).Bytes() returns the blank lines written before a trailing comment instead of dropping them, and a construct that re-renders after an edit keeps the blank lines that separated it from the one above -- including the ones written between its own leading comments.
  • **DecodeNode diagnostics name the file.** A node decoded out of a document loaded with ParseFile now reports its violations against that file, the same as Decode and every other surface; previously the filename was missing and the error read as a bare line:column.
  • Adding leading comments no longer doubles a blank line. Writing SetLeadingComments onto a key or table that had no comments but was preceded by a blank line emitted that blank line on both sides of the new comments, leaving them separated from what they describe. The run above is now emitted once.
  • Edits stop reformatting the lines around them. A value write now keeps the line's spacing, its inline comment and the key's quoting; a comment write keeps the value's base and quoting; editing one array element or inline-table pair leaves every sibling, separator and interior comment byte-identical; and a header keeps its own key spelling.
  • A re-rendered line no longer inserts a space before an inline comment that had none. x = 1# note edited elsewhere on the line comes back spelled the same way.
  • Writing into a file whose last line has no newline no longer corrupts it. The appended key or header used to continue the last line, producing a document that would not parse.
  • Writing text that is not valid UTF-8 is now refused instead of mangled. A string value, a map or []Pair key, a key a write's path names, a RenameKey target and a Marshal key that are not valid UTF-8 each report a bad-input diagnostic; before, they were written as replacement characters and read back as a different value.
  • **SetCreate no longer leaves empty tables behind when it refuses the value.** A write it will not perform -- a signed NaN, an unsupported type, an unsigned value past int64, a duplicate ordered key, invalid UTF-8 -- used to create the [header] tables its path named first; the document is now left byte-for-byte unchanged, and EnsureDefaults inherits it.
  • Comment text that would break the document is now refused instead of written. SetComment and SetLeadingComments used to write anything they were handed, so a comment carrying a newline, a carriage return, another control character or invalid UTF-8 produced a document that no longer parsed. Both now report a bad-input diagnostic and leave the document unchanged; each element of SetLeadingComments is one comment line.
  • **Diff no longer reports a difference between a document and itself.** A document holding a not-a-number compared unequal to a byte-identical copy, because the leaf comparison used IEEE float equality. Two not-a-numbers now compare equal, matching the rest of the library's treatment of them. The ruled semantics are pinned by test and stated in the doc comment: an integer and a float never compare equal (1 against 1.0 is a modification), and two spellings of one value never differ (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).
  • **Merge reads the source document through the read-layer, fixing three defects.** It folds the source and merges its records and entries rather than walking raw syntax and reconstructing table ownership from key-path prefixes. A sub-table written under an array-of-tables prefix ([[s]] ... [s.t]) now merges into the entry it was written under, where before it was merged as a plain table and every entry of the array-of-tables was silently dropped; merging into an inline table no longer pulls the source's unrelated top-level tables in with it (which previously failed the whole merge); and a table or an array-of-tables entry that is new in the target now arrives with the comments written above it, as the doc comment always promised.
  • **Fixed: Merge no longer rewrites an inline table it copies.** An inline table the target lacked used to arrive with its keys alphabetized -- t = {z = 1, a = 2, m = 3} merged as t = {a = 2, m = 3, z = 1} -- and now arrives in the order the source wrote it, at every depth and inside arrays. Two keys inside such a table also arrived saying something else: a dotted key {a.b = 1} became the single key {"a.b" = 1}, and a quoted key {"x.y" = 1} became {"\"x.y\"" = 1}. Both now keep their meaning.
  • Documentation: the package doc now states the two-surfaces contract. The package doc says which of the two read surfaces answers which question -- the AST for what the file contains and how it is written, the read-layer for what the document means. Scalar.AsTime, Document.GetTime, Cursor.Time and Decode state what each accepts as a time, and that a string is not one on either surface.
  • Documentation: the hand-written guides now describe the library that shipped. The README, the design guide and the usage guide were rewritten against the current surface: strict-only decoding, the Error/Errors diagnostic contract with its kinds and sentinels, the read-layer (Root/Record/Entry) as the logical read surface beside the AST, the Set equality-and-refusal contract, the value-returning decode entry points, the descriptor path, the structural operations, ParseFile/WriteFile, and the exported renderers and path helpers. Every deleted or renamed API is gone from the prose with its replacement named -- Get, the document's path-based Items/Len, Marshal, MergeDefaults, ParseError, DocumentNode, Rename, the token vocabulary -- as are the claims that no longer held: case-insensitive field matching, silently ignored unknown keys, Node.Value(), Set accepting a pre-built node, and Format's blank-line behavior. The README's import example uses the named form and states that the package has no runtime dependencies; the benchmark capture was re-run.
  • **Documentation: Span and the scalar Raw accessors carry godoc text.** Span() on every node kind, and Raw() on the eight scalar kinds, rendered in godoc with no description at all. They now state what they answer: a span's line, column and byte offset and the rule that edits do not recompute it, and that a scalar's Raw is a copy of the lexeme the value was written as.
  • Documentation: the comment-reading example, and the formatter's default blank line. The guide's only comment-READING example resolved a path and read the value node, which carries no comments -- both getters answered empty for every reader who copied it. It now reaches the enclosing *KeyValueNode through the container's Children(), and the guide states where comments live per construct (a key-bound value's on the pair, a table's on its header, an array element's on the element itself) and that reading is node-level while the setters are path-level. The claim that the formatter "opens no gap where the writer left none" was false at defaults, since WithTableBlankLine is ON by default and a plain Format() therefore inserts a blank line before every flush table header; the README, the design guide and the usage guide now state the default and scope the claim to WithTableBlankLine(false). Four smaller corrections: the decode entry points return *T rather than the value itself; an empty SetLeadingComments element writes "# " (a hash and a trailing space); missing required keys are reported after their record's entries, in lexicographic key order among themselves; and Node is sealed by unexported methods, so no caller can implement it.

#0.3.0

Position spans on every AST node: Span() exposes 1-based start/end line and column from the most recent parse.

Context

Consumers doing their own semantic validation over the AST (unknown keys, wrong types, out-of-range values) previously had positions only on ParseError. Spans are populated from lexer tokens at parse time and are deliberately not recomputed by edits: nodes created programmatically carry the zero (invalid) span, and edited documents must be re-parsed for fresh positions.

#Features

  • Position spans. Every AST node (tables, array tables, key-values, keys, scalars, arrays, inline tables, comments) now exposes Span() with 1-based start/end line and column from the most recent parse, enabling consumers to attach precise source positions to their own semantic validation diagnostics.

#0.2.2

Bug fix: ParseError positions for EOF and duplicate-key errors.

#Fixes

  • Bug fix. ParseError now reports correct line/column positions for EOF errors and duplicate-key/table errors (previously reported line 0, column 0).

#0.2.1

Deterministic inline table key ordering

Context

mapToInlineTableNode now sorts keys alphabetically, fixing non-deterministic output for deeply nested maps in Marshal.

#Fixes

  • Bug fix. Inline tables in Marshal output now have deterministic key ordering.

#0.2.0

Add Marshal for map-to-TOML serialization

Context

strictcli needs Marshal to write TOML config files. This adds the inverse of Unmarshal for map types (struct support deferred to v2).

#Features

  • New feature. Marshal(v any) ([]byte, error) serializes Go maps to TOML bytes. Supports flat and nested map[string]any with all primitive types. Nested maps become [section] headers; keys are sorted for deterministic output.

#0.1.2

Format() preserves intra-array comments; README Walk example corrected.

#Fixes

  • Fix. Preserve intra-array comments in Format() output.
  • Fix. Fix README Walk example to include required WalkMode parameter.

#0.1.1

Fidelity fixes: comments between array elements and key quoting styles now survive re-rendering; Walk takes an explicit WalkMode parameter (breaking).

#Breaking

  • Breaking. Walk now requires explicit WalkMode parameter (WalkLeaves or WalkAll).

#Fixes

  • Fix. Preserve comments between array elements when arrays are re-rendered.
  • Fix. Preserve original key formatting (quoted vs bare) for clean keys in dirty KVs; fix key node not marked dirty on Rename.

#0.1.0

Release-tooling bootstrap: no user-facing changes (version machinery for the rlsbl bump flow).

  • No user-facing changes.

#0.0.1

Initial release: full TOML 1.0 parser and comment-preserving editor -- round-trip serialization, editing API, typed getters, fluent cursor, Walk, Diff, Merge, Unmarshal/Decode, and official toml-test suite compliance.

#Features

  • Feature. Full TOML 1.0 parser with comment and whitespace preservation, zero-diff round-trip serialization, document editing API, typed getters, fluent cursor API, path resolution, formatter, and toml-test suite compliance.
  • Feature. Unmarshal and Decode with struct tags, embedded structs, custom Unmarshaler, TextUnmarshaler.
  • Feature. Items iterator (range-over-func) and Len for arrays.
  • Feature. SetComment and SetLeadingComments helpers.
  • Feature. Walk for depth-first document traversal.
  • Feature. MergeDefaults and Merge for deep merging.
  • Feature. Diff for comparing two documents.

#Fixes

  • Fix. Merge copies all [[array-table]] entries instead of only the first.
  • Fix. Walk emits correct paths for array-of-tables sub-tables.
  • Fix. SetComment on inline table members returns error.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search