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;
}
/* 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 */
.bg-editor {
background-color: #282c34;
}
.font-editor {
font-family: "Droid Sans Mono", monospace, monospace, "Droid Sans Fallback";
font-size: 14px;
}
.tiny-scrollbar::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;

View file

@ -1,6 +1,8 @@
defmodule LiveBookWeb.Cell do
use LiveBookWeb, :live_component
alias LiveBookWeb.Utils
def render(assigns) do
~L"""
<div id="cell-<%= @cell.id %>"
@ -131,7 +133,7 @@ defmodule LiveBookWeb.Cell do
assigns = %{outputs: outputs}
~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 %>
<div class="p-4">
<div class="max-h-80 overflow-auto tiny-scrollbar">
@ -152,11 +154,12 @@ defmodule LiveBookWeb.Cell do
end
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}
~L"""
<div class="whitespace-pre text-gray-500"><%= @inspected %></div>
<div class="whitespace-pre text-gray-500 elixir-inspect"><%= @inspected %></div>
"""
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)
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)
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",
%{conn: conn, session_id: session_id} do
section_id = insert_section(session_id)
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