Skip to main content

src/spruce@tree.erl

-module(spruce@tree).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/tree.gleam").
-export([root/1, child/2, ascii/1, enumerator/2, render/2]).
-export_type([branches/0, tree/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(
    " Pure nested tree rendering.\n"
    "\n"
    " Trees are data structures rendered with an explicit `Spruce` context. The\n"
    " default renderer uses Unicode branch glyphs when color is supported and a\n"
    " deterministic ASCII fallback when it is not.\n"
).

-type branches() :: auto |
    ascii |
    {custom, fun((integer(), boolean()) -> binary())}.

-opaque tree() :: {tree, binary(), list(tree()), branches()}.

-file("src/spruce/tree.gleam", 25).
?DOC(" Build a tree root with no children.\n").
-spec root(binary()) -> tree().
root(Label) ->
    {tree, Label, [], auto}.

-file("src/spruce/tree.gleam", 30).
?DOC(" Add a child to `parent`, preserving insertion order.\n").
-spec child(tree(), tree()) -> tree().
child(Parent, Child) ->
    {tree,
        erlang:element(2, Parent),
        lists:append(erlang:element(3, Parent), [Child]),
        erlang:element(4, Parent)}.

-file("src/spruce/tree.gleam", 35).
?DOC(" Force deterministic ASCII branch markers regardless of color support.\n").
-spec ascii(tree()) -> tree().
ascii(Tree) ->
    {tree, erlang:element(2, Tree), erlang:element(3, Tree), ascii}.

-file("src/spruce/tree.gleam", 44).
?DOC(
    " Set a custom branch enumerator for the rendered tree.\n"
    "\n"
    " The function receives the one-based depth of the node being rendered and\n"
    " whether it is the last child of its parent. It should return the complete\n"
    " branch marker to place before that node's first label line.\n"
).
-spec enumerator(tree(), fun((integer(), boolean()) -> binary())) -> tree().
enumerator(Tree, Branch) ->
    {tree, erlang:element(2, Tree), erlang:element(3, Tree), {custom, Branch}}.

-file("src/spruce/tree.gleam", 107).
-spec render_label(binary(), binary(), binary()) -> list(binary()).
render_label(Prefix, Follow, Label) ->
    case gleam@string:split(Label, <<"\n"/utf8>>) of
        [] ->
            [Prefix];

        [First | Rest] ->
            [<<Prefix/binary, First/binary>> |
                gleam@list:map(
                    Rest,
                    fun(Line) -> <<Follow/binary, Line/binary>> end
                )]
    end.

-file("src/spruce/tree.gleam", 196).
-spec spaces_like(binary()) -> binary().
spaces_like(Text) ->
    gleam@string:repeat(<<" "/utf8>>, spruce@align:visual_length(Text)).

-file("src/spruce/tree.gleam", 191).
-spec ascii_ancestor(boolean()) -> binary().
ascii_ancestor(Last) ->
    gleam@bool:guard(Last, <<"   "/utf8>>, fun() -> <<"|  "/utf8>> end).

-file("src/spruce/tree.gleam", 186).
-spec unicode_ancestor(boolean()) -> binary().
unicode_ancestor(Last) ->
    gleam@bool:guard(Last, <<"   "/utf8>>, fun() -> <<"│  "/utf8>> end).

-file("src/spruce/tree.gleam", 158).
-spec ancestor_token(spruce:spruce(), branches(), integer(), boolean()) -> binary().
ancestor_token(Sp, Branches, Depth, Last) ->
    case Branches of
        auto ->
            case spruce:supports_color(Sp) of
                true ->
                    unicode_ancestor(Last);

                false ->
                    ascii_ancestor(Last)
            end;

        ascii ->
            ascii_ancestor(Last);

        {custom, Branch} ->
            spaces_like(Branch(Depth, Last))
    end.

-file("src/spruce/tree.gleam", 149).
-spec follow_token(spruce:spruce(), branches(), integer(), boolean()) -> binary().
follow_token(Sp, Branches, Depth, Last) ->
    ancestor_token(Sp, Branches, Depth, Last).

-file("src/spruce/tree.gleam", 181).
-spec ascii_branch(boolean()) -> binary().
ascii_branch(Last) ->
    gleam@bool:guard(Last, <<"`- "/utf8>>, fun() -> <<"|- "/utf8>> end).

-file("src/spruce/tree.gleam", 176).
-spec unicode_branch(boolean()) -> binary().
unicode_branch(Last) ->
    gleam@bool:guard(Last, <<"└─ "/utf8>>, fun() -> <<"├─ "/utf8>> end).

-file("src/spruce/tree.gleam", 131).
-spec branch_token(spruce:spruce(), branches(), integer(), boolean()) -> binary().
branch_token(Sp, Branches, Depth, Last) ->
    case Branches of
        auto ->
            case spruce:supports_color(Sp) of
                true ->
                    unicode_branch(Last);

                false ->
                    ascii_branch(Last)
            end;

        ascii ->
            ascii_branch(Last);

        {custom, Branch} ->
            Branch(Depth, Last)
    end.

-file("src/spruce/tree.gleam", 117).
-spec ancestor_prefix(spruce:spruce(), branches(), list(boolean()), integer()) -> binary().
ancestor_prefix(Sp, Branches, Ancestors, Depth) ->
    case Ancestors of
        [] ->
            <<""/utf8>>;

        [Last | Rest] ->
            <<(ancestor_token(Sp, Branches, Depth, Last))/binary,
                (ancestor_prefix(Sp, Branches, Rest, Depth + 1))/binary>>
    end.

-file("src/spruce/tree.gleam", 83).
-spec render_node(
    spruce:spruce(),
    tree(),
    branches(),
    integer(),
    list(boolean()),
    binary(),
    boolean()
) -> list(binary()).
render_node(Sp, Tree, Branches, Depth, Ancestors, Base, Last) ->
    Ancestor = ancestor_prefix(Sp, Branches, Ancestors, 1),
    Prefix = <<<<Base/binary, Ancestor/binary>>/binary,
        (branch_token(Sp, Branches, Depth, Last))/binary>>,
    Follow = <<<<Base/binary, Ancestor/binary>>/binary,
        (follow_token(Sp, Branches, Depth, Last))/binary>>,
    _pipe = render_label(Prefix, Follow, erlang:element(2, Tree)),
    lists:append(
        _pipe,
        render_children(
            Sp,
            erlang:element(3, Tree),
            Branches,
            Depth + 1,
            lists:append(Ancestors, [Last]),
            Base
        )
    ).

-file("src/spruce/tree.gleam", 65).
-spec render_children(
    spruce:spruce(),
    list(tree()),
    branches(),
    integer(),
    list(boolean()),
    binary()
) -> list(binary()).
render_children(Sp, Children, Branches, Depth, Ancestors, Base) ->
    case Children of
        [] ->
            [];

        [Last_child] ->
            render_node(Sp, Last_child, Branches, Depth, Ancestors, Base, true);

        [First | Rest] ->
            _pipe = render_node(
                Sp,
                First,
                Branches,
                Depth,
                Ancestors,
                Base,
                false
            ),
            lists:append(
                _pipe,
                render_children(Sp, Rest, Branches, Depth, Ancestors, Base)
            )
    end.

-file("src/spruce/tree.gleam", 49).
?DOC(" Render a tree to a string.\n").
-spec render(spruce:spruce(), tree()) -> binary().
render(Sp, Tree) ->
    Base = gleam@string:repeat(<<"  "/utf8>>, spruce:depth(Sp)),
    Lines = begin
        _pipe = render_label(Base, Base, erlang:element(2, Tree)),
        lists:append(
            _pipe,
            render_children(
                Sp,
                erlang:element(3, Tree),
                erlang:element(4, Tree),
                1,
                [],
                Base
            )
        )
    end,
    gleam@string:join(Lines, <<"\n"/utf8>>).