-module(spruce@align).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/align.gleam").
-export([visual_length/1, pad_right/2, pad_left/2, pad_center/2, height/1, size/1, truncate/3, wrap/2]).
-export_type([piece/0, piece_kind/0, escape_state/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" ANSI-aware text alignment helpers.\n").
-type piece() :: {word, binary()} | {spaces, binary()}.
-type piece_kind() :: no_piece | word_piece | space_piece.
-type escape_state() :: normal_text |
escape_start |
csi_escape |
osc_escape |
osc_escape_after_esc.
-file("src/spruce/align.gleam", 149).
-spec in_range(integer(), integer(), integer()) -> boolean().
in_range(Value, First, Last) ->
(Value >= First) andalso (Value =< Last).
-file("src/spruce/align.gleam", 76).
-spec is_escape_final(binary()) -> boolean().
is_escape_final(Grapheme) ->
case gleam@string:to_utf_codepoints(Grapheme) of
[] ->
false;
[Codepoint | _] ->
in_range(gleam_stdlib:identity(Codepoint), 16#40, 16#7e)
end.
-file("src/spruce/align.gleam", 84).
-spec step_escape_state(escape_state(), binary()) -> escape_state().
step_escape_state(State, Char) ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
escape_start;
_ ->
normal_text
end;
escape_start ->
case Char of
<<"["/utf8>> ->
csi_escape;
<<"]"/utf8>> ->
osc_escape;
<<"\x{001b}"/utf8>> ->
escape_start;
_ ->
normal_text
end;
csi_escape ->
case is_escape_final(Char) of
true ->
normal_text;
false ->
csi_escape
end;
osc_escape ->
case Char of
<<"\x{0007}"/utf8>> ->
normal_text;
<<"\x{001b}"/utf8>> ->
osc_escape_after_esc;
_ ->
osc_escape
end;
osc_escape_after_esc ->
case Char of
<<"\\"/utf8>> ->
normal_text;
<<"\x{0007}"/utf8>> ->
normal_text;
<<"\x{001b}"/utf8>> ->
osc_escape_after_esc;
_ ->
osc_escape
end
end.
-file("src/spruce/align.gleam", 132).
-spec is_wide(integer()) -> boolean().
is_wide(Codepoint) ->
((((((((((((in_range(Codepoint, 16#1100, 16#115f) orelse in_range(
Codepoint,
16#2e80,
16#303e
))
orelse in_range(Codepoint, 16#3041, 16#33ff))
orelse in_range(Codepoint, 16#3400, 16#4dbf))
orelse in_range(Codepoint, 16#4e00, 16#9fff))
orelse in_range(Codepoint, 16#a000, 16#a4cf))
orelse in_range(Codepoint, 16#ac00, 16#d7a3))
orelse in_range(Codepoint, 16#f900, 16#faff))
orelse in_range(Codepoint, 16#fe10, 16#fe19))
orelse in_range(Codepoint, 16#fe30, 16#fe6f))
orelse in_range(Codepoint, 16#ff00, 16#ff60))
orelse in_range(Codepoint, 16#ffe0, 16#ffe6))
orelse in_range(Codepoint, 16#1f300, 16#1faff))
orelse in_range(Codepoint, 16#20000, 16#3fffd).
-file("src/spruce/align.gleam", 123).
-spec is_zero_width(integer()) -> boolean().
is_zero_width(Codepoint) ->
((((in_range(Codepoint, 16#0300, 16#036f) orelse (Codepoint =:= 16#0489))
orelse in_range(Codepoint, 16#200b, 16#200f))
orelse (Codepoint =:= 16#200d))
orelse in_range(Codepoint, 16#fe00, 16#fe0f))
orelse (Codepoint =:= 16#feff).
-file("src/spruce/align.gleam", 51).
-spec display_width(binary()) -> integer().
display_width(Grapheme) ->
case gleam@string:to_utf_codepoints(Grapheme) of
[] ->
0;
[Codepoint | _] ->
Codepoint@1 = gleam_stdlib:identity(Codepoint),
case is_zero_width(Codepoint@1) of
true ->
0;
false ->
case is_wide(Codepoint@1) of
true ->
2;
false ->
1
end
end
end.
-file("src/spruce/align.gleam", 35).
-spec count_visible(list(binary()), escape_state(), integer()) -> integer().
count_visible(Chars, State, Count) ->
case Chars of
[] ->
Count;
[Char | Rest] ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
count_visible(
Rest,
step_escape_state(normal_text, Char),
Count
);
_ ->
count_visible(
Rest,
normal_text,
Count + display_width(Char)
)
end;
_ ->
count_visible(Rest, step_escape_state(State, Char), Count)
end
end.
-file("src/spruce/align.gleam", 69).
-spec is_ascii(binary()) -> boolean().
is_ascii(Text) ->
_pipe = gleam@string:to_utf_codepoints(Text),
gleam@list:fold(
_pipe,
true,
fun(All_ascii, Codepoint) ->
All_ascii andalso (gleam_stdlib:identity(Codepoint) < 16#80)
end
).
-file("src/spruce/align.gleam", 28).
?DOC(" The visual length of a string, excluding ANSI escape codes.\n").
-spec visual_length(binary()) -> integer().
visual_length(Text) ->
case gleam_stdlib:contains_string(Text, <<"\x{001b}"/utf8>>) orelse not is_ascii(
Text
) of
true ->
count_visible(gleam@string:to_graphemes(Text), normal_text, 0);
false ->
string:length(Text)
end.
-file("src/spruce/align.gleam", 155).
?DOC(
" Pad `text` on the right with spaces until it reaches `width` visual columns.\n"
" Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_right(binary(), integer()) -> binary().
pad_right(Text, Width) ->
Padding = Width - visual_length(Text),
case Padding > 0 of
true ->
<<Text/binary, (gleam@string:repeat(<<" "/utf8>>, Padding))/binary>>;
false ->
Text
end.
-file("src/spruce/align.gleam", 165).
?DOC(
" Pad `text` on the left with spaces until it reaches `width` visual columns.\n"
" Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_left(binary(), integer()) -> binary().
pad_left(Text, Width) ->
Padding = Width - visual_length(Text),
case Padding > 0 of
true ->
<<(gleam@string:repeat(<<" "/utf8>>, Padding))/binary, Text/binary>>;
false ->
Text
end.
-file("src/spruce/align.gleam", 176).
?DOC(
" Pad `text` on both sides with spaces until it reaches `width` visual columns.\n"
" When an odd number of spaces is needed, the extra space is placed on the\n"
" right. Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_center(binary(), integer()) -> binary().
pad_center(Text, Width) ->
Padding = Width - visual_length(Text),
case Padding > 0 of
true ->
Left = Padding div 2,
Right = Padding - Left,
<<<<(gleam@string:repeat(<<" "/utf8>>, Left))/binary, Text/binary>>/binary,
(gleam@string:repeat(<<" "/utf8>>, Right))/binary>>;
false ->
Text
end.
-file("src/spruce/align.gleam", 189).
?DOC(" Count the number of lines in `text`.\n").
-spec height(binary()) -> integer().
height(Text) ->
_pipe = Text,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
erlang:length(_pipe@1).
-file("src/spruce/align.gleam", 196).
?DOC(" Return the widest visual line and the number of lines in `text`.\n").
-spec size(binary()) -> {integer(), integer()}.
size(Text) ->
Lines = gleam@string:split(Text, <<"\n"/utf8>>),
Width = begin
_pipe = Lines,
_pipe@1 = gleam@list:map(_pipe, fun visual_length/1),
gleam@list:fold(_pipe@1, 0, fun gleam@int:max/2)
end,
{Width, erlang:length(Lines)}.
-file("src/spruce/align.gleam", 707).
-spec chars_to_string(list(binary())) -> binary().
chars_to_string(Chars) ->
gleam@string:join(Chars, <<""/utf8>>).
-file("src/spruce/align.gleam", 615).
-spec take_visible_loop(
list(binary()),
integer(),
escape_state(),
list(binary())
) -> binary().
take_visible_loop(Chars, Remaining, State, Acc) ->
case Chars of
[] ->
chars_to_string(lists:reverse(Acc));
[Char | Rest] ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
take_visible_loop(
Rest,
Remaining,
step_escape_state(normal_text, Char),
[Char | Acc]
);
_ ->
Char_width = display_width(Char),
case (Char_width =:= 0) orelse (Char_width =< Remaining) of
true ->
take_visible_loop(
Rest,
Remaining - Char_width,
normal_text,
[Char | Acc]
);
false ->
take_visible_loop(Rest, 0, normal_text, Acc)
end
end;
_ ->
take_visible_loop(
Rest,
Remaining,
step_escape_state(State, Char),
[Char | Acc]
)
end
end.
-file("src/spruce/align.gleam", 608).
-spec take_visible(binary(), integer()) -> binary().
take_visible(Text, Width) ->
case Width =< 0 of
true ->
take_visible_loop(
gleam@string:to_graphemes(Text),
0,
normal_text,
[]
);
false ->
take_visible_loop(
gleam@string:to_graphemes(Text),
Width,
normal_text,
[]
)
end.
-file("src/spruce/align.gleam", 544).
-spec sgr_has_param(binary(), binary()) -> boolean().
sgr_has_param(Escape, Param) ->
((gleam_stdlib:contains_string(
Escape,
<<<<"["/utf8, Param/binary>>/binary, "m"/utf8>>
)
orelse gleam_stdlib:contains_string(
Escape,
<<<<"["/utf8, Param/binary>>/binary, ";"/utf8>>
))
orelse gleam_stdlib:contains_string(
Escape,
<<<<";"/utf8, Param/binary>>/binary, "m"/utf8>>
))
orelse gleam_stdlib:contains_string(
Escape,
<<<<";"/utf8, Param/binary>>/binary, ";"/utf8>>
).
-file("src/spruce/align.gleam", 537).
-spec is_sgr_reset(binary()) -> boolean().
is_sgr_reset(Escape) ->
((gleam_stdlib:contains_string(Escape, <<"[m"/utf8>>) orelse sgr_has_param(
Escape,
<<"0"/utf8>>
))
orelse sgr_has_param(Escape, <<"39"/utf8>>))
orelse sgr_has_param(Escape, <<"49"/utf8>>).
-file("src/spruce/align.gleam", 530).
-spec update_sgr_active(binary(), binary(), boolean()) -> boolean().
update_sgr_active(Escape, Final, Active) ->
case Final of
<<"m"/utf8>> ->
not is_sgr_reset(Escape);
_ ->
Active
end.
-file("src/spruce/align.gleam", 473).
-spec has_open_sgr_loop(list(binary()), escape_state(), binary(), boolean()) -> boolean().
has_open_sgr_loop(Chars, State, Escape, Active) ->
case Chars of
[] ->
Active;
[Char | Rest] ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
has_open_sgr_loop(
Rest,
step_escape_state(normal_text, Char),
Char,
Active
);
_ ->
has_open_sgr_loop(
Rest,
normal_text,
<<""/utf8>>,
Active
)
end;
escape_start ->
case Char of
<<"["/utf8>> ->
has_open_sgr_loop(
Rest,
csi_escape,
<<Escape/binary, Char/binary>>,
Active
);
<<"]"/utf8>> ->
has_open_sgr_loop(
Rest,
osc_escape,
<<""/utf8>>,
Active
);
_ ->
has_open_sgr_loop(
Rest,
step_escape_state(escape_start, Char),
<<""/utf8>>,
Active
)
end;
csi_escape ->
Escape@1 = <<Escape/binary, Char/binary>>,
Active@1 = case is_escape_final(Char) of
true ->
update_sgr_active(Escape@1, Char, Active);
false ->
Active
end,
has_open_sgr_loop(
Rest,
step_escape_state(csi_escape, Char),
Escape@1,
Active@1
);
osc_escape ->
has_open_sgr_loop(
Rest,
step_escape_state(State, Char),
<<""/utf8>>,
Active
);
osc_escape_after_esc ->
has_open_sgr_loop(
Rest,
step_escape_state(State, Char),
<<""/utf8>>,
Active
)
end
end.
-file("src/spruce/align.gleam", 469).
-spec has_open_sgr(binary()) -> boolean().
has_open_sgr(Text) ->
has_open_sgr_loop(
gleam@string:to_graphemes(Text),
normal_text,
<<""/utf8>>,
false
).
-file("src/spruce/align.gleam", 464).
-spec close_open_sgr(binary()) -> binary().
close_open_sgr(Text) ->
gleam@bool:guard(
not has_open_sgr(Text),
Text,
fun() -> <<Text/binary, "\x{001b}[0m"/utf8>> end
).
-file("src/spruce/align.gleam", 212).
?DOC(
" Truncate `text` to at most `width` visual columns, appending `ellipsis` when\n"
" truncation is needed.\n"
"\n"
" ANSI escape sequences do not count toward the width and are never split.\n"
" If `ellipsis` is wider than `width`, the ellipsis itself is visibly\n"
" truncated to fit. Widths less than or equal to zero return an empty string.\n"
).
-spec truncate(binary(), integer(), binary()) -> binary().
truncate(Text, Width, Ellipsis) ->
gleam@bool:guard(
Width =< 0,
<<""/utf8>>,
fun() ->
gleam@bool:guard(
visual_length(Text) =< Width,
Text,
fun() ->
Ellipsis_width = visual_length(Ellipsis),
case Ellipsis_width >= Width of
true ->
take_visible(Ellipsis, Width);
false ->
<<(close_open_sgr(
take_visible(Text, Width - Ellipsis_width)
))/binary,
Ellipsis/binary>>
end
end
)
end
).
-file("src/spruce/align.gleam", 354).
-spec drop_spaces(list(binary())) -> list(binary()).
drop_spaces(Chars) ->
case Chars of
[<<" "/utf8>> | Rest] ->
drop_spaces(Rest);
_ ->
Chars
end.
-file("src/spruce/align.gleam", 345).
-spec trim_trailing_spaces(binary()) -> binary().
trim_trailing_spaces(Text) ->
_pipe = Text,
_pipe@1 = gleam@string:to_graphemes(_pipe),
_pipe@2 = lists:reverse(_pipe@1),
_pipe@3 = drop_spaces(_pipe@2),
_pipe@4 = lists:reverse(_pipe@3),
chars_to_string(_pipe@4).
-file("src/spruce/align.gleam", 338).
-spec push_wrapped_line(binary(), list(binary())) -> list(binary()).
push_wrapped_line(Line, Lines) ->
case trim_trailing_spaces(Line) of
<<""/utf8>> ->
Lines;
Trimmed ->
[close_open_sgr(Trimmed) | Lines]
end.
-file("src/spruce/align.gleam", 663).
-spec drop_visible_loop(list(binary()), integer(), escape_state()) -> binary().
drop_visible_loop(Chars, Remaining, State) ->
case Chars of
[] ->
<<""/utf8>>;
[Char | Rest] ->
case State of
normal_text ->
case Remaining =< 0 of
true ->
chars_to_string(Chars);
false ->
case Char of
<<"\x{001b}"/utf8>> ->
drop_visible_loop(
Rest,
Remaining,
step_escape_state(normal_text, Char)
);
_ ->
Char_width = display_width(Char),
case Char_width > Remaining of
true ->
chars_to_string(Chars);
false ->
drop_visible_loop(
Rest,
Remaining - Char_width,
normal_text
)
end
end
end;
_ ->
drop_visible_loop(
Rest,
Remaining,
step_escape_state(State, Char)
)
end
end.
-file("src/spruce/align.gleam", 659).
-spec drop_visible(binary(), integer()) -> binary().
drop_visible(Text, Width) ->
drop_visible_loop(gleam@string:to_graphemes(Text), Width, normal_text).
-file("src/spruce/align.gleam", 580).
-spec first_positive_width_loop(list(binary()), escape_state()) -> integer().
first_positive_width_loop(Chars, State) ->
case Chars of
[] ->
0;
[Char | Rest] ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
first_positive_width_loop(
Rest,
step_escape_state(normal_text, Char)
);
_ ->
Width = display_width(Char),
case Width of
0 ->
first_positive_width_loop(Rest, normal_text);
_ ->
Width
end
end;
_ ->
first_positive_width_loop(
Rest,
step_escape_state(State, Char)
)
end
end.
-file("src/spruce/align.gleam", 576).
-spec first_positive_width(binary()) -> integer().
first_positive_width(Text) ->
first_positive_width_loop(gleam@string:to_graphemes(Text), normal_text).
-file("src/spruce/align.gleam", 551).
-spec wrap_long_word(binary(), integer()) -> list(binary()).
wrap_long_word(Word, Width) ->
case visual_length(Word) =< Width of
true ->
[Word];
false ->
Chunk = take_visible(Word, Width),
case visual_length(Chunk) =:= 0 of
true ->
First_width = first_positive_width(Word),
case First_width of
0 ->
[Word];
_ ->
[take_visible(Word, First_width) |
wrap_long_word(
drop_visible(Word, First_width),
Width
)]
end;
false ->
[Chunk | wrap_long_word(drop_visible(Word, Width), Width)]
end
end.
-file("src/spruce/align.gleam", 452).
-spec add_wrapped_chunks(list(binary()), list(binary())) -> {binary(),
integer(),
list(binary())}.
add_wrapped_chunks(Chunks, Lines) ->
case Chunks of
[] ->
{<<""/utf8>>, 0, Lines};
[Chunk] ->
{Chunk, visual_length(Chunk), Lines};
[Chunk@1 | Rest] ->
add_wrapped_chunks(Rest, [close_open_sgr(Chunk@1) | Lines])
end.
-file("src/spruce/align.gleam", 248).
-spec wrap_pieces(list(piece()), integer(), binary(), integer(), list(binary())) -> list(binary()).
wrap_pieces(Pieces, Width, Current, Current_width, Lines) ->
case Pieces of
[] ->
case Current of
<<""/utf8>> ->
lists:reverse(Lines);
_ ->
lists:reverse([Current | Lines])
end;
[{spaces, Spaces} | Rest] ->
Spaces_width = visual_length(Spaces),
case (Current_width + Spaces_width) =< Width of
true ->
wrap_pieces(
Rest,
Width,
<<Current/binary, Spaces/binary>>,
Current_width + Spaces_width,
Lines
);
false ->
case Current_width of
0 ->
{Next_current, Next_width, Next_lines} = add_wrapped_chunks(
wrap_long_word(Spaces, Width),
Lines
),
wrap_pieces(
Rest,
Width,
Next_current,
Next_width,
Next_lines
);
_ ->
wrap_pieces(
Rest,
Width,
<<""/utf8>>,
0,
push_wrapped_line(Current, Lines)
)
end
end;
[{word, Word} | Rest@1] ->
Word_width = visual_length(Word),
case Word_width > Width of
true ->
case Current_width of
0 ->
{Next_current@1, Next_width@1, Next_lines@1} = add_wrapped_chunks(
wrap_long_word(Word, Width),
Lines
),
wrap_pieces(
Rest@1,
Width,
Next_current@1,
Next_width@1,
Next_lines@1
);
_ ->
wrap_pieces(
Pieces,
Width,
<<""/utf8>>,
0,
push_wrapped_line(Current, Lines)
)
end;
false ->
case (Current_width + Word_width) =< Width of
true ->
wrap_pieces(
Rest@1,
Width,
<<Current/binary, Word/binary>>,
Current_width + Word_width,
Lines
);
false ->
wrap_pieces(
Pieces,
Width,
<<""/utf8>>,
0,
push_wrapped_line(Current, Lines)
)
end
end
end.
-file("src/spruce/align.gleam", 437).
-spec push_piece(piece_kind(), binary(), list(piece())) -> list(piece()).
push_piece(Kind, Current, Pieces) ->
case Current of
<<""/utf8>> ->
Pieces;
_ ->
case Kind of
space_piece ->
[{spaces, Current} | Pieces];
_ ->
[{word, Current} | Pieces]
end
end.
-file("src/spruce/align.gleam", 422).
-spec add_piece_grapheme(
binary(),
piece_kind(),
binary(),
piece_kind(),
list(piece())
) -> {binary(), piece_kind(), list(piece())}.
add_piece_grapheme(Char, Char_kind, Current, Kind, Pieces) ->
case {Kind, Char_kind} of
{no_piece, _} ->
{Char, Char_kind, Pieces};
{space_piece, space_piece} ->
{<<Current/binary, Char/binary>>, Kind, Pieces};
{word_piece, word_piece} ->
{<<Current/binary, Char/binary>>, Kind, Pieces};
{_, _} ->
{Char, Char_kind, push_piece(Kind, Current, Pieces)}
end.
-file("src/spruce/align.gleam", 365).
-spec split_pieces_loop(
list(binary()),
escape_state(),
binary(),
piece_kind(),
list(piece())
) -> list(piece()).
split_pieces_loop(Chars, State, Current, Kind, Pieces) ->
case Chars of
[] ->
lists:reverse(push_piece(Kind, Current, Pieces));
[Char | Rest] ->
case State of
normal_text ->
case Char of
<<"\x{001b}"/utf8>> ->
Kind@1 = case Kind of
no_piece ->
word_piece;
_ ->
Kind
end,
split_pieces_loop(
Rest,
step_escape_state(normal_text, Char),
<<Current/binary, Char/binary>>,
Kind@1,
Pieces
);
<<" "/utf8>> ->
{Current@1, Kind@2, Pieces@1} = add_piece_grapheme(
<<" "/utf8>>,
space_piece,
Current,
Kind,
Pieces
),
split_pieces_loop(
Rest,
normal_text,
Current@1,
Kind@2,
Pieces@1
);
_ ->
{Current@2, Kind@3, Pieces@2} = add_piece_grapheme(
Char,
word_piece,
Current,
Kind,
Pieces
),
split_pieces_loop(
Rest,
normal_text,
Current@2,
Kind@3,
Pieces@2
)
end;
_ ->
split_pieces_loop(
Rest,
step_escape_state(State, Char),
<<Current/binary, Char/binary>>,
Kind,
Pieces
)
end
end.
-file("src/spruce/align.gleam", 361).
-spec split_pieces(binary()) -> list(piece()).
split_pieces(Text) ->
split_pieces_loop(
gleam@string:to_graphemes(Text),
normal_text,
<<""/utf8>>,
no_piece,
[]
).
-file("src/spruce/align.gleam", 241).
-spec wrap_line(binary(), integer()) -> binary().
wrap_line(Line, Width) ->
_pipe = Line,
_pipe@1 = split_pieces(_pipe),
_pipe@2 = wrap_pieces(_pipe@1, Width, <<""/utf8>>, 0, []),
gleam@string:join(_pipe@2, <<"\n"/utf8>>).
-file("src/spruce/align.gleam", 233).
?DOC(
" Wrap `text` to `width` visual columns.\n"
"\n"
" ANSI escape sequences do not count toward the width and are never split.\n"
" Words longer than `width` are hard-wrapped at visible column boundaries.\n"
" Widths less than or equal to zero return the input unchanged.\n"
).
-spec wrap(binary(), integer()) -> binary().
wrap(Text, Width) ->
gleam@bool:guard(Width =< 0, Text, fun() -> _pipe = Text,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(_capture) -> wrap_line(_capture, Width) end
),
gleam@string:join(_pipe@2, <<"\n"/utf8>>) end).