Skip to content

Commit

Permalink
Refactor from_charlist/1
Browse files Browse the repository at this point in the history
* handle invalid sequences
* support complex sequences with multiple display attributes
* make output flat (or more flattened)
* improve test coverage
  • Loading branch information
fuelen committed Jul 23, 2024
1 parent d4bec69 commit 1cf0ed8
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 104 deletions.
72 changes: 47 additions & 25 deletions lib/owl/data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -318,24 +318,25 @@ defmodule Owl.Data do
This makes it possible to use data formatted outside of Owl with other Owl modules, like `Owl.Box`.
The `data` passed to this function must contain escape sequences as separate binaries, not concatenated with other data.
For instance, the following will work:
iex> Owl.Data.from_ansidata(["\e[31m", "hello"])
Owl.Data.tag("hello", :red)
Whereas this will not:
iex> Owl.Data.from_ansidata("\e[31mhello")
"\e[31mhello"
## Examples
iex> [:red, "hello"] |> IO.ANSI.format() |> Owl.Data.from_chardata()
Owl.Data.tag("hello", :red)
іex> {output, 0} = Owl.System.cmd("bat", ["README.md", "--color=always", "--style=plain"])
...> output
...> |> Owl.Data.from_chardata()
...> |> Owl.Box.new(title: Owl.Data.tag("README.md", :cyan), border_tag: :light_cyan)
...> |> Owl.IO.puts()
"""
@spec from_chardata(IO.chardata()) :: t()
def from_chardata(data) do
data =
Regex.split(~r/\e\[(\d+;)*\d+m/, IO.chardata_to_string(data),
include_captures: true,
trim: true
)

{data, _open_tags} = do_from_chardata(data, %{})
data
end
Expand All @@ -347,22 +348,24 @@ defmodule Owl.Data do
end

defp do_from_chardata(binary, open_tags) when is_binary(binary) do
case Sequence.ansi_to_type(binary) do
:reset ->
{[], %{}}
case Owl.Data.Sequence.split(binary) do
{:ok, sequences} ->
open_tags =
Enum.reduce(sequences, open_tags, fn sequence, acc ->
case Sequence.binary_to_name(sequence) do
nil -> acc
:reset -> %{}
name -> update_open_tags(acc, Sequence.type!(name), name)
end
end)

nil ->
{tag_all(binary, open_tags), open_tags}
{[], open_tags}

type ->
{[], update_open_tags(open_tags, type, Sequence.ansi_to_name(binary))}
:error ->
{tag_all(binary, open_tags), open_tags}
end
end

defp do_from_chardata(integer, open_tags) when is_integer(integer) do
{tag_all(integer, open_tags), open_tags}
end

defp do_from_chardata([], open_tags) do
{[], open_tags}
end
Expand All @@ -382,11 +385,30 @@ defmodule Owl.Data do
{_, []} ->
{head, open_tags}

{%Owl.Tag{data: head, sequences: s}, %Owl.Tag{data: tail, sequences: s}} ->
data = if is_list(tail), do: [head | tail], else: [head, tail]
{%Owl.Tag{data: p1, sequences: s}, %Owl.Tag{data: p2, sequences: s}} ->
data =
if is_list(p2) do
[p1 | p2]
else
[p1, p2]
end

{tag(data, s), open_tags}

_ ->
{%Owl.Tag{data: p1, sequences: s}, [%Owl.Tag{data: p2, sequences: s} | rest]} ->
data =
if is_list(p2) do
[p1 | p2]
else
[p1, p2]
end

{[tag(data, s) | rest], open_tags}

{head, tail} when is_list(tail) ->
{[head | tail], open_tags}

{head, tail} ->
{[head, tail], open_tags}
end
end
Expand Down
71 changes: 58 additions & 13 deletions lib/owl/data/sequence.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Owl.Data.Sequence.Helper do
defmodule Owl.Data.Sequence.DSL do
@moduledoc false

defmacro defsequence_type(name, type, define_name_by_sequence? \\ true) do
Expand All @@ -20,12 +20,66 @@ end
defmodule Owl.Data.Sequence do
@moduledoc false

import Owl.Data.Sequence.Helper
import Owl.Data.Sequence.DSL

def split(string) do
with {:ok, attributes} <- extract_display_attributes(string) do
{:ok, chunk_attributes(attributes)}
end
end

defp extract_display_attributes("\e[" <> rest) do
rest
|> String.split(";")
|> Enum.reduce_while([], fn
substring, acc ->
case Integer.parse(substring) do
:error -> {:halt, :error}
{number, ""} -> {:cont, [number | acc]}
{number, "m"} -> {:cont, [number | acc]}
{_number, _rest} -> {:halt, :error}
end
end)
|> case do
:error -> :error
attributes -> {:ok, Enum.reverse(attributes)}
end
end

defp extract_display_attributes(_), do: :error

defp chunk_attributes(attributes) do
attributes
|> chunk_attributes([])
|> Enum.reverse()
end

defp chunk_attributes([lead_attribute, 5, n | tail], acc)
when lead_attribute in [38, 48] do
chunk_attributes(tail, ["\e[#{lead_attribute};5;#{n}m" | acc])
end

defp chunk_attributes([lead_attribute, 2, r, g, b | tail], acc)
when lead_attribute in [38, 48] do
chunk_attributes(tail, ["\e[#{lead_attribute};2;#{r};#{g};#{b}m" | acc])
end

defp chunk_attributes([head | tail], acc) do
chunk_attributes(tail, ["\e[#{head}m" | acc])
end

defp chunk_attributes([], acc) do
acc
end

@doc """
Get the sequence name of an escape sequence or `nil` if not a sequence.
Try to convert binary sequence to a sequence name.
Returns binary if escape sequence is for colors.
Returns name as an atom if a sequence is supported.
Returns nil if the sequence is not supported.
"""
def ansi_to_name(binary) when is_binary(binary) do
def binary_to_name(binary) when is_binary(binary) do
case binary do
"\e[38;5;" <> _ -> binary
"\e[48;5;" <> _ -> binary
Expand All @@ -35,15 +89,6 @@ defmodule Owl.Data.Sequence do
end
end

@doc """
Get the sequence type of an escape sequence or `nil` if not a sequence.
"""
def ansi_to_type(binary) when is_binary(binary) do
if name = ansi_to_name(binary) do
type!(name)
end
end

@doc """
Get the sequence type of a sequence name.
"""
Expand Down
17 changes: 6 additions & 11 deletions test/owl/box_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,9 @@ defmodule Owl.BoxTest do
|> Owl.Data.from_chardata()
|> List.flatten() == [
Owl.Data.tag("Hi", :red),
" ",
"\n",
" \n",
Owl.Data.tag("there!", :red),
"\n",
"Hi",
"\nHi",
Owl.Data.tag("!!!", :green),
" "
]
Expand All @@ -325,13 +323,10 @@ defmodule Owl.BoxTest do
|> Owl.Data.to_chardata()
|> Owl.Data.from_chardata()
<~> [
[
[
[Owl.Data.tag("A", [:green_background, :red]), " "],
Owl.Data.tag("B", [:green_background, :red])
],
" "
],
Owl.Data.tag("A", [:green_background, :red]),
" ",
Owl.Data.tag("B", [:green_background, :red]),
" ",
Owl.Data.tag("C", [:green_background, :red])
]
end
Expand Down
Loading

0 comments on commit 1cf0ed8

Please sign in to comment.