pretty.go

  1package pretty
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"sort"
  7	"strconv"
  8)
  9
 10// Options is Pretty options
 11type 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.
 27var 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.
 31func Pretty(json []byte) []byte { return PrettyOptions(json, nil) }
 32
 33// PrettyOptions is like Pretty but with customized options.
 34func 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.
 53func 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.
 62func UglyInPlace(json []byte) []byte { return ugly(json, json) }
 63
 64func 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
 90func 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
 98func 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
128type pair struct {
129	kstart, kend int
130	vstart, vend int
131}
132
133type byKeyVal struct {
134	sorted bool
135	json   []byte
136	buf    []byte
137	pairs  []pair
138}
139
140func (arr *byKeyVal) Len() int {
141	return len(arr.pairs)
142}
143func (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}
152func (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
157type byKind int
158
159const (
160	byKey byKind = 0
161	byVal byKind = 1
162)
163
164type jtype int
165
166const (
167	jnull jtype = iota
168	jfalse
169	jnumber
170	jstring
171	jtrue
172	jjson
173)
174
175func 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
195func (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
234func 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
248func 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}
348func 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
370func 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
393func 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
404func 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
421type 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
429func 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
439var TerminalStyle *Style
440
441func 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.
465func 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.
604func 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.
610func SpecInPlace(src []byte) []byte {
611	return spec(src, src)
612}
613
614func 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}