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 }