mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
432 lines
12 KiB
Go
432 lines
12 KiB
Go
package yqlib
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
type tomlEncoder struct {
|
|
wroteRootAttr bool // Track if we wrote root-level attributes before tables
|
|
}
|
|
|
|
func NewTomlEncoder() Encoder {
|
|
return &tomlEncoder{}
|
|
}
|
|
|
|
func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
|
if node.Kind != MappingNode {
|
|
// For standalone selections, TOML tests expect raw value for scalars
|
|
if node.Kind == ScalarNode {
|
|
return writeString(writer, node.Value+"\n")
|
|
}
|
|
return fmt.Errorf("TOML encoder expects a mapping at the root level")
|
|
}
|
|
|
|
// Encode a root mapping as a sequence of attributes, tables, and arrays of tables
|
|
return te.encodeRootMapping(writer, node)
|
|
}
|
|
|
|
func (te *tomlEncoder) PrintDocumentSeparator(_ io.Writer) error {
|
|
return nil
|
|
}
|
|
|
|
func (te *tomlEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (te *tomlEncoder) CanHandleAliases() bool {
|
|
return false
|
|
}
|
|
|
|
// ---- helpers ----
|
|
|
|
func (te *tomlEncoder) formatScalar(node *CandidateNode) string {
|
|
switch node.Tag {
|
|
case "!!str":
|
|
// Quote strings per TOML spec
|
|
return fmt.Sprintf("%q", node.Value)
|
|
case "!!bool", "!!int", "!!float":
|
|
return node.Value
|
|
case "!!null":
|
|
// TOML does not have null; encode as empty string
|
|
return "\"\""
|
|
default:
|
|
return node.Value
|
|
}
|
|
}
|
|
|
|
func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error {
|
|
te.wroteRootAttr = false // Reset state
|
|
|
|
// Preserve existing order by iterating Content
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
keyNode := node.Content[i]
|
|
valNode := node.Content[i+1]
|
|
if err := te.encodeTopLevelEntry(w, []string{keyNode.Value}, valNode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// encodeTopLevelEntry encodes a key/value at the root, dispatching to attribute, table, or array-of-tables
|
|
func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *CandidateNode) error {
|
|
switch node.Kind {
|
|
case ScalarNode:
|
|
// key = value
|
|
return te.writeAttribute(w, path[len(path)-1], node)
|
|
case SequenceNode:
|
|
// Empty arrays should be encoded as [] attributes
|
|
if len(node.Content) == 0 {
|
|
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
|
}
|
|
|
|
// If all items are mappings => array of tables; else => array attribute
|
|
allMaps := true
|
|
for _, it := range node.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if allMaps {
|
|
key := path[len(path)-1]
|
|
for _, it := range node.Content {
|
|
// [[key]] then body
|
|
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
|
|
return err
|
|
}
|
|
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// Regular array attribute
|
|
return te.writeArrayAttribute(w, path[len(path)-1], node)
|
|
case MappingNode:
|
|
// Inline table if not EncodeSeparate, else emit separate tables/arrays of tables for children under this path
|
|
if !node.EncodeSeparate {
|
|
// If children contain mappings or arrays of mappings, prefer separate sections
|
|
if te.hasEncodeSeparateChild(node) || te.hasStructuralChildren(node) {
|
|
return te.encodeSeparateMapping(w, path, node)
|
|
}
|
|
return te.writeInlineTableAttribute(w, path[len(path)-1], node)
|
|
}
|
|
return te.encodeSeparateMapping(w, path, node)
|
|
default:
|
|
return fmt.Errorf("unsupported node kind for TOML: %v", node.Kind)
|
|
}
|
|
}
|
|
|
|
func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateNode) error {
|
|
te.wroteRootAttr = true // Mark that we wrote a root attribute
|
|
_, err := w.Write([]byte(key + " = " + te.formatScalar(value) + "\n"))
|
|
return err
|
|
}
|
|
|
|
func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *CandidateNode) error {
|
|
te.wroteRootAttr = true // Mark that we wrote a root attribute
|
|
|
|
// Handle empty arrays
|
|
if len(seq.Content) == 0 {
|
|
_, err := w.Write([]byte(key + " = []\n"))
|
|
return err
|
|
}
|
|
|
|
// Join scalars or nested arrays recursively into TOML array syntax
|
|
items := make([]string, 0, len(seq.Content))
|
|
for _, it := range seq.Content {
|
|
switch it.Kind {
|
|
case ScalarNode:
|
|
items = append(items, te.formatScalar(it))
|
|
case SequenceNode:
|
|
// Nested arrays: encode inline
|
|
nested, err := te.sequenceToInlineArray(it)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items = append(items, nested)
|
|
case MappingNode:
|
|
// Inline table inside array
|
|
inline, err := te.mappingToInlineTable(it)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items = append(items, inline)
|
|
case AliasNode:
|
|
return fmt.Errorf("aliases are not supported in TOML")
|
|
default:
|
|
return fmt.Errorf("unsupported array item kind: %v", it.Kind)
|
|
}
|
|
}
|
|
_, err := w.Write([]byte(key + " = [" + strings.Join(items, ", ") + "]\n"))
|
|
return err
|
|
}
|
|
|
|
func (te *tomlEncoder) sequenceToInlineArray(seq *CandidateNode) (string, error) {
|
|
items := make([]string, 0, len(seq.Content))
|
|
for _, it := range seq.Content {
|
|
switch it.Kind {
|
|
case ScalarNode:
|
|
items = append(items, te.formatScalar(it))
|
|
case SequenceNode:
|
|
nested, err := te.sequenceToInlineArray(it)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
items = append(items, nested)
|
|
case MappingNode:
|
|
inline, err := te.mappingToInlineTable(it)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
items = append(items, inline)
|
|
default:
|
|
return "", fmt.Errorf("unsupported array item kind: %v", it.Kind)
|
|
}
|
|
}
|
|
return "[" + strings.Join(items, ", ") + "]", nil
|
|
}
|
|
|
|
func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
|
|
// key = { a = 1, b = "x" }
|
|
parts := make([]string, 0, len(m.Content)/2)
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
k := m.Content[i].Value
|
|
v := m.Content[i+1]
|
|
switch v.Kind {
|
|
case ScalarNode:
|
|
parts = append(parts, fmt.Sprintf("%s = %s", k, te.formatScalar(v)))
|
|
case SequenceNode:
|
|
// inline array in inline table
|
|
arr, err := te.sequenceToInlineArray(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s = %s", k, arr))
|
|
case MappingNode:
|
|
// nested inline table
|
|
inline, err := te.mappingToInlineTable(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s = %s", k, inline))
|
|
default:
|
|
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
|
|
}
|
|
}
|
|
return "{ " + strings.Join(parts, ", ") + " }", nil
|
|
}
|
|
|
|
func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *CandidateNode) error {
|
|
inline, err := te.mappingToInlineTable(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = w.Write([]byte(key + " = " + inline + "\n"))
|
|
return err
|
|
}
|
|
|
|
func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string) error {
|
|
// Add blank line before table header if we wrote root attributes
|
|
prefix := ""
|
|
if te.wroteRootAttr {
|
|
prefix = "\n"
|
|
te.wroteRootAttr = false // Only add once
|
|
}
|
|
|
|
// Write headers progressively to ensure nested tables
|
|
// Collapse to a single header line [a.b.c]
|
|
header := prefix + "[" + strings.Join(path, ".") + "]\n"
|
|
_, err := w.Write([]byte(header))
|
|
return err
|
|
}
|
|
|
|
// encodeSeparateMapping handles a mapping that should be encoded as table sections.
|
|
// It emits the table header for this mapping if it has any content, then processes children.
|
|
func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *CandidateNode) error {
|
|
// Check if this mapping has any non-mapping, non-array-of-tables children (i.e., attributes)
|
|
hasAttrs := false
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
v := m.Content[i+1]
|
|
if v.Kind == ScalarNode {
|
|
hasAttrs = true
|
|
break
|
|
}
|
|
if v.Kind == SequenceNode {
|
|
// Check if it's NOT an array of tables
|
|
allMaps := true
|
|
for _, it := range v.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if !allMaps {
|
|
hasAttrs = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are attributes or if the mapping is empty, emit the table header
|
|
if hasAttrs || len(m.Content) == 0 {
|
|
if err := te.writeTableHeader(w, path); err != nil {
|
|
return err
|
|
}
|
|
if err := te.encodeMappingBodyWithPath(w, path, m); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// No attributes, just nested structures - process children
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
k := m.Content[i].Value
|
|
v := m.Content[i+1]
|
|
switch v.Kind {
|
|
case MappingNode:
|
|
// Emit [path.k]
|
|
newPath := append(append([]string{}, path...), k)
|
|
if err := te.writeTableHeader(w, newPath); err != nil {
|
|
return err
|
|
}
|
|
if err := te.encodeMappingBodyWithPath(w, newPath, v); err != nil {
|
|
return err
|
|
}
|
|
case SequenceNode:
|
|
// If sequence of maps, emit [[path.k]] per element
|
|
allMaps := true
|
|
for _, it := range v.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if allMaps {
|
|
key := strings.Join(append(append([]string{}, path...), k), ".")
|
|
for _, it := range v.Content {
|
|
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
|
|
return err
|
|
}
|
|
if err := te.encodeMappingBodyWithPath(w, append(append([]string{}, path...), k), it); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// Regular array attribute under the current table path
|
|
if err := te.writeArrayAttribute(w, k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case ScalarNode:
|
|
// Attributes directly under the current table path
|
|
if err := te.writeAttribute(w, k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (te *tomlEncoder) hasEncodeSeparateChild(m *CandidateNode) bool {
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
v := m.Content[i+1]
|
|
if v.Kind == MappingNode && v.EncodeSeparate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (te *tomlEncoder) hasStructuralChildren(m *CandidateNode) bool {
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
v := m.Content[i+1]
|
|
// Only consider it structural if mapping has EncodeSeparate or is non-empty
|
|
if v.Kind == MappingNode && v.EncodeSeparate {
|
|
return true
|
|
}
|
|
if v.Kind == SequenceNode {
|
|
allMaps := true
|
|
for _, it := range v.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if allMaps {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// encodeMappingBodyWithPath encodes attributes and nested arrays of tables using full dotted path context
|
|
func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *CandidateNode) error {
|
|
// First, attributes (scalars and non-map arrays)
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
k := m.Content[i].Value
|
|
v := m.Content[i+1]
|
|
switch v.Kind {
|
|
case ScalarNode:
|
|
if err := te.writeAttribute(w, k, v); err != nil {
|
|
return err
|
|
}
|
|
case SequenceNode:
|
|
allMaps := true
|
|
for _, it := range v.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if !allMaps {
|
|
if err := te.writeArrayAttribute(w, k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then, nested arrays of tables with full path
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
k := m.Content[i].Value
|
|
v := m.Content[i+1]
|
|
if v.Kind == SequenceNode {
|
|
allMaps := true
|
|
for _, it := range v.Content {
|
|
if it.Kind != MappingNode {
|
|
allMaps = false
|
|
break
|
|
}
|
|
}
|
|
if allMaps {
|
|
dotted := strings.Join(append(append([]string{}, path...), k), ".")
|
|
for _, it := range v.Content {
|
|
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
|
|
return err
|
|
}
|
|
if err := te.encodeMappingBodyWithPath(w, append(append([]string{}, path...), k), it); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, child mappings that are not marked EncodeSeparate get inlined as attributes
|
|
for i := 0; i < len(m.Content); i += 2 {
|
|
k := m.Content[i].Value
|
|
v := m.Content[i+1]
|
|
if v.Kind == MappingNode && !v.EncodeSeparate {
|
|
if err := te.writeInlineTableAttribute(w, k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|