ElixirでJSONを整形+シンタックスハイライトしてみた

本記事は[Elixir Advent Calendar 2018](https://qiita.com/advent-calendar/2018/elixir)の22日目です。

Elixirを作って構文解析をやってみたかったので、簡単そうなJSONを解析して  
整形 + シンタックスハイライトして標準出力に表示するライブラリを作ってみました

# 作ったもの

https://github.com/tamanugi/ex_json_coloring
https://hex.pm/packages/ex_json_coloring

![スクリーンショット 2018-12-23 19.33.46.png](https://qiita-image-store.s3.amazonaws.com/0/55219/e4defb11-757b-3332-7064-3221734e8978.png)


# 実装
 
## 構文解析の実装

構文解析では文字列のJSONを以下のようなトークンに変換することに決め、
実装を進めていきました。

```exs
# {"key": "value"}
[
  %ExJsonColoring.Token{type: :brace, value: "{"},
  %ExJsonColoring.Token{type: :key_string, value: "key"},
  %ExJsonColoring.Token{type: :colon, value: ":"},
  %ExJsonColoring.Token{type: :string, value: "value"},
  %ExJsonColoring.Token{type: :brace, value: "}"},
]
```

実装の際には[JSONのBNF](http://json.org/)に従って、進めていきました。

```elixir

 # array
  defp value("[" <> rest, acc, state_stack) do
    token = %Token{type: :square_bracket, value: "["}
    value(rest, [token | acc], [:array | state_stack])
  end

  defp value("]" <> rest, acc, [:array | tail ]) do
    token = %Token{type: :square_bracket, value: "]"}
    value(rest, [token | acc], tail)
  end

  defp value("," <> rest, acc, [:array | _] = state_stack) do
    token = %Token{type: :comma, value: ","}
    value(rest, [token | acc], state_stack)
  end

  # object
  defp value("{" <> rest, acc, state_stack) do
    token = %Token{type: :brace, value: "{"}
    key_string(rest, [token | acc], [:object | state_stack])
  end

  defp value(":" <> rest, acc, state_stack) do
    token = %Token{type: :colon, value: ":"}
    value(rest, [token | acc], state_stack)
  end

  defp value("}" <> rest, acc, [:object | tail]) do
    token = %Token{type: :brace, value: "}"}
    value(rest, [token | acc], tail)
  end

  defp value("," <> rest, acc, [:object | _] = state_stack) do
    token = %Token{type: :comma, value: ","}
    key_string(rest, [token | acc], state_stack)
  end
  
  defp key_string(rest, acc, state_stack) do
    {rest, str_val} = string_start(rest |> skip_ws)
    token = %Token{type: :key_string, value: str_val}

    value(rest, [token | acc], state_stack)
  end

  # string
  defp value(ws, acc, _) when ws in '\s\n\t\r', do: {"", acc}

  defp value(~s(") <> _ = string, acc, state_stack) do
    {rest, str_val} = string_start(string)
    token = %Token{type: :string, value: str_val}
    value(rest, [token | acc], state_stack)
  end

  defp string_start(~s(") <> rest) do
    string(rest, "")
  end

  defp string(~s(") <> rest, acc), do: {rest, acc}
  defp string(<<char>> <> rest, acc) do
    string(rest, acc <> <<char>>)
  end

  # number
  defp value(<<char>> <> rest, acc, state_stack) when char in '-0123456789'  do
    {rest_, number_val} = number(rest, [char])
    token = %Token{type: :number, value: number_val |> List.to_string}
    value(rest_, [token | acc], state_stack)
  end

  defp number(<<char>> <> rest, acc) when char in '0123456789.' do
    number(rest, acc ++ [char])
  end
  defp number(rest, acc), do: {rest, acc}

  # boolean

  defp value("true" <> rest, acc, state_stack) do
    token = %Token{type: :boolean, value: "true"}
    value(rest, [token | acc], state_stack)
  end

  defp value("false" <> rest, acc, state_stack) do
    token = %Token{type: :boolean, value: "false"}
    value(rest, [token | acc], state_stack)
  end

  # null

  defp value("null" <> rest, acc, state_stack) do
    token = %Token{type: :null, value: "null"}
    value(rest, [token | acc], state_stack)
  end

  # ws

  defp value(<<char>> <> rest, acc, state_stack) when char in '\s\n\t\r' do
    value(rest, acc, state_stack)
  end

  defp skip_ws(<<char>> <> rest) when char in  '\s\n\t\r' do
    skip_ws(rest)
  end

  defp skip_ws(string) do
    string
  end
```

最初の一文字目を使ったパターンマッチングやガード節などはElixirではおなじみのJSON Parserライブラリの[Poison](https://github.com/devinus/poison)を参考にさせてもらいました。

Doctestを使って1stepずつテストしながら実装を行っていくことで、スムーズに実装を進めていくことができました :sparkles: 

少し悩んだところがBNFではオブジェクトのキーも普通の文字列なのですが、
ハイライトするにあたってはオブジェクトのキーとしての文字列なのか、値としての文字列なのか
区別する必要があるところでした。

現在の解析している対象(オブジェクトor配列)をスタックとして持っておき、
オブジェクト解析中に`,`の後ろにある文字列をオブジェクトキーとするようにしました。

# 出力(整形, ハイライト)の実装

出力とフォーマットは簡単で、構文解析器にかけたトークンのリストを一つずつ見ていき、
色とインデント, 改行するしないを設定していくだけです。

Elixirでは標準出力の色付けに `IO.ANSI` モジュールが使えますが、
色の指定が面倒そうだったので[Bunt](https://github.com/rrrene/bunt) というライブラリを使うことにしました

```elixir
  defp format(:brace, value, indent_lv) do
    case value do
      "{" ->
        {[color(:brace), "{", "\n", indent(indent_lv + 1)], indent_lv + 1}
      "}" ->
        {["\n", indent(indent_lv - 1), color(:brace), "}"], indent_lv - 1}
    end
  end

  defp format(:square_bracket, value, indent_lv) do
    case value do
      "[" ->
        {[color(:square_bracket), "[", "\n", indent(indent_lv + 1)], indent_lv + 1}
      "]" ->
        {["\n", indent(indent_lv - 1), color(:square_bracket), "]"], indent_lv - 1}
    end
  end  

  defp format(:comma, ",", indent_lv) do
    {[color(:comma), ",", "\n", indent(indent_lv)], indent_lv}
  end

  defp format(:colon, ":", indent_lv) do
    {[":", " "], indent_lv}
  end

  defp format(type, value, indent_lv) when type in [:string, :key_string] do
    {[color(type), "\"#{value}\""], indent_lv}
  end

  defp format(type, value, indent_lv) do
    {[color(type), value], indent_lv}
  end

  defp indent(0), do: ""
  defp indent(indent_lv) do
    for _ <- 1..(indent_lv * 2), do: " "
  end

  defp color(:string), do: :green 
  defp color(:key_string), do: :fuchsia
  defp color(:boolean), do: :blue
  defp color(:number), do: :lightyellow
  defp color(:null), do: :lightgray
  defp color(_), do: Bunt.ANSI.reset
```

# 感想とか

昔応用情報を勉強したときには BNF のありがたみなどが全然わからなかったのですが、
構文解析器を実装するにあたって、BNFに従ってひとつずつ実装していくだけでよかったので
すごい楽でした。

エスケープ文字の実装などを行っておらず、またコードもあまりDRYにできていないので、
そのへんをきちんとして普通に使えるライブラリのレベルにできたらなーと思っています。
(テストとドキュメントも書かないとだめですね :sweat: )

またこのライブラリを使って `jq` のようなCLIツールをElixirで書きたいと考えていたりします。


もしご指摘などありましたら、じゃんじゃんもらえると嬉しいです！