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