// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
	// The fields are sorted in increasing index-length order. The winner
	// must therefore be one with the shortest index length. Drop all
	// longer entries, which is easy: just truncate the slice.
	length := len(fields[0].index)
	tagged := -1 // Index of first tagged field.
	for i, f := range fields {
		if len(f.index) > length {
			fields = fields[:i]
			break
		}
		if f.tag {
			if tagged >= 0 {
				// Multiple tagged fields at the same level: conflict.
				// Return no field.
				return field{}, false
			}
			tagged = i
		}
	}
	if tagged >= 0 {
		return fields[tagged], true
	}
	// All remaining fields have the same length. If there's more than one,
	// we have a conflict (two fields named "X" at the same level) and we
	// return no field.
	if len(fields) > 1 {
		return field{}, false
	}
	return fields[0], true
}

var fieldCache struct {
	sync.RWMutex
	m map[reflect.Type][]field
}

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
	fieldCache.RLock()
	f := fieldCache.m[t]
	fieldCache.RUnlock()
	if f != nil {
		return f
	}

	// Compute fields without lock.
	// Might duplicate effort but won't hold other computations back.
	f = typeFields(t)
	if f == nil {
		f = []field{}
	}

	fieldCache.Lock()
	if fieldCache.m == nil {
		fieldCache.m = map[reflect.Type][]field{}
	}
	fieldCache.m[t] = f
	fieldCache.Unlock()
	return f
}

func isValidTag(s string) bool {
	if s == "" {
		return false
	}
	for _, c := range s {
		switch {
		case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
			// Backslash and quote chars are reserved, but
			// otherwise any punctuation chars are allowed
			// in a tag name.
		default:
			if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
				return false
			}
		}
	}
	return true
}

const (
	caseMask     = ^byte(0x20) // Mask to ignore case in ASCII.
	kelvin       = ''
	smallLongEss = 'ſ'
)

// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
//   - S maps to s and to U+017F 'ſ' Latin small letter long s
//   - k maps to K and to U+212A 'K' Kelvin sign
//
// See http://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
	nonLetter := false
	special := false // special letter
	for _, b := range s {
		if b >= utf8.RuneSelf {
			return bytes.EqualFold
		}
		upper := b & caseMask
		if upper < 'A' || upper > 'Z' {
			nonLetter = true
		} else if upper == 'K' || upper == 'S' {
			// See above for why these letters are special.
			special = true
		}
	}
	if special {
		return equalFoldRight
	}
	if nonLetter {
		return asciiEqualFold
	}
	return simpleLetterEqualFold
}

// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
	for _, sb := range s {
		if len(t) == 0 {
			return false
		}
		tb := t[0]
		if tb < utf8.RuneSelf {
			if sb != tb {
				sbUpper := sb & caseMask
				if 'A' <= sbUpper && sbUpper <= 'Z' {
					if sbUpper != tb&caseMask {
						return false
					}
				} else {
					return false
				}
			}
			t = t[1:]
			continue
		}
		// sb is ASCII and t is not. t must be either kelvin
		// sign or long s; sb must be s, S, k, or K.
		tr, size := utf8.DecodeRune(t)
		switch sb {
		case 's', 'S':
			if tr != smallLongEss {
				return false
			}
		case 'k', 'K':
			if tr != kelvin {
				return false
			}
		default:
			return false
		}
		t = t[size:]

	}

	return len(t) <= 0
}

// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, sb := range s {
		tb := t[i]
		if sb == tb {
			continue
		}
		if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
			if sb&caseMask != tb&caseMask {
				return false
			}
		} else {
			return false
		}
	}
	return true
}

// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, b := range s {
		if b&caseMask != t[i]&caseMask {
			return false
		}
	}
	return true
}

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
	if idx := strings.Index(tag, ","); idx != -1 {
		return tag[:idx], tagOptions(tag[idx+1:])
	}
	return tag, tagOptions("")
}

// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
	if len(o) == 0 {
		return false
	}
	s := string(o)
	for s != "" {
		var next string
		i := strings.Index(s, ",")
		if i >= 0 {
			s, next = s[:i], s[i+1:]
		}
		if s == optionName {
			return true
		}
		s = next
	}
	return false
}



package kyaml

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	yaml "go.yaml.in/yaml/v3"
)

// Encoder formats objects or YAML data (JSON is valid YAML) into KYAML. KYAML
// is halfway between YAML and JSON, but is a strict subset of YAML, so it
// should should be readable by any YAML parser. It is designed to be explicit
// and unambiguous, and eschews significant whitespace.
type Encoder struct {
	// Compact tells the encoder to use compact formatting. This puts all the
	// data on one line, with no extra newlines, no comments, and no multi-line
	// formatting.
	Compact bool
}

