Skip to content

Commit 60b66ac

Browse files
author
José Valim
committed
Merge pull request #2562 from thmzlt/ansi
Implement IO.ANSI format/2 and format_fragment/2
2 parents 1fda82d + 9f867f2 commit 60b66ac

File tree

2 files changed

+169
-4
lines changed

2 files changed

+169
-4
lines changed

lib/elixir/lib/io/ansi.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,82 @@ defmodule IO.ANSI do
134134
raise ArgumentError, "invalid ANSI sequence specification: #{other}"
135135
end
136136

137+
@doc ~S"""
138+
Formats a chardata-like argument by converting named ANSI sequences into actual
139+
ANSI codes.
140+
141+
The named sequences are represented by atoms.
142+
143+
It will also append an `IO.ANSI.reset` to the chardata when a conversion is
144+
performed. If you don't want this behaviour, use `format_fragment/2`.
145+
146+
An optional boolean parameter can be passed to enable or disable
147+
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
148+
By default, standard output will be checked if it is a terminal capable
149+
of handling these sequences (using `terminal?/1` function)
150+
151+
## Examples
152+
153+
iex> IO.ANSI.format(["Hello, ", :red, :bright, "world!"], true)
154+
[[[[[[], "Hello, "], "\e[31m"], "\e[1m"], "world!"] | "\e[0m"]
155+
"""
156+
def format(chardata, emit \\ terminal?) do
157+
do_format(chardata, [], [], emit, :maybe)
158+
end
159+
160+
@doc ~S"""
161+
Formats a chardata-like argument by converting named ANSI sequences into actual
162+
ANSI codes.
163+
164+
The named sequences are represented by atoms.
165+
166+
An optional boolean parameter can be passed to enable or disable
167+
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
168+
By default, standard output will be checked if it is a terminal capable
169+
of handling these sequences (using `terminal?/1` function)
170+
171+
## Examples
172+
173+
iex> IO.ANSI.format_fragment([:bright, 'Word'], true)
174+
[[[[[[], "\e[1m"], 87], 111], 114], 100]
175+
"""
176+
def format_fragment(chardata, emit \\ terminal?) do
177+
do_format(chardata, [], [], emit, false)
178+
end
179+
180+
defp do_format([term | rest], rem, acc, emit, append_reset) do
181+
do_format(term, [rest | rem], acc, emit, append_reset)
182+
end
183+
184+
defp do_format(term, rem, acc, true, append_reset) when is_atom(term) do
185+
try do
186+
do_format([], rem, [acc | [apply(IO.ANSI, term, [])]], true, !!append_reset)
187+
rescue
188+
_ in UndefinedFunctionError ->
189+
raise ArgumentError, message: "invalid ANSI sequence specification: #{term}"
190+
end
191+
end
192+
193+
defp do_format(term, rem, acc, false, append_reset) when is_atom(term) do
194+
do_format([], rem, acc, false, append_reset)
195+
end
196+
197+
defp do_format(term, rem, acc, emit, append_reset) when not is_list(term) do
198+
do_format([], rem, [acc | [term]], emit, append_reset)
199+
end
200+
201+
defp do_format([], [next | rest], acc, emit, append_reset) do
202+
do_format(next, rest, acc, emit, append_reset)
203+
end
204+
205+
defp do_format([], [], acc, true, true) do
206+
[acc | IO.ANSI.reset]
207+
end
208+
209+
defp do_format([], [], acc, _emit, _append_reset) do
210+
acc
211+
end
212+
137213
@doc ~S"""
138214
Escapes a string by converting named ANSI sequences into actual ANSI codes.
139215

lib/elixir/test/elixir/io/ansi_test.exs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,95 @@ Code.require_file "../test_helper.exs", __DIR__
33
defmodule IO.ANSITest do
44
use ExUnit.Case, async: true
55

6+
test :format_ansicode do
7+
assert IO.chardata_to_string(IO.ANSI.format(:green, true)) ==
8+
"#{IO.ANSI.green}#{IO.ANSI.reset}"
9+
assert IO.chardata_to_string(IO.ANSI.format(:green, false)) ==
10+
""
11+
end
12+
13+
test :format_binary do
14+
assert IO.chardata_to_string(IO.ANSI.format("Hello, world!", true)) ==
15+
"Hello, world!"
16+
assert IO.chardata_to_string(IO.ANSI.format("A map: %{foo: :bar}", false)) ==
17+
"A map: %{foo: :bar}"
18+
end
19+
20+
test :format_empty_list do
21+
assert IO.chardata_to_string(IO.ANSI.format([], true)) ==
22+
""
23+
assert IO.chardata_to_string(IO.ANSI.format([], false)) ==
24+
""
25+
end
26+
27+
test :format_ansicode_list do
28+
assert IO.chardata_to_string(IO.ANSI.format([:red, :bright], true)) ==
29+
"#{IO.ANSI.red}#{IO.ANSI.bright}#{IO.ANSI.reset}"
30+
assert IO.chardata_to_string(IO.ANSI.format([:red, :bright], false)) ==
31+
""
32+
end
33+
34+
test :format_binary_list do
35+
assert IO.chardata_to_string(IO.ANSI.format(["Hello, ", "world!"], true)) ==
36+
"Hello, world!"
37+
assert IO.chardata_to_string(IO.ANSI.format(["Hello, ", "world!"], false)) ==
38+
"Hello, world!"
39+
end
40+
41+
test :format_char_list do
42+
assert IO.chardata_to_string(IO.ANSI.format('Hello, world!', true)) ==
43+
"Hello, world!"
44+
assert IO.chardata_to_string(IO.ANSI.format('Hello, world!', false)) ==
45+
"Hello, world!"
46+
end
47+
48+
test :format_mixed_list do
49+
data = ["Hello", ?,, 32, :red, "world!"]
50+
51+
assert IO.chardata_to_string(IO.ANSI.format(data, true)) ==
52+
"Hello, #{IO.ANSI.red}world!#{IO.ANSI.reset}"
53+
assert IO.chardata_to_string(IO.ANSI.format(data, false)) ==
54+
"Hello, world!"
55+
end
56+
57+
test :format_nested_list do
58+
data = ["Hello, ", ["nested", 32, :red, "world!"]]
59+
60+
assert IO.chardata_to_string(IO.ANSI.format(data, true)) ==
61+
"Hello, nested #{IO.ANSI.red}world!#{IO.ANSI.reset}"
62+
assert IO.chardata_to_string(IO.ANSI.format(data, false)) ==
63+
"Hello, nested world!"
64+
end
65+
66+
test :format_improper_list do
67+
data = ["Hello, ", :red, "world" | "!"]
68+
69+
assert IO.chardata_to_string(IO.ANSI.format(data, true)) ==
70+
"Hello, #{IO.ANSI.red}world!#{IO.ANSI.reset}"
71+
assert IO.chardata_to_string(IO.ANSI.format(data, false)) ==
72+
"Hello, world!"
73+
end
74+
75+
test :format_nested_improper_list do
76+
data = [["Hello, " | :red], "world!" | :green]
77+
78+
assert IO.chardata_to_string(IO.ANSI.format(data, true)) ==
79+
"Hello, #{IO.ANSI.red}world!#{IO.ANSI.green}#{IO.ANSI.reset}"
80+
assert IO.chardata_to_string(IO.ANSI.format(data, false)) ==
81+
"Hello, world!"
82+
end
83+
84+
test :format_fragment do
85+
assert IO.chardata_to_string(IO.ANSI.format_fragment([:red, "Hello!"], true)) ==
86+
"#{IO.ANSI.red}Hello!"
87+
end
88+
89+
test :format_invalid_sequence do
90+
assert_raise ArgumentError, "invalid ANSI sequence specification: brigh", fn ->
91+
IO.ANSI.format([:brigh, "Hello!"], true)
92+
end
93+
end
94+
695
test :escape_single do
796
assert IO.ANSI.escape("Hello, %{red}world!", true) ==
897
"Hello, #{IO.ANSI.red}world!#{IO.ANSI.reset}"
@@ -26,24 +115,24 @@ defmodule IO.ANSITest do
26115
"Hello, #{IO.ANSI.red}#{IO.ANSI.bright}world!#{IO.ANSI.reset}"
27116
end
28117

29-
test :no_emit do
118+
test :escape_no_emit do
30119
assert IO.ANSI.escape("Hello, %{}world!", false) ==
31120
"Hello, world!"
32121

33122
assert IO.ANSI.escape("Hello, %{red,bright}world!", false) ==
34123
"Hello, world!"
35124
end
36125

37-
test :fragment do
126+
test :escape_fragment do
38127
assert IO.ANSI.escape("%{red}", true) == "#{IO.ANSI.red}#{IO.ANSI.reset}"
39128
assert IO.ANSI.escape_fragment("", true) == ""
40129
end
41130

42-
test :noop do
131+
test :escape_noop do
43132
assert IO.ANSI.escape("") == ""
44133
end
45134

46-
test :invalid do
135+
test :escape_invalid do
47136
assert_raise ArgumentError, "invalid ANSI sequence specification: brigh", fn ->
48137
IO.ANSI.escape("%{brigh}, yes")
49138
end

0 commit comments

Comments
 (0)