mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-03-09 21:37:42 +08:00
Extend ANSI code parser to handle multiple arguments (#569)
* Extend ANSI code parser to handle multiple arguments * Update changelog
This commit is contained in:
parent
026945e669
commit
a57927ec2a
3 changed files with 139 additions and 85 deletions
|
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Added support for configuring file systems using env variables ([#498](https://github.com/livebook-dev/livebook/pull/498))
|
||||
- Added a keyboard shortcut for triggering on-hover docs ([#508](https://github.com/livebook-dev/livebook/pull/508))
|
||||
- Added `--open-new` CLI flag to `livebook server` ([#529](https://github.com/livebook-dev/livebook/pull/529))
|
||||
- Nx introductory notebook ([#528](https://github.com/livebook-dev/livebook/pull/528))
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -25,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Improved the evaluator process to not consume user-submitted messages from inbox ([#502](https://github.com/livebook-dev/livebook/pull/502))
|
||||
- Improved sections panel UI to better handle numerous sections or long section names ([#534](https://github.com/livebook-dev/livebook/pull/534) and [#537](https://github.com/livebook-dev/livebook/pull/537))
|
||||
- Fixed branching section evaluation when the parent section is empty ([#560](https://github.com/livebook-dev/livebook/pull/560)
|
||||
- Fixed ANSI support to handle multi-code escape sequences ([#569](https://github.com/livebook-dev/livebook/pull/569)
|
||||
|
||||
## [v0.2.3](https://github.com/livebook-dev/livebook/tree/v0.2.3) (2021-08-12)
|
||||
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
defmodule Livebook.Utils.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 Livebook.Utils.ANSI do
|
||||
@moduledoc false
|
||||
|
||||
import Livebook.Utils.ANSI.Modifier
|
||||
|
||||
@type modifier ::
|
||||
{:font_weight, :bold | :light}
|
||||
| {:font_style, :italic}
|
||||
|
@ -54,12 +40,12 @@ defmodule Livebook.Utils.ANSI do
|
|||
{tail_parts, _} =
|
||||
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)
|
||||
case ansi_prefix_to_modifiers(string) do
|
||||
{:ok, new_modifiers, rest} ->
|
||||
modifiers = Enum.reduce(new_modifiers, modifiers, &apply_modifier(&2, &1))
|
||||
{modifiers, rest}
|
||||
|
||||
{:error, _rest} ->
|
||||
:error ->
|
||||
{modifiers, "\e" <> string}
|
||||
end
|
||||
|
||||
|
@ -83,84 +69,138 @@ defmodule Livebook.Utils.ANSI do
|
|||
merge_adjacent_parts(parts, [part | acc])
|
||||
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}
|
||||
defp ansi_prefix_to_modifiers("[1A" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[1B" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[1C" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[1D" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[2J" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[2K" <> rest), do: {:ok, [:ignored], rest}
|
||||
defp ansi_prefix_to_modifiers("[H" <> rest), do: {:ok, [:ignored], rest}
|
||||
|
||||
defmodifier(:reset, 0)
|
||||
# "\e(B" is RFC1468's switch to ASCII character set and can be ignored. This
|
||||
# can appear even when JIS character sets aren't in use
|
||||
defp ansi_prefix_to_modifiers("(B" <> rest), do: {:ok, [:ignored], rest}
|
||||
|
||||
# When the code is missing (i.e., "\e[m"), it is 0 for reset.
|
||||
defmodifier(:reset, "")
|
||||
defp ansi_prefix_to_modifiers("[" <> rest) do
|
||||
with [args_string, rest] <- String.split(rest, "m", parts: 2),
|
||||
{:ok, args} <- parse_ansi_args(args_string),
|
||||
{:ok, modifiers} <- ansi_args_to_modifiers(args, []) do
|
||||
{:ok, modifiers, rest}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp ansi_prefix_to_modifiers(_string), do: :error
|
||||
|
||||
defp parse_ansi_args(args_string) do
|
||||
args_string
|
||||
|> String.split(";")
|
||||
|> Enum.reduce_while([], fn arg, parsed ->
|
||||
case parse_ansi_arg(arg) do
|
||||
{:ok, n} -> {:cont, [n | parsed]}
|
||||
:error -> {:halt, :error}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
:error -> :error
|
||||
parsed -> {:ok, Enum.reverse(parsed)}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_ansi_arg(""), do: {:ok, 0}
|
||||
|
||||
defp parse_ansi_arg(string) do
|
||||
case Integer.parse(string) do
|
||||
{n, ""} -> {:ok, n}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp ansi_args_to_modifiers([], acc), do: {:ok, Enum.reverse(acc)}
|
||||
|
||||
defp ansi_args_to_modifiers(args, acc) do
|
||||
case ansi_args_to_modifier(args) do
|
||||
{:ok, modifier, args} -> ansi_args_to_modifiers(args, [modifier | acc])
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@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
|
||||
defp ansi_args_to_modifier(args) do
|
||||
case args do
|
||||
[0 | args] ->
|
||||
{:ok, :reset, args}
|
||||
|
||||
defmodifier({:foreground_color, :reset}, 39)
|
||||
defmodifier({:background_color, :reset}, 49)
|
||||
[1 | args] ->
|
||||
{:ok, {:font_weight, :bold}, args}
|
||||
|
||||
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)
|
||||
[2 | args] ->
|
||||
{:ok, {:font_weight, :light}, args}
|
||||
|
||||
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
|
||||
[3 | args] ->
|
||||
{:ok, {:font_style, :italic}, args}
|
||||
|
||||
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
|
||||
[4 | args] ->
|
||||
{:ok, {:text_decoration, :underline}, args}
|
||||
|
||||
# "\e(B" is RFC1468's switch to ASCII character set and can be ignored. This
|
||||
# can appear even when JIS character sets aren't in use.
|
||||
defp ansi_prefix_to_modifier("(B" <> rest) do
|
||||
{:ok, :ignored, rest}
|
||||
end
|
||||
[9 | args] ->
|
||||
{:ok, {:text_decoration, :line_through}, args}
|
||||
|
||||
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}
|
||||
[22 | args] ->
|
||||
{:ok, {:font_weight, :reset}, args}
|
||||
|
||||
[23 | args] ->
|
||||
{:ok, {:font_style, :reset}, args}
|
||||
|
||||
[24 | args] ->
|
||||
{:ok, {:text_decoration, :reset}, args}
|
||||
|
||||
[n | args] when n in 30..37 ->
|
||||
color = Enum.at(@colors, n - 30)
|
||||
{:ok, {:foreground_color, color}, args}
|
||||
|
||||
[38, 5, bit8 | args] when bit8 in 0..255 ->
|
||||
color = color_from_code(bit8)
|
||||
{:ok, {:foreground_color, color}, args}
|
||||
|
||||
[39 | args] ->
|
||||
{:ok, {:foreground_color, :reset}, args}
|
||||
|
||||
[n | args] when n in 40..47 ->
|
||||
color = Enum.at(@colors, n - 40)
|
||||
{:ok, {:background_color, color}, args}
|
||||
|
||||
[48, 5, bit8 | args] when bit8 in 0..255 ->
|
||||
color = color_from_code(bit8)
|
||||
{:ok, {:background_color, color}, args}
|
||||
|
||||
[49 | args] ->
|
||||
{:ok, {:background_color, :reset}, args}
|
||||
|
||||
[53 | args] ->
|
||||
{:ok, {:text_decoration, :overline}, args}
|
||||
|
||||
[55 | args] ->
|
||||
{:ok, {:text_decoration, :reset}, args}
|
||||
|
||||
[n | args] when n in 90..97 ->
|
||||
color = Enum.at(@colors, n - 90)
|
||||
{:ok, {:foreground_color, :"light_#{color}"}, args}
|
||||
|
||||
[n | args] when n in 100..107 ->
|
||||
color = Enum.at(@colors, n - 100)
|
||||
{:ok, {:background_color, :"light_#{color}"}, args}
|
||||
|
||||
[n | args] when n <= 107 ->
|
||||
{:ok, :ignored, args}
|
||||
|
||||
_ ->
|
||||
{:error, string}
|
||||
:error
|
||||
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
|
||||
|
@ -184,8 +224,8 @@ defmodule Livebook.Utils.ANSI do
|
|||
{: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)
|
||||
defp apply_modifier(modifiers, :ignored), do: modifiers
|
||||
defp apply_modifier(_modifiers, :reset), do: %{}
|
||||
defp apply_modifier(modifiers, {key, :reset}), do: Map.delete(modifiers, key)
|
||||
defp apply_modifier(modifiers, {key, value}), do: Map.put(modifiers, key, value)
|
||||
end
|
||||
|
|
|
@ -57,12 +57,24 @@ defmodule Livebook.Utils.ANSITest do
|
|||
assert ANSI.parse_ansi_string("\e[H\e[1Acat") == [{[], "cat"}]
|
||||
end
|
||||
|
||||
test "returns the whole string if on ANSI code detected" do
|
||||
test "returns the whole string if no ANSI code is detected" do
|
||||
assert ANSI.parse_ansi_string("\e[300mcat") == [{[], "\e[300mcat"}]
|
||||
assert ANSI.parse_ansi_string("\ehmmcat") == [{[], "\ehmmcat"}]
|
||||
end
|
||||
|
||||
test "ignores RFC 1468 switch to ASCII" do
|
||||
assert ANSI.parse_ansi_string("\e(Bcat") == [{[], "cat"}]
|
||||
end
|
||||
|
||||
test "supports multiple codes separated by semicolon" do
|
||||
assert ANSI.parse_ansi_string("\e[0;34mcat") == [{[foreground_color: :blue], "cat"}]
|
||||
|
||||
assert ANSI.parse_ansi_string("\e[34;41mcat\e[0m") ==
|
||||
[{[background_color: :red, foreground_color: :blue], "cat"}]
|
||||
|
||||
# 8-bit rgb color followed by background color
|
||||
assert ANSI.parse_ansi_string("\e[38;5;67;41mcat\e[0m") ==
|
||||
[{[background_color: :red, foreground_color: {:rgb6, 1, 2, 3}], "cat"}]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue