defmodule Jason.Formatter do @moduledoc ~S""" Pretty-printing and minimizing functions for JSON-encoded data. Input is required to be in an 8-bit-wide encoding such as UTF-8 or Latin-1 in `t:iodata/0` format. Input must have valid JSON, invalid JSON may produce unexpected results or errors. """ @type opts :: [ {:indent, iodata} | {:line_separator, iodata} | {:record_separator, iodata} | {:after_colon, iodata} ] import Record defrecordp :opts, [:indent, :line, :record, :colon] @dialyzer :no_improper_lists @doc ~S""" Pretty-prints JSON-encoded `input`. `input` may contain multiple JSON objects or arrays, optionally separated by whitespace (e.g., one object per line). Objects in output will be separated by newlines. No trailing newline is emitted. ## Options * `:indent` - used for nested objects and arrays (default: two spaces - `" "`); * `:line_separator` - used in nested objects (default: `"\n"`); * `:record_separator` - separates root-level objects and arrays (default is the value for `:line_separator` option); * `:after_colon` - printed after a colon inside objects (default: one space - `" "`). ## Examples iex> Jason.Formatter.pretty_print(~s|{"a":{"b": [1, 2]}}|) ~s|{ "a": { "b": [ 1, 2 ] } }| """ @spec pretty_print(iodata, opts) :: binary def pretty_print(input, opts \\ []) do input |> pretty_print_to_iodata(opts) |> IO.iodata_to_binary() end @doc ~S""" Pretty-prints JSON-encoded `input` and returns iodata. This function should be preferred to `pretty_print/2`, if the pretty-printed JSON will be handed over to one of the IO functions or sent over the socket. The Erlang runtime is able to leverage vectorised writes and avoid allocating a continuous buffer for the whole resulting string, lowering memory use and increasing performance. """ @spec pretty_print_to_iodata(iodata, opts) :: iodata def pretty_print_to_iodata(input, opts \\ []) do opts = parse_opts(opts, " ", "\n", nil, " ") depth = :first empty = false {output, _state} = pp_iodata(input, [], depth, empty, opts) output end @doc ~S""" Minimizes JSON-encoded `input`. `input` may contain multiple JSON objects or arrays, optionally separated by whitespace (e.g., one object per line). Minimized output will contain one object per line. No trailing newline is emitted. ## Options * `:record_separator` - controls the string used as newline (default: `"\n"`). ## Examples iex> Jason.Formatter.minimize(~s|{ "a" : "b" , "c": \n\n 2}|) ~s|{"a":"b","c":2}| """ @spec minimize(iodata, opts) :: binary def minimize(input, opts \\ []) do input |> minimize_to_iodata(opts) |> IO.iodata_to_binary() end @doc ~S""" Minimizes JSON-encoded `input` and returns iodata. This function should be preferred to `minimize/2`, if the minimized JSON will be handed over to one of the IO functions or sent over the socket. The Erlang runtime is able to leverage vectorised writes and avoid allocating a continuous buffer for the whole resulting string, lowering memory use and increasing performance. """ @spec minimize_to_iodata(iodata, opts) :: iodata def minimize_to_iodata(input, opts) do record = Keyword.get(opts, :record_separator, "\n") opts = opts(indent: "", line: "", record: record, colon: "") depth = :first empty = false {output, _state} = pp_iodata(input, [], depth, empty, opts) output end defp parse_opts([{option, value} | opts], indent, line, record, colon) do value = IO.iodata_to_binary(value) case option do :indent -> parse_opts(opts, value, line, record, colon) :record_separator -> parse_opts(opts, indent, line, value, colon) :after_colon -> parse_opts(opts, indent, line, record, value) :line_separator -> parse_opts(opts, indent, value, record || value, colon) end end defp parse_opts([], indent, line, record, colon) do opts(indent: indent, line: line, record: record || line, colon: colon) end for depth <- 1..16 do defp tab(" ", unquote(depth)), do: unquote(String.duplicate(" ", depth)) end defp tab("", _), do: "" defp tab(indent, depth), do: List.duplicate(indent, depth) defp pp_iodata(<<>>, output_acc, depth, empty, opts) do {output_acc, &pp_iodata(&1, &2, depth, empty, opts)} end defp pp_iodata(<>, output_acc, depth, empty, opts) do pp_byte(byte, rest, output_acc, depth, empty, opts) end defp pp_iodata([], output_acc, depth, empty, opts) do {output_acc, &pp_iodata(&1, &2, depth, empty, opts)} end defp pp_iodata([byte | rest], output_acc, depth, empty, opts) when is_integer(byte) do pp_byte(byte, rest, output_acc, depth, empty, opts) end defp pp_iodata([head | tail], output_acc, depth, empty, opts) do {output_acc, cont} = pp_iodata(head, output_acc, depth, empty, opts) cont.(tail, output_acc) end defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ' \n\r\t' do pp_iodata(rest, output, depth, empty, opts) end defp pp_byte(byte, rest, output, depth, empty, opts) when byte in '{[' do {out, depth} = cond do depth == :first -> {byte, 1} depth == 0 -> {[opts(opts, :record), byte], 1} empty -> {[opts(opts, :line), tab(opts(opts, :indent), depth), byte], depth + 1} true -> {byte, depth + 1} end empty = true pp_iodata(rest, [output, out], depth, empty, opts) end defp pp_byte(byte, rest, output, depth, true = _empty, opts) when byte in '}]' do empty = false depth = depth - 1 pp_iodata(rest, [output, byte], depth, empty, opts) end defp pp_byte(byte, rest, output, depth, false = empty, opts) when byte in '}]' do depth = depth - 1 out = [opts(opts, :line), tab(opts(opts, :indent), depth), byte] pp_iodata(rest, [output, out], depth, empty, opts) end defp pp_byte(byte, rest, output, depth, _empty, opts) when byte in ',' do empty = false out = [byte, opts(opts, :line), tab(opts(opts, :indent), depth)] pp_iodata(rest, [output, out], depth, empty, opts) end defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ':' do out = [byte, opts(opts, :colon)] pp_iodata(rest, [output, out], depth, empty, opts) end defp pp_byte(byte, rest, output, depth, empty, opts) do out = if empty, do: [opts(opts, :line), tab(opts(opts, :indent), depth), byte], else: byte empty = false if byte == ?" do pp_string(rest, [output, out], _in_bs = false, &pp_iodata(&1, &2, depth, empty, opts)) else pp_iodata(rest, [output, out], depth, empty, opts) end end defp pp_string(<<>>, output_acc, in_bs, cont) do {output_acc, &pp_string(&1, &2, in_bs, cont)} end defp pp_string(binary, output_acc, true = _in_bs, cont) when is_binary(binary) do <> = binary pp_string(rest, [output_acc, byte], false, cont) end defp pp_string(binary, output_acc, false = _in_bs, cont) when is_binary(binary) do case :binary.match(binary, ["\"", "\\"]) do :nomatch -> {[output_acc | binary], &pp_string(&1, &2, false, cont)} {pos, 1} -> {head, tail} = :erlang.split_binary(binary, pos + 1) case :binary.at(binary, pos) do ?\\ -> pp_string(tail, [output_acc | head], true, cont) ?" -> cont.(tail, [output_acc | head]) end end end defp pp_string([], output_acc, in_bs, cont) do {output_acc, &pp_string(&1, &2, in_bs, cont)} end defp pp_string([byte | rest], output_acc, in_bs, cont) when is_integer(byte) do cond do in_bs -> pp_string(rest, [output_acc, byte], false, cont) byte == ?" -> cont.(rest, [output_acc, byte]) true -> pp_string(rest, [output_acc, byte], byte == ?\\, cont) end end defp pp_string([head | tail], output_acc, in_bs, cont) do {output_acc, cont} = pp_string(head, output_acc, in_bs, cont) cont.(tail, output_acc) end end