Current section

Files

Jump to
doctest src doctest_term.erl
Raw

src/doctest_term.erl

%%%---------------------------------------------------------------------
%%% Copyright 2024 William Fank Thomé
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%---------------------------------------------------------------------
-module(doctest_term).
% API functions
-export([write/1, format/1]).
% Macros
-define(is_string(X), (is_list(X) orelse is_binary(X))).
-define(is_attrs(X), (is_list(X) orelse is_tuple(X))).
%%%=====================================================================
%%% API functions
%%%=====================================================================
% TODO: Global attrs, e.g.:
% 1> write([{"foo", {fg, blue}}, "bar"], [{bg, red}]).
% That results in "foo" and "bar" with a red background.
write(Term) ->
fwrite("~ts~n", [to_bin(format(Term))]).
format(Str) when is_binary(Str) ->
Str;
format({fmt, Fmt, Args}) ->
format_bin(Fmt, Args);
format({to_bin, Term}) ->
to_bin(Term);
format({{fmt, Fmt, Args}, Attrs}) ->
format({format_bin(Fmt, Args), Attrs});
format({{to_bin, Term}, Attrs}) ->
format({to_bin(Term), Attrs});
format({Str, Attrs}) when is_binary(Str), is_list(Attrs) ->
f(Str, parse_attrs(Attrs));
format({Str, Attr}) when is_binary(Str), is_tuple(Attr) ->
f(Str, parse_attrs([Attr]));
format({Str, Attr}) when is_binary(Str), is_atom(Attr) ->
f(Str, parse_attrs([Attr]));
format(Attrs) when is_list(Attrs) ->
[format(Attr) || Attr <- Attrs].
%%%=====================================================================
%%% Internal functions
%%%=====================================================================
f(Str, Attrs) ->
format_bin("\e[~sm~ts\e[0m", [to_bin(Attrs), Str]).
fwrite(Fmt, Args) ->
io:fwrite(user, Fmt, Args).
parse_attrs(Attrs) when is_list(Attrs) ->
to_bin(lists:join($;, [parse_attr(Attr) || Attr <- Attrs])).
% See https://stackoverflow.com/a/33206814/14166584
parse_attr(Attr) when is_integer(Attr), Attr >= 0, Attr =< 107 ->
integer_to_binary(Attr);
% TODO: Deal with \r\n and \r
parse_attr(nl) ->
<<"\n">>;
parse_attr({nl, N}) when N >= 1 ->
to_bin(lists:duplicate(N, <<"\n">>));
parse_attr(ws) ->
<<"\s">>;
parse_attr({ws, N}) when N >= 1 ->
to_bin(lists:duplicate(N, <<"\s">>));
parse_attr(tab) ->
<<"\t">>;
parse_attr({tab, N}) when N >= 1 ->
to_bin(lists:duplicate(N, <<"\t">>));
parse_attr(reset) ->
<<"0">>; % Reset / Normal | all attributes off.
parse_attr(bold) ->
<<"1">>; % Bold or increased intensity | .
parse_attr(faint) ->
<<"2">>; % Faint (decreased intensity) | Not widely supported.
parse_attr(italic) ->
<<"3">>; % Italic | Not widely supported. Sometimes treated as inverse.
parse_attr(underline) ->
<<"4">>; % Underline
parse_attr(slow_blink) ->
<<"5">>; % Slow Blink | less than 150 per minute
parse_attr(rapid_blink) ->
<<"6">>; % Rapid Blink | MS-DOS ANSI.SYS; 150+ per minute; not widely supported
parse_attr(reverse_video) ->
<<"7">>; % [[reverse video]] | swap foreground and background colors
parse_attr(conceal) ->
<<"8">>; % Conceal | Not widely supported.
parse_attr(crossed_out) ->
<<"9">>; % Crossed-out | Characters legible, but marked for deletion. Not widely supported.
parse_attr({font, 1}) ->
<<"10">>; % Primary(default) font
parse_attr({font, 2}) ->
<<"11">>; % Alternate font.
parse_attr({font, 3}) ->
<<"12">>; % Alternate font.
parse_attr({font, 4}) ->
<<"13">>; % Alternate font.
parse_attr({font, 5}) ->
<<"14">>; % Alternate font.
parse_attr({font, 6}) ->
<<"15">>; % Alternate font.
parse_attr({font, 7}) ->
<<"16">>; % Alternate font.
parse_attr({font, 8}) ->
<<"17">>; % Alternate font.
parse_attr({font, 9}) ->
<<"18">>; % Alternate font.
parse_attr({font, 10}) ->
<<"19">>; % Alternate font.
parse_attr(fraktur) ->
<<"20">>;
% parse_attr(_NotImplementedYet) ->
% 21; % Bold off or Double Underline | Bold off not widely supported; double underline hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 22; % Normal color or intensity | Neither bold nor faint
% parse_attr(_NotImplementedYet) ->
% 23; % Not italic, not Fraktur |
% parse_attr(_NotImplementedYet) ->
% 24; % Underline off | Not singly or doubly underlined
% parse_attr(_NotImplementedYet) ->
% 25; % Blink off |
% parse_attr(_NotImplementedYet) ->
% 27; % Inverse off |
% parse_attr(_NotImplementedYet) ->
% 28; % Reveal | conceal off
% parse_attr(_NotImplementedYet) ->
% 29; % Not crossed out |
parse_attr({fg, Color}) when is_atom(Color) ->
format_bin("~p", [ansi_fg_color(Color)]);
parse_attr({fg, 30}) ->
<<"30">>; % Set foreground color
parse_attr({fg, 31}) ->
<<"31">>; % Set foreground color
parse_attr({fg, 32}) ->
<<"32">>; % Set foreground color
parse_attr({fg, 33}) ->
<<"33">>; % Set foreground color
parse_attr({fg, 34}) ->
<<"34">>; % Set foreground color
parse_attr({fg, 35}) ->
<<"35">>; % Set foreground color
parse_attr({fg, 36}) ->
<<"36">>; % Set foreground color
parse_attr({fg, 37}) ->
<<"37">>; % Set foreground color
parse_attr({fg, {R, G, B}}) ->
format_bin("38;5;~p", [rgb_to_ansi_color(R, G, B)]);
parse_attr({fg, 39}) ->
<<"39">>; % Default foreground color | implementation defined (according to standard)
parse_attr({fg, 90}) ->
<<"90">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 91}) ->
<<"91">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 92}) ->
<<"92">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 93}) ->
<<"93">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 94}) ->
<<"94">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 95}) ->
<<"95">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 96}) ->
<<"96">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg, 97}) ->
<<"97">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({bg, Color}) when is_atom(Color) ->
format_bin("~p", [ansi_bg_color(Color)]);
parse_attr({bg, 40}) ->
<<"40">>; % Set background color
parse_attr({bg, 41}) ->
<<"41">>; % Set background color
parse_attr({bg, 42}) ->
<<"42">>; % Set background color
parse_attr({bg, 43}) ->
<<"43">>; % Set background color
parse_attr({bg, 44}) ->
<<"44">>; % Set background color
parse_attr({bg, 45}) ->
<<"45">>; % Set background color
parse_attr({bg, 46}) ->
<<"46">>; % Set background color
parse_attr({bg, 47}) ->
<<"47">>; % Set background color
parse_attr({bg, {R, G, B}}) ->
format_bin("48;5;~p", [rgb_to_ansi_color(R, G, B)]);
parse_attr({bg, 49}) ->
<<"49">>; % Default background color | implementation defined (according to standard)
parse_attr({bg, 100}) ->
<<"100">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 101}) ->
<<"101">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 102}) ->
<<"102">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 103}) ->
<<"103">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 104}) ->
<<"104">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 105}) ->
<<"105">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 106}) ->
<<"106">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg, 107}) ->
<<"107">>; % Set bright background color | aixterm (not in standard.
parse_attr(framed) ->
<<"51">>; % Framed
parse_attr(encircled) ->
<<"52">>; % Encircled.
parse_attr(overlined) ->
<<"53">>; % Overlined.
% parse_attr(_NotImplementedYet) ->
% 54; % Not framed or encircled.
% parse_attr(_NotImplementedYet) ->
% 55; % Not overlined.
% parse_attr(_NotImplementedYet) ->
% 60; % ideogram underline | hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 61; % ideogram double underline | hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 62; % ideogram overline | hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 63; % ideogram double overline | hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 64; % ideogram stress marking | hardly ever supported.
% parse_attr(_NotImplementedYet) ->
% 65; % ideogram attributes off | reset the effects of all of 60-64.
parse_attr({fg_bright, 1}) ->
<<"90">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 2}) ->
<<"91">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 3}) ->
<<"92">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 4}) ->
<<"93">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 5}) ->
<<"94">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 6}) ->
<<"95">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 7}) ->
<<"96">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({fg_bright, 8}) ->
<<"97">>; % Set bright foreground color | aixterm (not in standard).
parse_attr({bg_bright, 1}) ->
<<"100">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 2}) ->
<<"101">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 3}) ->
<<"102">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 4}) ->
<<"103">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 5}) ->
<<"104">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 6}) ->
<<"105">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 7}) ->
<<"106">>; % Set bright background color | aixterm (not in standard.
parse_attr({bg_bright, 8}) ->
<<"107">>; % Set bright background color | aixterm (not in standard.
parse_attr(clear_line) ->
<<"2K">>; % Clear Line,
parse_attr(cursor_pos) ->
<<"6n">>;
parse_attr({cursor_pos, {Ln, Col}}) ->
format_bin("~p;~pH", [Ln, Col]); % Put the cursor at line L and column C.
parse_attr({cursor_up, N}) ->
format_bin("~pA", [N]); % Move the cursor up N lines")
parse_attr(cursor_up) ->
<<"1A">>; % Move the cursor up 1 line
parse_attr({cursor_down, N}) ->
format_bin("~pB", [N]); % Move the cursor down N lines")
parse_attr(cursor_down) ->
<<"1B">>; % Move the cursor down 1 line
parse_attr({cursor_right, N}) ->
format_bin("~pC", [N]); % Move the cursor forward N columns")
parse_attr(cursor_right) ->
<<"1C">>; % Move the cursor forward 1 line
parse_attr({cursor_left, N}) ->
format_bin("~pD", [N]); % Move the cursor backward N columns\n")
parse_attr(cursor_left) ->
<<"1D">>; % Move the cursor backward 1 line
parse_attr(clear) ->
<<"2J">>; % Clear the screen, move to (0,0)")
parse_attr(erase) ->
<<"K">>; % Erase to end of line
parse_attr(cursor_save) ->
<<"s">>; % Save cursor position
parse_attr(cursor_restore) ->
<<"u">>. % Restore cursor position
ansi_fg_color(Color) ->
case Color of
black -> 30;
red -> 31;
green -> 32;
yellow -> 33;
blue -> 34;
magenta -> 35;
cyan -> 36;
white -> 37;
bright_black -> 90;
bright_red -> 91;
bright_green -> 92;
bright_yellow -> 93;
bright_blue -> 94;
bright_magenta -> 95;
bright_cyan -> 96;
bright_white -> 97
end.
ansi_bg_color(Color) ->
ansi_fg_color(Color) + 10.
rgb_to_ansi_color(R, R, R) when R < 8 ->
16;
rgb_to_ansi_color(R, R, R) when R > 248 ->
231;
rgb_to_ansi_color(R, R, R) ->
round(((R - 8) / 247) * 24 + 232);
rgb_to_ansi_color(R, G, B) ->
16
+ (36 * round(R / 255 * 5))
+ (6 * round(G / 255 * 5))
+ round(B / 255 * 5).
format_bin(Fmt, Args) ->
to_bin(io_lib:format(Fmt, Args)).
to_bin(Bin) when is_binary(Bin) ->
Bin;
to_bin(Str) when is_list(Str) ->
unicode:characters_to_binary(Str);
to_bin(Int) when is_integer(Int) ->
integer_to_binary(Int);
to_bin(Atom) when is_atom(Atom) ->
atom_to_binary(Atom, utf8);
to_bin(Term) ->
unicode:characters_to_binary(io_lib:format("~p", [Term])).