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 instaed 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)
    map = 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}, ' ')
      end
      |> Enum.join("")
    end
    |> Enum.join("\n")
    |> IO.puts
  end

end