Files

364 lines
9.9 KiB
Go

// Copyright 2011-2019 Canonical Ltd
// Copyright 2025 The go-yaml Project Contributors
// SPDX-License-Identifier: Apache-2.0
// Node types and constants for YAML tree representation.
// Defines Kind, Style, and Node structure for intermediate YAML representation.
package libyaml
import (
"reflect"
"strings"
"unicode"
"unicode/utf8"
)
// Tag constants for YAML types
const (
nullTag = "!!null"
boolTag = "!!bool"
strTag = "!!str"
intTag = "!!int"
floatTag = "!!float"
timestampTag = "!!timestamp"
seqTag = "!!seq"
mapTag = "!!map"
binaryTag = "!!binary"
mergeTag = "!!merge"
)
const longTagPrefix = "tag:yaml.org,2002:"
var (
longTags = make(map[string]string)
shortTags = make(map[string]string)
)
func init() {
for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} {
ltag := longTag(stag)
longTags[stag] = ltag
shortTags[ltag] = stag
}
}
func shortTag(tag string) string {
if strings.HasPrefix(tag, longTagPrefix) {
if stag, ok := shortTags[tag]; ok {
return stag
}
return "!!" + tag[len(longTagPrefix):]
}
return tag
}
func longTag(tag string) string {
if strings.HasPrefix(tag, "!!") {
if ltag, ok := longTags[tag]; ok {
return ltag
}
return longTagPrefix + tag[2:]
}
return tag
}
// Kind represents the type of YAML node
type Kind uint32
const (
DocumentNode Kind = 1 << iota
SequenceNode
MappingNode
ScalarNode
AliasNode
StreamNode
)
// Style represents the formatting style of a YAML node
type Style uint32
const (
TaggedStyle Style = 1 << iota
DoubleQuotedStyle
SingleQuotedStyle
LiteralStyle
FoldedStyle
FlowStyle
)
// StreamVersionDirective represents a YAML %YAML version directive for stream nodes.
type StreamVersionDirective struct {
Major int
Minor int
}
// StreamTagDirective represents a YAML %TAG directive for stream nodes.
type StreamTagDirective struct {
Handle string
Prefix string
}
// Node represents an element in the YAML document hierarchy. While documents
// are typically encoded and decoded into higher level types, such as structs
// and maps, Node is an intermediate representation that allows detailed
// control over the content being decoded or encoded.
//
// It's worth noting that although Node offers access into details such as
// line numbers, columns, and comments, the content when re-encoded will not
// have its original textual representation preserved. An effort is made to
// render the data pleasantly, and to preserve comments near the data they
// describe, though.
//
// Values that make use of the Node type interact with the yaml package in the
// same way any other type would do, by encoding and decoding yaml data
// directly or indirectly into them.
//
// For example:
//
// var person struct {
// Name string
// Address yaml.Node
// }
// err := yaml.Unmarshal(data, &person)
//
// Or by itself:
//
// var person Node
// err := yaml.Unmarshal(data, &person)
type Node struct {
// Kind defines whether the node is a document, a mapping, a sequence,
// a scalar value, or an alias to another node. The specific data type of
// scalar nodes may be obtained via the ShortTag and LongTag methods.
Kind Kind
// Style allows customizing the appearance of the node in the tree.
Style Style
// Tag holds the YAML tag defining the data type for the value.
// When decoding, this field will always be set to the resolved tag,
// even when it wasn't explicitly provided in the YAML content.
// When encoding, if this field is unset the value type will be
// implied from the node properties, and if it is set, it will only
// be serialized into the representation if TaggedStyle is used or
// the implicit tag diverges from the provided one.
Tag string
// Value holds the unescaped and unquoted representation of the value.
Value string
// Anchor holds the anchor name for this node, which allows aliases to point to it.
Anchor string
// Alias holds the node that this alias points to. Only valid when Kind is AliasNode.
Alias *Node
// Content holds contained nodes for documents, mappings, and sequences.
Content []*Node
// HeadComment holds any comments in the lines preceding the node and
// not separated by an empty line.
HeadComment string
// LineComment holds any comments at the end of the line where the node is in.
LineComment string
// FootComment holds any comments following the node and before empty lines.
FootComment string
// Line and Column hold the node position in the decoded YAML text.
// These fields are not respected when encoding the node.
Line int
Column int
// StreamNode-specific fields (only valid when Kind == StreamNode)
// Encoding holds the stream encoding (UTF-8, UTF-16LE, UTF-16BE).
// Only valid for StreamNode.
Encoding Encoding
// Version holds the YAML version directive (%YAML).
// Only valid for StreamNode.
Version *StreamVersionDirective
// TagDirectives holds the %TAG directives.
// Only valid for StreamNode.
TagDirectives []StreamTagDirective
}
// IsZero returns whether the node has all of its fields unset.
func (n *Node) IsZero() bool {
return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil &&
n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 &&
n.Encoding == 0 && n.Version == nil && n.TagDirectives == nil
}
// LongTag returns the long form of the tag that indicates the data type for
// the node. If the Tag field isn't explicitly defined, one will be computed
// based on the node properties.
func (n *Node) LongTag() string {
return longTag(n.ShortTag())
}
// ShortTag returns the short form of the YAML tag that indicates data type for
// the node. If the Tag field isn't explicitly defined, one will be computed
// based on the node properties.
func (n *Node) ShortTag() string {
if n.indicatedString() {
return strTag
}
if n.Tag == "" || n.Tag == "!" {
switch n.Kind {
case MappingNode:
return mapTag
case SequenceNode:
return seqTag
case AliasNode:
if n.Alias != nil {
return n.Alias.ShortTag()
}
case ScalarNode:
return strTag
case 0:
// Special case to make the zero value convenient.
if n.IsZero() {
return nullTag
}
}
return ""
}
return shortTag(n.Tag)
}
func (n *Node) indicatedString() bool {
return n.Kind == ScalarNode &&
(shortTag(n.Tag) == strTag ||
(n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0)
}
// shouldUseLiteralStyle determines if a string should use literal style.
// It returns true if the string contains newlines AND meets additional criteria:
// - is at least 2 characters long
// - contains at least one non-whitespace character
func shouldUseLiteralStyle(s string) bool {
if !strings.Contains(s, "\n") || len(s) < 2 {
return false
}
// Must contain at least one non-whitespace character
for _, r := range s {
if !unicode.IsSpace(r) {
return true
}
}
return false
}
// SetString is a convenience function that sets the node to a string value
// and defines its style in a pleasant way depending on its content.
func (n *Node) SetString(s string) {
n.Kind = ScalarNode
if utf8.ValidString(s) {
n.Value = s
n.Tag = strTag
} else {
n.Value = encodeBase64(s)
n.Tag = binaryTag
}
if shouldUseLiteralStyle(n.Value) {
n.Style = LiteralStyle
}
}
// Decode decodes the node and stores its data into the value pointed to by v.
//
// See the documentation for Unmarshal for details about the
// conversion of YAML into a Go value.
func (n *Node) Decode(v any) (err error) {
d := NewConstructor(DefaultOptions)
defer handleErr(&err)
out := reflect.ValueOf(v)
if out.Kind() == reflect.Pointer && !out.IsNil() {
out = out.Elem()
}
d.Construct(n, out)
if len(d.TypeErrors) > 0 {
return &LoadErrors{Errors: d.TypeErrors}
}
return nil
}
// Load decodes the node and stores its data into the value pointed to by v,
// applying the given options.
//
// This method is useful when you need to preserve options like WithKnownFields()
// inside custom UnmarshalYAML implementations.
//
// Maps and pointers (to a struct, string, int, etc) are accepted as v
// values. If an internal pointer within a struct is not initialized,
// the yaml package will initialize it if necessary. The v parameter
// must not be nil.
//
// See the documentation of the package-level Load function for details
// about YAML to Go conversion and tag options.
func (n *Node) Load(v any, opts ...Option) (err error) {
defer handleErr(&err)
o, err := ApplyOptions(opts...)
if err != nil {
return err
}
d := NewConstructor(o)
out := reflect.ValueOf(v)
if out.Kind() == reflect.Pointer && !out.IsNil() {
out = out.Elem()
}
d.Construct(n, out)
if len(d.TypeErrors) > 0 {
return &LoadErrors{Errors: d.TypeErrors}
}
return nil
}
// Encode encodes value v and stores its representation in n.
//
// See the documentation for Marshal for details about the
// conversion of Go values into YAML.
func (n *Node) Encode(v any) (err error) {
defer handleErr(&err)
e := NewRepresenter(noWriter, DefaultOptions)
defer e.Destroy()
e.MarshalDoc("", reflect.ValueOf(v))
e.Finish()
p := NewComposer(e.Out)
p.Textless = true
defer p.Destroy()
doc := p.Parse()
*n = *doc.Content[0]
return nil
}
// Dump encodes value v and stores its representation in n,
// applying the given options.
//
// This method is useful when you need to apply specific encoding options
// while building Node trees programmatically.
//
// See the documentation for Marshal for details about the
// conversion of Go values into YAML.
func (n *Node) Dump(v any, opts ...Option) (err error) {
defer handleErr(&err)
o, err := ApplyOptions(opts...)
if err != nil {
return err
}
e := NewRepresenter(noWriter, o)
defer e.Destroy()
e.MarshalDoc("", reflect.ValueOf(v))
e.Finish()
p := NewComposer(e.Out)
p.Textless = true
defer p.Destroy()
doc := p.Parse()
*n = *doc.Content[0]
return nil
}