1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
// Copyright 2017, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.
// Package jsonfmt provides functionality for formatting JSON.
package jsonfmt
import "fmt"
var (
newlineBytes = []byte{'\n'}
spaceBytes = []byte{' '}
)
type (
jsonValue interface {
isValue() // Satisfied by (*jsonObject | *jsonArray | jsonString | jsonNumber | jsonLiteral | jsonInvalid)
}
jsonObject struct {
// '{'
preRecords jsonMeta // If non-empty, ends with jsonNewlines
records []jsonRecord
postRecords jsonMeta // Never contains jsonNewlines
// '}'
}
jsonRecord struct {
preKey jsonMeta // Never contains jsonNewlines
key jsonValue
postKey jsonMeta
// ':'
preVal jsonMeta
val jsonValue
postVal jsonMeta
// ','
postComma jsonMeta // If non-empty, ends with jsonNewlines
}
jsonArray struct {
// '['
preElems jsonMeta // If non-empty, ends with jsonNewlines
elems []jsonElement
postElems jsonMeta // Never contains jsonNewlines
// ']'
}
jsonElement struct {
preVal jsonMeta // Never contains jsonNewlines
val jsonValue
postVal jsonMeta
// ','
postComma jsonMeta // If non-empty, ends with jsonNewlines
}
jsonString []byte // Quoted string
jsonNumber []byte // Numeric value
jsonLiteral []byte // "true" | "false" | "null"
jsonMeta []interface {
isMeta() // Implemented by (jsonComment | jsonNewlines | jsonInvalid)
}
jsonComment []byte // Comment of either "//" or "/**/" form without trailing newlines
jsonNewlines int // Number of newlines
// When a parsing error occurs, then the remainder of the input is stored
// as jsonInvalid in the AST.
jsonInvalid []byte // May possibly be an empty string
)
func (*jsonObject) isValue() {}
func (*jsonArray) isValue() {}
func (jsonString) isValue() {}
func (jsonNumber) isValue() {}
func (jsonLiteral) isValue() {}
func (jsonInvalid) isValue() {}
func (jsonComment) isMeta() {}
func (jsonNewlines) isMeta() {}
func (jsonInvalid) isMeta() {}
// Option configures how to format JSON.
type Option interface {
option()
}
type (
minify struct{ Option }
standardize struct{ Option }
)
// TODO: Make these an user Option?
const defaultColumnLimit = 80
const defaultAlignLimit = 20
// Minify configures Format to produce the minimal representation of the input.
// If Format returns no error, then the output is guaranteed to be valid JSON,
func Minify() Option { return minify{} }
// Standardize configures Format to produce valid JSON according to ECMA-404.
// This strips any comments and trailing commas.
func Standardize() Option { return standardize{} }
// Format parses and formats the input JSON according to provided Options.
// If err is non-nil, then the output is a best effort at processing the input.
//
// This function accepts a superset of the JSON specification that allows
// comments and trailing commas after the last element in an object or array.
func Format(in []byte, opts ...Option) (out []byte, err error) {
// Process the provided options.
var st state
for _, opt := range opts {
switch opt.(type) {
case minify:
st.minify = true
st.standardize = true
case standardize:
st.standardize = true
default:
panic(fmt.Sprintf("unknown option: %#v", opt))
}
}
// Attempt to parse and format the JSON data.
err = st.parse(in)
if !st.minify {
expandAST(st.val, defaultColumnLimit)
}
out = st.format()
if !st.minify {
out = alignJSON(out, defaultAlignLimit)
}
return out, err
}
type state struct {
in []byte
preVal jsonMeta
val jsonValue
postVal jsonMeta
last interface{} // T where T = (*jsonValue | *jsonMeta)
out []byte
// Parsing and formatting options.
minify bool // If set, implies standardize is set too
standardize bool // If set, output will be ECMA-404 compliant
trailingComma bool // Set by parser if any trailing commas detected
hasNewlines bool // Set by formatter if any newlines are emitted
newlines []byte // Pending newlines to output
indents []byte // Indents to output per line
}
func (s *state) pushIndent() {
s.indents = append(s.indents, '\t')
}
func (s *state) popIndent() {
s.indents = s.indents[:len(s.indents)-1]
}
type jsonError struct {
line, column int
message string
}
func (e jsonError) Error() string {
if e.line > 0 && e.column > 0 {
return "jsonfmt: " + e.message
}
return fmt.Sprintf("jsonfmt: line %d, column %d: %v", e.line, e.column, e.message)
}
|