Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Owl.Data.from_ansidata/1 #11

Merged
merged 6 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 108 additions & 59 deletions lib/owl/data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Owl.Data do
A set of functions for `t:iodata/0` with [tags](`Owl.Tag`).
"""

alias Owl.Data.Sequence

@typedoc """
A recursive data type that is similar to `t:iodata/0`, but additionally supports `t:Owl.Tag.t/1`.

Expand Down Expand Up @@ -274,7 +276,7 @@ defmodule Owl.Data do
Enum.reduce(new_open_tags, [], fn {sequence_type, sequence}, acc ->
case Map.get(open_tags, sequence_type) do
nil ->
return_to = default_value_by_sequence_type(sequence_type)
return_to = Sequence.default_value_by_type!(sequence_type)

[return_to | acc]

Expand All @@ -296,31 +298,87 @@ defmodule Owl.Data do

defp do_to_ansidata(term, _open_tags), do: term

defp maybe_wrap_to_tag([], [element]), do: element
defp maybe_wrap_to_tag([], data), do: data
@doc """
Transforms data from `t:IO.ANSI.ansidata/0`, replacing raw escape sequences with tags (see `tag/2`).

defp maybe_wrap_to_tag(sequences1, [%Owl.Tag{sequences: sequences2, data: data}]) do
tag(data, collapse_sequences(sequences1 ++ sequences2))
This makes it possible to use data formatted outside of Owl with other Owl modules, like `Owl.Box`.

The `ansidata` 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> [[[[[[[] | "\e[46m"] | "\e[31m"], "hello"] | "\e[39m"] | "\e[49m"] | "\e[0m"]
...> |> Owl.Data.from_ansidata()
Owl.Data.tag("hello", [:cyan_background, :red])

iex> [:red, "hello"] |> IO.ANSI.format() |> Owl.Data.from_ansidata()
Owl.Data.tag("hello", :red)

"""
@spec from_ansidata(IO.ANSI.ansidata()) :: t()
def from_ansidata(ansidata) do
{data, _open_tags} = do_from_ansidata(ansidata, %{})
data
end

defp maybe_wrap_to_tag(sequences, data) do
tag(data, collapse_sequences(sequences))
defp do_from_ansidata(binary, open_tags) when is_binary(binary) do
case Sequence.ansi_to_type(binary) do
:reset ->
{[], %{}}

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

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

defp reverse_and_tag(sequences, [%Owl.Tag{sequences: last_sequences} | _] = data) do
maybe_wrap_to_tag(sequences -- last_sequences, Enum.reverse(data))
defp do_from_ansidata(integer, open_tags) when is_integer(integer) do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at coverage and this line is red. This is for char list, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it’s just to handle individual character literals in ansidata. [“hello”, \?]

Should’ve added a test, sorry!

{tag_all(integer, open_tags), open_tags}
end

defp reverse_and_tag(sequences, data) do
maybe_wrap_to_tag(sequences, Enum.reverse(data))
defp do_from_ansidata([], open_tags) do
{[], open_tags}
end

# last write wins
defp collapse_sequences(sequences) do
%{foreground: nil, background: nil}
|> sequences_to_state(sequences)
|> Map.values()
|> Enum.reject(&is_nil/1)
defp do_from_ansidata([inner], open_tags) do
do_from_ansidata(inner, open_tags)
end

defp do_from_ansidata([head | tail], open_tags) do
{head, open_tags} = do_from_ansidata(head, open_tags)
{tail, open_tags} = do_from_ansidata(tail, open_tags)

case {head, tail} do
{[], _} -> {tail, open_tags}
{_, []} -> {head, open_tags}
_ -> {[head, tail], open_tags}
end
end

defp tag_all(data, open_tags) do
case Map.values(open_tags) do
[] -> data
tags -> tag(data, tags)
end
end

defp update_open_tags(open_tags, type, name) do
if name == Sequence.default_value_by_type!(type) do
fuelen marked this conversation as resolved.
Show resolved Hide resolved
Map.delete(open_tags, type)
else
Map.put(open_tags, type, name)
end
end

@doc """
Expand Down Expand Up @@ -350,48 +408,6 @@ defmodule Owl.Data do
)
end

defp sequences_to_state(init, sequences) do
Enum.reduce(sequences, init, fn sequence, acc ->
Map.put(acc, sequence_type(sequence), sequence)
end)
end

for color <- [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white] do
defp sequence_type(unquote(color)), do: :foreground
defp sequence_type(unquote(:"light_#{color}")), do: :foreground
defp sequence_type(unquote(:"#{color}_background")), do: :background
defp sequence_type(unquote(:"light_#{color}_background")), do: :background
end

defp sequence_type(:default_color), do: :foreground
defp sequence_type(:default_background), do: :background

defp sequence_type(:blink_slow), do: :blink
defp sequence_type(:blink_rapid), do: :blink
defp sequence_type(:faint), do: :intensity
defp sequence_type(:bright), do: :intensity
defp sequence_type(:inverse), do: :inverse
defp sequence_type(:underline), do: :underline
defp sequence_type(:italic), do: :italic
defp sequence_type(:overlined), do: :overlined
defp sequence_type(:reverse), do: :reverse

# https://github.com/elixir-lang/elixir/blob/74bfab8ee271e53d24cb0012b5db1e2a931e0470/lib/elixir/lib/io/ansi.ex#L73
defp sequence_type("\e[38;5;" <> _), do: :foreground

# https://github.com/elixir-lang/elixir/blob/74bfab8ee271e53d24cb0012b5db1e2a931e0470/lib/elixir/lib/io/ansi.ex#L87
defp sequence_type("\e[48;5;" <> _), do: :background

defp default_value_by_sequence_type(:foreground), do: :default_color
defp default_value_by_sequence_type(:background), do: :default_background
defp default_value_by_sequence_type(:blink), do: :blink_off
defp default_value_by_sequence_type(:intensity), do: :normal
defp default_value_by_sequence_type(:inverse), do: :inverse_off
defp default_value_by_sequence_type(:underline), do: :no_underline
defp default_value_by_sequence_type(:italic), do: :not_italic
defp default_value_by_sequence_type(:overlined), do: :not_overlined
defp default_value_by_sequence_type(:reverse), do: :reverse_off

@doc """
Truncates data, so the length of returning data is <= `length`.

Expand Down Expand Up @@ -614,6 +630,39 @@ defmodule Owl.Data do
do_chunk_by(<<value::utf8>>, chunk_acc, chunk_fun, acc, acc_sequences)
end

defp maybe_wrap_to_tag([], [element]), do: element
defp maybe_wrap_to_tag([], data), do: data

defp maybe_wrap_to_tag(sequences1, [%Owl.Tag{sequences: sequences2, data: data}]) do
tag(data, collapse_sequences(sequences1 ++ sequences2))
end

defp maybe_wrap_to_tag(sequences, data) do
tag(data, collapse_sequences(sequences))
end

defp reverse_and_tag(sequences, [%Owl.Tag{sequences: last_sequences} | _] = data) do
maybe_wrap_to_tag(sequences -- last_sequences, Enum.reverse(data))
end

defp reverse_and_tag(sequences, data) do
maybe_wrap_to_tag(sequences, Enum.reverse(data))
end

# last write wins
defp collapse_sequences(sequences) do
%{foreground: nil, background: nil}
|> sequences_to_state(sequences)
|> Map.values()
|> Enum.reject(&is_nil/1)
end

defp sequences_to_state(init, sequences) do
Enum.reduce(sequences, init, fn sequence, acc ->
Map.put(acc, Sequence.type!(sequence), sequence)
end)
end

defp put_nonempty_head([], tail), do: tail
defp put_nonempty_head(head, tail), do: [head | tail]
end
98 changes: 98 additions & 0 deletions lib/owl/data/sequence.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
defmodule Owl.Data.Sequence.Helper do
@moduledoc false

defmacro defsequence_type(name, type, define_name_by_sequence? \\ true) do
quote bind_quoted: [
name: name,
type: type,
define_name_by_sequence?: define_name_by_sequence?
] do
if define_name_by_sequence? do
seq = apply(IO.ANSI, name, [])
defp name_by_sequence(unquote(seq)), do: unquote(name)
end

defp type_by_name(unquote(name)), do: unquote(type)
end
end
end

defmodule Owl.Data.Sequence do
@moduledoc false

import Owl.Data.Sequence.Helper

@doc """
Get the sequence name of an escape sequence or `nil` if not a sequence.
"""
def ansi_to_name(binary) when is_binary(binary) do
case binary do
"\e[38;5;" <> _ -> binary
"\e[48;5;" <> _ -> binary
_ -> name_by_sequence(binary)
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
fuelen marked this conversation as resolved.
Show resolved Hide resolved
type!(name)
end
end

@doc """
Get the sequence type of a sequence name.
"""
def type!(sequence) when is_atom(sequence), do: type_by_name(sequence)
def type!("\e[38;5;" <> _), do: :foreground
def type!("\e[48;5;" <> _), do: :background

@doc """
Get the default value of a sequence type.
"""
def default_value_by_type!(:foreground), do: :default_color
def default_value_by_type!(:background), do: :default_background
def default_value_by_type!(:blink), do: :blink_off
def default_value_by_type!(:intensity), do: :normal
def default_value_by_type!(:underline), do: :no_underline
def default_value_by_type!(:italic), do: :not_italic
def default_value_by_type!(:overlined), do: :not_overlined
def default_value_by_type!(:inverse), do: :inverse_off
def default_value_by_type!(:reverse), do: :reverse_off

defsequence_type(:reset, :reset)

for color <- [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white] do
defsequence_type(color, :foreground)
defsequence_type(:"light_#{color}", :foreground)
defsequence_type(:"#{color}_background", :background)
defsequence_type(:"light_#{color}_background", :background)
end

defsequence_type(:default_color, :foreground)
defsequence_type(:default_background, :background)

defsequence_type(:blink_off, :blink)
defsequence_type(:blink_slow, :blink)
defsequence_type(:blink_rapid, :blink)

defsequence_type(:normal, :intensity)
defsequence_type(:faint, :intensity)
defsequence_type(:bright, :intensity)

defsequence_type(:inverse, :inverse)
defsequence_type(:reverse, :reverse, false)

defsequence_type(:underline, :underline)
defsequence_type(:no_underline, :underline)

defsequence_type(:italic, :italic)
defsequence_type(:not_italic, :italic)

defsequence_type(:overlined, :overlined)
defsequence_type(:not_overlined, :overlined)

defp name_by_sequence(_), do: nil
end
Loading