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
DocumentNodeis nowDocument-- replace every*tomledit.DocumentNodewith*tomledit.Document(Parse,DiffandMergetake and return the new spelling). The key-renaming methodRenameis nowRenameKey-- replacedoc.Rename(path, newKey)withdoc.RenameKey(path, newKey), signature unchanged. - Breaking.
ParseErroris replaced byError, one diagnostic type for the whole library. Replace*tomledit.ParseErrorwith*tomledit.Error, and read positions through the embeddedPosfield (err.Pos.Linerather thanerr.Line), which also carries a byteOffset. Errors can now be matched by kind witherrors.Is(err, tomledit.ErrSyntax)and structurally witherrors.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 oldline 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, soexpected RightBracket, got Newlinenow readsexpected ']', got newline,expected value, got EOFreadsexpected a value, got end of input, andunexpected token Equalsreads'=' 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 andMergeall return*Errorvalues matchable witherrors.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.
Getis deleted, and a path that names no single node is now refused instead of resolving to an internal placeholder.Getswallowed every diagnostic and returnednil, so a typo and a type error looked alike; useResolvefor the node with a reason for failure. An array-of-tables (products) and a table nothing spells out on its own (awhere only[a.b]ora.b.c = 1appears) now makeResolvereturn aKindWrongContainerdiagnostic, andLookup/Hasreport false -- read those throughdoc.Root(), or address a concrete entry with an index (products[0]). Editing is unchanged:Set("products[0].x", ...)andDelete("products[0]")still address entries. - Cursor. A cursor chain crosses every table spelling the same way --
doc.Key("a").Key("b")reachesbwhetherais a header table, an inline table, or a table only implied by[a.b]-- andNode()on a position no single node stands for (an array-of-tables, an implied table) returns nil and reports aKindWrongContainerdiagnostic throughErr()instead of handing back an internal placeholder. - Breaking: decoding is strict, and strictness is the only mode.
Unmarshal,Decodeand the newValidaterefuse 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 -- atomltag on an unexported field binds nothing and changes that in no way;requiredis the onlytomltag 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*Errorsaggregate 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
tomltag -- 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 (hostreached an untagged fieldHost,HOSTreached 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/Decodeand aSpechanded toValidatematch keys the same way and report a mis-cased key identically. - Breaking: creating a table refuses a name that is already taken.
NewTableandNewArrayTableconsult the document's logical tree before appending a header, and refuse with aKindConflictdiagnostic 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.
RenameKeynow 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 aKindConflictdiagnostic. 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.
SetandSetCreatereportKindWrongContainerwhen 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.**SetCommentandSetLeadingCommentson a key of an inline table used to reportKindConflict. 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 matchingErrConflicton this refusal must matchErrWrongContainerinstead. - **Breaking:
MergeDefaultsis replaced byEnsureDefaults.** 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.
Nodeno longer hasValue(); the value-carrying node kinds implement the newScalarinterface, which embedsNodeand carries it. Read a scalar withnode.(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()andLeadingComments()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-nodeSetComment/SetLeadingCommentsmethods 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,AppendToArrayand the other value-taking write entry points refuse anything implementingNodewithErrBadInput; 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. - **
Setis 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 withKindBadInput. - **
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.WithTableBlankLineis now insertion-only -- it adds a blank line before a table that has none and never removes the separation the document already carries, soWithTableBlankLine(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,GetFloatandGetTimeon a document, andString(),Int(),Bool(),Float()andTime()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 unexplainedfalse. Scalar nodes gain the matchingAsString,AsInt,AsFloat,AsBool,AsTime,AsLocalDateTime,AsLocalDateandAsLocalTime. All three surfaces read the same conversion table the decoder uses, so a float read now accepts an integer the target holds exactly, and atime.Timeread accepts a local date-time or a local date. - Decode returns the value it decodes.
Unmarshal,DecodeandDecodeNodeno 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)becomescfg, err := tomledit.Unmarshal[Config](data);doc.Decode(&cfg)becomestomledit.Decode[Config](doc)(a package function, because Go has no parameterized methods);tomledit.DecodeNode(node, &cfg)becomestomledit.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 newDecodeOver. - **Removed: the document's path-based
ItemsandLen.**(*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.Entriesenumerates a table's keys in first-appearance order,Entry.Recordshands out an array-of-tables' entries (its length is the count), and an array node'sElementshands out a plain array's. The deletedLenreported-1for a missing path, a scalar and a table alike; the replacements distinguish them. - **Removed:
Marshal.** The map-only encoder is gone: it acceptedmap[string]anyand 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-TOMLMarshalis deferred work. Build a document through the editing surface instead:SetCreatewrites a map as a container value, andBytesorFormatrenders it. - **Removed: the exported token vocabulary and the
Triviastruct.**Token,TokenTypeand the 25Token*constants, and theTriviastruct, 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 isComment(),LeadingComments()andRaw()on theNodeinterface, as it already was. - **Breaking:
Formatalways inserts the blank line before a table header.**WithTableBlankLineandFormatConfig.TableBlankLineare 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, andFormatis 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 passingWithTableBlankLine(true)drop the option; callers passingWithTableBlankLine(false)useBytesinstead if they need the writer's own spacing kept. - Breaking: a decode target never decodes itself. The
Unmarshalerinterface (UnmarshalTOML(node) error) is gone, and a target'sencoding.TextUnmarshaleris 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. TheKindHookErrorkind and itsErrHookErrorsentinel are gone with them. One visible consequence: a struct field of typetime.Timeno longer accepts an RFC 3339 string -- it is nowKindTypeMismatch, exactly asScalar.AsTime,Document.GetTimeandCursor.Timehave always answered. Decode such a field as astringand calltime.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.
Positionnow carries anOffsetfield 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 asconfig.toml:2:1: duplicate key. A file that cannot be read comes back as the underlying read error. - Path helpers.
tomledit.ParsePathandtomledit.JoinPathexpose 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.JoinPathis 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 intoResolve,SetorDelete. - The read-layer.
doc.Root()returns the document as a logical tree ofRecords andEntrys: 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. - **
LookupandHas.**doc.Lookup(path)returns(Node, bool)anddoc.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,ValidateandDecodeNode.** A document shape can now be described as data --tomledit.Spec{Fields: map[string]tomledit.Field{...}}, withDynamicfor tables of arbitrary keys andFieldAny()for values left undescribed -- and checked withdoc.Validate(spec), which is the same engineDecoderuns, 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 positioni, and it must name every child exactly once, so a wrong length, an out-of-range index or a repeated one is refused withKindBadInputand nothing is reordered. Each child's comments and blank lines travel with it.AppendToArray(path, value)andRemoveFromArray(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[]PairtoSet,SetCreate,AppendToArrayorEnsureDefaultsand the inline table is written in the order given, where amap[string]anyis written with its keys sorted. APair'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 withKindBadInput. - Repeated reads no longer refold the document. The logical read-layer behind
Root, the path getters,Resolve, the Cursor andDecodeis 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,QuoteKeyandFormatFloatwrite the same TOML spellings the library writes;FormatFloatis total, rendering every float64 including NaN and the infinities. - **
RenameKeyrenames 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. - **
WriteFilewrites 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. - **
DecodeOverdecodes 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. - **
DecodeSpecreturns a hand-built descriptor's values.**(*Document).DecodeSpec(spec)runs the same engineValidateruns and, when the document satisfies the descriptor, answers with the document as native Go data:map[string]anyfor every table spelling,[]anyfor 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. - **
Walkdocuments itself as the syntactic traversal.** Its doc comment now states the contract the library's two read surfaces divide between them:Walkhands 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;Rootand 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[]Pairwrite, the comment setters,QuoteString,QuoteKey,FormatFloat,ParsePath/JoinPathand the*Errorcontract each have a godoc example that runs in the suite. - **Path-based comment getters:
GetCommentandGetLeadingComments.** 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, anddoc.GetLeadingComments("server.host")the comment lines above it. Both resolve exactly asSetCommentandSetLeadingCommentsdo, 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 answersnil, and neither is an error. The node-levelComment()andLeadingComments()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 itsaelement (silently, formap[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 insidereflectinstead 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/uint64above 9223372036854775807) is refused with aKindBadInputdiagnostic 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.
Deletereports 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.
NewTableandNewArrayTableno 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 anapple.colordotted key -- were both refused, with a message claiming a rule TOML does not have. - Fix.
NewTableandNewArrayTablerefuse 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]besidea = { b = 1 }is not a document that re-parses. - Fix.
Set,SetCreateandEnsureDefaultswrite 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. Seedingserver.log.fileinto a[server]table that spells its options aslog.level = "info"used to fail outright. - Fix.
Deleteremoves a key whose parent table no single node stands for, instead of reporting success and leaving the document untouched:Delete("a.b")ona.b = 1removes 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.
SetCommentandSetLeadingCommentswrite 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 withKindWrongContainerrather than accepted and dropped. - Fix.
SetCreateno 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.
Deleteremoves 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. - **
DecodeNodediagnostics name the file.** A node decoded out of a document loaded withParseFilenow reports its violations against that file, the same asDecodeand 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
SetLeadingCommentsonto 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# noteedited 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
[]Pairkey, a key a write's path names, aRenameKeytarget and aMarshalkey 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. - **
SetCreateno 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, andEnsureDefaultsinherits it. - Comment text that would break the document is now refused instead of written.
SetCommentandSetLeadingCommentsused 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 ofSetLeadingCommentsis one comment line. - **
Diffno 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 (0x2Aagainst42,1_000against1000, one instant in two zone offsets, a literal string against a basic one, an array-of-tables against an inline array of inline tables). - **
Mergereads 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:
Mergeno 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 ast = {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.TimeandDecodestate 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/Errorsdiagnostic contract with its kinds and sentinels, the read-layer (Root/Record/Entry) as the logical read surface beside the AST, theSetequality-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-basedItems/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(),Setaccepting a pre-built node, andFormat'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:
Spanand the scalarRawaccessors carry godoc text.**Span()on every node kind, andRaw()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'sRawis 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
*KeyValueNodethrough the container'sChildren(), 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, sinceWithTableBlankLineis ON by default and a plainFormat()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 toWithTableBlankLine(false). Four smaller corrections: the decode entry points return*Trather than the value itself; an emptySetLeadingCommentselement writes"# "(a hash and a trailing space); missing required keys are reported after their record's entries, in lexicographic key order among themselves; andNodeis 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
Marshaloutput 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 nestedmap[string]anywith 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.