diff --git a/backend/utils/convert/json_convert.go b/backend/utils/convert/json_convert.go index 73ab02c..c7e731a 100644 --- a/backend/utils/convert/json_convert.go +++ b/backend/utils/convert/json_convert.go @@ -1,9 +1,8 @@ package convutil import ( - "bytes" - "encoding/json" "strings" + strutil "tinyrdm/backend/utils/string" ) type JsonConvert struct{} @@ -16,18 +15,11 @@ func (JsonConvert) Decode(str string) (string, bool) { trimedStr := strings.TrimSpace(str) if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) || (strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) { - var out bytes.Buffer - if err := json.Indent(&out, []byte(trimedStr), "", " "); err == nil { - return out.String(), true - } + return strutil.JSONBeautify(trimedStr, " "), true } return str, false } func (JsonConvert) Encode(str string) (string, bool) { - var dst bytes.Buffer - if err := json.Compact(&dst, []byte(str)); err != nil { - return str, false - } - return dst.String(), true + return strutil.JSONMinify(str), true } diff --git a/backend/utils/convert/unicode_json_convert.go b/backend/utils/convert/unicode_json_convert.go index 9e62b7f..4f39723 100644 --- a/backend/utils/convert/unicode_json_convert.go +++ b/backend/utils/convert/unicode_json_convert.go @@ -2,9 +2,9 @@ package convutil import ( "bytes" - "encoding/json" "strconv" "strings" + strutil "tinyrdm/backend/utils/string" "unicode" "unicode/utf16" "unicode/utf8" @@ -20,22 +20,16 @@ func (UnicodeJsonConvert) Decode(str string) (string, bool) { trimedStr := strings.TrimSpace(str) if (strings.HasPrefix(trimedStr, "{") && strings.HasSuffix(trimedStr, "}")) || (strings.HasPrefix(trimedStr, "[") && strings.HasSuffix(trimedStr, "]")) { - var out bytes.Buffer - if err := json.Indent(&out, []byte(trimedStr), "", " "); err == nil { - if quoteStr, ok := UnquoteUnicodeJson(out.Bytes()); ok { - return string(quoteStr), true - } + resultStr := strutil.JSONBeautify(trimedStr, " ") + if quoteStr, ok := UnquoteUnicodeJson([]byte(resultStr)); ok { + return string(quoteStr), true } } return str, false } func (UnicodeJsonConvert) Encode(str string) (string, bool) { - var dst bytes.Buffer - if err := json.Compact(&dst, []byte(str)); err != nil { - return str, false - } - return dst.String(), true + return strutil.JSONMinify(str), true } func UnquoteUnicodeJson(s []byte) ([]byte, bool) { diff --git a/backend/utils/string/json_formatter.go b/backend/utils/string/json_formatter.go new file mode 100644 index 0000000..f94d28a --- /dev/null +++ b/backend/utils/string/json_formatter.go @@ -0,0 +1,158 @@ +package strutil + +import ( + "strings" + "unicode" +) + +// Convert from https://github.com/ObuchiYuki/SwiftJSONFormatter + +// ArrayIterator defines the iterator for an array +type ArrayIterator[T any] struct { + array []T + head int +} + +// NewArrayIterator initializes a new ArrayIterator with the given array +func NewArrayIterator[T any](array []T) *ArrayIterator[T] { + return &ArrayIterator[T]{ + array: array, + head: -1, + } +} + +// HasNext returns true if there are more elements to iterate over +func (it *ArrayIterator[T]) HasNext() bool { + return it.head+1 < len(it.array) +} + +// PeekNext returns the next element without advancing the iterator +func (it *ArrayIterator[T]) PeekNext() *T { + if it.head+1 < len(it.array) { + return &it.array[it.head+1] + } + return nil +} + +// Next returns the next element and advances the iterator +func (it *ArrayIterator[T]) Next() *T { + defer func() { + it.head++ + }() + return it.PeekNext() +} + +// JSONBeautify formats a JSON string with indentation +func JSONBeautify(value string, indent string) string { + if len(indent) <= 0 { + indent = " " + } + return format(value, indent, "\n", " ") +} + +// JSONMinify formats a JSON string by removing all unnecessary whitespace +func JSONMinify(value string) string { + return format(value, "", "", "") +} + +// format applies the specified formatting to a JSON string +func format(value string, indent string, newLine string, separator string) string { + var formatted strings.Builder + chars := NewArrayIterator([]rune(value)) + indentLevel := 0 + + for chars.HasNext() { + if char := chars.Next(); char != nil { + switch *char { + case '{', '[': + formatted.WriteRune(*char) + consumeWhitespaces(chars) + peeked := chars.PeekNext() + if peeked != nil && (*peeked == '}' || *peeked == ']') { + chars.Next() + formatted.WriteRune(*peeked) + } else { + indentLevel++ + formatted.WriteString(newLine) + formatted.WriteString(strings.Repeat(indent, indentLevel)) + } + case '}', ']': + indentLevel-- + formatted.WriteString(newLine) + formatted.WriteString(strings.Repeat(indent, max(0, indentLevel))) + formatted.WriteRune(*char) + case '"': + str := consumeString(chars) + //str = convertUnicodeString(str) + formatted.WriteString(str) + case ',': + consumeWhitespaces(chars) + formatted.WriteRune(',') + peeked := chars.PeekNext() + if peeked != nil && *peeked != '}' && *peeked != ']' { + formatted.WriteString(newLine) + formatted.WriteString(strings.Repeat(indent, max(0, indentLevel))) + } + case ':': + formatted.WriteString(":" + separator) + default: + if !unicode.IsSpace(*char) { + formatted.WriteRune(*char) + } + } + } + } + + return formatted.String() +} + +// consumeWhitespaces advances the iterator past any whitespace characters +func consumeWhitespaces(iter *ArrayIterator[rune]) { + for iter.HasNext() { + if peeked := iter.PeekNext(); peeked != nil && unicode.IsSpace(*peeked) { + iter.Next() + } else { + break + } + } +} + +// consumeString consumes a JSON string value from the iterator +func consumeString(iter *ArrayIterator[rune]) string { + var sb strings.Builder + sb.WriteRune('"') + escaping := false + + for iter.HasNext() { + if char := iter.Next(); char != nil { + if *char == '\n' { + return sb.String() // Unterminated string + } + + sb.WriteRune(*char) + + if escaping { + escaping = false + } else { + if *char == '\\' { + escaping = true + } + if *char == '"' { + break + } + } + } + } + + return sb.String() +} + +func convertUnicodeString(str string) string { + // TODO: quote UTF-16 characters + //if len(str) > 2 { + // if unqStr, err := strconv.Unquote(str); err == nil { + // return strconv.Quote(unqStr) + // } + //} + return str +}