tiny-rdm/backend/utils/string/json_formatter.go

159 lines
3.6 KiB
Go

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
}