>, skip, original, acc, len) do
- part = binary_part(original, skip, len)
- to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)])
- end
- end
-
- defp to_iodata(<<_char, rest::bits>>, skip, original, acc, len) do
- to_iodata(rest, skip, original, acc, len + 1)
- end
-
- defp to_iodata(<<>>, 0, original, _acc, _len) do
- original
- end
-
- defp to_iodata(<<>>, skip, original, acc, len) do
- [acc | binary_part(original, skip, len)]
- end
end
diff --git a/lib/live_book/runtime/standalone.ex b/lib/live_book/runtime/standalone.ex
index 5f437c083..faaa56b6a 100644
--- a/lib/live_book/runtime/standalone.ex
+++ b/lib/live_book/runtime/standalone.ex
@@ -58,8 +58,9 @@ defmodule LiveBook.Runtime.Standalone do
eval,
# Minimize shedulers busy wait threshold,
# so that they go to sleep immediately after evaluation.
+ # Enable ANSI escape codes as we handle them with HTML.
"--erl",
- "+sbwt none +sbwtdcpu none +sbwtdio none"
+ "+sbwt none +sbwtdcpu none +sbwtdio none -elixir ansi_enabled true"
]
])
diff --git a/lib/live_book_web/helpers.ex b/lib/live_book_web/helpers.ex
index b5bd53d97..d26243bad 100644
--- a/lib/live_book_web/helpers.ex
+++ b/lib/live_book_web/helpers.ex
@@ -29,4 +29,114 @@ defmodule LiveBookWeb.Helpers do
defp linux?(user_agent), do: String.match?(user_agent, ~r/Linux/)
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
+
+ @doc """
+ Takes a string with ANSI escape codes and build a HTML safe string
+ with `span` tags having classes corresponding to the escape codes.
+
+ Note that currently only one escape at a time is supported.
+ Any HTML in the string is escaped.
+ """
+ @spec ansi_string_to_html(String.t()) :: Phoenix.HTML.safe()
+ def ansi_string_to_html(string) do
+ [head | tail] = String.split(string, "\e[")
+
+ {:safe, head_html} = Phoenix.HTML.html_escape(head)
+
+ tail_html =
+ Enum.map(tail, fn string ->
+ {class, rest} = ansi_prefix_to_class(string)
+ {:safe, content} = Phoenix.HTML.html_escape(rest)
+
+ if class do
+ [~s{}, content, ~s{}]
+ else
+ content
+ end
+ end)
+
+ Phoenix.HTML.raw([head_html, tail_html])
+ end
+
+ @ansi_code_with_class [
+ {"0m", nil},
+ {"1A", "cursor-up"},
+ {"1B", "cursor-down"},
+ {"1C", "cursor-right"},
+ {"1D", "cursor-left"},
+ {"1m", "bright"},
+ {"2J", "clear"},
+ {"2K", "clear-line"},
+ {"2m", "faint"},
+ {"3m", "italic"},
+ {"4m", "underline"},
+ {"5m", "blink-slow"},
+ {"6m", "blink-rapid"},
+ {"7m", "inverse"},
+ {"8m", "conceal"},
+ {"9m", "crossed-out"},
+ {"10m", "primary-font"},
+ {"11m", "font-1"},
+ {"12m", "font-2"},
+ {"13m", "font-3"},
+ {"14m", "font-4"},
+ {"15m", "font-5"},
+ {"16m", "font-6"},
+ {"17m", "font-7"},
+ {"18m", "font-8"},
+ {"19m", "font-9"},
+ {"22m", "normal"},
+ {"23m", "not-italic"},
+ {"24m", "no-underline"},
+ {"25m", "blink-off"},
+ {"27m", "inverse-off"},
+ {"30m", "black"},
+ {"31m", "red"},
+ {"32m", "green"},
+ {"33m", "yellow"},
+ {"34m", "blue"},
+ {"35m", "magenta"},
+ {"36m", "cyan"},
+ {"37m", "white"},
+ {"39m", "default-color"},
+ {"40m", "black-background"},
+ {"41m", "red-background"},
+ {"42m", "green-background"},
+ {"43m", "yellow-background"},
+ {"44m", "blue-background"},
+ {"45m", "magenta-background"},
+ {"46m", "cyan-background"},
+ {"47m", "white-background"},
+ {"49m", "default-background"},
+ {"51m", "framed"},
+ {"52m", "encircled"},
+ {"53m", "overlined"},
+ {"54m", "not-framed-encircled"},
+ {"55m", "not-overlined"},
+ {"90m", "light-black"},
+ {"91m", "light-red"},
+ {"92m", "light-green"},
+ {"93m", "light-yellow"},
+ {"94m", "light-blue"},
+ {"95m", "light-magenta"},
+ {"96m", "light-cyan"},
+ {"97m", "light-white"},
+ {"100m", "light-black-background"},
+ {"101m", "light-red-background"},
+ {"102m", "light-green-background"},
+ {"103m", "light-yellow-background"},
+ {"104m", "light-blue-background"},
+ {"105m", "light-magenta-background"},
+ {"106m", "light-cyan-background"},
+ {"107m", "light-white-background"},
+ {"H", "home"}
+ ]
+
+ for {code, class} <- @ansi_code_with_class do
+ defp ansi_prefix_to_class(unquote(code) <> rest) do
+ {unquote(class), rest}
+ end
+ end
+
+ defp ansi_prefix_to_class(string), do: {nil, string}
end
diff --git a/lib/live_book_web/live/cell_component.ex b/lib/live_book_web/live/cell_component.ex
index 737279e94..3d12817d9 100644
--- a/lib/live_book_web/live/cell_component.ex
+++ b/lib/live_book_web/live/cell_component.ex
@@ -156,18 +156,20 @@ defmodule LiveBookWeb.CellComponent do
end
defp render_output(output) when is_binary(output) do
- assigns = %{output: output}
+ output_html = ansi_string_to_html(output)
+ assigns = %{output_html: output_html}
~L"""
- <%= @output %>
+ <%= @output_html %>
"""
end
- defp render_output({:inspect_html, inspected_html}) do
+ defp render_output({:inspect, inspected}) do
+ inspected_html = ansi_string_to_html(inspected)
assigns = %{inspected_html: inspected_html}
~L"""
- <%= raw @inspected_html %>
+ <%= @inspected_html %>
"""
end
diff --git a/test/live_book/evaluator/string_formatter_test.exs b/test/live_book/evaluator/string_formatter_test.exs
deleted file mode 100644
index f41d31a5e..000000000
--- a/test/live_book/evaluator/string_formatter_test.exs
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule LiveBook.Evaluator.StringFormatterTest do
- use ExUnit.Case, async: true
-
- alias LiveBook.Evaluator.StringFormatter
-
- doctest StringFormatter
-
- describe "inspect_as_html/2" do
- test "uses span tags for term highlighting" do
- assert ~s{[1, 2]} ==
- StringFormatter.inspect_as_html([1, 2])
- end
-
- test "escapes HTML in the inspect result" do
- assert ~s{"1 < 2"} ==
- StringFormatter.inspect_as_html("1 < 2")
- end
- end
-end
diff --git a/test/live_book_web/helpers_test.exs b/test/live_book_web/helpers_test.exs
new file mode 100644
index 000000000..d90dcbb89
--- /dev/null
+++ b/test/live_book_web/helpers_test.exs
@@ -0,0 +1,17 @@
+defmodule LiveBookWeb.HelpersTest do
+ use ExUnit.Case, async: true
+
+ alias LiveBookWeb.Helpers
+
+ describe "ansi_string_to_html/1" do
+ test "converts ANSI escape codes to span tags" do
+ assert ~s{:cat} ==
+ Helpers.ansi_string_to_html("\e[34m:cat\e[0m") |> Phoenix.HTML.safe_to_string()
+ end
+
+ test "escapes HTML in the inspect result" do
+ assert ~s{<div>} ==
+ Helpers.ansi_string_to_html("") |> Phoenix.HTML.safe_to_string()
+ end
+ end
+end