Use inspect to highlight cell result (#18)

* Use inspect to highlight cell result

* Use invalid UTF-8 byte for color separation

* Match editor typography in cell output
This commit is contained in:
Jonatan Kłosko 2021-02-03 13:13:56 +01:00 committed by GitHub
parent 77b60c8110
commit a8b5227dd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 5 deletions

View file

@ -116,12 +116,59 @@ iframe[hidden] {
@apply mb-0; @apply mb-0;
} }
/* Elixir HTML-ized inspect result */
.elixir-inspect .atom {
color: #61afef;
}
.elixir-inspect .binary {
color: #5c6370;
}
.elixir-inspect .boolean {
color: #c678dd;
}
.elixir-inspect .list {
color: #5c6370;
}
.elixir-inspect .map {
color: #5c6370;
}
.elixir-inspect .nil {
color: #c678dd;
}
.elixir-inspect .number {
color: #d19a66;
}
.elixir-inspect .regex {
color: #e06c75;
}
.elixir-inspect .string {
color: #98c379;
}
.elixir-inspect .tuple {
color: #5c6370;
}
/* Other */ /* Other */
.bg-editor { .bg-editor {
background-color: #282c34; background-color: #282c34;
} }
.font-editor {
font-family: "Droid Sans Mono", monospace, monospace, "Droid Sans Fallback";
font-size: 14px;
}
.tiny-scrollbar::-webkit-scrollbar { .tiny-scrollbar::-webkit-scrollbar {
width: 0.4rem; width: 0.4rem;
height: 0.4rem; height: 0.4rem;

View file

@ -1,6 +1,8 @@
defmodule LiveBookWeb.Cell do defmodule LiveBookWeb.Cell do
use LiveBookWeb, :live_component use LiveBookWeb, :live_component
alias LiveBookWeb.Utils
def render(assigns) do def render(assigns) do
~L""" ~L"""
<div id="cell-<%= @cell.id %>" <div id="cell-<%= @cell.id %>"
@ -131,7 +133,7 @@ defmodule LiveBookWeb.Cell do
assigns = %{outputs: outputs} assigns = %{outputs: outputs}
~L""" ~L"""
<div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 text-sm"> <div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 font-editor">
<%= for output <- Enum.reverse(@outputs) do %> <%= for output <- Enum.reverse(@outputs) do %>
<div class="p-4"> <div class="p-4">
<div class="max-h-80 overflow-auto tiny-scrollbar"> <div class="max-h-80 overflow-auto tiny-scrollbar">
@ -152,11 +154,12 @@ defmodule LiveBookWeb.Cell do
end end
defp render_output({:ok, value}) do defp render_output({:ok, value}) do
inspected = inspect(value, pretty: true, width: 140) inspected = Utils.inspect_as_html(value, pretty: true, width: 140)
assigns = %{inspected: inspected} assigns = %{inspected: inspected}
~L""" ~L"""
<div class="whitespace-pre text-gray-500"><%= @inspected %></div> <div class="whitespace-pre text-gray-500 elixir-inspect"><%= @inspected %></div>
""" """
end end

View file

@ -0,0 +1,63 @@
defmodule LiveBookWeb.Utils do
@doc """
Wraps `inspect/2` to include HTML tags in the final string for syntax highlighting.
Any options given as the second argument are passed directly to `inspect/2`.
## Examples
iex(2)> LiveBookWeb.Utils.inspect_as_html(:test, [])
{:safe, "<span class=\\"atom\\">:test</span>"}
"""
@spec inspect_as_html(Inspect.t(), keyword()) :: Phoenix.HTML.safe()
def inspect_as_html(term, opts \\ []) do
# Inspect coloring primary tragets terminals,
# so the colors are usually ANSI escape codes
# and their effect is reverted by a special reset escape code.
#
# In our case we need HTML tags for syntax highlighting,
# so as the colors we use sequences like \xfeatom\xfe
# then we HTML-escape the string and finally replace
# these special sequences with actual <span> tags.
#
# Note that the surrounding \xfe byte is invalid in a UTF-8 sequence,
# so we can be certain it won't appear in the normal `inspect` result.
term
|> inspect(Keyword.merge(opts, syntax_colors: inspect_html_colors()))
|> Phoenix.HTML.html_escape()
|> Phoenix.HTML.safe_to_string()
|> replace_colors_with_tags()
|> Phoenix.HTML.raw()
end
defp inspect_html_colors() do
delim = "\xfe"
[
atom: delim <> "atom" <> delim,
binary: delim <> "binary" <> delim,
boolean: delim <> "boolean" <> delim,
list: delim <> "list" <> delim,
map: delim <> "map" <> delim,
number: delim <> "number" <> delim,
nil: delim <> "nil" <> delim,
regex: delim <> "regex" <> delim,
string: delim <> "string" <> delim,
tuple: delim <> "tuple" <> delim,
reset: delim <> "reset" <> delim
]
end
defp replace_colors_with_tags(string) do
colors = inspect_html_colors()
Enum.reduce(colors, string, fn
{:reset, color}, string ->
String.replace(string, color, "</span>")
{key, color}, string ->
String.replace(string, color, "<span class=\"#{Atom.to_string(key)}\">")
end)
end
end

View file

@ -92,7 +92,8 @@ defmodule LiveBookWeb.SessionLiveTest do
Session.get_data(session_id) Session.get_data(session_id)
end end
test "queueing cell evaluation updates the shared state", %{conn: conn, session_id: session_id} do test "queueing cell evaluation updates the shared state",
%{conn: conn, session_id: session_id} do
section_id = insert_section(session_id) section_id = insert_section(session_id)
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)") cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
@ -108,7 +109,6 @@ defmodule LiveBookWeb.SessionLiveTest do
test "queueing cell evaluation defaults to the focused cell if no cell id is given", test "queueing cell evaluation defaults to the focused cell if no cell id is given",
%{conn: conn, session_id: session_id} do %{conn: conn, session_id: session_id} do
section_id = insert_section(session_id) section_id = insert_section(session_id)
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)") cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")

View file

@ -0,0 +1,20 @@
defmodule LiveBookWeb.UtilsTest do
use ExUnit.Case, async: true
alias LiveBookWeb.Utils
doctest Utils
describe "inspect_as_html/2" do
test "uses span tags for term highlighting" do
assert {:safe,
~s{<span class="list">[</span><span class="number">1</span><span class="list">,</span> <span class="number">2</span><span class="list">]</span>}} ==
Utils.inspect_as_html([1, 2])
end
test "escapes HTML in the inspect result" do
assert {:safe, ~s{<span class="string">&quot;1 &lt; 2&quot;</span>}} ==
Utils.inspect_as_html("1 < 2")
end
end
end