Created
December 8, 2020 20:32
-
-
Save kevinkoltz/df83f380f13ead40638fbb69b087897f to your computer and use it in GitHub Desktop.
EEx helper for inspecting Elixir terms in HTML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExDump do | |
@moduledoc """ | |
Helper for dumping variables in HTML for easy inspection. | |
""" | |
use Hapi.Constants.SharedConstants | |
use Phoenix.HTML | |
@doc """ | |
Dumps a term to html in a easy to read format. | |
""" | |
@spec dump(any(), keyword()) :: {:safe, String.t()} | |
def dump(term, opts \\ []) do | |
theme = Keyword.get(opts, :theme, :default) | |
{:safe, dump_html} = to_html(term) | |
[ | |
~s(<div id="dump-wrapper" class="-ex-dump">), | |
dump_html, | |
~s(</div>), | |
css(theme) | |
] | |
|> Enum.join("\n") | |
|> raw() | |
end | |
defp to_html(nil), do: dump_simple_term(:empty, "nil") | |
defp to_html(value) when is_binary(value), do: dump_simple_term(:binary, value) | |
defp to_html(value) when is_boolean(value), do: dump_simple_term(:boolean, value) | |
defp to_html(value) when is_integer(value), do: dump_simple_term(:integer, value) | |
defp to_html(value) when is_tuple(value), do: dump_simple_term(:tuple, inspect(value)) | |
defp to_html(%Decimal{} = value), do: dump_simple_term(:decimal, value) | |
defp to_html(%Date{} = value), do: dump_simple_term("Date", value) | |
defp to_html(%DateTime{} = value), do: dump_simple_term("Datetime", value) | |
defp to_html(value) when is_atom(value), do: dump_simple_term(:atom, inspect(value)) | |
defp to_html(%NaiveDateTime{} = value) do | |
# TODO: look this up from the server? | |
dump_simple_term("Naive Datetime (#{@local_timezone})", value) | |
end | |
defp to_html(%{__struct__: struct} = value) do | |
struct_name = inspect(struct) | |
value | |
|> Map.from_struct() | |
|> dump_complex_term("map", ~s[Struct: #{struct_name}]) | |
end | |
defp to_html(value) when is_map(value), do: dump_complex_term(value, "map", "Map") | |
defp to_html(values) when is_list(values) do | |
values | |
# TODO: reimplement these two steps as one operation | |
|> Enum.with_index() | |
|> Enum.map(fn {value, index} -> {index + 1, value} end) | |
|> dump_complex_term("list", "List") | |
end | |
# catch all | |
defp to_html(values), do: dump_simple_term("uknown", inspect(values)) | |
defp dump_simple_term(type, value) | |
when type in [:atom, :binary, :boolean, :decimal, :integer, :tuple] do | |
""" | |
<table> | |
<tbody> | |
<tr> | |
<td class="dump-data-type">#{type}</td> | |
<td class="dump-data-value">#{value}</td> | |
</tr> | |
</tbody> | |
</table> | |
""" | |
|> raw() | |
end | |
defp dump_simple_term(type, value) do | |
""" | |
<table> | |
<tbody> | |
<tr> | |
<td class="dump-data-type">#{type}</td> | |
</tr> | |
<tr> | |
<td class="dump-data-value">#{value}</td> | |
</tr> | |
</tbody> | |
</table> | |
""" | |
|> raw() | |
end | |
defp dump_complex_term(values, name, title) do | |
fingerprint = random_string() | |
rows = | |
values | |
|> Enum.map(fn {key, value} -> | |
{:safe, value_html} = to_html(value) | |
""" | |
<tr class="#{fingerprint}"> | |
<td class="dump-#{name}-header">#{key}</td> | |
<td class="dump-#{name}-value"> | |
#{value_html} | |
</td> | |
</tr> | |
""" | |
end) | |
|> Enum.join("\n") | |
toggle_javascript = | |
"var elements = document.getElementsByClassName('#{fingerprint}'); " <> | |
"for(var i = 0; i < elements.length; i++) { " <> | |
"elements[i].style.display = el.style.display == 'none' ? '' : 'none'; }" | |
""" | |
<table> | |
<tbody> | |
<tr> | |
<td class="dump-#{name}-header" onclick="#{toggle_javascript}" colspan="3" style="cursor:pointer;"> | |
<span>#{title}</span> | |
</td> | |
</tr> | |
#{rows} | |
</tbody> | |
</table> | |
""" | |
|> raw() | |
end | |
# TODO: update these colors to not be so offensive. | |
defp css(:default) do | |
""" | |
<style type="text/css"> | |
#dump-wrapper table { | |
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; | |
font-size: 11px; | |
empty-cells: show; | |
color: #000; | |
border-collapse: collapse; | |
} | |
#dump-wrapper td { | |
border: 1px solid #000; | |
vertical-align: top; | |
padding: 2px; | |
empty-cells: show; | |
} | |
#dump-wrapper td span { | |
font-weight: bold; | |
} | |
#dump-wrapper td.dump-map-value { | |
color: #333399; | |
border-color: #333399; | |
background-color: #ccf; | |
} | |
#dump-wrapper td.dump-map-header { | |
color: #333399; | |
border-color: #333399; | |
background-color: #99f; | |
} | |
#dump-wrapper td.dump-data-value { | |
color: #990000; | |
border-color: #990000; | |
background-color: #fc9; | |
} | |
#dump-wrapper td.dump-data-type { | |
color: #990000; | |
border-color: #990000; | |
background-color: #f60; | |
} | |
#dump-wrapper td.dump-list-value { | |
color: #336600; | |
border-color: #336600; | |
background-color: #cf3; | |
} | |
#dump-wrapper td.dump-list-header { | |
color: #336600; | |
border-color: #336600; | |
background-color: #9c3; | |
} | |
</style> | |
""" | |
end | |
defp random_string(length \\ 6) do | |
length |> :crypto.strong_rand_bytes() |> Base.url_encode64() |> binary_part(0, length) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment