├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── go.mod ├── pretty.go └── pretty_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty 2 | 3 | [![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/tidwall/pretty) 4 | 5 | Pretty is a Go package that provides [fast](#performance) methods for formatting JSON for human readability, or to compact JSON for smaller payloads. 6 | 7 | Getting Started 8 | =============== 9 | 10 | ## Installing 11 | 12 | To start using Pretty, install Go and run `go get`: 13 | 14 | ```sh 15 | $ go get -u github.com/tidwall/pretty 16 | ``` 17 | 18 | This will retrieve the library. 19 | 20 | ## Pretty 21 | 22 | Using this example: 23 | 24 | ```json 25 | {"name": {"first":"Tom","last":"Anderson"}, "age":37, 26 | "children": ["Sara","Alex","Jack"], 27 | "fav.movie": "Deer Hunter", "friends": [ 28 | {"first": "Janet", "last": "Murphy", "age": 44} 29 | ]} 30 | ``` 31 | 32 | The following code: 33 | ```go 34 | result = pretty.Pretty(example) 35 | ``` 36 | 37 | Will format the json to: 38 | 39 | ```json 40 | { 41 | "name": { 42 | "first": "Tom", 43 | "last": "Anderson" 44 | }, 45 | "age": 37, 46 | "children": ["Sara", "Alex", "Jack"], 47 | "fav.movie": "Deer Hunter", 48 | "friends": [ 49 | { 50 | "first": "Janet", 51 | "last": "Murphy", 52 | "age": 44 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | ## Color 59 | 60 | Color will colorize the json for outputing to the screen. 61 | 62 | ```go 63 | result = pretty.Color(json, nil) 64 | ``` 65 | 66 | Will add color to the result for printing to the terminal. 67 | The second param is used for a customizing the style, and passing nil will use the default `pretty.TerminalStyle`. 68 | 69 | ## Ugly 70 | 71 | The following code: 72 | ```go 73 | result = pretty.Ugly(example) 74 | ``` 75 | 76 | Will format the json to: 77 | 78 | ```json 79 | {"name":{"first":"Tom","last":"Anderson"},"age":37,"children":["Sara","Alex","Jack"],"fav.movie":"Deer Hunter","friends":[{"first":"Janet","last":"Murphy","age":44}]}``` 80 | ``` 81 | 82 | ## Customized output 83 | 84 | There's a `PrettyOptions(json, opts)` function which allows for customizing the output with the following options: 85 | 86 | ```go 87 | type Options struct { 88 | // Width is an max column width for single line arrays 89 | // Default is 80 90 | Width int 91 | // Prefix is a prefix for all lines 92 | // Default is an empty string 93 | Prefix string 94 | // Indent is the nested indentation 95 | // Default is two spaces 96 | Indent string 97 | // SortKeys will sort the keys alphabetically 98 | // Default is false 99 | SortKeys bool 100 | } 101 | ``` 102 | ## Performance 103 | 104 | Benchmarks of Pretty alongside the builtin `encoding/json` Indent/Compact methods. 105 | ``` 106 | BenchmarkPretty-16 1000000 1034 ns/op 720 B/op 2 allocs/op 107 | BenchmarkPrettySortKeys-16 586797 1983 ns/op 2848 B/op 14 allocs/op 108 | BenchmarkUgly-16 4652365 254 ns/op 240 B/op 1 allocs/op 109 | BenchmarkUglyInPlace-16 6481233 183 ns/op 0 B/op 0 allocs/op 110 | BenchmarkJSONIndent-16 450654 2687 ns/op 1221 B/op 0 allocs/op 111 | BenchmarkJSONCompact-16 685111 1699 ns/op 442 B/op 0 allocs/op 112 | ``` 113 | 114 | *These benchmarks were run on a MacBook Pro 2.4 GHz 8-Core Intel Core i9.* 115 | 116 | ## Contact 117 | Josh Baker [@tidwall](http://twitter.com/tidwall) 118 | 119 | ## License 120 | 121 | Pretty source code is available under the MIT [License](/LICENSE). 122 | 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/pretty 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /pretty.go: -------------------------------------------------------------------------------- 1 | package pretty 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | // Options is Pretty options 11 | type Options struct { 12 | // Width is an max column width for single line arrays 13 | // Default is 80 14 | Width int 15 | // Prefix is a prefix for all lines 16 | // Default is an empty string 17 | Prefix string 18 | // Indent is the nested indentation 19 | // Default is two spaces 20 | Indent string 21 | // SortKeys will sort the keys alphabetically 22 | // Default is false 23 | SortKeys bool 24 | } 25 | 26 | // DefaultOptions is the default options for pretty formats. 27 | var DefaultOptions = &Options{Width: 80, Prefix: "", Indent: " ", SortKeys: false} 28 | 29 | // Pretty converts the input json into a more human readable format where each 30 | // element is on it's own line with clear indentation. 31 | func Pretty(json []byte) []byte { return PrettyOptions(json, nil) } 32 | 33 | // PrettyOptions is like Pretty but with customized options. 34 | func PrettyOptions(json []byte, opts *Options) []byte { 35 | if opts == nil { 36 | opts = DefaultOptions 37 | } 38 | buf := make([]byte, 0, len(json)) 39 | if len(opts.Prefix) != 0 { 40 | buf = append(buf, opts.Prefix...) 41 | } 42 | buf, _, _, _ = appendPrettyAny(buf, json, 0, true, 43 | opts.Width, opts.Prefix, opts.Indent, opts.SortKeys, 44 | 0, 0, -1) 45 | if len(buf) > 0 { 46 | buf = append(buf, '\n') 47 | } 48 | return buf 49 | } 50 | 51 | // Ugly removes insignificant space characters from the input json byte slice 52 | // and returns the compacted result. 53 | func Ugly(json []byte) []byte { 54 | buf := make([]byte, 0, len(json)) 55 | return ugly(buf, json) 56 | } 57 | 58 | // UglyInPlace removes insignificant space characters from the input json 59 | // byte slice and returns the compacted result. This method reuses the 60 | // input json buffer to avoid allocations. Do not use the original bytes 61 | // slice upon return. 62 | func UglyInPlace(json []byte) []byte { return ugly(json, json) } 63 | 64 | func ugly(dst, src []byte) []byte { 65 | dst = dst[:0] 66 | for i := 0; i < len(src); i++ { 67 | if src[i] > ' ' { 68 | dst = append(dst, src[i]) 69 | if src[i] == '"' { 70 | for i = i + 1; i < len(src); i++ { 71 | dst = append(dst, src[i]) 72 | if src[i] == '"' { 73 | j := i - 1 74 | for ; ; j-- { 75 | if src[j] != '\\' { 76 | break 77 | } 78 | } 79 | if (j-i)%2 != 0 { 80 | break 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | return dst 88 | } 89 | 90 | func isNaNOrInf(src []byte) bool { 91 | return src[0] == 'i' || //Inf 92 | src[0] == 'I' || // inf 93 | src[0] == '+' || // +Inf 94 | src[0] == 'N' || // Nan 95 | (src[0] == 'n' && len(src) > 1 && src[1] != 'u') // nan 96 | } 97 | 98 | func appendPrettyAny(buf, json []byte, i int, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) { 99 | for ; i < len(json); i++ { 100 | if json[i] <= ' ' { 101 | continue 102 | } 103 | if json[i] == '"' { 104 | return appendPrettyString(buf, json, i, nl) 105 | } 106 | 107 | if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' || isNaNOrInf(json[i:]) { 108 | return appendPrettyNumber(buf, json, i, nl) 109 | } 110 | if json[i] == '{' { 111 | return appendPrettyObject(buf, json, i, '{', '}', pretty, width, prefix, indent, sortkeys, tabs, nl, max) 112 | } 113 | if json[i] == '[' { 114 | return appendPrettyObject(buf, json, i, '[', ']', pretty, width, prefix, indent, sortkeys, tabs, nl, max) 115 | } 116 | switch json[i] { 117 | case 't': 118 | return append(buf, 't', 'r', 'u', 'e'), i + 4, nl, true 119 | case 'f': 120 | return append(buf, 'f', 'a', 'l', 's', 'e'), i + 5, nl, true 121 | case 'n': 122 | return append(buf, 'n', 'u', 'l', 'l'), i + 4, nl, true 123 | } 124 | } 125 | return buf, i, nl, true 126 | } 127 | 128 | type pair struct { 129 | kstart, kend int 130 | vstart, vend int 131 | } 132 | 133 | type byKeyVal struct { 134 | sorted bool 135 | json []byte 136 | buf []byte 137 | pairs []pair 138 | } 139 | 140 | func (arr *byKeyVal) Len() int { 141 | return len(arr.pairs) 142 | } 143 | func (arr *byKeyVal) Less(i, j int) bool { 144 | if arr.isLess(i, j, byKey) { 145 | return true 146 | } 147 | if arr.isLess(j, i, byKey) { 148 | return false 149 | } 150 | return arr.isLess(i, j, byVal) 151 | } 152 | func (arr *byKeyVal) Swap(i, j int) { 153 | arr.pairs[i], arr.pairs[j] = arr.pairs[j], arr.pairs[i] 154 | arr.sorted = true 155 | } 156 | 157 | type byKind int 158 | 159 | const ( 160 | byKey byKind = 0 161 | byVal byKind = 1 162 | ) 163 | 164 | type jtype int 165 | 166 | const ( 167 | jnull jtype = iota 168 | jfalse 169 | jnumber 170 | jstring 171 | jtrue 172 | jjson 173 | ) 174 | 175 | func getjtype(v []byte) jtype { 176 | if len(v) == 0 { 177 | return jnull 178 | } 179 | switch v[0] { 180 | case '"': 181 | return jstring 182 | case 'f': 183 | return jfalse 184 | case 't': 185 | return jtrue 186 | case 'n': 187 | return jnull 188 | case '[', '{': 189 | return jjson 190 | default: 191 | return jnumber 192 | } 193 | } 194 | 195 | func (arr *byKeyVal) isLess(i, j int, kind byKind) bool { 196 | k1 := arr.json[arr.pairs[i].kstart:arr.pairs[i].kend] 197 | k2 := arr.json[arr.pairs[j].kstart:arr.pairs[j].kend] 198 | var v1, v2 []byte 199 | if kind == byKey { 200 | v1 = k1 201 | v2 = k2 202 | } else { 203 | v1 = bytes.TrimSpace(arr.buf[arr.pairs[i].vstart:arr.pairs[i].vend]) 204 | v2 = bytes.TrimSpace(arr.buf[arr.pairs[j].vstart:arr.pairs[j].vend]) 205 | if len(v1) >= len(k1)+1 { 206 | v1 = bytes.TrimSpace(v1[len(k1)+1:]) 207 | } 208 | if len(v2) >= len(k2)+1 { 209 | v2 = bytes.TrimSpace(v2[len(k2)+1:]) 210 | } 211 | } 212 | t1 := getjtype(v1) 213 | t2 := getjtype(v2) 214 | if t1 < t2 { 215 | return true 216 | } 217 | if t1 > t2 { 218 | return false 219 | } 220 | if t1 == jstring { 221 | s1 := parsestr(v1) 222 | s2 := parsestr(v2) 223 | return string(s1) < string(s2) 224 | } 225 | if t1 == jnumber { 226 | n1, _ := strconv.ParseFloat(string(v1), 64) 227 | n2, _ := strconv.ParseFloat(string(v2), 64) 228 | return n1 < n2 229 | } 230 | return string(v1) < string(v2) 231 | 232 | } 233 | 234 | func parsestr(s []byte) []byte { 235 | for i := 1; i < len(s); i++ { 236 | if s[i] == '\\' { 237 | var str string 238 | json.Unmarshal(s, &str) 239 | return []byte(str) 240 | } 241 | if s[i] == '"' { 242 | return s[1:i] 243 | } 244 | } 245 | return nil 246 | } 247 | 248 | func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) { 249 | var ok bool 250 | if width > 0 { 251 | if pretty && open == '[' && max == -1 { 252 | // here we try to create a single line array 253 | max := width - (len(buf) - nl) 254 | if max > 3 { 255 | s1, s2 := len(buf), i 256 | buf, i, _, ok = appendPrettyObject(buf, json, i, '[', ']', false, width, prefix, "", sortkeys, 0, 0, max) 257 | if ok && len(buf)-s1 <= max { 258 | return buf, i, nl, true 259 | } 260 | buf = buf[:s1] 261 | i = s2 262 | } 263 | } else if max != -1 && open == '{' { 264 | return buf, i, nl, false 265 | } 266 | } 267 | buf = append(buf, open) 268 | i++ 269 | var pairs []pair 270 | if open == '{' && sortkeys { 271 | pairs = make([]pair, 0, 8) 272 | } 273 | var n int 274 | for ; i < len(json); i++ { 275 | if json[i] <= ' ' { 276 | continue 277 | } 278 | if json[i] == close { 279 | if pretty { 280 | if open == '{' && sortkeys { 281 | buf = sortPairs(json, buf, pairs) 282 | } 283 | if n > 0 { 284 | nl = len(buf) 285 | if buf[nl-1] == ' ' { 286 | buf[nl-1] = '\n' 287 | } else { 288 | buf = append(buf, '\n') 289 | } 290 | } 291 | if buf[len(buf)-1] != open { 292 | buf = appendTabs(buf, prefix, indent, tabs) 293 | } 294 | } 295 | buf = append(buf, close) 296 | return buf, i + 1, nl, open != '{' 297 | } 298 | if open == '[' || json[i] == '"' { 299 | if n > 0 { 300 | buf = append(buf, ',') 301 | if width != -1 && open == '[' { 302 | buf = append(buf, ' ') 303 | } 304 | } 305 | var p pair 306 | if pretty { 307 | nl = len(buf) 308 | if buf[nl-1] == ' ' { 309 | buf[nl-1] = '\n' 310 | } else { 311 | buf = append(buf, '\n') 312 | } 313 | if open == '{' && sortkeys { 314 | p.kstart = i 315 | p.vstart = len(buf) 316 | } 317 | buf = appendTabs(buf, prefix, indent, tabs+1) 318 | } 319 | if open == '{' { 320 | buf, i, nl, _ = appendPrettyString(buf, json, i, nl) 321 | if sortkeys { 322 | p.kend = i 323 | } 324 | buf = append(buf, ':') 325 | if pretty { 326 | buf = append(buf, ' ') 327 | } 328 | } 329 | buf, i, nl, ok = appendPrettyAny(buf, json, i, pretty, width, prefix, indent, sortkeys, tabs+1, nl, max) 330 | if max != -1 && !ok { 331 | return buf, i, nl, false 332 | } 333 | if pretty && open == '{' && sortkeys { 334 | p.vend = len(buf) 335 | if p.kstart > p.kend || p.vstart > p.vend { 336 | // bad data. disable sorting 337 | sortkeys = false 338 | } else { 339 | pairs = append(pairs, p) 340 | } 341 | } 342 | i-- 343 | n++ 344 | } 345 | } 346 | return buf, i, nl, open != '{' 347 | } 348 | func sortPairs(json, buf []byte, pairs []pair) []byte { 349 | if len(pairs) == 0 { 350 | return buf 351 | } 352 | vstart := pairs[0].vstart 353 | vend := pairs[len(pairs)-1].vend 354 | arr := byKeyVal{false, json, buf, pairs} 355 | sort.Stable(&arr) 356 | if !arr.sorted { 357 | return buf 358 | } 359 | nbuf := make([]byte, 0, vend-vstart) 360 | for i, p := range pairs { 361 | nbuf = append(nbuf, buf[p.vstart:p.vend]...) 362 | if i < len(pairs)-1 { 363 | nbuf = append(nbuf, ',') 364 | nbuf = append(nbuf, '\n') 365 | } 366 | } 367 | return append(buf[:vstart], nbuf...) 368 | } 369 | 370 | func appendPrettyString(buf, json []byte, i, nl int) ([]byte, int, int, bool) { 371 | s := i 372 | i++ 373 | for ; i < len(json); i++ { 374 | if json[i] == '"' { 375 | var sc int 376 | for j := i - 1; j > s; j-- { 377 | if json[j] == '\\' { 378 | sc++ 379 | } else { 380 | break 381 | } 382 | } 383 | if sc%2 == 1 { 384 | continue 385 | } 386 | i++ 387 | break 388 | } 389 | } 390 | return append(buf, json[s:i]...), i, nl, true 391 | } 392 | 393 | func appendPrettyNumber(buf, json []byte, i, nl int) ([]byte, int, int, bool) { 394 | s := i 395 | i++ 396 | for ; i < len(json); i++ { 397 | if json[i] <= ' ' || json[i] == ',' || json[i] == ':' || json[i] == ']' || json[i] == '}' { 398 | break 399 | } 400 | } 401 | return append(buf, json[s:i]...), i, nl, true 402 | } 403 | 404 | func appendTabs(buf []byte, prefix, indent string, tabs int) []byte { 405 | if len(prefix) != 0 { 406 | buf = append(buf, prefix...) 407 | } 408 | if len(indent) == 2 && indent[0] == ' ' && indent[1] == ' ' { 409 | for i := 0; i < tabs; i++ { 410 | buf = append(buf, ' ', ' ') 411 | } 412 | } else { 413 | for i := 0; i < tabs; i++ { 414 | buf = append(buf, indent...) 415 | } 416 | } 417 | return buf 418 | } 419 | 420 | // Style is the color style 421 | type Style struct { 422 | Key, String, Number [2]string 423 | True, False, Null [2]string 424 | Escape [2]string 425 | Brackets [2]string 426 | Append func(dst []byte, c byte) []byte 427 | } 428 | 429 | func hexp(p byte) byte { 430 | switch { 431 | case p < 10: 432 | return p + '0' 433 | default: 434 | return (p - 10) + 'a' 435 | } 436 | } 437 | 438 | // TerminalStyle is for terminals 439 | var TerminalStyle *Style 440 | 441 | func init() { 442 | TerminalStyle = &Style{ 443 | Key: [2]string{"\x1B[1m\x1B[94m", "\x1B[0m"}, 444 | String: [2]string{"\x1B[32m", "\x1B[0m"}, 445 | Number: [2]string{"\x1B[33m", "\x1B[0m"}, 446 | True: [2]string{"\x1B[36m", "\x1B[0m"}, 447 | False: [2]string{"\x1B[36m", "\x1B[0m"}, 448 | Null: [2]string{"\x1B[2m", "\x1B[0m"}, 449 | Escape: [2]string{"\x1B[35m", "\x1B[0m"}, 450 | Brackets: [2]string{"\x1B[1m", "\x1B[0m"}, 451 | Append: func(dst []byte, c byte) []byte { 452 | if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') { 453 | dst = append(dst, "\\u00"...) 454 | dst = append(dst, hexp((c>>4)&0xF)) 455 | return append(dst, hexp((c)&0xF)) 456 | } 457 | return append(dst, c) 458 | }, 459 | } 460 | } 461 | 462 | // Color will colorize the json. The style parma is used for customizing 463 | // the colors. Passing nil to the style param will use the default 464 | // TerminalStyle. 465 | func Color(src []byte, style *Style) []byte { 466 | if style == nil { 467 | style = TerminalStyle 468 | } 469 | apnd := style.Append 470 | if apnd == nil { 471 | apnd = func(dst []byte, c byte) []byte { 472 | return append(dst, c) 473 | } 474 | } 475 | type stackt struct { 476 | kind byte 477 | key bool 478 | } 479 | var dst []byte 480 | var stack []stackt 481 | for i := 0; i < len(src); i++ { 482 | if src[i] == '"' { 483 | key := len(stack) > 0 && stack[len(stack)-1].key 484 | if key { 485 | dst = append(dst, style.Key[0]...) 486 | } else { 487 | dst = append(dst, style.String[0]...) 488 | } 489 | dst = apnd(dst, '"') 490 | esc := false 491 | uesc := 0 492 | for i = i + 1; i < len(src); i++ { 493 | if src[i] == '\\' { 494 | if key { 495 | dst = append(dst, style.Key[1]...) 496 | } else { 497 | dst = append(dst, style.String[1]...) 498 | } 499 | dst = append(dst, style.Escape[0]...) 500 | dst = apnd(dst, src[i]) 501 | esc = true 502 | if i+1 < len(src) && src[i+1] == 'u' { 503 | uesc = 5 504 | } else { 505 | uesc = 1 506 | } 507 | } else if esc { 508 | dst = apnd(dst, src[i]) 509 | if uesc == 1 { 510 | esc = false 511 | dst = append(dst, style.Escape[1]...) 512 | if key { 513 | dst = append(dst, style.Key[0]...) 514 | } else { 515 | dst = append(dst, style.String[0]...) 516 | } 517 | } else { 518 | uesc-- 519 | } 520 | } else { 521 | dst = apnd(dst, src[i]) 522 | } 523 | if src[i] == '"' { 524 | j := i - 1 525 | for ; ; j-- { 526 | if src[j] != '\\' { 527 | break 528 | } 529 | } 530 | if (j-i)%2 != 0 { 531 | break 532 | } 533 | } 534 | } 535 | if esc { 536 | dst = append(dst, style.Escape[1]...) 537 | } else if key { 538 | dst = append(dst, style.Key[1]...) 539 | } else { 540 | dst = append(dst, style.String[1]...) 541 | } 542 | } else if src[i] == '{' || src[i] == '[' { 543 | stack = append(stack, stackt{src[i], src[i] == '{'}) 544 | dst = append(dst, style.Brackets[0]...) 545 | dst = apnd(dst, src[i]) 546 | dst = append(dst, style.Brackets[1]...) 547 | } else if (src[i] == '}' || src[i] == ']') && len(stack) > 0 { 548 | stack = stack[:len(stack)-1] 549 | dst = append(dst, style.Brackets[0]...) 550 | dst = apnd(dst, src[i]) 551 | dst = append(dst, style.Brackets[1]...) 552 | } else if (src[i] == ':' || src[i] == ',') && len(stack) > 0 && stack[len(stack)-1].kind == '{' { 553 | stack[len(stack)-1].key = !stack[len(stack)-1].key 554 | dst = append(dst, style.Brackets[0]...) 555 | dst = apnd(dst, src[i]) 556 | dst = append(dst, style.Brackets[1]...) 557 | } else { 558 | var kind byte 559 | if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' || isNaNOrInf(src[i:]) { 560 | kind = '0' 561 | dst = append(dst, style.Number[0]...) 562 | } else if src[i] == 't' { 563 | kind = 't' 564 | dst = append(dst, style.True[0]...) 565 | } else if src[i] == 'f' { 566 | kind = 'f' 567 | dst = append(dst, style.False[0]...) 568 | } else if src[i] == 'n' { 569 | kind = 'n' 570 | dst = append(dst, style.Null[0]...) 571 | } else { 572 | dst = apnd(dst, src[i]) 573 | } 574 | if kind != 0 { 575 | for ; i < len(src); i++ { 576 | if src[i] <= ' ' || src[i] == ',' || src[i] == ':' || src[i] == ']' || src[i] == '}' { 577 | i-- 578 | break 579 | } 580 | dst = apnd(dst, src[i]) 581 | } 582 | if kind == '0' { 583 | dst = append(dst, style.Number[1]...) 584 | } else if kind == 't' { 585 | dst = append(dst, style.True[1]...) 586 | } else if kind == 'f' { 587 | dst = append(dst, style.False[1]...) 588 | } else if kind == 'n' { 589 | dst = append(dst, style.Null[1]...) 590 | } 591 | } 592 | } 593 | } 594 | return dst 595 | } 596 | 597 | // Spec strips out comments and trailing commas and convert the input to a 598 | // valid JSON per the official spec: https://tools.ietf.org/html/rfc8259 599 | // 600 | // The resulting JSON will always be the same length as the input and it will 601 | // include all of the same line breaks at matching offsets. This is to ensure 602 | // the result can be later processed by a external parser and that that 603 | // parser will report messages or errors with the correct offsets. 604 | func Spec(src []byte) []byte { 605 | return spec(src, nil) 606 | } 607 | 608 | // SpecInPlace is the same as Spec, but this method reuses the input json 609 | // buffer to avoid allocations. Do not use the original bytes slice upon return. 610 | func SpecInPlace(src []byte) []byte { 611 | return spec(src, src) 612 | } 613 | 614 | func spec(src, dst []byte) []byte { 615 | dst = dst[:0] 616 | for i := 0; i < len(src); i++ { 617 | if src[i] == '/' { 618 | if i < len(src)-1 { 619 | if src[i+1] == '/' { 620 | dst = append(dst, ' ', ' ') 621 | i += 2 622 | for ; i < len(src); i++ { 623 | if src[i] == '\n' { 624 | dst = append(dst, '\n') 625 | break 626 | } else if src[i] == '\t' || src[i] == '\r' { 627 | dst = append(dst, src[i]) 628 | } else { 629 | dst = append(dst, ' ') 630 | } 631 | } 632 | continue 633 | } 634 | if src[i+1] == '*' { 635 | dst = append(dst, ' ', ' ') 636 | i += 2 637 | for ; i < len(src)-1; i++ { 638 | if src[i] == '*' && src[i+1] == '/' { 639 | dst = append(dst, ' ', ' ') 640 | i++ 641 | break 642 | } else if src[i] == '\n' || src[i] == '\t' || 643 | src[i] == '\r' { 644 | dst = append(dst, src[i]) 645 | } else { 646 | dst = append(dst, ' ') 647 | } 648 | } 649 | continue 650 | } 651 | } 652 | } 653 | dst = append(dst, src[i]) 654 | if src[i] == '"' { 655 | for i = i + 1; i < len(src); i++ { 656 | dst = append(dst, src[i]) 657 | if src[i] == '"' { 658 | j := i - 1 659 | for ; ; j-- { 660 | if src[j] != '\\' { 661 | break 662 | } 663 | } 664 | if (j-i)%2 != 0 { 665 | break 666 | } 667 | } 668 | } 669 | } else if src[i] == '}' || src[i] == ']' { 670 | for j := len(dst) - 2; j >= 0; j-- { 671 | if dst[j] <= ' ' { 672 | continue 673 | } 674 | if dst[j] == ',' { 675 | dst[j] = ' ' 676 | } 677 | break 678 | } 679 | } 680 | } 681 | return dst 682 | } 683 | -------------------------------------------------------------------------------- /pretty_test.go: -------------------------------------------------------------------------------- 1 | package pretty 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func j(js interface{}) string { 15 | var v interface{} 16 | if err := json.Unmarshal([]byte(fmt.Sprintf("%s", js)), &v); err != nil { 17 | fmt.Printf(">>%s<<\n", js) 18 | panic(err) 19 | } 20 | data, err := json.Marshal(v) 21 | if err != nil { 22 | panic(err) 23 | } 24 | return string(data) 25 | } 26 | 27 | var example1 = []byte(` 28 | { 29 | "name": { 30 | "last": "Sanders", 31 | "first": "Janet" 32 | }, 33 | "children": [ 34 | "Andy", "Carol", "Mike" 35 | ], 36 | "values": [ 37 | 10.10, true, false, null, "hello", {} 38 | ], 39 | "values2": {}, 40 | "values3": [], 41 | "deep": {"deep":{"deep":[1,2,3,4,5]}} 42 | } 43 | `) 44 | 45 | var example2 = `[ 0, 10, 10.10, true, false, null, "hello \" "]` 46 | 47 | func assertEqual(t *testing.T, a, b interface{}) { 48 | t.Helper() 49 | if !reflect.DeepEqual(a, b) { 50 | t.Fatalf("Not equal\n\t'%v'\n\t'%v'", a, b) 51 | } 52 | } 53 | 54 | func TestPretty(t *testing.T) { 55 | pretty := Pretty(Ugly(Pretty([]byte(example1)))) 56 | assertEqual(t, j(pretty), j(pretty)) 57 | assertEqual(t, j(example1), j(pretty)) 58 | pretty = Pretty(Ugly(Pretty([]byte(example2)))) 59 | assertEqual(t, j(pretty), j(pretty)) 60 | assertEqual(t, j(example2), j(pretty)) 61 | pretty = Pretty([]byte(" ")) 62 | assertEqual(t, "", string(pretty)) 63 | opts := *DefaultOptions 64 | opts.SortKeys = true 65 | pretty = PrettyOptions(Ugly(Pretty([]byte(example2))), &opts) 66 | assertEqual(t, j(pretty), j(pretty)) 67 | assertEqual(t, j(example2), j(pretty)) 68 | } 69 | 70 | func TestUgly(t *testing.T) { 71 | ugly := Ugly([]byte(example1)) 72 | var buf bytes.Buffer 73 | err := json.Compact(&buf, []byte(example1)) 74 | assertEqual(t, nil, err) 75 | assertEqual(t, buf.Bytes(), ugly) 76 | ugly = UglyInPlace(ugly) 77 | assertEqual(t, nil, err) 78 | assertEqual(t, buf.Bytes(), ugly) 79 | } 80 | 81 | func TestRandom(t *testing.T) { 82 | rand.Seed(time.Now().UnixNano()) 83 | for i := 0; i < 100000; i++ { 84 | b := make([]byte, 1024) 85 | rand.Read(b) 86 | Pretty(b) 87 | Ugly(b) 88 | } 89 | } 90 | func TestBig(t *testing.T) { 91 | json := `[ 92 | { 93 | "_id": "58d19e070f4898817162964a", 94 | "index": "", 95 | "guid": "65d46c3e-9d3a-4bfe-bab2-252f36a53c6b", 96 | "isActive": false, 97 | "balance": "$1,064.00", 98 | "picture": "http://placehold.it/32x32", 99 | "age": 37, 100 | "eyeColor": "brown", 101 | "name": "Chan Orr", 102 | "gender": "male", 103 | "company": "SURETECH", 104 | "email": "chanorr@suretech.com", 105 | "phone": "+1 (808) 496-3754", 106 | "address": "792 Bushwick Place, Glenbrook, Vermont, 9893", 107 | "about": "Amet consequat eu enim laboris cillum ad laboris in quis laboris reprehenderit. Eu deserunt occaecat dolore eu veniam non dolore et magna ex incididunt. Ea dolor laboris ex officia culpa laborum amet adipisicing laboris tempor magna elit mollit ad. Tempor ex aliqua mollit enim laboris sunt fugiat. Sint sunt ex est non dolore consectetur culpa ullamco id dolor nulla labore. Sunt duis fugiat cupidatat sunt deserunt qui aute elit consequat sint cupidatat. Consequat ullamco aliqua nulla velit tempor aute.\r\n", 108 | "registered": "2014-08-04T04:09:10 +07:00", 109 | "latitude": 80.707807, 110 | "longitude": 18.857548, 111 | "tags": [ 112 | "consectetur", 113 | "est", 114 | "cupidatat", 115 | "nisi", 116 | "incididunt", 117 | "aliqua", 118 | "ullamco" 119 | ], 120 | "friends": [ 121 | { 122 | "id": 0, 123 | "name": "Little Edwards" 124 | }, 125 | { 126 | "id": 1, 127 | "name": "Gay Johns" 128 | }, 129 | { 130 | "id": 2, 131 | "name": "Hoover Noble" 132 | } 133 | ], 134 | "greeting": "Hello, Chan Orr! You have 3 unread messages.", 135 | "favoriteFruit": "banana" 136 | }, 137 | { 138 | "_id": "58d19e07c2119248f8fa11ff", 139 | "index": "", 140 | "guid": "b362f0a0-d1ed-4b94-9d6b-213712620a20", 141 | "isActive": false, 142 | "balance": "$1,321.26", 143 | "picture": "http://placehold.it/32x32", 144 | "age": 28, 145 | "eyeColor": "blue", 146 | "name": "Molly Hyde", 147 | "gender": "female", 148 | "company": "QUALITEX", 149 | "email": "mollyhyde@qualitex.com", 150 | "phone": "+1 (849) 455-2934", 151 | "address": "440 Visitation Place, Bridgetown, Palau, 5053", 152 | "about": "Ipsum reprehenderit nulla est nostrud ad incididunt officia in commodo id esse id. Ullamco ullamco commodo mollit ut id cupidatat veniam nostrud minim duis qui sit. Occaecat esse nostrud velit qui non dolor proident. Ipsum ipsum anim non mollit minim voluptate amet irure in. Sunt commodo occaecat aute ullamco sunt fugiat laboris culpa Lorem anim. Aliquip tempor excepteur labore aute deserunt consectetur incididunt aute eu est ullamco consectetur excepteur. Sunt sint consequat cupidatat nisi exercitation minim enim occaecat esse ex amet ex non.\r\n", 153 | "registered": "2014-09-12T08:51:11 +07:00", 154 | "latitude": 15.867177, 155 | "longitude": 165.862595, 156 | "tags": [ 157 | "enim", 158 | "sint", 159 | "elit", 160 | "laborum", 161 | "elit", 162 | "cupidatat", 163 | "ipsum" 164 | ], 165 | "friends": [ 166 | { 167 | "id": 0, 168 | "name": "Holmes Hurley" 169 | }, 170 | { 171 | "id": 1, 172 | "name": "Rhoda Spencer" 173 | }, 174 | { 175 | "id": 2, 176 | "name": "Tommie Gallegos" 177 | } 178 | ], 179 | "greeting": "Hello, Molly Hyde! You have 10 unread messages.", 180 | "favoriteFruit": "banana" 181 | }, 182 | { 183 | "_id": "58d19e07fc27eedd9159d710", 184 | "index": "", 185 | "guid": "1d343fd3-44f7-4246-a5e6-a9297afb3146", 186 | "isActive": false, 187 | "balance": "$1,459.65", 188 | "picture": "http://placehold.it/32x32", 189 | "age": 26, 190 | "eyeColor": "brown", 191 | "name": "Jaime Kennedy", 192 | "gender": "female", 193 | "company": "RECRITUBE", 194 | "email": "jaimekennedy@recritube.com", 195 | "phone": "+1 (983) 483-3522", 196 | "address": "997 Vanderveer Street, Alamo, Marshall Islands, 4767", 197 | "about": "Qui consequat veniam ex enim excepteur aliqua dolor duis Lorem deserunt. Lorem occaecat laboris quis nisi Lorem aute exercitation consectetur officia velit aliqua aliquip commodo. Tempor irure ad ipsum aliquip. Incididunt mollit aute cillum non magna duis officia anim laboris deserunt voluptate.\r\n", 198 | "registered": "2015-08-31T06:51:25 +07:00", 199 | "latitude": -7.486839, 200 | "longitude": 57.659287, 201 | "tags": [ 202 | "veniam", 203 | "aliqua", 204 | "aute", 205 | "amet", 206 | "laborum", 207 | "quis", 208 | "sint" 209 | ], 210 | "friends": [ 211 | { 212 | "id": 0, 213 | "name": "Brown Christensen" 214 | }, 215 | { 216 | "id": 1, 217 | "name": "Robyn Whitehead" 218 | }, 219 | { 220 | "id": 2, 221 | "name": "Dolly Weaver" 222 | } 223 | ], 224 | "greeting": "Hello, Jaime Kennedy! You have 3 unread messages.", 225 | "favoriteFruit": "banana" 226 | }, 227 | { 228 | "_id": "58d19e0783c362da4b71240d", 229 | "index": "", 230 | "guid": "dbe60229-60d2-4879-82f3-d9aca0baaf6f", 231 | "isActive": false, 232 | "balance": "$3,221.63", 233 | "picture": "http://placehold.it/32x32", 234 | "age": 32, 235 | "eyeColor": "green", 236 | "name": "Cherie Vinson", 237 | "gender": "female", 238 | "company": "SLAX", 239 | "email": "cherievinson@slax.com", 240 | "phone": "+1 (905) 474-3132", 241 | "address": "563 Macdougal Street, Navarre, New York, 8733", 242 | "about": "Ad laborum et magna quis veniam duis magna consectetur mollit in minim non officia aliquip. Ullamco dolor qui consectetur adipisicing. Incididunt ad ad incididunt duis velit laboris. Reprehenderit ullamco magna quis exercitation excepteur nisi labore pariatur laborum consequat eu laboris amet velit. Et dolore aliqua proident sunt dolore incididunt dolore fugiat ipsum tempor occaecat.\r\n", 243 | "registered": "2015-03-19T08:48:47 +07:00", 244 | "latitude": -56.480034, 245 | "longitude": -59.894094, 246 | "tags": [ 247 | "irure", 248 | "commodo", 249 | "quis", 250 | "cillum", 251 | "quis", 252 | "nulla", 253 | "irure" 254 | ], 255 | "friends": [ 256 | { 257 | "id": 0, 258 | "name": "Danielle Mullins" 259 | }, 260 | { 261 | "id": 1, 262 | "name": "Maxine Peters" 263 | }, 264 | { 265 | "id": 2, 266 | "name": "Francine James" 267 | } 268 | ], 269 | "greeting": "Hello, Cherie Vinson! You have 1 unread messages.", 270 | "favoriteFruit": "apple" 271 | }, 272 | { 273 | "_id": "58d19e07b8f1ea8e3451870d", 274 | "index": "", 275 | "guid": "91fd9527-770c-4006-a0ed-64ca0d819199", 276 | "isActive": true, 277 | "balance": "$2,387.38", 278 | "picture": "http://placehold.it/32x32", 279 | "age": 37, 280 | "eyeColor": "blue", 281 | "name": "Glenna Hanson", 282 | "gender": "female", 283 | "company": "ACUMENTOR", 284 | "email": "glennahanson@acumentor.com", 285 | "phone": "+1 (965) 564-3926", 286 | "address": "323 Seigel Street, Rosedale, Florida, 2700", 287 | "about": "Commodo id ex velit nulla incididunt occaecat aliquip ullamco consequat est. Esse officia adipisicing magna et et incididunt sit deserunt ex mollit id. Laborum proident sit sit duis proident cillum irure aliquip et commodo.\r\n", 288 | "registered": "2014-06-29T02:48:04 +07:00", 289 | "latitude": -6.141759, 290 | "longitude": 155.991532, 291 | "tags": [ 292 | "amet", 293 | "pariatur", 294 | "culpa", 295 | "eu", 296 | "commodo", 297 | "magna", 298 | "excepteur" 299 | ], 300 | "friends": [ 301 | { 302 | "id": 0, 303 | "name": "Blanchard Blackburn" 304 | }, 305 | { 306 | "id": 1, 307 | "name": "Ayers Guy" 308 | }, 309 | { 310 | "id": 2, 311 | "name": "Powers Salinas" 312 | } 313 | ], 314 | "greeting": "Hello, Glenna Hanson! You have 4 unread messages.", 315 | "favoriteFruit": "strawberry" 316 | }, 317 | { 318 | "_id": "58d19e07f1ad063dac8b72dc", 319 | "index": "", 320 | "guid": "9b8c6cef-cfcd-4e6d-85e4-fe2e6920ec31", 321 | "isActive": true, 322 | "balance": "$1,828.58", 323 | "picture": "http://placehold.it/32x32", 324 | "age": 29, 325 | "eyeColor": "green", 326 | "name": "Hays Shields", 327 | "gender": "male", 328 | "company": "ISOLOGICA", 329 | "email": "haysshields@isologica.com", 330 | "phone": "+1 (882) 469-3201", 331 | "address": "574 Columbus Place, Singer, Georgia, 8716", 332 | "about": "Consectetur et adipisicing ad quis incididunt qui labore et ex elit esse. Ad elit officia ullamco dolor reprehenderit. Sunt nisi ullamco mollit incididunt consectetur nostrud anim adipisicing ullamco aliqua eiusmod ad. Et excepteur voluptate adipisicing velit id quis duis Lorem id deserunt esse irure Lorem. Est irure sint Lorem aliqua adipisicing velit irure Lorem. Ex in culpa laborum nostrud esse eu laboris velit. Anim excepteur ex ipsum amet nostrud cillum.\r\n", 333 | "registered": "2014-02-10T07:17:14 +07:00", 334 | "latitude": -66.354543, 335 | "longitude": 138.400461, 336 | "tags": [ 337 | "mollit", 338 | "labore", 339 | "id", 340 | "labore", 341 | "dolor", 342 | "in", 343 | "elit" 344 | ], 345 | "friends": [ 346 | { 347 | "id": 0, 348 | "name": "Mendoza Craig" 349 | }, 350 | { 351 | "id": 1, 352 | "name": "Rowena Carey" 353 | }, 354 | { 355 | "id": 2, 356 | "name": "Barry Francis" 357 | } 358 | ], 359 | "greeting": "Hello, Hays Shields! You have 10 unread messages.", 360 | "favoriteFruit": "strawberry" 361 | } 362 | ]` 363 | 364 | opts := *DefaultOptions 365 | opts.SortKeys = true 366 | jsonb := PrettyOptions(Ugly([]byte(json)), &opts) 367 | assertEqual(t, j(jsonb), j(json)) 368 | } 369 | 370 | func TestColor(t *testing.T) { 371 | res := Color(Pretty([]byte(` 372 | {"hello":"world","what":123, 373 | "arr":["1","2",1,2,true,false,null], 374 | "obj":{"key1":null,"ar`+"\x1B[36m"+`Cyanr2":[1,2,3,"123","456"]}} 375 | `)), nil) 376 | if string(res) != `{ 377 | "hello": "world", 378 | "what": 123, 379 | "arr": ["1", "2", 1, 2, true, false, null], 380 | "obj": { 381 | "key1": null, 382 | "ar\u001b[36mCyanr2": [1, 2, 3, "123", "456"] 383 | } 384 | } 385 | ` { 386 | t.Fatal("invalid output") 387 | } 388 | } 389 | 390 | func BenchmarkPretty(t *testing.B) { 391 | t.ReportAllocs() 392 | t.ResetTimer() 393 | for i := 0; i < t.N; i++ { 394 | Pretty(example1) 395 | } 396 | } 397 | 398 | func BenchmarkPrettySortKeys(t *testing.B) { 399 | opts := *DefaultOptions 400 | opts.SortKeys = true 401 | t.ReportAllocs() 402 | t.ResetTimer() 403 | for i := 0; i < t.N; i++ { 404 | PrettyOptions(example1, &opts) 405 | } 406 | } 407 | func BenchmarkUgly(t *testing.B) { 408 | t.ReportAllocs() 409 | t.ResetTimer() 410 | for i := 0; i < t.N; i++ { 411 | Ugly(example1) 412 | } 413 | } 414 | 415 | func BenchmarkUglyInPlace(t *testing.B) { 416 | example2 := []byte(string(example1)) 417 | t.ReportAllocs() 418 | t.ResetTimer() 419 | for i := 0; i < t.N; i++ { 420 | UglyInPlace(example2) 421 | } 422 | } 423 | 424 | var example3 = []byte(` 425 | { 426 | /* COMMENT 1 */ 427 | "name": { 428 | "last": "Sanders", // outer 1 429 | "first": "Janet", // outer 2 430 | }, 431 | // COMMENT 2 432 | "children": [ 433 | "Andy", "Carol", "Mike", // outer 3 434 | ], 435 | /* 436 | COMMENT 3 437 | */ 438 | "values": [ 439 | 10.10, true, false, null, "hello", {}, 440 | ], 441 | "values2": {}, 442 | "values3": [], 443 | "deep": {"deep":{"deep":[1,2,3,4,5,],}} 444 | } 445 | `) 446 | 447 | func BenchmarkSpec(t *testing.B) { 448 | t.ReportAllocs() 449 | t.ResetTimer() 450 | for i := 0; i < t.N; i++ { 451 | Ugly(example3) 452 | } 453 | } 454 | 455 | func BenchmarkSpecInPlace(t *testing.B) { 456 | example4 := []byte(string(example3)) 457 | t.ReportAllocs() 458 | t.ResetTimer() 459 | for i := 0; i < t.N; i++ { 460 | UglyInPlace(example4) 461 | } 462 | } 463 | 464 | func BenchmarkJSONIndent(t *testing.B) { 465 | var dst bytes.Buffer 466 | t.ReportAllocs() 467 | t.ResetTimer() 468 | for i := 0; i < t.N; i++ { 469 | json.Indent(&dst, example1, "", " ") 470 | } 471 | } 472 | 473 | func BenchmarkJSONCompact(t *testing.B) { 474 | var dst bytes.Buffer 475 | t.ReportAllocs() 476 | t.ResetTimer() 477 | for i := 0; i < t.N; i++ { 478 | json.Compact(&dst, example1) 479 | } 480 | } 481 | 482 | func TestPrettyNoSpaceAfterNewline(t *testing.T) { 483 | json := `[{"foo":1,"bar":2},{"foo":3,"bar":4}]` 484 | json = string(Pretty([]byte(json))) 485 | if strings.Index(json, " \n") != -1 { 486 | t.Fatal("found a space followed by a newline, which should not be allowed") 487 | } 488 | } 489 | 490 | func TestPrettyStableSort(t *testing.T) { 491 | json := `{"c":3,"b":3,"a":3,"c":2,"b":2,"a":2,"c":1,"b":1,"a":1}` 492 | opts := *DefaultOptions 493 | opts.SortKeys = true 494 | json = string(Ugly(PrettyOptions([]byte(json), &opts))) 495 | if json != `{"a":1,"a":2,"a":3,"b":1,"b":2,"b":3,"c":1,"c":2,"c":3}` { 496 | t.Fatal("out of order") 497 | } 498 | } 499 | 500 | func TestPrettyColor(t *testing.T) { 501 | json := `"abc\u0020def\nghi"` 502 | ret := string(Color([]byte(json), nil)) 503 | exp := "" + 504 | TerminalStyle.String[0] + `"abc` + TerminalStyle.String[1] + 505 | TerminalStyle.Escape[0] + `\u0020` + TerminalStyle.Escape[1] + 506 | TerminalStyle.String[0] + `def` + TerminalStyle.String[1] + 507 | TerminalStyle.Escape[0] + `\n` + TerminalStyle.Escape[1] + 508 | TerminalStyle.String[0] + `ghi"` + TerminalStyle.String[1] 509 | if ret != exp { 510 | t.Fatalf("expected '%s', got '%s'", exp, ret) 511 | } 512 | } 513 | 514 | func TestSpec(t *testing.T) { 515 | json := ` 516 | { // hello 517 | "c": 3,"b":3, // jello 518 | /* SOME 519 | LIKE 520 | IT 521 | HAUT */ 522 | "d": [ 1, /* 2 */ 3, 4, ], 523 | }` 524 | expect := ` 525 | { 526 | "c": 3,"b":3, 527 | 528 | 529 | 530 | 531 | "d": [ 1, 3, 4 ] 532 | }` 533 | out := string(Spec([]byte(json))) 534 | if out != expect { 535 | t.Fatalf("expected '%s', got '%s'", expect, out) 536 | } 537 | out = string(SpecInPlace([]byte(json))) 538 | if out != expect { 539 | t.Fatalf("expected '%s', got '%s'", expect, out) 540 | } 541 | } 542 | 543 | func TestStableSort10(t *testing.T) { 544 | expect := `{"key":"abc","key":"bbb","key":"rrr","key":"value","key3":3}` 545 | jsons := []string{ 546 | `{"key3":3,"key":"abc","key":"value","key":"rrr","key":"bbb"}`, 547 | `{"key":"abc","key":"bbb","key":"value","key3":3,"key":"rrr"}`, 548 | `{"key":"bbb","key":"value","key":"rrr","key3":3,"key":"abc"}`, 549 | `{"key3":3,"key":"abc","key":"bbb","key":"value","key":"rrr"}`, 550 | `{"key3":3,"key":"abc","key":"bbb","key":"value","key":"rrr"}`, 551 | } 552 | opts := *DefaultOptions 553 | opts.SortKeys = true 554 | for _, json := range jsons { 555 | json = string(Ugly(PrettyOptions([]byte(json), &opts))) 556 | if json != expect { 557 | t.Fatalf("expected '%s', got '%s'", expect, json) 558 | } 559 | } 560 | } 561 | 562 | func TestNaN(t *testing.T) { 563 | vals := []string{"NaN", "nan", "Nan", "nAn", "inf", "Inf", "-inf", "+Inf"} 564 | for _, val := range vals { 565 | json := `{"num":` + val + `}` 566 | res := string(Ugly(Pretty([]byte(json)))) 567 | if res != json { 568 | t.Fatalf("expected '%s', got '%s'", json, res) 569 | } 570 | } 571 | } 572 | --------------------------------------------------------------------------------