// FromYAML renders a KYAML (multi-)document from YAML bytes (JSON is YAML),
// including the KYAML header. The result always has a trailing newline.
func (ky *Encoder) FromYAML(in io.Reader, out io.Writer) error {
	// We need a YAML decoder to handle multi-document streams.
	dec := yaml.NewDecoder(in)

	// Process each document in the stream.
	for {
		var doc yaml.Node
		err := dec.Decode(&doc)
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("error decoding: %v", err)
		}
		if doc.Kind != yaml.DocumentNode {
			return fmt.Errorf("kyaml internal error: line %d: expected a document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
		}

		// Always emit a document separator, which helps disambiguate between YAML
		// and JSON.
		if _, err := fmt.Fprintln(out, "---"); err != nil {
			return err
		}

		if err := ky.renderDocument(&doc, 0, ky.flags(), out); err != nil {
			return err
		}
		fmt.Fprintf(out, "
")
	}

	return nil
}

// FromObject renders a KYAML document from a Go object, including the KYAML
// header. The result always has a trailing newline.
func (ky *Encoder) FromObject(obj any, out io.Writer) error {
	jb, err := json.Marshal(obj)
	if err != nil {
		return fmt.Errorf("error marshaling to JSON: %v", err)
	}
	// JSON is YAML.
	return ky.FromYAML(bytes.NewReader(jb), out)
}

// Marshal renders a single Go object as KYAML, without the header or trailing
// newline.
func (ky *Encoder) Marshal(obj any) ([]byte, error) {
	// Convert the object to JSON bytes to take advantage of all the JSON tag
	// handling and things like that.
	jb, err := json.Marshal(obj)
	if err != nil {
		return nil, fmt.Errorf("error marshaling to JSON: %v", err)
	}

	buf := &bytes.Buffer{}
	// JSON is YAML.
	if err := ky.fromObjectYAML(bytes.NewReader(jb), buf); err != nil {
		return nil, fmt.Errorf("error rendering object: %v", err)
	}
	return buf.Bytes(), nil
}

func (ky *Encoder) fromObjectYAML(in io.Reader, out io.Writer) error {
	yb, err := io.ReadAll(in)
	if err != nil {
		return err
	}

	var doc yaml.Node
	if err := yaml.Unmarshal(yb, &doc); err != nil {
		return fmt.Errorf("error decoding: %v", err)
	}
	if doc.Kind != yaml.DocumentNode {
		return fmt.Errorf("kyaml internal error: line %d: expected document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
	}

	if err := ky.renderNode(&doc, 0, ky.flags(), out); err != nil {
		return fmt.Errorf("error rendering document: %v", err)
	}

	return nil
}

// From the YAML spec.
const (
	intTag       = "!!int"
	floatTag     = "!!float"
	boolTag      = "!!bool"
	strTag       = "!!str"
	timestampTag = "!!timestamp"
	seqTag       = "!!seq"
	mapTag       = "!!map"
	nullTag      = "!!null"
	binaryTag    = "!!binary"
	mergeTag     = "!!merge"
)

type flagMask uint64

const (
	flagsNone     flagMask = 0
	flagLazyQuote flagMask = 0x01
	flagCompact   flagMask = 0x02
)

// flags returns a flagMask representing the current encoding options. It can
// be used directly or OR'ed with another mask.
func (ky *Encoder) flags() flagMask {
	flags := flagsNone
	if ky.Compact {
		flags |= flagCompact
	}
	return flags
}

// renderNode processes a YAML node, calling the appropriate render function
// for its type.  Each render function should assume that the output "cursor"
// is positioned at the start of the node and should not emit a final newline.
// If a render function needs to linewrap or indent (e.g. a struct), it should
// assume the indent level is currently correct for the node type itself, and
// may need to indent more.
func (ky *Encoder) renderNode(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if node == nil {
		return nil
	}

	switch node.Kind {
	case yaml.DocumentNode:
		return ky.renderDocument(node, indent, flags, out)
	case yaml.ScalarNode:
		return ky.renderScalar(node, indent, flags, out)
	case yaml.SequenceNode:
		return ky.renderSequence(node, indent, flags, out)
	case yaml.MappingNode:
		return ky.renderMapping(node, indent, flags, out)
	case yaml.AliasNode:
		return ky.renderAlias(node, indent, flags, out)
	}
	return fmt.Errorf("kyaml internal error: line %d: unknown node kind %v", node.Line, node.Kind)
}

// renderDocument processes a YAML document node, rendering it to the output.
// This function assumes that the output "cursor" is positioned at the start of
// the document. This does not emit a final newline.
func (ky *Encoder) renderDocument(doc *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if len(doc.Content) == 0 {
		return fmt.Errorf("kyaml internal error: line %d: document has no content node (%d)", doc.Line, len(doc.Content))
	}
	if len(doc.Content) > 1 {
		return fmt.Errorf("kyaml internal error: line %d: document has more than one content node (%d)", doc.Line, len(doc.Content))
	}
	if indent != 0 {
		return fmt.Errorf("kyaml internal error: line %d: document non-zero indent (%d)", doc.Line, indent)
	}

	compact := flags&flagCompact != 0

	// For document nodes, the cursor is assumed to be ready to render.
	child := doc.Content[0]
	if !compact {
		if len(doc.HeadComment) > 0 {
			ky.renderComments(doc.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
		if len(child.HeadComment) > 0 {
			ky.renderComments(child.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
	}
	if err := ky.renderNode(child, indent, flags, out); err != nil {
		return err
	}
	if !compact {
		if len(child.LineComment) > 0 {
			ky.renderComments(" "+child.LineComment, 0, out)
		}
		if len(child.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(child.FootComment, indent, out)
		}
		if len(doc.LineComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(" "+doc.LineComment, 0, out)
		}
		if len(doc.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(doc.FootComment, indent, out)
		}
	}
	return nil
}

// renderScalar processes a YAML scalar node, rendering it to the output.  This
// DOES NOT render a trailing newline or head/line/foot comments, as those
// require the parent context.
func (ky *Encoder) renderScalar(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	switch node.Tag {
	case intTag, floatTag, boolTag, nullTag:
		fmt.Fprint(out, node.Value)
	case strTag, timestampTag:
		return ky.renderString(node.Value, indent+1, flags, out)
	default:
		return fmt.Errorf("kyaml internal error: line %d: unknown tag %q on scalar node %q", node.Line, node.Tag, node.Value)
	}
	return nil
}

const kyamlFoldStr = "\
"

var regularEscapeMap = map[rune]string{
	'
': "\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable
	'	': "	",                 // literal tab
}
var compactEscapeMap = map[rune]string{
	'
': "\n",
	'	': "\t",
}

Loading resume...

Last updated: January 2026