256 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			256 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
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(<<byte, rest::binary>>, 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 ~c' \n\r\t' do
 | 
						|
    pp_iodata(rest, output, depth, empty, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ~c'{[' 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 ~c'}]' 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 ~c'}]' 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 ~c',' 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 ~c':' 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
 | 
						|
    <<byte, rest::binary>> = 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
 |