mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-27 09:19:02 +08:00
Respect CR in cell output (#137)
* Respect CR in cell output * Update test/livebook_web/helpers_test.exs Co-authored-by: José Valim <jose.valim@dashbit.co> * Improve rewind implementation Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
d93b5d8450
commit
7d1d1f4d98
5 changed files with 85 additions and 31 deletions
|
@ -37,12 +37,16 @@ defmodule LivebookWeb.ANSI do
|
|||
* `:renderer` - a function used to render styled HTML content.
|
||||
The function receives HTML styles string and HTML-escaped content (iodata).
|
||||
By default the renderer wraps the whole content in a single `<span>` tag with the given style.
|
||||
Note that the style may be an empty string for plain text.
|
||||
"""
|
||||
@spec ansi_string_to_html(String.t(), keyword()) :: Phoenix.HTML.safe()
|
||||
def ansi_string_to_html(string, opts \\ []) do
|
||||
renderer = Keyword.get(opts, :renderer, &default_renderer/2)
|
||||
|
||||
[head | ansi_prefixed_strings] = String.split(string, "\e[")
|
||||
|
||||
{:safe, head_html} = Phoenix.HTML.html_escape(head)
|
||||
head_html = renderer.("", head_html)
|
||||
|
||||
# Each pair has the form of {modifiers, html_content}
|
||||
{pairs, _} =
|
||||
|
@ -63,7 +67,6 @@ defmodule LivebookWeb.ANSI do
|
|||
|
||||
pairs = Enum.filter(pairs, fn {_modifiers, content} -> content not in ["", []] end)
|
||||
|
||||
renderer = Keyword.get(opts, :renderer, &default_renderer/2)
|
||||
tail_html = pairs_to_html(pairs, renderer)
|
||||
|
||||
Phoenix.HTML.raw([head_html, tail_html])
|
||||
|
@ -177,10 +180,6 @@ defmodule LivebookWeb.ANSI do
|
|||
pairs_to_html([{modifiers, [content1, content2]} | pairs], iodata, renderer)
|
||||
end
|
||||
|
||||
defp pairs_to_html([{modifiers, content} | pairs], iodata, renderer) when modifiers == %{} do
|
||||
pairs_to_html(pairs, [iodata, content], renderer)
|
||||
end
|
||||
|
||||
defp pairs_to_html([{modifiers, content} | pairs], iodata, renderer) do
|
||||
style = modifiers_to_css(modifiers)
|
||||
rendered = renderer.(style, content)
|
||||
|
@ -188,7 +187,11 @@ defmodule LivebookWeb.ANSI do
|
|||
pairs_to_html(pairs, [iodata, rendered], renderer)
|
||||
end
|
||||
|
||||
defp default_renderer(style, content) do
|
||||
def default_renderer("", content) do
|
||||
content
|
||||
end
|
||||
|
||||
def default_renderer(style, content) do
|
||||
[~s{<span style="#{style}">}, content, ~s{</span>}]
|
||||
end
|
||||
|
||||
|
|
|
@ -31,8 +31,6 @@ defmodule LivebookWeb.Helpers do
|
|||
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
|
||||
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
|
||||
|
||||
defdelegate ansi_string_to_html(string, opts \\ []), to: LivebookWeb.ANSI
|
||||
|
||||
@doc """
|
||||
Returns [Remix](https://remixicon.com) icon tag.
|
||||
"""
|
||||
|
@ -41,4 +39,42 @@ defmodule LivebookWeb.Helpers do
|
|||
attrs = Keyword.update(attrs, :class, icon_class, fn class -> "#{icon_class} #{class}" end)
|
||||
content_tag(:i, "", attrs)
|
||||
end
|
||||
|
||||
defdelegate ansi_string_to_html(string, opts \\ []), to: LivebookWeb.ANSI
|
||||
|
||||
@doc """
|
||||
Converts a string with ANSI escape codes into HTML lines.
|
||||
|
||||
This method is similar to `ansi_string_to_html/2`,
|
||||
but makes sure each line is itself a valid HTML
|
||||
(as opposed to just splitting HTML into lines).
|
||||
"""
|
||||
@spec ansi_to_html_lines(String.t()) :: list(Phoenix.HTML.safe())
|
||||
def ansi_to_html_lines(string) do
|
||||
string
|
||||
|> ansi_string_to_html(
|
||||
# Make sure every line is styled separately,
|
||||
# so that later we can safely split the whole HTML
|
||||
# into valid HTML lines.
|
||||
renderer: fn style, content ->
|
||||
content
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&apply_rewind/1)
|
||||
|> Enum.map(&LivebookWeb.ANSI.default_renderer(style, &1))
|
||||
|> Enum.intersperse("\n")
|
||||
end
|
||||
)
|
||||
|> Phoenix.HTML.safe_to_string()
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&Phoenix.HTML.raw/1)
|
||||
end
|
||||
|
||||
# Respect \r indicating the line should be cleared
|
||||
defp apply_rewind(line) do
|
||||
line
|
||||
|> String.split("\r")
|
||||
|> Enum.reverse()
|
||||
|> Enum.find("", &(&1 != ""))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -232,7 +232,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
~L"""
|
||||
<div id="<%= @id %>" phx-hook="VirtualizedLines" data-max-height="300" data-follow="true">
|
||||
<div data-template class="hidden"><%= for line <- @lines do %><div><%= raw line %></div><% end %></div>
|
||||
<div data-template class="hidden"><%= for line <- @lines do %><div><%= line %></div><% end %></div>
|
||||
<div data-content phx-update="ignore" class="overflow-auto whitespace-pre text-gray-500 tiny-scrollbar"></div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -244,7 +244,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
~L"""
|
||||
<div id="<%= @id %>" phx-hook="VirtualizedLines" data-max-height="300" data-follow="false">
|
||||
<div data-template class="hidden"><%= for line <- @lines do %><div><%= raw line %></div><% end %></div>
|
||||
<div data-template class="hidden"><%= for line <- @lines do %><div><%= line %></div><% end %></div>
|
||||
<div data-content phx-update="ignore" class="overflow-auto whitespace-pre text-gray-500 tiny-scrollbar"></div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -258,24 +258,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp ansi_to_html_lines(string) do
|
||||
string
|
||||
|> ansi_string_to_html(
|
||||
# Make sure every line is styled separately,
|
||||
# so tht later we can safely split the whole HTML
|
||||
# into valid HTML lines.
|
||||
renderer: fn style, content ->
|
||||
content
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&[~s{<span style="#{style}">}, &1, ~s{</span>}])
|
||||
|> Enum.intersperse("\n")
|
||||
end
|
||||
)
|
||||
|> Phoenix.HTML.safe_to_string()
|
||||
|> String.split("\n")
|
||||
end
|
||||
|
||||
defp render_cell_status(validity_status, evaluation_status, changed)
|
||||
|
||||
defp render_cell_status(_, :evaluating, changed) do
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule LivebookWeb.ANSITest do
|
|||
|
||||
alias LivebookWeb.ANSI
|
||||
|
||||
describe "ansi_string_to_html/1" do
|
||||
describe "ansi_string_to_html/2" 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()
|
||||
|
@ -70,13 +70,24 @@ defmodule LivebookWeb.ANSITest do
|
|||
end
|
||||
|
||||
test "given custom renderer uses it to generate HTML" do
|
||||
div_renderer = fn style, content ->
|
||||
[~s{<div style="#{style}">}, content, ~s{</div>}]
|
||||
div_renderer = fn
|
||||
"", content -> content
|
||||
style, content -> [~s{<div style="#{style}">}, content, ~s{</div>}]
|
||||
end
|
||||
|
||||
assert ~s{<div style="color: var(--ansi-color-blue);">cat</div>} ==
|
||||
ANSI.ansi_string_to_html("\e[34mcat\e[0m", renderer: div_renderer)
|
||||
|> Phoenix.HTML.safe_to_string()
|
||||
end
|
||||
|
||||
test "given custom renderer uses it for style-less text as well" do
|
||||
div_renderer = fn _style, content ->
|
||||
[~s{<div>}, content, ~s{</div>}]
|
||||
end
|
||||
|
||||
assert ~s{<div>cat</div>} ==
|
||||
ANSI.ansi_string_to_html("cat", renderer: div_renderer)
|
||||
|> Phoenix.HTML.safe_to_string()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
22
test/livebook_web/helpers_test.exs
Normal file
22
test/livebook_web/helpers_test.exs
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule LivebookWeb.HelpersTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias LivebookWeb.Helpers
|
||||
|
||||
describe "ansi_to_html_lines/1" do
|
||||
test "puts every line in its own tag" do
|
||||
assert [
|
||||
{:safe, ~s{<span style="color: var(--ansi-color-blue);">smiley</span>}},
|
||||
{:safe, ~s{<span style="color: var(--ansi-color-blue);">cat</span>}}
|
||||
] ==
|
||||
Helpers.ansi_to_html_lines("\e[34msmiley\ncat\e[0m")
|
||||
end
|
||||
|
||||
test "respects CR as line cleaner" do
|
||||
assert [
|
||||
{:safe, ~s{<span style="color: var(--ansi-color-blue);">cat</span>}}
|
||||
] ==
|
||||
Helpers.ansi_to_html_lines("\e[34msmiley\rcat\r\e[0m")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue