feat(encode): write the TOML 1.1 output form
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -28,18 +28,50 @@ var (
|
||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||
)
|
||||
|
||||
// inlineLimit is the column past which an inline table is written across
|
||||
// lines. TOML 1.1 lets an inline table carry newlines and a trailing comma, so
|
||||
// a long one stays readable instead of running off the line.
|
||||
const inlineLimit = 100
|
||||
|
||||
// noInlineBreak is the limit a measuring encoder carries, high enough that the
|
||||
// form it renders is always the single-line one.
|
||||
const noInlineBreak = 1 << 30
|
||||
|
||||
// encoder produces a TOML document from a Go value via a small intermediate
|
||||
// representation that preserves the order in which fields were declared.
|
||||
type encoder struct {
|
||||
buf bytes.Buffer
|
||||
ctx context.Context
|
||||
opts Encoder
|
||||
|
||||
// inlineDepth is the nesting level inside inline tables, which decides
|
||||
// their indentation.
|
||||
inlineDepth int
|
||||
|
||||
// limit is the column at which an inline table is broken; only a
|
||||
// measuring encoder raises it.
|
||||
limit int
|
||||
}
|
||||
|
||||
func newEncoder() *encoder { return &encoder{} }
|
||||
func newEncoder() *encoder { return &encoder{limit: inlineLimit} }
|
||||
|
||||
// flat returns an encoder that measures a value by rendering it on one line,
|
||||
// so a caller can decide which form to write before writing it.
|
||||
func (e *encoder) flat() *encoder {
|
||||
return &encoder{ctx: e.ctx, opts: e.opts, limit: noInlineBreak}
|
||||
}
|
||||
|
||||
func (e *encoder) bytes() []byte { return e.buf.Bytes() }
|
||||
|
||||
// column reports how many bytes the current line already holds, so a form can
|
||||
// be measured against the limit before it is written.
|
||||
func (e *encoder) column() int {
|
||||
if i := bytes.LastIndexByte(e.buf.Bytes(), '\n'); i >= 0 {
|
||||
return e.buf.Len() - i - 1
|
||||
}
|
||||
return e.buf.Len()
|
||||
}
|
||||
|
||||
func (e *encoder) checkCtx() error {
|
||||
if e.ctx == nil {
|
||||
return nil
|
||||
@@ -891,7 +923,7 @@ func (e *encoder) writeValue(val any) error {
|
||||
case float64:
|
||||
return e.writeFloat(v)
|
||||
case time.Time:
|
||||
e.buf.WriteString(v.Format(time.RFC3339Nano))
|
||||
e.buf.WriteString(offsetString(v))
|
||||
return nil
|
||||
case LocalDateTime:
|
||||
e.buf.WriteString(v.String())
|
||||
@@ -915,7 +947,7 @@ func (e *encoder) writeValue(val any) error {
|
||||
e.buf.WriteByte(']')
|
||||
return nil
|
||||
case map[string]any:
|
||||
return e.writeInlineTable(v)
|
||||
return e.writeInlineMap(v)
|
||||
case nil:
|
||||
return fmt.Errorf("interpres: cannot encode nil value")
|
||||
default:
|
||||
@@ -923,10 +955,24 @@ func (e *encoder) writeValue(val any) error {
|
||||
}
|
||||
}
|
||||
|
||||
// writeInlineTable renders m as a TOML inline table with sorted keys, the
|
||||
// order buildMapDoc uses for header tables. It backs the table elements of a
|
||||
// value array, where the [[header]] form is not available.
|
||||
func (e *encoder) writeInlineTable(m map[string]any) error {
|
||||
// writeInlineMap renders m as a TOML inline table, on one line when it fits
|
||||
// there and across lines when it does not.
|
||||
func (e *encoder) writeInlineMap(m map[string]any) error {
|
||||
flat := e.flat()
|
||||
if err := flat.writeInlineMapFlat(m); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.column()+flat.buf.Len() <= e.limit {
|
||||
e.buf.Write(flat.buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
return e.writeInlineMapMultiline(m)
|
||||
}
|
||||
|
||||
// writeInlineMapFlat renders m as a single-line inline table with sorted keys,
|
||||
// the order buildMapDoc uses for header tables. It backs the table elements of
|
||||
// a value array, where the [[header]] form is not available.
|
||||
func (e *encoder) writeInlineMapFlat(m map[string]any) error {
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
e.buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
@@ -945,6 +991,36 @@ func (e *encoder) writeInlineTable(m map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeInlineMapMultiline renders m with one entry per line and a trailing
|
||||
// comma, the form TOML 1.1 allows for an inline table too long for one line.
|
||||
func (e *encoder) writeInlineMapMultiline(m map[string]any) error {
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
e.buf.WriteString("{\n")
|
||||
e.inlineDepth++
|
||||
for _, k := range keys {
|
||||
e.writeInlineIndent()
|
||||
if err := e.writeKey(k); err != nil {
|
||||
return err
|
||||
}
|
||||
e.buf.WriteString(" = ")
|
||||
if err := e.writeValue(m[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
e.buf.WriteString(",\n")
|
||||
}
|
||||
e.inlineDepth--
|
||||
e.writeInlineIndent()
|
||||
e.buf.WriteByte('}')
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeInlineIndent writes one tab per inline-table nesting level.
|
||||
func (e *encoder) writeInlineIndent() {
|
||||
for range e.inlineDepth {
|
||||
e.buf.WriteByte('\t')
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) writeStringVal(s string) error {
|
||||
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
|
||||
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
|
||||
|
||||
Reference in New Issue
Block a user