mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-28 18:50:48 +08:00
Extend ANSI codes support (#67)
* Extend ANSI escape codes support * Add tests * Apply suggestions
This commit is contained in:
parent
90a7b599df
commit
1996dfada9
5 changed files with 321 additions and 189 deletions
|
@ -1,70 +1,25 @@
|
||||||
/*
|
/*
|
||||||
Classes for HTML-ized ANSI string.
|
Variables for HTML-ized ANSI string.
|
||||||
|
|
||||||
Many colors are taken from the One Dark theme
|
Many colors are taken from the One Dark theme
|
||||||
to be consistent with the editor.
|
to be consistent with the editor.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.ansi.black {
|
:root {
|
||||||
@apply text-black;
|
--ansi-color-black: black;
|
||||||
}
|
--ansi-color-red: #e06c75;
|
||||||
|
--ansi-color-green: #98c379;
|
||||||
.ansi.red {
|
--ansi-color-yellow: #e5c07b;
|
||||||
color: #e06c75;
|
--ansi-color-blue: #61afef;
|
||||||
}
|
--ansi-color-magenta: #c678dd;
|
||||||
|
--ansi-color-cyan: #56b6c2;
|
||||||
.ansi.green {
|
--ansi-color-white: white;
|
||||||
color: #98c379;
|
--ansi-color-light-black: #5c6370;
|
||||||
}
|
--ansi-color-light-red: #f87171;
|
||||||
|
--ansi-color-light-green: #34d399;
|
||||||
.ansi.yellow {
|
--ansi-color-light-yellow: #fde68a;
|
||||||
color: #e5c07b;
|
--ansi-color-light-blue: #93c5fd;
|
||||||
}
|
--ansi-color-light-magenta: #F472b6;
|
||||||
|
--ansi-color-light-cyan: #6be3f2;
|
||||||
.ansi.blue {
|
--ansi-color-light-white: white;
|
||||||
color: #61afef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.magenta {
|
|
||||||
color: #c678dd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.cyan {
|
|
||||||
color: #56b6c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.white {
|
|
||||||
@apply text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-black {
|
|
||||||
color: #5c6370;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-red {
|
|
||||||
@apply text-red-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-green {
|
|
||||||
@apply text-green-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-yellow {
|
|
||||||
@apply text-yellow-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-blue {
|
|
||||||
@apply text-blue-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-magenta {
|
|
||||||
@apply text-pink-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-cyan {
|
|
||||||
color: #6be3f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ansi.light-white {
|
|
||||||
@apply text-white;
|
|
||||||
}
|
}
|
||||||
|
|
230
lib/live_book_web/ansi.ex
Normal file
230
lib/live_book_web/ansi.ex
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
defmodule LiveBook.ANSI.Modifier do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
defmacro defmodifier(modifier, code, terminator \\ "m") do
|
||||||
|
quote bind_quoted: [modifier: modifier, code: code, terminator: terminator] do
|
||||||
|
defp ansi_prefix_to_modifier(unquote("#{code}#{terminator}") <> rest) do
|
||||||
|
{:ok, unquote(modifier), rest}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule LiveBookWeb.ANSI do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
import LiveBook.ANSI.Modifier
|
||||||
|
|
||||||
|
# modifier ::
|
||||||
|
# :reset
|
||||||
|
# | {:font_weight, :bold | :light | :reset}
|
||||||
|
# | {:font_style, :italic | :reset}
|
||||||
|
# | {:text_decoration, :underline | :line_through | :overline | :reset}
|
||||||
|
# | {:foreground_color, color() | :reset}
|
||||||
|
# | {:background_color, color() | :reset}
|
||||||
|
# | :ignored
|
||||||
|
|
||||||
|
# color :: atom() | {:grayscale24, 0..23} | {:rgb6, 0..5, 0..5, 0..5}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Takes a string with ANSI escape codes and build a HTML safe string
|
||||||
|
with `span` tags having classes corresponding to the escape codes.
|
||||||
|
|
||||||
|
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 | ansi_prefixed_strings] = String.split(string, "\e[")
|
||||||
|
|
||||||
|
{:safe, head_html} = Phoenix.HTML.html_escape(head)
|
||||||
|
|
||||||
|
# Each pair has the form of {modifiers, html_content}
|
||||||
|
{pairs, _} =
|
||||||
|
Enum.map_reduce(ansi_prefixed_strings, %{}, fn string, modifiers ->
|
||||||
|
{modifiers, rest} =
|
||||||
|
case ansi_prefix_to_modifier(string) do
|
||||||
|
{:ok, modifier, rest} ->
|
||||||
|
modifiers = add_modifier(modifiers, modifier)
|
||||||
|
{modifiers, rest}
|
||||||
|
|
||||||
|
{:error, _rest} ->
|
||||||
|
{modifiers, "\e[" <> string}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:safe, content} = Phoenix.HTML.html_escape(rest)
|
||||||
|
{{modifiers, content}, modifiers}
|
||||||
|
end)
|
||||||
|
|
||||||
|
pairs = Enum.filter(pairs, fn {_modifiers, content} -> content not in ["", []] end)
|
||||||
|
|
||||||
|
tail_html = pairs_to_html(pairs)
|
||||||
|
|
||||||
|
Phoenix.HTML.raw([head_html, tail_html])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Below goes a number of `ansi_prefix_to_modifier` function definitions,
|
||||||
|
# that take a string like "32msomething" (starting with ANSI code without the leading "\e[")
|
||||||
|
# and parse the prefix into the corresponding modifier.
|
||||||
|
# The function returns either {:ok, modifier, rest} or {:error, rest}
|
||||||
|
|
||||||
|
defmodifier(:reset, 0)
|
||||||
|
|
||||||
|
@colors [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
||||||
|
|
||||||
|
for {color, index} <- Enum.with_index(@colors) do
|
||||||
|
defmodifier({:foreground_color, color}, 30 + index)
|
||||||
|
defmodifier({:background_color, color}, 40 + index)
|
||||||
|
defmodifier({:foreground_color, :"light_#{color}"}, 90 + index)
|
||||||
|
defmodifier({:background_color, :"light_#{color}"}, 100 + index)
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodifier({:foreground_color, :reset}, 39)
|
||||||
|
defmodifier({:background_color, :reset}, 49)
|
||||||
|
|
||||||
|
defmodifier({:font_weight, :bold}, 1)
|
||||||
|
defmodifier({:font_weight, :light}, 2)
|
||||||
|
defmodifier({:font_style, :italic}, 3)
|
||||||
|
defmodifier({:text_decoration, :underline}, 4)
|
||||||
|
defmodifier({:text_decoration, :line_through}, 9)
|
||||||
|
defmodifier({:font_weight, :reset}, 22)
|
||||||
|
defmodifier({:font_style, :reset}, 23)
|
||||||
|
defmodifier({:text_decoration, :reset}, 24)
|
||||||
|
defmodifier({:text_decoration, :overline}, 53)
|
||||||
|
defmodifier({:text_decoration, :reset}, 55)
|
||||||
|
|
||||||
|
defp ansi_prefix_to_modifier("38;5;" <> string) do
|
||||||
|
with {:ok, color, rest} <- bit8_prefix_to_color(string) do
|
||||||
|
{:ok, {:foreground_color, color}, rest}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ansi_prefix_to_modifier("48;5;" <> string) do
|
||||||
|
with {:ok, color, rest} <- bit8_prefix_to_color(string) do
|
||||||
|
{:ok, {:background_color, color}, rest}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp bit8_prefix_to_color(string) do
|
||||||
|
case Integer.parse(string) do
|
||||||
|
{n, "m" <> rest} when n in 0..255 ->
|
||||||
|
color = color_from_code(n)
|
||||||
|
{:ok, color, rest}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, string}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ignored_codes = [5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 25, 27, 51, 52, 54]
|
||||||
|
|
||||||
|
for code <- ignored_codes do
|
||||||
|
defmodifier(:ignored, code)
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodifier(:ignored, 1, "A")
|
||||||
|
defmodifier(:ignored, 1, "B")
|
||||||
|
defmodifier(:ignored, 1, "C")
|
||||||
|
defmodifier(:ignored, 1, "D")
|
||||||
|
defmodifier(:ignored, 2, "J")
|
||||||
|
defmodifier(:ignored, 2, "K")
|
||||||
|
defmodifier(:ignored, "", "H")
|
||||||
|
|
||||||
|
defp ansi_prefix_to_modifier(string), do: {:error, string}
|
||||||
|
|
||||||
|
defp color_from_code(code) when code in 0..7 do
|
||||||
|
Enum.at(@colors, code)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp color_from_code(code) when code in 8..15 do
|
||||||
|
color = Enum.at(@colors, code - 8)
|
||||||
|
:"light_#{color}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp color_from_code(code) when code in 16..231 do
|
||||||
|
rgb_code = code - 16
|
||||||
|
b = rgb_code |> rem(6)
|
||||||
|
g = rgb_code |> div(6) |> rem(6)
|
||||||
|
r = rgb_code |> div(36)
|
||||||
|
|
||||||
|
{:rgb6, r, g, b}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp color_from_code(code) when code in 232..255 do
|
||||||
|
level = code - 232
|
||||||
|
{:grayscale24, level}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_modifier(modifiers, :ignored), do: modifiers
|
||||||
|
defp add_modifier(_modifiers, :reset), do: %{}
|
||||||
|
defp add_modifier(modifiers, {key, :reset}), do: Map.delete(modifiers, key)
|
||||||
|
defp add_modifier(modifiers, {key, value}), do: Map.put(modifiers, key, value)
|
||||||
|
|
||||||
|
# Converts a list of {modifiers, html_content} pairs
|
||||||
|
# into HTML with appropriate styling.
|
||||||
|
defp pairs_to_html(pairs, iodata \\ [])
|
||||||
|
|
||||||
|
defp pairs_to_html([], iodata), do: iodata
|
||||||
|
|
||||||
|
defp pairs_to_html([{modifiers, content1}, {modifiers, content2} | pairs], iodata) do
|
||||||
|
# Merge content with the same modifiers, so we don't produce unnecessary tags
|
||||||
|
pairs_to_html([{modifiers, [content1, content2]} | pairs], iodata)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pairs_to_html([{modifiers, content} | pairs], iodata) when modifiers == %{} do
|
||||||
|
pairs_to_html(pairs, [iodata, content])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pairs_to_html([{modifiers, content} | pairs], iodata) do
|
||||||
|
style = modifiers_to_css(modifiers)
|
||||||
|
pairs_to_html(pairs, [iodata, ~s{<span style="#{style}">}, content, ~s{</span>}])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp modifiers_to_css(modifiers) do
|
||||||
|
modifiers
|
||||||
|
|> Enum.map(&modifier_to_css/1)
|
||||||
|
|> Enum.join()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp modifier_to_css({:font_weight, :bold}), do: "font-weight: 600;"
|
||||||
|
defp modifier_to_css({:font_weight, :light}), do: "font-weight: 200;"
|
||||||
|
|
||||||
|
defp modifier_to_css({:font_style, :italic}), do: "font-style: italic;"
|
||||||
|
|
||||||
|
defp modifier_to_css({:text_decoration, :underline}), do: "text-decoration: underline;"
|
||||||
|
defp modifier_to_css({:text_decoration, :line_through}), do: "text-decoration: line-through;"
|
||||||
|
defp modifier_to_css({:text_decoration, :overline}), do: "text-decoration: overline;"
|
||||||
|
|
||||||
|
defp modifier_to_css({:foreground_color, color}), do: "color: #{color_to_css(color)};"
|
||||||
|
|
||||||
|
defp modifier_to_css({:background_color, color}),
|
||||||
|
do: "background-color: #{color_to_css(color)};"
|
||||||
|
|
||||||
|
defp color_to_css(:black), do: "var(--ansi-color-black)"
|
||||||
|
defp color_to_css(:light_black), do: "var(--ansi-color-light-black)"
|
||||||
|
defp color_to_css(:red), do: "var(--ansi-color-red)"
|
||||||
|
defp color_to_css(:light_red), do: "var(--ansi-color-light-red)"
|
||||||
|
defp color_to_css(:green), do: "var(--ansi-color-green)"
|
||||||
|
defp color_to_css(:light_green), do: "var(--ansi-color-light-green)"
|
||||||
|
defp color_to_css(:yellow), do: "var(--ansi-color-yellow)"
|
||||||
|
defp color_to_css(:light_yellow), do: "var(--ansi-color-light-yellow)"
|
||||||
|
defp color_to_css(:blue), do: "var(--ansi-color-blue)"
|
||||||
|
defp color_to_css(:light_blue), do: "var(--ansi-color-light-blue)"
|
||||||
|
defp color_to_css(:magenta), do: "var(--ansi-color-magenta)"
|
||||||
|
defp color_to_css(:light_magenta), do: "var(--ansi-color-light-magenta)"
|
||||||
|
defp color_to_css(:cyan), do: "var(--ansi-color-cyan)"
|
||||||
|
defp color_to_css(:light_cyan), do: "var(--ansi-color-light-cyan)"
|
||||||
|
defp color_to_css(:white), do: "var(--ansi-color-white)"
|
||||||
|
defp color_to_css(:light_white), do: "var(--ansi-color-light-white)"
|
||||||
|
|
||||||
|
defp color_to_css({:rgb6, r, g, b}) do
|
||||||
|
r = div(255 * r, 5)
|
||||||
|
g = div(255 * g, 5)
|
||||||
|
b = div(255 * b, 5)
|
||||||
|
"rgb(#{r}, #{g}, #{b})"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp color_to_css({:grayscale24, level}) do
|
||||||
|
value = div(255 * level, 23)
|
||||||
|
"rgb(#{value}, #{value}, #{value})"
|
||||||
|
end
|
||||||
|
end
|
|
@ -30,113 +30,5 @@ defmodule LiveBookWeb.Helpers do
|
||||||
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
|
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
|
||||||
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
|
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
|
||||||
|
|
||||||
@doc """
|
defdelegate ansi_string_to_html(string), to: LiveBookWeb.ANSI
|
||||||
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{<span class="ansi #{class}">}, content, ~s{</span>}]
|
|
||||||
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
|
end
|
||||||
|
|
72
test/live_book_web/ansi_test.exs
Normal file
72
test/live_book_web/ansi_test.exs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
defmodule LiveBookWeb.ANSITest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias LiveBookWeb.ANSI
|
||||||
|
|
||||||
|
describe "ansi_string_to_html/1" do
|
||||||
|
test "converts ANSI escape codes to span tags" do
|
||||||
|
assert ~s{<span style="color: var(--ansi-color-blue);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[34mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
|
||||||
|
assert ~s{<span style="background-color: var(--ansi-color-blue);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[44mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
|
||||||
|
assert ~s{<span style="font-weight: 600;">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[1mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
|
||||||
|
assert ~s{<span style="text-decoration: underline;">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[4mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "supports multiple escape codes at the same time" do
|
||||||
|
assert ~s{<span style="background-color: var(--ansi-color-red);color: var(--ansi-color-blue);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[34m\e[41mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "overriding a particular style property keeps the others" do
|
||||||
|
assert ~s{<span style="background-color: var(--ansi-color-red);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[34m\e[41m\e[39mcat\e[0m")
|
||||||
|
|> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adjacent content with the same properties is wrapped in a single element" do
|
||||||
|
assert ~s{<span style="color: var(--ansi-color-blue);">coolcats</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[34mcool\e[0m\e[34mcats\e[0m")
|
||||||
|
|> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "modifiers have effect until reset" do
|
||||||
|
assert ~s{<span style="color: var(--ansi-color-blue);">cool</span><span style="color: var(--ansi-color-blue);text-decoration: underline;">cats</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[34mcool\e[4mcats\e[0m")
|
||||||
|
|> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "supports 8-bit rgb colors" do
|
||||||
|
assert ~s{<span style="color: rgb(51, 102, 153);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[38;5;67mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "supports 8-bit grayscale range" do
|
||||||
|
assert ~s{<span style="color: rgb(88, 88, 88);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[38;5;240mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "supports 8-bit well known colors" do
|
||||||
|
assert ~s{<span style="color: var(--ansi-color-red);">cat</span>} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[38;5;1mcat\e[0m") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignores valid but irrelevant escape codes" do
|
||||||
|
assert ~s{cat} == ANSI.ansi_string_to_html("\e[H\e[1Acat") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the whole string if on ANSI code detected" do
|
||||||
|
assert ~s{\e[300mcat} ==
|
||||||
|
ANSI.ansi_string_to_html("\e[300mcat") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "escapes HTML in the resulting string" do
|
||||||
|
assert ~s{<div>} == ANSI.ansi_string_to_html("<div>") |> Phoenix.HTML.safe_to_string()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,17 +0,0 @@
|
||||||
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{<span class="ansi blue">:cat</span>} ==
|
|
||||||
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("<div>") |> Phoenix.HTML.safe_to_string()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in a new issue