formatter.ex (8189B)
1 defmodule Jason.Formatter do 2 @moduledoc ~S""" 3 Pretty-printing and minimizing functions for JSON-encoded data. 4 5 Input is required to be in an 8-bit-wide encoding such as UTF-8 or Latin-1 6 in `t:iodata/0` format. Input must have valid JSON, invalid JSON may produce 7 unexpected results or errors. 8 """ 9 10 @type opts :: [ 11 {:indent, iodata} 12 | {:line_separator, iodata} 13 | {:record_separator, iodata} 14 | {:after_colon, iodata} 15 ] 16 17 import Record 18 defrecordp :opts, [:indent, :line, :record, :colon] 19 20 @dialyzer :no_improper_lists 21 22 @doc ~S""" 23 Pretty-prints JSON-encoded `input`. 24 25 `input` may contain multiple JSON objects or arrays, optionally separated 26 by whitespace (e.g., one object per line). Objects in output will be 27 separated by newlines. No trailing newline is emitted. 28 29 ## Options 30 31 * `:indent` - used for nested objects and arrays (default: two spaces - `" "`); 32 * `:line_separator` - used in nested objects (default: `"\n"`); 33 * `:record_separator` - separates root-level objects and arrays 34 (default is the value for `:line_separator` option); 35 * `:after_colon` - printed after a colon inside objects (default: one space - `" "`). 36 37 ## Examples 38 39 iex> Jason.Formatter.pretty_print(~s|{"a":{"b": [1, 2]}}|) 40 ~s|{ 41 "a": { 42 "b": [ 43 1, 44 2 45 ] 46 } 47 }| 48 49 """ 50 @spec pretty_print(iodata, opts) :: binary 51 def pretty_print(input, opts \\ []) do 52 input 53 |> pretty_print_to_iodata(opts) 54 |> IO.iodata_to_binary() 55 end 56 57 @doc ~S""" 58 Pretty-prints JSON-encoded `input` and returns iodata. 59 60 This function should be preferred to `pretty_print/2`, if the pretty-printed 61 JSON will be handed over to one of the IO functions or sent 62 over the socket. The Erlang runtime is able to leverage vectorised 63 writes and avoid allocating a continuous buffer for the whole 64 resulting string, lowering memory use and increasing performance. 65 """ 66 @spec pretty_print_to_iodata(iodata, opts) :: iodata 67 def pretty_print_to_iodata(input, opts \\ []) do 68 opts = parse_opts(opts, " ", "\n", nil, " ") 69 70 depth = :first 71 empty = false 72 73 {output, _state} = pp_iodata(input, [], depth, empty, opts) 74 75 output 76 end 77 78 @doc ~S""" 79 Minimizes JSON-encoded `input`. 80 81 `input` may contain multiple JSON objects or arrays, optionally 82 separated by whitespace (e.g., one object per line). Minimized 83 output will contain one object per line. No trailing newline is emitted. 84 85 ## Options 86 87 * `:record_separator` - controls the string used as newline (default: `"\n"`). 88 89 ## Examples 90 91 iex> Jason.Formatter.minimize(~s|{ "a" : "b" , "c": \n\n 2}|) 92 ~s|{"a":"b","c":2}| 93 94 """ 95 @spec minimize(iodata, opts) :: binary 96 def minimize(input, opts \\ []) do 97 input 98 |> minimize_to_iodata(opts) 99 |> IO.iodata_to_binary() 100 end 101 102 @doc ~S""" 103 Minimizes JSON-encoded `input` and returns iodata. 104 105 This function should be preferred to `minimize/2`, if the minimized 106 JSON will be handed over to one of the IO functions or sent 107 over the socket. The Erlang runtime is able to leverage vectorised 108 writes and avoid allocating a continuous buffer for the whole 109 resulting string, lowering memory use and increasing performance. 110 """ 111 @spec minimize_to_iodata(iodata, opts) :: iodata 112 def minimize_to_iodata(input, opts) do 113 record = Keyword.get(opts, :record_separator, "\n") 114 opts = opts(indent: "", line: "", record: record, colon: "") 115 116 depth = :first 117 empty = false 118 119 {output, _state} = pp_iodata(input, [], depth, empty, opts) 120 121 output 122 end 123 124 defp parse_opts([{option, value} | opts], indent, line, record, colon) do 125 value = IO.iodata_to_binary(value) 126 case option do 127 :indent -> parse_opts(opts, value, line, record, colon) 128 :record_separator -> parse_opts(opts, indent, line, value, colon) 129 :after_colon -> parse_opts(opts, indent, line, record, value) 130 :line_separator -> parse_opts(opts, indent, value, record || value, colon) 131 end 132 end 133 134 defp parse_opts([], indent, line, record, colon) do 135 opts(indent: indent, line: line, record: record || line, colon: colon) 136 end 137 138 for depth <- 1..16 do 139 defp tab(" ", unquote(depth)), do: unquote(String.duplicate(" ", depth)) 140 end 141 142 defp tab("", _), do: "" 143 defp tab(indent, depth), do: List.duplicate(indent, depth) 144 145 defp pp_iodata(<<>>, output_acc, depth, empty, opts) do 146 {output_acc, &pp_iodata(&1, &2, depth, empty, opts)} 147 end 148 149 defp pp_iodata(<<byte, rest::binary>>, output_acc, depth, empty, opts) do 150 pp_byte(byte, rest, output_acc, depth, empty, opts) 151 end 152 153 defp pp_iodata([], output_acc, depth, empty, opts) do 154 {output_acc, &pp_iodata(&1, &2, depth, empty, opts)} 155 end 156 157 defp pp_iodata([byte | rest], output_acc, depth, empty, opts) when is_integer(byte) do 158 pp_byte(byte, rest, output_acc, depth, empty, opts) 159 end 160 161 defp pp_iodata([head | tail], output_acc, depth, empty, opts) do 162 {output_acc, cont} = pp_iodata(head, output_acc, depth, empty, opts) 163 cont.(tail, output_acc) 164 end 165 166 defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ' \n\r\t' do 167 pp_iodata(rest, output, depth, empty, opts) 168 end 169 170 defp pp_byte(byte, rest, output, depth, empty, opts) when byte in '{[' do 171 {out, depth} = 172 cond do 173 depth == :first -> {byte, 1} 174 depth == 0 -> {[opts(opts, :record), byte], 1} 175 empty -> {[opts(opts, :line), tab(opts(opts, :indent), depth), byte], depth + 1} 176 true -> {byte, depth + 1} 177 end 178 179 empty = true 180 pp_iodata(rest, [output, out], depth, empty, opts) 181 end 182 183 defp pp_byte(byte, rest, output, depth, true = _empty, opts) when byte in '}]' do 184 empty = false 185 depth = depth - 1 186 pp_iodata(rest, [output, byte], depth, empty, opts) 187 end 188 189 defp pp_byte(byte, rest, output, depth, false = empty, opts) when byte in '}]' do 190 depth = depth - 1 191 out = [opts(opts, :line), tab(opts(opts, :indent), depth), byte] 192 pp_iodata(rest, [output, out], depth, empty, opts) 193 end 194 195 defp pp_byte(byte, rest, output, depth, _empty, opts) when byte in ',' do 196 empty = false 197 out = [byte, opts(opts, :line), tab(opts(opts, :indent), depth)] 198 pp_iodata(rest, [output, out], depth, empty, opts) 199 end 200 201 defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ':' do 202 out = [byte, opts(opts, :colon)] 203 pp_iodata(rest, [output, out], depth, empty, opts) 204 end 205 206 defp pp_byte(byte, rest, output, depth, empty, opts) do 207 out = if empty, do: [opts(opts, :line), tab(opts(opts, :indent), depth), byte], else: byte 208 empty = false 209 210 if byte == ?" do 211 pp_string(rest, [output, out], _in_bs = false, &pp_iodata(&1, &2, depth, empty, opts)) 212 else 213 pp_iodata(rest, [output, out], depth, empty, opts) 214 end 215 end 216 217 defp pp_string(<<>>, output_acc, in_bs, cont) do 218 {output_acc, &pp_string(&1, &2, in_bs, cont)} 219 end 220 221 defp pp_string(binary, output_acc, true = _in_bs, cont) when is_binary(binary) do 222 <<byte, rest::binary>> = binary 223 pp_string(rest, [output_acc, byte], false, cont) 224 end 225 226 defp pp_string(binary, output_acc, false = _in_bs, cont) when is_binary(binary) do 227 case :binary.match(binary, ["\"", "\\"]) do 228 :nomatch -> 229 {[output_acc | binary], &pp_string(&1, &2, false, cont)} 230 {pos, 1} -> 231 {head, tail} = :erlang.split_binary(binary, pos + 1) 232 case :binary.at(binary, pos) do 233 ?\\ -> pp_string(tail, [output_acc | head], true, cont) 234 ?" -> cont.(tail, [output_acc | head]) 235 end 236 end 237 end 238 239 defp pp_string([], output_acc, in_bs, cont) do 240 {output_acc, &pp_string(&1, &2, in_bs, cont)} 241 end 242 243 defp pp_string([byte | rest], output_acc, in_bs, cont) when is_integer(byte) do 244 cond do 245 in_bs -> pp_string(rest, [output_acc, byte], false, cont) 246 byte == ?" -> cont.(rest, [output_acc, byte]) 247 true -> pp_string(rest, [output_acc, byte], byte == ?\\, cont) 248 end 249 end 250 251 defp pp_string([head | tail], output_acc, in_bs, cont) do 252 {output_acc, cont} = pp_string(head, output_acc, in_bs, cont) 253 cont.(tail, output_acc) 254 end 255 end