469 lines
12 KiB
Go
469 lines
12 KiB
Go
package hjson
|
|
|
|
import (
|
|
"bytes"
|
|
"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
|
|
// Deprecated: Hjson always emits braces
|
|
EmitRootBraces bool
|
|
// Always place string in quotes
|
|
QuoteAlways bool
|
|
// Indent string
|
|
IndentBy string
|
|
// Allow the -0 value (unlike ES6)
|
|
AllowMinusZero bool
|
|
// Encode unknown values as 'null'
|
|
UnknownAsNull bool
|
|
}
|
|
|
|
// DefaultOptions returns the default encoding options.
|
|
func DefaultOptions() EncoderOptions {
|
|
opt := EncoderOptions{}
|
|
opt.Eol = "\n"
|
|
opt.BracesSameLine = false
|
|
opt.EmitRootBraces = true
|
|
opt.QuoteAlways = false
|
|
opt.IndentBy = " "
|
|
opt.AllowMinusZero = false
|
|
opt.UnknownAsNull = false
|
|
return opt
|
|
}
|
|
|
|
type hjsonEncoder struct {
|
|
bytes.Buffer // output
|
|
EncoderOptions
|
|
indent int
|
|
}
|
|
|
|
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, \r, \\, \", \t)
|
|
var x08Or9 = `\x08` // `\x09` for the old behavior
|
|
needsEscapeML = regexp.MustCompile(`'''|^[\s]+$|[\x00-` + x08Or9 + `\x0b\x0c\x0e-\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) quote(value string, separator string, isRootObject 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 ||
|
|
needsQuotes.MatchString(value) ||
|
|
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)
|
|
} else {
|
|
e.WriteString(separator + `"` + e.quoteReplace(value) + `"`)
|
|
}
|
|
} else {
|
|
// return without quotes
|
|
e.WriteString(separator + value)
|
|
}
|
|
}
|
|
|
|
func (e *hjsonEncoder) mlString(value string, separator string) {
|
|
// wrap the string into the ''' (multiline) format
|
|
|
|
a := strings.Split(strings.Replace(value, "\r", "", -1), "\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 {
|
|
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) {
|
|
if needsEscape.MatchString(name) {
|
|
name = e.quoteReplace(name)
|
|
}
|
|
return `"` + name + `"`
|
|
}
|
|
// without quotes
|
|
return name
|
|
}
|
|
|
|
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 s[i].String() < s[j].String()
|
|
}
|
|
|
|
func (e *hjsonEncoder) writeIndent(indent int) {
|
|
e.WriteString(e.Eol)
|
|
for i := 0; i < indent; i++ {
|
|
e.WriteString(e.IndentBy)
|
|
}
|
|
}
|
|
|
|
func (e *hjsonEncoder) useMarshaler(value reflect.Value, separator string) error {
|
|
b, err := value.Interface().(json.Marshaler).MarshalJSON()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.WriteString(separator)
|
|
e.WriteString(string(b))
|
|
return nil
|
|
}
|
|
|
|
var marshaler = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
|
|
|
func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, isRootObject bool) error {
|
|
|
|
// Produce a string from value.
|
|
|
|
kind := value.Kind()
|
|
|
|
if kind == reflect.Interface || kind == reflect.Ptr {
|
|
if value.IsNil() {
|
|
e.WriteString(separator)
|
|
e.WriteString("null")
|
|
return nil
|
|
}
|
|
value = value.Elem()
|
|
kind = value.Kind()
|
|
}
|
|
|
|
if value.Type().Implements(marshaler) {
|
|
return e.useMarshaler(value, separator)
|
|
}
|
|
|
|
switch kind {
|
|
case reflect.String:
|
|
e.quote(value.String(), separator, isRootObject)
|
|
|
|
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 !e.AllowMinusZero && 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:
|
|
|
|
len := value.Len()
|
|
if len == 0 {
|
|
e.WriteString(separator)
|
|
e.WriteString("[]")
|
|
break
|
|
}
|
|
|
|
indent1 := e.indent
|
|
e.indent++
|
|
|
|
if !noIndent && !e.BracesSameLine {
|
|
e.writeIndent(indent1)
|
|
} else {
|
|
e.WriteString(separator)
|
|
}
|
|
e.WriteString("[")
|
|
|
|
// Join all of the element texts together, separated with newlines
|
|
for i := 0; i < len; i++ {
|
|
e.writeIndent(e.indent)
|
|
if err := e.str(value.Index(i), true, "", false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
e.writeIndent(indent1)
|
|
e.WriteString("]")
|
|
|
|
e.indent = indent1
|
|
|
|
case reflect.Map:
|
|
|
|
len := value.Len()
|
|
if len == 0 {
|
|
e.WriteString(separator)
|
|
e.WriteString("{}")
|
|
break
|
|
}
|
|
|
|
indent1 := e.indent
|
|
e.indent++
|
|
if !noIndent && !e.BracesSameLine {
|
|
e.writeIndent(indent1)
|
|
} else {
|
|
e.WriteString(separator)
|
|
}
|
|
e.WriteString("{")
|
|
|
|
keys := value.MapKeys()
|
|
sort.Sort(sortAlpha(keys))
|
|
|
|
// Join all of the member texts together, separated with newlines
|
|
for i := 0; i < len; i++ {
|
|
e.writeIndent(e.indent)
|
|
e.WriteString(e.quoteName(keys[i].String()))
|
|
e.WriteString(":")
|
|
if err := e.str(value.MapIndex(keys[i]), false, " ", false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
e.writeIndent(indent1)
|
|
e.WriteString("}")
|
|
e.indent = indent1
|
|
|
|
case reflect.Struct:
|
|
|
|
l := value.NumField()
|
|
if l == 0 {
|
|
e.WriteString(separator)
|
|
e.WriteString("{}")
|
|
break
|
|
}
|
|
|
|
indent1 := e.indent
|
|
e.indent++
|
|
if !noIndent && !e.BracesSameLine {
|
|
e.writeIndent(indent1)
|
|
} else {
|
|
e.WriteString(separator)
|
|
}
|
|
e.WriteString("{")
|
|
|
|
// Join all of the member texts together, separated with newlines
|
|
for i := 0; i < l; i++ {
|
|
curStructField := value.Type().Field(i)
|
|
curField := value.Field(i)
|
|
|
|
name := curStructField.Name
|
|
jsonTag := curStructField.Tag.Get("json")
|
|
jsonComment := curStructField.Tag.Get("comment")
|
|
omitEmpty := false
|
|
if jsonTag == "-" {
|
|
continue
|
|
}
|
|
splits := strings.Split(jsonTag, ",")
|
|
if splits[0] != "" {
|
|
name = splits[0]
|
|
}
|
|
if len(splits) > 1 {
|
|
for _, opt := range splits[1:] {
|
|
if opt == "omitempty" {
|
|
omitEmpty = true
|
|
}
|
|
}
|
|
}
|
|
if omitEmpty && isEmptyValue(curField) {
|
|
continue
|
|
}
|
|
if len(jsonComment) > 0 {
|
|
for _, line := range strings.Split(jsonComment, e.Eol) {
|
|
e.writeIndent(e.indent)
|
|
e.WriteString(fmt.Sprintf("# %s", line))
|
|
}
|
|
}
|
|
e.writeIndent(e.indent)
|
|
e.WriteString(e.quoteName(name))
|
|
e.WriteString(":")
|
|
if err := e.str(curField, false, " ", false); err != nil {
|
|
return err
|
|
}
|
|
if len(jsonComment) > 0 && i < l-1 {
|
|
e.WriteString(e.Eol)
|
|
}
|
|
}
|
|
|
|
e.writeIndent(indent1)
|
|
e.WriteString("}")
|
|
|
|
e.indent = indent1
|
|
|
|
default:
|
|
if e.UnknownAsNull {
|
|
// Use null as a placeholder for non-JSON values.
|
|
e.WriteString("null")
|
|
} else {
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Marshal traverses the value v recursively.
|
|
//
|
|
// Boolean values encode as JSON booleans.
|
|
//
|
|
// Floating point, integer, and Number values encode as JSON numbers.
|
|
//
|
|
// String values encode as Hjson strings (quoteless, multiline or
|
|
// JSON).
|
|
//
|
|
// Array and slice values encode as JSON arrays.
|
|
//
|
|
// Map values encode as JSON objects. The map's key type must be a
|
|
// string. The map keys are sorted and used as JSON object keys.
|
|
//
|
|
// 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.
|
|
//
|
|
// JSON cannot represent cyclic data structures and Marshal does not
|
|
// handle them. Passing cyclic structures to Marshal will result in
|
|
// an infinite recursion.
|
|
//
|
|
func MarshalWithOptions(v interface{}, options EncoderOptions) ([]byte, error) {
|
|
e := &hjsonEncoder{}
|
|
e.indent = 0
|
|
e.Eol = options.Eol
|
|
e.BracesSameLine = options.BracesSameLine
|
|
e.QuoteAlways = options.QuoteAlways
|
|
e.IndentBy = options.IndentBy
|
|
|
|
err := e.str(reflect.ValueOf(v), true, "", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return e.Bytes(), nil
|
|
}
|