Files
doll/vendor/github.com/hjson/hjson-go/v4/encode.go
2025-10-26 23:36:16 +03:00

710 lines
20 KiB
Go

package hjson
import (
"bytes"
"encoding"
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf8"
)
// EncoderOptions defines options for encoding to Hjson.
type EncoderOptions struct {
// End of line, should be either \n or \r\n
Eol string
// Place braces on the same line
BracesSameLine bool
// Emit braces for the root object
EmitRootBraces bool
// Always place string in quotes
QuoteAlways bool
// Place string in quotes if it could otherwise be a number, boolean or null
QuoteAmbiguousStrings bool
// Indent string
IndentBy string
// Base indentation string
BaseIndentation string
// Write comments, if any are found in hjson.Node structs or as tags on
// other structs.
Comments bool
}
// DefaultOptions returns the default encoding options.
// Eol = "\n"
// BracesSameLine = true
// EmitRootBraces = true
// QuoteAlways = false
// QuoteAmbiguousStrings = true
// IndentBy = " "
// BaseIndentation = ""
// Comments = true
func DefaultOptions() EncoderOptions {
return EncoderOptions{
Eol: "\n",
BracesSameLine: true,
EmitRootBraces: true,
QuoteAlways: false,
QuoteAmbiguousStrings: true,
IndentBy: " ",
BaseIndentation: "",
Comments: true,
}
}
// Start looking for circular references below this depth.
const depthLimit = 1024
type hjsonEncoder struct {
bytes.Buffer // output
EncoderOptions
indent int
pDepth uint
parents map[uintptr]struct{} // Starts to be filled after pDepth has reached depthLimit
structTypeCache map[reflect.Type][]structFieldInfo
}
var JSONNumberType = reflect.TypeOf(json.Number(""))
var needsEscape, needsQuotes, needsEscapeML, startsWithKeyword, needsEscapeName *regexp.Regexp
func init() {
var commonRange = `\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}`
// needsEscape tests if the string can be written without escapes
needsEscape = regexp.MustCompile(`[\\\"\x00-\x1f` + commonRange + `]`)
// needsQuotes tests if the string can be written as a quoteless string (includes needsEscape but without \\ and \")
needsQuotes = regexp.MustCompile(`^\s|^"|^'|^#|^/\*|^//|^\{|^\}|^\[|^\]|^:|^,|\s$|[\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
// needsEscapeML tests if the string can be written as a multiline string (like needsEscape but without \n, \\, \", \t)
needsEscapeML = regexp.MustCompile(`'''|^[\s]+$|[\x00-\x08\x0b-\x1f` + commonRange + `]`)
// starts with a keyword and optionally is followed by a comment
startsWithKeyword = regexp.MustCompile(`^(true|false|null)\s*((,|\]|\}|#|//|/\*).*)?$`)
needsEscapeName = regexp.MustCompile(`[,\{\[\}\]\s:#"']|//|/\*`)
}
var meta = map[byte][]byte{
// table of character substitutions
'\b': []byte("\\b"),
'\t': []byte("\\t"),
'\n': []byte("\\n"),
'\f': []byte("\\f"),
'\r': []byte("\\r"),
'"': []byte("\\\""),
'\\': []byte("\\\\"),
}
func (e *hjsonEncoder) quoteReplace(text string) string {
return string(needsEscape.ReplaceAllFunc([]byte(text), func(a []byte) []byte {
c := meta[a[0]]
if c != nil {
return c
}
r, _ := utf8.DecodeRune(a)
return []byte(fmt.Sprintf("\\u%04x", r))
}))
}
func (e *hjsonEncoder) quoteForComment(cmStr string) bool {
chars := []rune(cmStr)
for _, r := range chars {
switch r {
case '\r', '\n':
return false
case '/', '#':
return true
}
}
return false
}
func (e *hjsonEncoder) quote(value string, separator string, isRootObject bool,
keyComment string, hasCommentAfter bool) {
// Check if we can insert this string without quotes
// see hjson syntax (must not parse as true, false, null or number)
if len(value) == 0 {
e.WriteString(separator + `""`)
} else if e.QuoteAlways ||
hasCommentAfter ||
needsQuotes.MatchString(value) ||
(e.QuoteAmbiguousStrings && (startsWithNumber([]byte(value)) ||
startsWithKeyword.MatchString(value))) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we first check if the string can be expressed in multiline
// format or we must replace the offending characters with safe escape
// sequences.
if !needsEscape.MatchString(value) {
e.WriteString(separator + `"` + value + `"`)
} else if !needsEscapeML.MatchString(value) && !isRootObject {
e.mlString(value, separator, keyComment)
} else {
e.WriteString(separator + `"` + e.quoteReplace(value) + `"`)
}
} else {
// return without quotes
e.WriteString(separator + value)
}
}
func (e *hjsonEncoder) mlString(value string, separator string, keyComment string) {
a := strings.Split(value, "\n")
if len(a) == 1 {
// The string contains only a single line. We still use the multiline
// format as it avoids escaping the \ character (e.g. when used in a
// regex).
e.WriteString(separator + "'''")
e.WriteString(a[0])
} else {
if !strings.Contains(keyComment, "\n") {
e.writeIndent(e.indent + 1)
}
e.WriteString("'''")
for _, v := range a {
indent := e.indent + 1
if len(v) == 0 {
indent = 0
}
e.writeIndent(indent)
e.WriteString(v)
}
e.writeIndent(e.indent + 1)
}
e.WriteString("'''")
}
func (e *hjsonEncoder) quoteName(name string) string {
if len(name) == 0 {
return `""`
}
// Check if we can insert this name without quotes
if needsEscapeName.MatchString(name) || needsEscape.MatchString(name) {
return `"` + e.quoteReplace(name) + `"`
}
// without quotes
return name
}
func (e *hjsonEncoder) bracesIndent(isObjElement, isEmpty bool, cm Comments,
separator string) {
if !isObjElement || cm.Key == "" {
if isObjElement && !e.BracesSameLine && (!isEmpty || cm.InsideFirst != "") {
e.writeIndent(e.indent)
} else {
e.WriteString(separator)
}
}
}
type sortAlpha []reflect.Value
func (s sortAlpha) Len() int {
return len(s)
}
func (s sortAlpha) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortAlpha) Less(i, j int) bool {
return fmt.Sprintf("%v", s[i]) < fmt.Sprintf("%v", s[j])
}
func (e *hjsonEncoder) writeIndentNoEOL(indent int) {
e.WriteString(e.BaseIndentation)
for i := 0; i < indent; i++ {
e.WriteString(e.IndentBy)
}
}
func (e *hjsonEncoder) writeIndent(indent int) {
e.WriteString(e.Eol)
e.writeIndentNoEOL(indent)
}
func (e *hjsonEncoder) useMarshalerJSON(
value reflect.Value,
noIndent bool,
separator string,
isRootObject,
isObjElement bool,
) error {
b, err := value.Interface().(json.Marshaler).MarshalJSON()
if err != nil {
return err
}
decOpt := DefaultDecoderOptions()
decOpt.UseJSONNumber = true
var dummyDest interface{}
jsonRoot, err := orderedUnmarshal(b, &dummyDest, decOpt, false, false)
if err != nil {
return err
}
// Output Hjson with our current options, instead of JSON.
return e.str(reflect.ValueOf(jsonRoot), noIndent, separator, isRootObject,
isObjElement, Comments{})
}
var marshalerJSON = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
var marshalerText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
func (e *hjsonEncoder) unpackNode(value reflect.Value, cm Comments) (reflect.Value, Comments) {
if value.IsValid() {
if node, ok := value.Interface().(Node); ok {
value = reflect.ValueOf(node.Value)
if e.Comments {
cm = node.Cm
}
} else if pNode, ok := value.Interface().(*Node); ok {
value = reflect.ValueOf(pNode.Value)
if e.Comments {
cm = pNode.Cm
}
}
}
return value, cm
}
// This function can often be called from within itself, so do not output
// anything from the upper half of it.
func (e *hjsonEncoder) str(
value reflect.Value,
noIndent bool,
separator string,
isRootObject,
isObjElement bool,
cm Comments,
) error {
// Produce a string from value.
// Unpack *Node, possibly overwrite cm.
value, cm = e.unpackNode(value, cm)
if cm.Key != "" {
separator = ""
}
kind := value.Kind()
switch kind {
case reflect.Ptr, reflect.Slice, reflect.Map:
if e.pDepth++; e.pDepth > depthLimit {
if e.parents == nil {
e.parents = map[uintptr]struct{}{}
}
p := value.Pointer()
if _, ok := e.parents[p]; ok {
return errors.New("Circular reference found, pointer of type " + value.Type().String())
}
e.parents[p] = struct{}{}
defer delete(e.parents, p)
}
defer func() { e.pDepth-- }()
}
if !value.IsValid() {
e.WriteString(separator)
e.WriteString("null")
return nil
}
if kind == reflect.Interface || kind == reflect.Ptr {
if value.IsNil() {
e.WriteString(separator)
e.WriteString("null")
return nil
}
return e.str(value.Elem(), noIndent, separator, isRootObject, isObjElement, cm)
}
// Our internal orderedMap implements marshalerJSON. We must therefore place
// this check before checking marshalerJSON. Calling orderedMap.MarshalJSON()
// from this function would cause an infinite loop.
if om, ok := value.Interface().(OrderedMap); ok {
var fis []fieldInfo
for _, key := range om.Keys {
fis = append(fis, fieldInfo{
field: reflect.ValueOf(om.Map[key]),
name: key,
})
}
return e.writeFields(fis, noIndent, separator, isRootObject, isObjElement, cm)
}
if value.Type().Implements(marshalerJSON) {
return e.useMarshalerJSON(value, noIndent, separator, isRootObject, isObjElement)
}
if value.Type().Implements(marshalerText) {
b, err := value.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil {
return err
}
return e.str(reflect.ValueOf(string(b)), noIndent, separator, isRootObject,
isObjElement, cm)
}
switch kind {
case reflect.String:
if value.Type() == JSONNumberType {
n := value.String()
if n == "" {
n = "0"
}
// without quotes
e.WriteString(separator + n)
} else {
e.quote(value.String(), separator, isRootObject, cm.Key,
e.quoteForComment(cm.After))
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
e.WriteString(separator)
e.WriteString(strconv.FormatInt(value.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
e.WriteString(separator)
e.WriteString(strconv.FormatUint(value.Uint(), 10))
case reflect.Float32, reflect.Float64:
// JSON numbers must be finite. Encode non-finite numbers as null.
e.WriteString(separator)
number := value.Float()
if math.IsInf(number, 0) || math.IsNaN(number) {
e.WriteString("null")
} else if number == -0 {
e.WriteString("0")
} else {
// find shortest representation ('G' does not work)
val := strconv.FormatFloat(number, 'f', -1, 64)
exp := strconv.FormatFloat(number, 'E', -1, 64)
if len(exp) < len(val) {
val = strings.ToLower(exp)
}
e.WriteString(val)
}
case reflect.Bool:
e.WriteString(separator)
if value.Bool() {
e.WriteString("true")
} else {
e.WriteString("false")
}
case reflect.Slice, reflect.Array:
e.bracesIndent(isObjElement, value.Len() == 0, cm, separator)
e.WriteString("[" + cm.InsideFirst)
if value.Len() == 0 {
if cm.InsideFirst != "" || cm.InsideLast != "" {
e.WriteString(e.Eol)
if cm.InsideLast == "" {
e.writeIndentNoEOL(e.indent)
}
}
e.WriteString(cm.InsideLast + "]")
return nil
}
indent1 := e.indent
e.indent++
// Join all of the element texts together, separated with newlines
for i := 0; i < value.Len(); i++ {
elem, elemCm := e.unpackNode(value.Index(i), Comments{})
if elemCm.Before == "" && elemCm.Key == "" {
e.writeIndent(e.indent)
} else {
e.WriteString(e.Eol + elemCm.Before + elemCm.Key)
}
if err := e.str(elem, true, "", false, false, elemCm); err != nil {
return err
}
e.WriteString(elemCm.After)
}
if cm.InsideLast != "" {
e.WriteString(e.Eol + cm.InsideLast)
} else {
e.writeIndent(indent1)
}
e.WriteString("]")
e.indent = indent1
case reflect.Map:
var fis []fieldInfo
useMarshalText := value.Type().Key().Implements(marshalerText)
keys := value.MapKeys()
sort.Sort(sortAlpha(keys))
for _, key := range keys {
var name string
if useMarshalText {
keyBytes, err := key.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil {
return err
}
name = string(keyBytes)
} else {
name = fmt.Sprintf("%v", key)
}
fis = append(fis, fieldInfo{
field: value.MapIndex(key),
name: name,
})
}
return e.writeFields(fis, noIndent, separator, isRootObject, isObjElement, cm)
case reflect.Struct:
// Struct field info is identical for all instances of the same type.
// Only the values on the fields can be different.
t := value.Type()
sfis, ok := e.structTypeCache[t]
if !ok {
sfis = getStructFieldInfoSlice(t)
e.structTypeCache[t] = sfis
}
// Collect fields first, too see if any should be shown (considering
// "omitEmpty").
var fis []fieldInfo
FieldLoop:
for _, sfi := range sfis {
// The field might be found on the root struct or in embedded structs.
fv := value
for _, i := range sfi.indexPath {
if fv.Kind() == reflect.Ptr {
if fv.IsNil() {
continue FieldLoop
}
fv = fv.Elem()
}
fv = fv.Field(i)
}
if sfi.omitEmpty && isEmptyValue(fv) {
continue
}
fi := fieldInfo{
field: fv,
name: sfi.name,
}
if e.Comments {
fi.comment = sfi.comment
}
fis = append(fis, fi)
}
return e.writeFields(fis, noIndent, separator, isRootObject, isObjElement, cm)
default:
return errors.New("Unsupported type " + value.Type().String())
}
return nil
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
default:
return false
}
}
func investigateComment(txt string) (
endsInsideComment,
endsWithLineFeed bool,
) {
var prev rune
for _, rn := range txt {
switch rn {
case '\n':
endsInsideComment = false
case '#':
endsInsideComment = true
case '/':
if prev == '/' {
endsInsideComment = true
}
}
endsWithLineFeed = (rn == '\n')
prev = rn
}
return
}
// Marshal returns the Hjson encoding of v using
// default options.
//
// See MarshalWithOptions.
func Marshal(v interface{}) ([]byte, error) {
return MarshalWithOptions(v, DefaultOptions())
}
// MarshalWithOptions returns the Hjson encoding of v.
//
// The value v is traversed recursively.
//
// Boolean values are written as true or false.
//
// Floating point, integer, and json.Number values are written as numbers (with
// decimals only if needed, using . as decimals separator).
//
// String values encode as Hjson strings (quoteless, multiline or
// JSON).
//
// Array and slice values encode as arrays, surrounded by []. Unlike
// json.Marshal, hjson.Marshal will encode a nil-slice as [] instead of null.
//
// Map values encode as objects, surrounded by {}. The map's key type must be
// possible to print to a string using fmt.Sprintf("%v", key), or implement
// encoding.TextMarshaler. The map keys are sorted alphabetically and
// used as object keys. Unlike json.Marshal, hjson.Marshal will encode a
// nil-map as {} instead of null.
//
// Struct values also encode as objects, surrounded by {}. Only the exported
// fields are encoded to Hjson. The fields will appear in the same order as in
// the struct.
//
// The encoding of each struct field can be customized by the format string
// stored under the "json" key in the struct field's tag.
// The format string gives the name of the field, possibly followed by a comma
// and "omitempty". The name may be empty in order to specify "omitempty"
// without overriding the default field name.
//
// The "omitempty" option specifies that the field should be omitted
// from the encoding if the field has an empty value, defined as
// false, 0, a nil pointer, a nil interface value, and any empty array,
// slice, map, or string.
//
// As a special case, if the field tag is "-", the field is always omitted.
// Note that a field with name "-" can still be generated using the tag "-,".
//
// Comments can be set on struct fields using the "comment" key in the struct
// field's tag. The comment will be written on the line before the field key,
// prefixed with #. Or possible several lines prefixed by #, if there are line
// breaks (\n) in the comment text.
//
// If both the "json" and the "comment" tag keys are used on a struct field
// they should be separated by whitespace.
//
// Examples of struct field tags and their meanings:
//
// // Field appears in Hjson as key "myName".
// Field int `json:"myName"`
//
// // Field appears in Hjson as key "myName" and the field is omitted from
// // the object if its value is empty, as defined above.
// Field int `json:"myName,omitempty"`
//
// // Field appears in Hjson as key "Field" (the default), but the field is
// // skipped if empty. Note the leading comma.
// Field int `json:",omitempty"`
//
// // Field is ignored by this package.
// Field int `json:"-"`
//
// // Field appears in Hjson as key "-".
// Field int `json:"-,"`
//
// // Field appears in Hjson preceded by a line just containing `# A comment.`
// Field int `comment:"A comment."`
//
// // Field appears in Hjson as key "myName" preceded by a line just
// // containing `# A comment.`
// Field int `json:"myName" comment:"A comment."`
//
// Anonymous struct fields are usually marshaled as if their inner exported fields
// were fields in the outer struct, subject to the usual Go visibility rules amended
// as described in the next paragraph.
// An anonymous struct field with a name given in its JSON tag is treated as
// having that name, rather than being anonymous.
// An anonymous struct field of interface type is treated the same as having
// that type as its name, rather than being anonymous.
//
// The Go visibility rules for struct fields are amended for JSON when
// deciding which field to marshal or unmarshal. If there are
// multiple fields at the same level, and that level is the least
// nested (and would therefore be the nesting level selected by the
// usual Go rules), the following extra rules apply:
//
// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered,
// even if there are multiple untagged fields that would otherwise conflict.
//
// 2) If there is exactly one field (tagged or not according to the first rule), that is selected.
//
// 3) Otherwise there are multiple fields, and all are ignored; no error occurs.
//
// Pointer values encode as the value pointed to.
// A nil pointer encodes as the null JSON value.
//
// Interface values encode as the value contained in the interface.
// A nil interface value encodes as the null JSON value.
//
// If an encountered value implements the json.Marshaler interface then the
// function MarshalJSON() is called on it. The JSON is then converted to Hjson
// using the current indentation and options given in the call to json.Marshal().
//
// If an encountered value implements the encoding.TextMarshaler interface
// but not the json.Marshaler interface, then the function MarshalText() is
// called on it to get a text.
//
// Channel, complex, and function values cannot be encoded in Hjson, will
// result in an error.
//
// Hjson cannot represent cyclic data structures and Marshal does not handle
// them. Passing cyclic structures to Marshal will result in an error.
func MarshalWithOptions(v interface{}, options EncoderOptions) ([]byte, error) {
e := &hjsonEncoder{
indent: 0,
EncoderOptions: options,
structTypeCache: map[reflect.Type][]structFieldInfo{},
}
value := reflect.ValueOf(v)
_, cm := e.unpackNode(value, Comments{})
e.WriteString(cm.Before + cm.Key)
err := e.str(value, true, e.BaseIndentation, true, false, cm)
if err != nil {
return nil, err
}
e.WriteString(cm.After)
return e.Bytes(), nil
}