diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go index ea8789614ea..a2eeb1e2c6c 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go @@ -17,6 +17,8 @@ limitations under the License. package genericclioptions import ( + "os" + "slices" "strings" "github.com/spf13/cobra" @@ -29,7 +31,12 @@ func (f *JSONYamlPrintFlags) AllowedFormats() []string { if f == nil { return []string{} } - return []string{"json", "yaml"} + formats := []string{"json", "yaml"} + // We can't use the cmdutil pkg directly because of import cycle. + if strings.ToLower(os.Getenv("KUBECTL_KYAML")) == "true" { + formats = append(formats, "kyaml") + } + return formats } // JSONYamlPrintFlags provides default flags necessary for json/yaml printing. @@ -47,11 +54,19 @@ func (f *JSONYamlPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePr var printer printers.ResourcePrinter outputFormat = strings.ToLower(outputFormat) + + valid := f.AllowedFormats() + if !slices.Contains(valid, outputFormat) { + return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: valid} + } + switch outputFormat { case "json": printer = &printers.JSONPrinter{} case "yaml": printer = &printers.YAMLPrinter{} + case "kyaml": + printer = &printers.KYAMLPrinter{} default: return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()} } diff --git a/staging/src/k8s.io/cli-runtime/pkg/printers/json_test.go b/staging/src/k8s.io/cli-runtime/pkg/printers/json_test.go index df14d0037e2..d196b82ecde 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/printers/json_test.go +++ b/staging/src/k8s.io/cli-runtime/pkg/printers/json_test.go @@ -105,8 +105,9 @@ func TestPrintersSuccess(t *testing.T) { om := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{Name: name} } genericPrinters := map[string]ResourcePrinter{ - "json": NewTypeSetter(scheme.Scheme).ToPrinter(&JSONPrinter{}), - "yaml": NewTypeSetter(scheme.Scheme).ToPrinter(&YAMLPrinter{}), + "json": NewTypeSetter(scheme.Scheme).ToPrinter(&JSONPrinter{}), + "yaml": NewTypeSetter(scheme.Scheme).ToPrinter(&YAMLPrinter{}), + "kyaml": NewTypeSetter(scheme.Scheme).ToPrinter(&KYAMLPrinter{}), } objects := map[string]runtime.Object{ "pod": &v1.Pod{ObjectMeta: om("pod")}, diff --git a/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml.go b/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml.go new file mode 100644 index 00000000000..285da2037dc --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml.go @@ -0,0 +1,66 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// package kyaml provides a printer for Kubernetes objects that formats them +// as KYAML, a strict subset of YAML that is designed to be explicit and +// unambiguous. KYAML is YAML. +package printers + +import ( + "bytes" + "errors" + "fmt" + "io" + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml/kyaml" +) + +// KYAMLPrinter is an implementation of ResourcePrinter which formats data into +// a specific dialect of YAML, known as KYAML. KYAML is halfway between YAML +// and JSON, but is a strict subset of YAML, and so it should should be +// readable by any YAML parser. It is designed to be explicit and unambiguous, +// and eschews significant whitespace. +type KYAMLPrinter struct { + encoder kyaml.Encoder +} + +// PrintObj prints the data as KYAML to the specified writer. +func (p *KYAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error { + // We use reflect.Indirect here in order to obtain the actual value from a pointer. + // We need an actual value in order to retrieve the package path for an object. + // Using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers. + if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) { + return errors.New(InternalObjectPrinterErr) + } + + switch obj := obj.(type) { + case *metav1.WatchEvent: + if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) { + return errors.New(InternalObjectPrinterErr) + } + case *runtime.Unknown: + return p.encoder.FromYAML(bytes.NewReader(obj.Raw), w) + } + + if obj.GetObjectKind().GroupVersionKind().Empty() { + return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type") + } + + return p.encoder.FromObject(obj, w) +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml_test.go b/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml_test.go new file mode 100644 index 00000000000..b184da3649d --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/printers/kyaml_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package printers + +import ( + "testing" + + "sigs.k8s.io/yaml" + + "k8s.io/client-go/kubernetes/scheme" +) + +func TestKYAMLPrinter(t *testing.T) { + testPrinter(t, NewTypeSetter(scheme.Scheme).ToPrinter(&KYAMLPrinter{}), kyamlUnmarshal) +} + +func kyamlUnmarshal(data []byte, v interface{}) error { + return yaml.Unmarshal(data, v) +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go index 2857d5a1477..28268204444 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go @@ -455,6 +455,12 @@ const ( // Transition to WebSockets. RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS" PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS" + + // owner: @thockin + // kep: https://kep.k8s.io/5296 + // + // Support KYAML output. + KYAMLOutput FeatureGate = "KUBECTL_KYAML" ) // IsEnabled returns true iff environment variable is set to true. diff --git a/vendor/modules.txt b/vendor/modules.txt index e052e6a45d1..1849d1e5802 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1288,3 +1288,4 @@ sigs.k8s.io/structured-merge-diff/v6/value # sigs.k8s.io/yaml v1.6.0 ## explicit; go 1.22 sigs.k8s.io/yaml +sigs.k8s.io/yaml/kyaml diff --git a/vendor/sigs.k8s.io/yaml/kyaml/kyaml.go b/vendor/sigs.k8s.io/yaml/kyaml/kyaml.go new file mode 100644 index 00000000000..f07b0b3e96c --- /dev/null +++ b/vendor/sigs.k8s.io/yaml/kyaml/kyaml.go @@ -0,0 +1,828 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kyaml provides an encoder for KYAML, a strict subset of YAML that is +// designed to be explicit and unambiguous. KYAML is YAML, so any YAML parser +// should be able to read it. +// +// KYAML is designed to be halfway between YAML and JSON, with the following +// properties: +// - Not whitespace-sensitive +// - Allows comments +// - Allows trailing commas +// - Does not require quoted keys +// +// KYAML is an output format, and will follow these conventions: +// - Always double-quote strings, even if they are not ambiguous. +// - Only quote keys that might be ambiguously interpreted (e.g. "no" is +// always quoted). +// - Always use `{}` for structs and maps, and `[]` for lists. +// - Economize on vertical space by cuddling some kinds of brackets together. +// - Render multi-line strings with YAML's line folding, which is close to +// the Go string literal format. +// +// KYAML also includes a document-separator "header" (still valid YAML), which +// helps to disambiguate a KYAML document from an ill-formed JSON document. +// +// Because KYAML is YAML, a KYAML multi-document is a YAML multi-document. +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, "\n") + } + + 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, "\n") + } + if len(child.HeadComment) > 0 { + ky.renderComments(child.HeadComment, indent, out) + fmt.Fprint(out, "\n") + } + } + 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, "\n") + ky.renderComments(child.FootComment, indent, out) + } + if len(doc.LineComment) > 0 { + fmt.Fprint(out, "\n") + ky.renderComments(" "+doc.LineComment, 0, out) + } + if len(doc.FootComment) > 0 { + fmt.Fprint(out, "\n") + 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 = "\\\n" + +var regularEscapeMap = map[rune]string{ + '\n': "\\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable + '\t': "\t", // literal tab +} +var compactEscapeMap = map[rune]string{ + '\n': "\\n", + '\t': "\\t", +} + +// renderString processes a string (either single-line or multi-line), +// rendering it to the output. This DOES NOT render a trailing newline. +func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.Writer) error { + lazyQuote := flags&flagLazyQuote != 0 + compact := flags&flagCompact != 0 + multi := strings.Contains(val, "\n") + + if !multi && lazyQuote && !needsQuotes(val) { + fmt.Fprint(out, val) + return nil + } + + // Special cases for certain input. + escapeOverrides := regularEscapeMap + if compact { + escapeOverrides = compactEscapeMap + } + + // + // The rest of this is borrowed from Go's strconv.Quote implementation. + // + + // accumulate into a buffer + buf := &bytes.Buffer{} + + // opening quote + fmt.Fprint(buf, `"`) + if multi && !compact { + fmt.Fprint(buf, kyamlFoldStr) + } + + // Iterating a string with invalid UTF8 returns RuneError rather than the + // bytes, so we iterate the string and decode the runes. This is a bit + // slower, but gives us a better result. + s := val + for width := 0; len(s) > 0; s = s[width:] { + r := rune(s[0]) + width = 1 + if r >= utf8.RuneSelf { + r, width = utf8.DecodeRuneInString(s) + } + if width == 1 && r == utf8.RuneError { + fmt.Fprint(buf, `\x`) + fmt.Fprintf(buf, "%02x", s[0]) + continue + } + ky.appendEscapedRune(r, indent, escapeOverrides, buf) + } + + // closing quote + afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n' + if multi && !compact { + if !afterNewline { + fmt.Fprint(buf, kyamlFoldStr) + } + ky.writeIndent(indent, buf) + } + fmt.Fprint(buf, `"`) + + fmt.Fprint(out, buf.String()) + + return nil +} + +var allowedUnquotedAnywhere = map[rune]bool{ + '_': true, +} + +var allowedUnquotedInterior = map[rune]bool{ + '-': true, + '.': true, + '/': true, +} + +func needsQuotes(s string) bool { + if s == "" { + return true + } + if isTypeAmbiguous(s) { + return true + } + runes := []rune(s) + for i, r := range runes { + if unicode.IsLetter(r) || unicode.IsNumber(r) || allowedUnquotedAnywhere[r] { + continue + } + if i > 0 && i < len(runes)-1 && allowedUnquotedInterior[r] { + continue + } + // it's something we don't explicitly allow + return true + } + return false +} + +// From https://yaml.org/type/int.html and https://yaml.org/type/float.html +var sexagesimalRE = regexp.MustCompile(`^[+-]?[1-9][0-9_]*(:[0-5]?[0-9])+(\.[0-9_]*)?$`) + +// isTypeAmbiguous returns true if a YAML parser might interpret the unquoted +// form of the string argument as a YAML type other than string (e.g. `true` +// would be interpreted as a boolean). +func isTypeAmbiguous(s string) bool { + // Null-like strings: https://yaml.org/type/null.html + if len(s) <= 5 { + switch strings.ToLower(s) { + case "null", "~", "": + return true + } + } + + // Boolean-like strings: https://yaml.org/type/bool.html + if _, err := strconv.ParseBool(s); err == nil { + return true + } + if len(s) <= 5 { + switch strings.ToLower(s) { + case "true", "y", "yes", "on", "false", "n", "no", "off": + return true + } + } + + // Number-like strings: https://yaml.org/type/int.html and + // https://yaml.org/type/float.html + // + // NOTE: the stripping of underscores is gross. + sWithoutUnderscores := strings.ReplaceAll(s, "_", "") + // Handles binary ("0b"), octal ("0" or "0o"), decimal, and hex ("0x") + if _, err := strconv.ParseInt(sWithoutUnderscores, 0, 64); err == nil && !isSyntaxError(err) { + return true + } + // Handles standard and scientific notation. + if _, err := strconv.ParseFloat(sWithoutUnderscores, 64); err == nil && !isSyntaxError(err) { + return true + } + + // Sexagesimal strings like "11:00" (in YAML 1.1, removed in 1.2): + // https://yaml.org/type/int.html and https://yaml.org/type/float.html + if sexagesimalRE.MatchString(s) { + return true + } + + // Infinity and NaN: https://yaml.org/type/float.html + if len(s) <= 5 { + switch strings.ToLower(s) { + case ".inf", "-.inf", "+.inf", ".nan": + return true + } + } + + // Time-like strings + if _, matches := parseTimestamp(s); matches { + return true + } + + return false +} + +func isSyntaxError(err error) bool { + var numerr *strconv.NumError + if ok := errors.As(err, &numerr); ok { + return errors.Is(numerr.Err, strconv.ErrSyntax) + } + return false +} + +// This is a subset of the formats allowed by the regular expression +// defined at http://yaml.org/type/timestamp.html. +// +// NOTE: This was copied from go.yaml.in/yaml/v2 +var allowedTimestampFormats = []string{ + "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // space separated with no time zone + "2006-1-2", // date only + // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" + // from the set of examples. +} + +// parseTimestamp parses s as a timestamp string and +// returns the timestamp and reports whether it succeeded. +// Timestamp formats are defined at http://yaml.org/type/timestamp.html +// +// NOTE: This was copied from go.yaml.in/yaml/v2 +func parseTimestamp(s string) (time.Time, bool) { + // TODO write code to check all the formats supported by + // http://yaml.org/type/timestamp.html instead of using time.Parse. + + // Quick check: all date formats start with YYYY-. + i := 0 + for ; i < len(s); i++ { + if c := s[i]; c < '0' || c > '9' { + break + } + } + if i != 4 || i == len(s) || s[i] != '-' { + return time.Time{}, false + } + for _, format := range allowedTimestampFormats { + if t, err := time.Parse(format, s); err == nil { + return t, true + } + } + return time.Time{}, false +} + +// We use a buffer here so we can peek backwards. +func (ky *Encoder) appendEscapedRune(r rune, indent int, escapeOverrides map[rune]string, buf *bytes.Buffer) { + afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n' + + if afterNewline { + ky.writeIndent(indent, buf) + // We want to preserve leading whitespace in the source string, so if + // we find whitespace, we need to escape it. We don't want to + // escape lines without leading whitespace, but we DO want to render + // the result with fidelity to vertical alignment, so we write an extra + // space. This is OK, because all whitespace before the first + // non-whitespace character is dropped, as per YAML spec. If there are + // no lines with leading whitespace it looks like the indent is one too + // many, which seems OK. + if unicode.IsSpace(r) && r != '\n' { + buf.WriteRune('\\') + } else { + buf.WriteRune(' ') + } + } + if s, found := escapeOverrides[r]; found { + buf.WriteString(s) + return + } + if r == '"' || r == '\\' { // always escaped + buf.WriteRune('\\') + buf.WriteRune(r) + return + } + if unicode.IsPrint(r) { + buf.WriteRune(r) + return + } + switch r { + case '\a': + buf.WriteString(`\a`) + case '\b': + buf.WriteString(`\b`) + case '\f': + buf.WriteString(`\f`) + case '\n': + buf.WriteString(`\n`) + case '\r': + buf.WriteString(`\r`) + case '\t': + buf.WriteString(`\t`) + case '\v': + buf.WriteString(`\v`) + case '\x00': + buf.WriteString(`\0`) + case '\x1b': + buf.WriteString(`\e`) + case '\x85': + buf.WriteString(`\N`) + case '\xa0': + buf.WriteString(`\_`) + case '\u2028': + buf.WriteString(`\L`) + case '\u2029': + buf.WriteString(`\P`) + default: + const hexits = "0123456789abcdef" + switch { + case r < ' ' || r == 0x7f: + buf.WriteString(`\x`) + buf.WriteByte(hexits[byte(r)>>4]) + buf.WriteByte(hexits[byte(r)&0xF]) + case !utf8.ValidRune(r): + r = utf8.RuneError + fallthrough + case r < 0x10000: + buf.WriteString(`\u`) + for s := 12; s >= 0; s -= 4 { + buf.WriteByte(hexits[r>>uint(s)&0xF]) + } + default: + buf.WriteString(`\U`) + for s := 28; s >= 0; s -= 4 { + buf.WriteByte(hexits[r>>uint(s)&0xF]) + } + } + } +} + +// renderSequence processes a YAML sequence node, rendering it to the output. This +// DOES NOT render a trailing newline or head/line/foot comments of the sequence +// itself, but DOES render comments of the child nodes. +func (ky *Encoder) renderSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error { + if len(node.Content) == 0 { + fmt.Fprint(out, "[]") + return nil + } + if flags&flagCompact != 0 { + return ky.renderCompactSequence(node, flags, out) + } + + // See if this list can use cuddled formatting. + cuddle := true + for _, child := range node.Content { + if !isCuddledKind(child) { + cuddle = false + break + } + if len(child.HeadComment)+len(child.LineComment)+len(child.FootComment) > 0 { + cuddle = false + break + } + } + + if cuddle { + return ky.renderCuddledSequence(node, indent, flags, out) + } + return ky.renderUncuddledSequence(node, indent, flags, out) +} + +// renderCompactSequence renders a YAML sequence node in compact form. +func (ky *Encoder) renderCompactSequence(node *yaml.Node, flags flagMask, out io.Writer) error { + fmt.Fprint(out, "[") + for i, child := range node.Content { + if i > 0 { + fmt.Fprint(out, ", ") + } + if err := ky.renderNode(child, 0, flags, out); err != nil { + return err + } + } + fmt.Fprint(out, "]") + return nil +} + +// renderCuddledSequence processes a YAML sequence node which has already been +// determined to be cuddled. We only cuddle sequences of structs or lists +// which have no comments. +func (ky *Encoder) renderCuddledSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error { + fmt.Fprint(out, "[") + for i, child := range node.Content { + // Each iteration should leave us cuddled for the next item. + if i > 0 { + fmt.Fprint(out, ", ") + } + if err := ky.renderNode(child, indent, flags, out); err != nil { + return err + } + } + fmt.Fprint(out, "]") + return nil +} + +func (ky *Encoder) renderUncuddledSequence(node *yaml.Node, indent int, flags flagMask, out io.Writer) error { + // Get into the right state for the first item. + fmt.Fprint(out, "[\n") + ky.writeIndent(indent, out) + for _, child := range node.Content { + // Each iteration should leave us ready to close the list. Since we + // have an item to render, we need 1 more indent. + ky.writeIndent(1, out) + + if len(child.HeadComment) > 0 { + ky.renderComments(child.HeadComment, indent+1, out) + fmt.Fprint(out, "\n") + ky.writeIndent(indent+1, out) + } + + if err := ky.renderNode(child, indent+1, flags, out); err != nil { + return err + } + + fmt.Fprint(out, ",") + if len(child.LineComment) > 0 { + ky.renderComments(" "+child.LineComment, 0, out) + } + fmt.Fprint(out, "\n") + ky.writeIndent(indent, out) + if len(child.FootComment) > 0 { + ky.writeIndent(1, out) + ky.renderComments(child.FootComment, indent+1, out) + fmt.Fprint(out, "\n") + ky.writeIndent(indent, out) + } + } + fmt.Fprint(out, "]") + + return nil +} + +func (ky *Encoder) nodeKindString(kind yaml.Kind) string { + switch kind { + case yaml.DocumentNode: + return "document" + case yaml.ScalarNode: + return "scalar" + case yaml.MappingNode: + return "mapping" + case yaml.SequenceNode: + return "sequence" + case yaml.AliasNode: + return "alias" + default: + return "unknown" + } +} + +func isCuddledKind(node *yaml.Node) bool { + if node == nil { + return false + } + switch node.Kind { + case yaml.SequenceNode, yaml.MappingNode: + return true + case yaml.AliasNode: + return isCuddledKind(node.Alias) + } + return false +} + +// renderMapping processes a YAML mapping node, rendering it to the output. This +// DOES NOT render a trailing newline or head/line/foot comments of the mapping +// itself, but DOES render comments of the child nodes. +func (ky *Encoder) renderMapping(node *yaml.Node, indent int, flags flagMask, out io.Writer) error { + if len(node.Content) == 0 { + fmt.Fprint(out, "{}") + return nil + } + + if flags&flagCompact != 0 { + return ky.renderCompactMapping(node, flags, out) + } + + joinComments := func(a, b string) string { + if len(a) > 0 && len(b) > 0 { + return a + "\n" + b + } + return a + b + } + + fmt.Fprint(out, "{\n") + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + val := node.Content[i+1] + + ky.writeIndent(indent+1, out) + + // Only one of these should be set. + if comments := joinComments(key.HeadComment, val.HeadComment); len(comments) > 0 { + ky.renderComments(comments, indent+1, out) + fmt.Fprint(out, "\n") + ky.writeIndent(indent+1, out) + } + + // Mapping keys are always strings in KYAML, even if the YAML node says + // otherwise. + if err := ky.renderString(key.Value, indent+1, flagLazyQuote|flagCompact, out); err != nil { + return err + } + fmt.Fprint(out, ": ") + if err := ky.renderNode(val, indent+1, flags, out); err != nil { + return err + } + fmt.Fprint(out, ",") + if len(key.LineComment) > 0 && len(val.LineComment) > 0 { + return fmt.Errorf("kyaml internal error: line %d: both key and value have line comments", key.Line) + } + if len(key.LineComment) > 0 { + ky.renderComments(" "+key.LineComment, 0, out) + } else if len(val.LineComment) > 0 { + ky.renderComments(" "+val.LineComment, 0, out) + } + fmt.Fprint(out, "\n") + // Only one of these should be set. + if comments := joinComments(key.FootComment, val.FootComment); len(comments) > 0 { + ky.writeIndent(indent+1, out) + ky.renderComments(comments, indent+1, out) + fmt.Fprint(out, "\n") + } + } + ky.writeIndent(indent, out) + fmt.Fprint(out, "}") + return nil +} + +// renderCompactMapping renders a YAML mapping node in compact form. +func (ky *Encoder) renderCompactMapping(node *yaml.Node, flags flagMask, out io.Writer) error { + fmt.Fprint(out, "{") + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + val := node.Content[i+1] + + if i > 0 { + fmt.Fprint(out, ", ") + } + // Mapping keys are always strings in KYAML, even if the YAML node says + // otherwise. + if err := ky.renderString(key.Value, 0, flags|flagLazyQuote|flagCompact, out); err != nil { + return err + } + fmt.Fprint(out, ": ") + if err := ky.renderNode(val, 0, flags, out); err != nil { + return err + } + } + fmt.Fprint(out, "}") + return nil +} + +func (ky *Encoder) writeIndent(level int, out io.Writer) { + const indentString = " " + for range level { + fmt.Fprint(out, indentString) + } +} + +// renderCommentBlock writes the comments node to the output. This assumes the +// cursor is at the right place to start writing and DOES NOT render a trailing +// newline. +func (ky *Encoder) renderComments(comments string, indent int, out io.Writer) { + if len(comments) == 0 { + return + } + lines := strings.Split(comments, "\n") + for i, line := range lines { + if i > 0 { + fmt.Fprint(out, "\n") + ky.writeIndent(indent, out) + } + fmt.Fprint(out, line) + } +} + +func (ky *Encoder) renderAlias(node *yaml.Node, indent int, flags flagMask, out io.Writer) error { + if node.Alias != nil { + return ky.renderNode(node.Alias, indent+1, flags, out) + } + return nil +}