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 // 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 // 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.QuoteAmbiguousStrings = true opt.IndentBy = " " opt.BaseIndentation = "" 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) || (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) } 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) e.WriteString(e.BaseIndentation) 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 } return e.str(value.Elem(),noIndent,separator,isRootObject) } 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 if !isRootObject || e.EmitRootBraces { if !noIndent && !e.BracesSameLine { e.writeIndent(e.indent) } else { e.WriteString(separator) } e.indent++ 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++ { if i > 0 || !isRootObject || e.EmitRootBraces { 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 } } if !isRootObject || e.EmitRootBraces { 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 if !isRootObject || e.EmitRootBraces { if !noIndent && !e.BracesSameLine { e.writeIndent(e.indent) } else { e.WriteString(separator) } e.indent++ 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) { if i > 0 || !isRootObject || e.EmitRootBraces { e.writeIndent(e.indent) } e.WriteString(fmt.Sprintf("# %s", line)) } } if i > 0 || !isRootObject || e.EmitRootBraces { 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) } } if !isRootObject || e.EmitRootBraces { 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.EmitRootBraces = options.EmitRootBraces e.QuoteAlways = options.QuoteAlways e.QuoteAmbiguousStrings = options.QuoteAmbiguousStrings e.IndentBy = options.IndentBy e.BaseIndentation = options.BaseIndentation err := e.str(reflect.ValueOf(v), true, e.BaseIndentation, true) if err != nil { return nil, err } return e.Bytes(), nil }