From d24a3c4583a88be2c3ce8105b0c764189ffd1c22 Mon Sep 17 00:00:00 2001 From: bingoohuang Date: Wed, 12 Mar 2025 21:20:58 +0800 Subject: [PATCH] Add colorization support for Hjson encoding --- color.go | 42 ++++++++++++++++++++++++++ encode.go | 86 +++++++++++++++++++++++++++++++++++++++++------------- structs.go | 12 ++++++-- 3 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 color.go diff --git a/color.go b/color.go new file mode 100644 index 0000000..7cb5e43 --- /dev/null +++ b/color.go @@ -0,0 +1,42 @@ +package hjson + +// Style is the color style +type Style struct { + Key, String, Number [2]string + True, False, Null [2]string + Escape [2]string + Remark [2]string + Append func(dst []byte, c byte) []byte +} + +// TerminalStyle is for terminals +var TerminalStyle *Style + +func init() { + TerminalStyle = &Style{ + Key: [2]string{"\x1B[94m", "\x1B[0m"}, + String: [2]string{"\x1B[92m", "\x1B[0m"}, + Number: [2]string{"\x1B[93m", "\x1B[0m"}, + True: [2]string{"\x1B[96m", "\x1B[0m"}, + False: [2]string{"\x1B[96m", "\x1B[0m"}, + Null: [2]string{"\x1B[91m", "\x1B[0m"}, + Escape: [2]string{"\x1B[35m", "\x1B[0m"}, + Remark: [2]string{"\x1B[90m", "\x1B[0m"}, + Append: func(dst []byte, c byte) []byte { + if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') { + dst = append(dst, "\\u00"...) + dst = append(dst, hexp((c>>4)&0xF)) + return append(dst, hexp((c)&0xF)) + } + return append(dst, c) + }, + } +} +func hexp(p byte) byte { + switch { + case p < 10: + return p + '0' + default: + return (p - 10) + 'a' + } +} diff --git a/encode.go b/encode.go index 1245cdd..eac881e 100644 --- a/encode.go +++ b/encode.go @@ -34,6 +34,11 @@ type EncoderOptions struct { // Write comments, if any are found in hjson.Node structs or as tags on // other structs. Comments bool + + // EnableColor enables colorized output + EnableColor bool + // ColorStyle is the style to use for colorized output + ColorStyle *Style } // DefaultOptions returns the default encoding options. @@ -55,6 +60,8 @@ func DefaultOptions() EncoderOptions { IndentBy: " ", BaseIndentation: "", Comments: true, + EnableColor: false, + ColorStyle: TerminalStyle, } } @@ -123,14 +130,18 @@ func (e *hjsonEncoder) quoteForComment(cmStr string) bool { return false } -func (e *hjsonEncoder) quote(value string, separator string, isRootObject bool, +func (e *hjsonEncoder) quote(value, 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) + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.String[0], e.ColorStyle.String[1] + } if len(value) == 0 { - e.WriteString(separator + `""`) + e.WriteString(separator + l + `""` + r) } else if e.QuoteAlways || hasCommentAfter || needsQuotes.MatchString(value) || @@ -144,26 +155,27 @@ func (e *hjsonEncoder) quote(value string, separator string, isRootObject bool, // sequences. if !needsEscape.MatchString(value) { - e.WriteString(separator + `"` + value + `"`) + + e.WriteString(separator + l + `"` + value + `"` + r) } else if !needsEscapeML.MatchString(value) && !isRootObject { - e.mlString(value, separator, keyComment) + e.mlString(value, separator, keyComment, l, r) } else { - e.WriteString(separator + `"` + e.quoteReplace(value) + `"`) + e.WriteString(separator + l + `"` + e.quoteReplace(value) + `"` + r) } } else { // return without quotes - e.WriteString(separator + value) + e.WriteString(separator + l + value + r) } } -func (e *hjsonEncoder) mlString(value string, separator string, keyComment string) { +func (e *hjsonEncoder) mlString(value, separator, keyComment, lColor, rColor 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(separator + lColor + "'''") e.WriteString(a[0]) } else { if !strings.Contains(keyComment, "\n") { @@ -176,11 +188,11 @@ func (e *hjsonEncoder) mlString(value string, separator string, keyComment strin indent = 0 } e.writeIndent(indent) - e.WriteString(v) + e.WriteString(lColor + v + rColor) } e.writeIndent(e.indent + 1) } - e.WriteString("'''") + e.WriteString("'''" + rColor) } func (e *hjsonEncoder) quoteName(name string) string { @@ -279,6 +291,14 @@ func (e *hjsonEncoder) unpackNode(value reflect.Value, cm Comments) (reflect.Val return value, cm } +func (e *hjsonEncoder) writeNull() { + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Null[0], e.ColorStyle.Null[1] + } + e.WriteString(l + "null" + r) +} + // This function can often be called from within itself, so do not output // anything from the upper half of it. func (e *hjsonEncoder) str( @@ -319,14 +339,14 @@ func (e *hjsonEncoder) str( if !value.IsValid() { e.WriteString(separator) - e.WriteString("null") + e.writeNull() return nil } if kind == reflect.Interface || kind == reflect.Ptr { if value.IsNil() { e.WriteString(separator) - e.WriteString("null") + e.writeNull() return nil } return e.str(value.Elem(), noIndent, separator, isRootObject, isObjElement, cm) @@ -367,8 +387,14 @@ func (e *hjsonEncoder) str( if n == "" { n = "0" } + e.WriteString(separator) + + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Number[0], e.ColorStyle.Number[1] + } // without quotes - e.WriteString(separator + n) + e.WriteString(l + n + r) } else { e.quote(value.String(), separator, isRootObject, cm.Key, e.quoteForComment(cm.After)) @@ -376,21 +402,33 @@ func (e *hjsonEncoder) str( case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: e.WriteString(separator) - e.WriteString(strconv.FormatInt(value.Int(), 10)) + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Number[0], e.ColorStyle.Number[1] + } + e.WriteString(l + strconv.FormatInt(value.Int(), 10) + r) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: e.WriteString(separator) - e.WriteString(strconv.FormatUint(value.Uint(), 10)) + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Number[0], e.ColorStyle.Number[1] + } + e.WriteString(l + strconv.FormatUint(value.Uint(), 10) + r) case reflect.Float32, reflect.Float64: // JSON numbers must be finite. Encode non-finite numbers as null. e.WriteString(separator) number := value.Float() + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Number[0], e.ColorStyle.Number[1] + } if math.IsInf(number, 0) || math.IsNaN(number) { - e.WriteString("null") + e.writeNull() } else if number == -0 { - e.WriteString("0") + e.WriteString(l + "0" + r) } else { // find shortest representation ('G' does not work) val := strconv.FormatFloat(number, 'f', -1, 64) @@ -398,15 +436,23 @@ func (e *hjsonEncoder) str( if len(exp) < len(val) { val = strings.ToLower(exp) } - e.WriteString(val) + + e.WriteString(l + val + r) } case reflect.Bool: e.WriteString(separator) + l, r := "", "" if value.Bool() { - e.WriteString("true") + if e.EnableColor { + l, r = e.ColorStyle.True[0], e.ColorStyle.True[1] + } + e.WriteString(l + "true" + r) } else { - e.WriteString("false") + if e.EnableColor { + l, r = e.ColorStyle.False[0], e.ColorStyle.False[1] + } + e.WriteString(l + "false" + r) } case reflect.Slice, reflect.Array: diff --git a/structs.go b/structs.go index 981858a..791f639 100644 --- a/structs.go +++ b/structs.go @@ -299,7 +299,11 @@ func (e *hjsonEncoder) writeFields( if len(fi.comment) > 0 { for _, line := range strings.Split(fi.comment, e.Eol) { e.writeIndentNoEOL(e.indent) - e.WriteString(fmt.Sprintf("# %s\n", line)) + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Remark[0], e.ColorStyle.Remark[1] + } + e.WriteString(fmt.Sprintf("%s# %s%s\n", l, line, r)) } } if elemCm.Before == "" { @@ -307,7 +311,11 @@ func (e *hjsonEncoder) writeFields( } else { e.WriteString(elemCm.Before) } - e.WriteString(e.quoteName(fi.name)) + l, r := "", "" + if e.EnableColor { + l, r = e.ColorStyle.Key[0], e.ColorStyle.Key[1] + } + e.WriteString(l + e.quoteName(fi.name) + r) e.WriteString(":") e.WriteString(elemCm.Key)