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 }