lib/railroad.ex

defmodule Xpeg.Railroad do
  def new(w \\ 0, y0 \\ 0, y1 \\ 0) do
    %{w: w, y0: y0, y1: y1, kids: [], syms: []}
  end

  def poke(n, s, x \\ 0, y \\ 0) when is_binary(s) do
    syms =
      s
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce(n.syms, fn {c, i}, syms -> [%{x: x + i, y: y, c: c} | syms] end)

    %{n | syms: syms, w: max(n.w, String.length(s) + x), y0: min(n.y0, y), y1: max(n.y1, y)}
  end

  def add(n, nc, dx \\ 0, dy \\ 0) do
    kids = [%{dx: dx, dy: dy, n: nc} | n.kids]

    %{
      n
      | kids: kids,
        w: max(n.w, nc.w + dx),
        y0: min(n.y0, nc.y0 + dy),
        y1: max(n.y1, nc.y1 + dy)
    }
  end

  def pad(n, l \\ 1, r \\ 1) do
    new(n.w, 0, 0)
    |> poke(String.duplicate("─", l), 0, 0)
    |> add(n, l, 0)
    |> poke(String.duplicate("─", r), n.w + l, 0)
  end

  defp vlines(n, y, y1, x0, x1) do
    if y <= y1 do
      n |> poke("│", x0, y) |> poke("│", x1, y) |> vlines(y + 1, y1, x0, x1)
    else
      n
    end
  end

  defp set_to_string(v) do
    case v do
      cs when is_list(cs) -> Enum.map(cs, &set_to_string/1) |> Enum.join(",")
      {:.., _, [a, b]} -> set_to_string(a) <> ".." <> set_to_string(b)
      v when is_integer(v) -> inspect(List.to_string([v]))
      _ -> inspect(v)
    end
  end

  defp mk_opt(nc) do
    ntop = new() |> pad(nc.w + 2, 0)

    new()
    |> add(pad(nc, 2, 2))
    |> add(ntop, 1, nc.y0 - 1)
    |> vlines(nc.y0, -1, 0, nc.w + 3)
    |> poke("╭", 0, nc.y0 - 1)
    |> poke("╮", nc.w + 3, nc.y0 - 1)
    |> poke("┴", 0, 0)
    |> poke("┴", nc.w + 3, 0)
    |> poke("»", div(nc.w, 2) + 2, nc.y0 - 1)
  end

  defp mk_plus(nc) do
    nbot = new() |> pad(nc.w + 2, 0)

    new()
    |> poke("┬", 0, 0)
    |> poke("┬", nc.w + 3, 0)
    |> add(pad(nc, 2, 2))
    |> add(nbot, 1, nc.y1 + 1)
    |> vlines(1, nc.y1, 0, nc.w + 3)
    |> poke("╰", 0, nc.y1 + 1)
    |> poke("╯", nc.w + 3, nc.y1 + 1)
    |> poke("«", div(nc.w, 2) + 2, nc.y1 + 1)
  end

  defp mk_concat(n1, n2) do
    n1 = pad(n1, 0, 1)
    n2 = pad(n2, 1, 0)
    n1 |> add(n2, n1.w + 1) |> poke("»", n1.w, 0)
  end

  defp mk_repeat(nc, i, lo, hi) do
    cond do
      i < lo -> mk_concat(nc, mk_repeat(nc, i + 1, lo, hi))
      i < hi -> mk_concat(nc, mk_opt(mk_repeat(nc, i + 1, lo, hi)))
      true -> nc
    end
  end

  defp add_to_choice(ps, p) do
    case p do
      {:|, _, [p1, p2]} -> ps |> add_to_choice(p1) |> add_to_choice(p2)
      _ -> [p | ps]
    end
  end

  defp mk_choice(p1, p2) do
    ns = [] |> add_to_choice(p1) |> add_to_choice(p2) |> Enum.reverse() |> Enum.map(&parse/1)
    last = Enum.count(ns) - 1
    wmax = (Enum.map(ns, & &1.w) |> Enum.max()) + 2

    {n, _y} =
      Enum.reduce(Enum.with_index(ns), {new(), 0}, fn {nc, i}, {n, y} ->
        case i do
          0 ->
            n =
              n
              |> add(pad(nc, 1, wmax - nc.w), 1, y)
              |> vlines(y + 1, y + nc.y1, 0, wmax + 2)
              |> poke("┬", 0, y)
              |> poke("┬", wmax + 2, y)

            {n, y + nc.y1 + 1}

          ^last ->
            n =
              n
              |> add(pad(nc, 1, wmax - nc.w), 1, y - nc.y0)
              |> vlines(y, y - nc.y0 - 1, 0, wmax + 2)
              |> poke("╰", 0, y - nc.y0)
              |> poke("╯", wmax + 2, y - nc.y0)

            {n, y + nc.y1 - nc.y0 + 1}

          _ ->
            n =
              n
              |> add(pad(nc, 1, wmax - nc.w), 1, y - nc.y0)
              |> vlines(y, y - nc.y0 - 1, 0, wmax + 2)
              |> vlines(y - nc.y0 + 1, y - nc.y0 + nc.y1, 0, wmax + 2)
              |> poke("├", 0, y - nc.y0)
              |> poke("┤", wmax + 2, y - nc.y0)

            {n, y + nc.y1 - nc.y0 + 1}
        end
      end)

    n
  end

  def parse(v) do
    case v do
      # Parse a grammar consisting of a list of named rules
      {:__block__, _, ps} ->
        n = new()

        {n, _} =
          Enum.reduce(ps, {n, 0}, fn pc, {n, y} ->
            nc = parse(pc)
            n = n |> add(nc, 0, y - nc.y0)
            y = y + nc.y1 - nc.y0 + 2
            {n, y}
          end)

        n

      # Parse one named rule
      {:<-, _, [name, patt]} ->
        name = to_string(Xpeg.unalias(name))
        nc_name = new() |> poke(name <> " o──")
        nc_patt = parse(patt)
        nc_end = new() |> poke("──o")
        nc_name |> add(nc_patt, nc_name.w) |> add(nc_end, nc_name.w + nc_patt.w)

      # infix: '*' Concatenation
      {:*, _, [p1, p2]} ->
        parse(p1) |> mk_concat(parse(p2))

      # infix '|': Ordered choice
      {:|, _, [p1, p2]} ->
        mk_choice(p1, p2)

      # prefix '+': one-or-more operator
      {:+, _, [p]} ->
        mk_plus(parse(p))

      # Infix '-': difference
      {:-, _, [p1, p2]} ->
        mk_concat(
          new() |> poke("!") |> add(parse(p2), 1, 0),
          parse(p1)
        )

      # prefix '*': zero-or-more operator
      {:star, _, [p]} ->
        mk_opt(mk_plus(parse(p)))

      # prefix 'opt': zero-or-one operator
      {:opt, _, [p]} ->
        mk_opt(parse(p))

      # prefix '!', '@', '&': operator
      {op, _, p} when op in [:!, :@, :&] ->
        new() |> poke(to_string(op)) |> add(parse(p), 1, 0)

      # Charset
      {:{}, _, val} ->
        new() |> poke("{#{set_to_string(val)}}")

      # Repetition count [low..hi]
      {{:., _, [Access, :get]}, _, [p, {:.., _, [lo, hi]}]} ->
        mk_repeat(parse(p), 1, lo, hi)

      # Repetition count [n]
      {{:., _, [Access, :get]}, _, [p, lo]} ->
        mk_repeat(parse(p), 1, lo, lo)

      # Capture
      {cap, _, [p]} when cap in [:str, :int, :float] ->
        parse(p)

      # Code block
      {:fn, _, _code} ->
        new() |> poke("fn()")

      # Aliased atoms, for Capital names instead of :colon names
      {:__aliases__, _, [id]} ->
        new() |> poke("[#{id}]")

      # Delegate two-tuple :{} set to the above parse function
      {p1, p2} ->
        parse({:{}, 0, [p1, p2]})

      # String literal
      v when is_binary(v) ->
        new() |> poke(inspect(v))

      # Number, :any operator with non-zero count
      v when is_number(v) ->
        new() |> poke(to_string(v))

      {what, _, _val} ->
        new()
        |> poke(inspect({what}))

      e ->
        new()
        |> poke(inspect(e))
    end
  end

  defp render(map, n, dx, dy) do
    map =
      Enum.reduce(n.kids, map, fn kid, map ->
        render(map, kid.n, kid.dx + dx, kid.dy + dy)
      end)

    Enum.reduce(n.syms, map, fn sym, map ->
      Map.put(map, {sym.x + dx, sym.y + dy}, sym.c)
    end)
  end

  def draw(v) do
    n = parse(v)
    map = render(%{}, n, 0, 0)
    {xlo, xhi} = Enum.map(Map.keys(map), &elem(&1, 0)) |> Enum.min_max()
    {ylo, yhi} = Enum.map(Map.keys(map), &elem(&1, 1)) |> Enum.min_max()

    for y <- ylo..yhi do
      for x <- xlo..xhi do
        Map.get(map, {x, y}, ~c" ")
      end
      |> Enum.join("")
    end
    |> Enum.join("\n")
    |> IO.puts()
  end
end