%%%% lfmt_fezzik_render: the mutually-recursive rendering core (one SCC).
%%%% Layer: render depends on util (one-way); fezzik calls print_node/3 here.
-module(lfmt_fezzik_render).
-include("lfmt_fezzik.hrl").
-export([print_node/3]).
%%====================================================================
%% Internal: main printer — flat vs broken decision
%%====================================================================
%% print_node: print a node starting at column Col.
%% Returns {IO, NewCol} where NewCol is the column after the last printed char.
%% Flat if: no multi-line token, fits in WIDTH, and no trivia that would be
%% lost in flat mode (dangling on this node, or any trivia on any descendant).
%% The node's own leading/trailing are always emitted by the parent context and
%% do NOT prevent flat rendering.
%% InData: true when inside a quote/quasiquote context (data, not code).
-spec print_node(lfmt_fezzik_cst:cst_node(), non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer()}.
print_node(Node, Col, InData) ->
W = lfmt_fezzik_util:flat_width(Node),
Fits = W =/= infinity
andalso Col + W =< ?WIDTH
andalso not lfmt_fezzik_util:has_internal_trivia(Node),
case Fits of
true -> {lfmt_fezzik_util:flat_render(Node), Col + W};
false -> print_broken(Node, Col, InData)
end.
%%====================================================================
%% Internal: broken printing
%%====================================================================
%% print_broken: broken form for containers, prefixed, and multi-line leaves.
%% Transitions InData at quote/quasiquote (→ true) and unquote/-splicing (→ false).
-spec print_broken(lfmt_fezzik_cst:cst_node(), non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer()}.
print_broken(Node, Col, InData) ->
case lfmt_fezzik_cst:type(Node) of
T when T =:= list; T =:= tuple; T =:= map; T =:= binary; T =:= eval ->
print_broken_container(Node, Col, InData);
prefixed ->
PfxText = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:prefix(Node)),
PfxKind = lfmt_fezzik_lexer:kind(lfmt_fezzik_cst:prefix(Node)),
[Inner] = lfmt_fezzik_cst:children(Node),
InnerInData = case PfxKind of
quote -> true;
quasiquote -> true;
unquote -> false;
unquote_splicing -> false;
_ -> InData
end,
{InnerIO, InnerCol} = print_node(Inner, Col + length(PfxText), InnerInData),
{[PfxText, InnerIO], InnerCol};
_ ->
%% Leaf with multi-line token (tqstring/tqbstring): emit verbatim.
Text = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Node)),
{Text, lfmt_fezzik_util:col_after_text(Text, Col)}
end.
%% print_broken_container: branches on regime/2 (A7·S2b-2).
%% canonical → head-classified indentation (A4) + map pair alignment (S3a)
%% break_preserving → author break positions preserved (A7·S2b)
%%
%% Dangling trivia always at C+2; close on its own line when dangling present
%% or last child has trailing comment. All A3 trivia rules unchanged.
-spec print_broken_container(lfmt_fezzik_cst:cst_node(), non_neg_integer(),
boolean()) ->
{iolist(), non_neg_integer()}.
print_broken_container(Node, C, InData) ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Node)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Node)),
Children = lfmt_fezzik_cst:children(Node),
Dangling = lfmt_fezzik_cst:dangling(Node),
Indent = C + 2,
IndentStr = lists:duplicate(Indent, $\s),
CIndStr = lists:duplicate(C, $\s),
CloseLen = length(Close),
OpenLen = length(Open),
case Children of
[] ->
case Dangling of
[] ->
{[Open, Close], C + OpenLen + CloseLen};
_ ->
DangIO = lfmt_fezzik_util:emit_dangling(Dangling, IndentStr),
{[Open, DangIO, "\n", CIndStr, Close], C + CloseLen}
end;
[Head | RestChildren] ->
case lfmt_fezzik_util:regime(Node, InData) of
canonical ->
case lfmt_fezzik_util:head_has_leading_comment(Head) of
true ->
%% Opener alone; all children at Indent (fix1 idempotency).
{AllIO, LastCol, HasTrail} =
print_rest_loop([Head | RestChildren],
Indent, IndentStr, true, InData),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[Open, AllIO, CloseIO], CloseCol};
false ->
case lfmt_fezzik_cst:type(Node) of
T when T =:= list; T =:= eval ->
Class = lfmt_fezzik_util:classify_head(Head),
print_classified(Class, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData);
map ->
print_map_pairs(Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData)
end
end;
break_preserving ->
print_bp_container(Node, C, Open, OpenLen, Close, CloseLen,
Head, RestChildren, Dangling,
Indent, IndentStr, CIndStr, InData)
end
end.
%%====================================================================
%% Internal: break-preserving renderer (A7·S2b-2)
%%====================================================================
%% print_bp_container: render a break-preserving container.
%%
%% Flat path is already handled by print_node (tried before print_broken is called).
%% Here we are in the broken path.
%%
%% If Head has a leading comment: opener alone, all children (incl. Head) via
%% print_rest_loop at Indent — comment safety (same as canonical fix1 path).
%%
%% Otherwise:
%% - If nl_before(Head)=true: head on new line at C+2, AlignCol=C+2.
%% - Else: head on opener line at C+OpenLen.
%% AlignCol = column of first argument:
%% - If first arg has nl_before or head has trailing comment → C+2 (hanging).
%% - Else → HTC+1 (align under first arg).
%%
%% Subsequent children via bp_rest_loop: new line if nl_before OR overflow OR
%% has leading comment; otherwise space-separated on current line.
%% Close hugs last child unless dangling or last-child trailing comment.
-spec print_bp_container(lfmt_fezzik_cst:cst_node(),
non_neg_integer(), string(), non_neg_integer(),
string(), non_neg_integer(),
lfmt_fezzik_cst:cst_node(), [lfmt_fezzik_cst:cst_node()],
[lfmt_fezzik_cst:trivia()],
non_neg_integer(), string(), string(), boolean()) ->
{iolist(), non_neg_integer()}.
print_bp_container(Node, C, Open, _OpenLen, Close, _CloseLen,
Head, RestChildren, Dangling,
Indent, IndentStr, CIndStr, InData) ->
DotTok = lfmt_fezzik_cst:dot_token(Node),
{RestBody, MaybeTail} = lfmt_fezzik_util:split_dot_tail(DotTok, RestChildren),
IsCondHead = (lfmt_fezzik_cst:type(Head) =:= symbol)
andalso (lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Head)) =:= "cond"),
case lfmt_fezzik_util:head_has_leading_comment(Head) of
true ->
case InData of
false ->
%% Code list: opener-alone, all children one-per-line at Indent (unchanged).
{AllIO, LastCol, HasTrail} =
print_rest_loop([Head | RestBody], Indent, IndentStr, true, InData),
{DotIO, DotCol, DotHasTrail} =
lfmt_fezzik_util:apply_dot_suffix(MaybeTail, LastCol, HasTrail),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, DotHasTrail, DotCol,
Indent, IndentStr, C, CIndStr, Close),
{[Open, AllIO, DotIO, CloseIO], CloseCol};
true ->
%% Data list (§3.9): first head comment on opener line; rest +
%% elements at AlignCol = C+len(Open).
AlignCol = C + length(Open),
AlignStr = lists:duplicate(AlignCol, $\s),
HeadLeading = lfmt_fezzik_cst:leading(Head),
Comments = [lfmt_fezzik_lexer:text(Tok)
|| {comment, Tok} <- HeadLeading],
HeadLeadIO =
case Comments of
[] ->
[];
[First | More] ->
MoreIO = [[AlignStr, T, "\n"] || T <- More],
[First, "\n" | MoreIO]
end,
{HeadIO, HeadCol} = print_node(Head, AlignCol, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(Head), HeadCol),
{RestIO, BodyLastCol, BodyHasTrail} =
bp_rest_loop(RestBody, AlignCol, AlignStr, HTC, InData),
{DotIO, DotCol, DotHasTrail} =
lfmt_fezzik_util:apply_dot_suffix(MaybeTail, BodyLastCol, BodyHasTrail),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, DotHasTrail, DotCol,
AlignCol, AlignStr, C, CIndStr, Close),
{[Open, HeadLeadIO, AlignStr, HeadIO, HeadTrailIO,
RestIO, DotIO, CloseIO], CloseCol}
end;
false ->
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(Head), CIndStr),
HeadCol = C + length(Open),
case lfmt_fezzik_cst:nl_before(Head) of
true ->
%% Head on new line at C+2; all args also at C+2.
HangStr = IndentStr,
{HeadIO, HCol} = print_node(Head, Indent, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(Head), HCol),
{RestIO, BodyLastCol, BodyHasTrail} =
case IsCondHead of
true -> bp_clause_rest_loop(RestBody, Indent, HangStr, HTC, InData);
false -> bp_rest_loop(RestBody, Indent, HangStr, HTC, InData)
end,
{DotIO, DotCol, DotHasTrail} =
lfmt_fezzik_util:apply_dot_suffix(MaybeTail, BodyLastCol, BodyHasTrail),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, DotHasTrail, DotCol,
Indent, HangStr, C, CIndStr, Close),
{[HeadLeadIO, Open, "\n", HangStr, HeadIO, HeadTrailIO,
RestIO, DotIO, CloseIO], CloseCol};
false ->
{HeadIO, HCol} = print_node(Head, HeadCol, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(Head), HCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(Head) =/= [],
case RestBody of
[] ->
{DotIO, DotCol, DotHasTrail} =
lfmt_fezzik_util:apply_dot_suffix(MaybeTail, HTC, HeadHasTrail),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, DotHasTrail, DotCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, DotIO, CloseIO], CloseCol};
[FirstArg | OtherArgs] ->
%% AlignCol = column where first arg lands.
%% Hanging (C+2) when: head has trailing comment, first arg
%% has nl_before, OR first arg would overflow the current line.
%% "Overflow" here uses >= so a token at exactly col 80
%% triggers wrapping (col 80 = 81st char on the line, over limit).
FirstArgNL = lfmt_fezzik_cst:nl_before(FirstArg),
FirstArgW = lfmt_fezzik_util:flat_width(FirstArg),
FirstArgOverflows =
FirstArgW =:= infinity
orelse HTC + 1 + FirstArgW >= ?WIDTH,
{AlignCol, AlignStr} =
case HeadHasTrail orelse FirstArgNL orelse FirstArgOverflows of
true -> {Indent, IndentStr};
false -> {HTC + 1, lists:duplicate(HTC + 1, $\s)}
end,
IsMultiline = lfmt_fezzik_cst:multiline(Node),
{RestIO, BodyLastCol, BodyHasTrail} =
case {IsCondHead, IsMultiline orelse OtherArgs =:= []} of
{true, true} ->
bp_clause_rest_loop(RestBody, AlignCol, AlignStr, HTC, InData);
{true, false} ->
{FirstIO, _, _} =
bp_clause_rest_loop([FirstArg], AlignCol, AlignStr, HTC, InData),
{OtherIO, OtherLastCol, OtherHasTrail} =
print_clause_loop(OtherArgs, AlignCol, AlignStr, false, InData),
{[FirstIO, OtherIO], OtherLastCol, OtherHasTrail};
{false, true} ->
bp_rest_loop(RestBody, AlignCol, AlignStr, HTC, InData);
{false, false} ->
{FirstIO, _, _} =
bp_rest_loop([FirstArg], AlignCol, AlignStr, HTC, InData),
{OtherIO, OtherLastCol, OtherHasTrail} =
print_rest_loop(OtherArgs, AlignCol, AlignStr, false, InData),
{[FirstIO, OtherIO], OtherLastCol, OtherHasTrail}
end,
{DotIO, DotCol, DotHasTrail} =
lfmt_fezzik_util:apply_dot_suffix(MaybeTail, BodyLastCol, BodyHasTrail),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, DotHasTrail, DotCol,
AlignCol, AlignStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, RestIO, DotIO, CloseIO],
CloseCol}
end
end
end.
%% bp_rest_loop: render children preserving nl_before break positions.
%% Each child starts a new line iff:
%% • nl_before(Child) is true (author broke here), OR
%% • it has a leading comment (comment safety), OR
%% • it would overflow column 80 on the current line.
%% Otherwise it is appended space-separated on the current line.
-spec bp_rest_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(), string(),
non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
bp_rest_loop([], _AlignCol, _AlignStr, CurCol, _InData) ->
{[], CurCol, false};
bp_rest_loop([Child | Rest], AlignCol, AlignStr, CurCol, InData) ->
W = lfmt_fezzik_util:flat_width(Child),
NlBefore = lfmt_fezzik_cst:nl_before(Child),
HasLead = lfmt_fezzik_util:has_comment_leading(lfmt_fezzik_cst:leading(Child)),
Overflow = W =:= infinity orelse CurCol + 1 + W >= ?WIDTH,
NewLine = NlBefore orelse HasLead orelse Overflow,
{StartCol, Prefix} =
case NewLine of
true -> {AlignCol, ["\n",
lfmt_fezzik_util:emit_child_leading(
lfmt_fezzik_cst:leading(Child), AlignStr, false),
AlignStr]};
false -> {CurCol + 1, " "}
end,
{ChildIO, ChildCol} = print_node(Child, StartCol, InData),
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Child), ChildCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Child) =/= [],
{[Prefix, ChildIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} =
bp_rest_loop(Rest, AlignCol, AlignStr, TrailCol, InData),
{[Prefix, ChildIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% bp_clause_rest_loop: like bp_rest_loop but uses render_clause for each child.
%% Used for cond clauses (preserves nl_before positioning while applying the
%% trivial/non-trivial clause rule to each clause's internal rendering).
-spec bp_clause_rest_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(), string(),
non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
bp_clause_rest_loop([], _AlignCol, _AlignStr, CurCol, _InData) ->
{[], CurCol, false};
bp_clause_rest_loop([Clause | Rest], AlignCol, AlignStr, CurCol, InData) ->
W = lfmt_fezzik_util:flat_width(Clause),
NlBefore = lfmt_fezzik_cst:nl_before(Clause),
HasLead = lfmt_fezzik_util:has_comment_leading(lfmt_fezzik_cst:leading(Clause)),
Overflow = W =:= infinity orelse CurCol + 1 + W >= ?WIDTH,
NewLine = NlBefore orelse HasLead orelse Overflow,
{StartCol, Prefix} =
case NewLine of
true -> {AlignCol, ["\n",
lfmt_fezzik_util:emit_child_leading(
lfmt_fezzik_cst:leading(Clause), AlignStr, false),
AlignStr]};
false -> {CurCol + 1, " "}
end,
{ClauseIO, ClauseCol} = render_clause(Clause, StartCol, InData),
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Clause), ClauseCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Clause) =/= [],
{[Prefix, ClauseIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} =
bp_clause_rest_loop(Rest, AlignCol, AlignStr, TrailCol, InData),
{[Prefix, ClauseIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%%====================================================================
%% Internal: map key-value pair rendering (A4·S3a)
%%====================================================================
%% print_map_pairs: render map children as key-value pairs (style guide §6).
%% First pair on the opener line: #m(k1 v1
%% Subsequent pairs aligned at C+OpenLen:
%% k2 v2
%% k3 v3)
%% If any direct map child carries a leading or trailing comment, fall back
%% to element-per-line (identical to list_head rendering) so no comment
%% swallows a paired value.
-spec print_map_pairs(lfmt_fezzik_cst:cst_node(), [lfmt_fezzik_cst:cst_node()],
[lfmt_fezzik_cst:trivia()],
non_neg_integer(), string(), non_neg_integer(),
string(), non_neg_integer(),
non_neg_integer(), string(), string(), boolean()) ->
{iolist(), non_neg_integer()}.
print_map_pairs(Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData) ->
AllChildren = [Head | RestChildren],
AnyTrivia = lists:any(
fun(Child) ->
lfmt_fezzik_cst:leading(Child) =/= []
orelse lfmt_fezzik_cst:trailing(Child) =/= []
end, AllChildren),
case AnyTrivia of
true ->
%% Fall back: element-per-line (reuse list_head; safe with trivia).
print_classified(list_head, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData);
false ->
AlignCol = C + OpenLen,
AlignStr = lists:duplicate(AlignCol, $\s),
{PairsIO, LastCol, HasTrail} =
print_map_pairs_list(AllChildren, AlignCol, AlignStr, InData),
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[Open, PairsIO, CloseIO], CloseCol}
end.
%% print_map_pairs_list: render the full list of map children starting at
%% AlignCol (first pair on the opener line, no leading newline).
-spec print_map_pairs_list([lfmt_fezzik_cst:cst_node()],
non_neg_integer(), string(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_map_pairs_list([K, V], AlignCol, _AlignStr, InData) ->
{KIO, KCol} = print_node(K, AlignCol, InData),
{VIO, VCol} = print_node(V, KCol + 1, InData),
VTrail = lfmt_fezzik_cst:trailing(V) =/= [],
{[KIO, " ", VIO], VCol, VTrail};
print_map_pairs_list([K, V | Rest], AlignCol, AlignStr, InData) ->
{KIO, KCol} = print_node(K, AlignCol, InData),
{VIO, _VCol} = print_node(V, KCol + 1, InData),
{RestIO, LastCol, HasTrail} = print_map_pairs_rest(Rest, AlignCol, AlignStr, InData),
{[KIO, " ", VIO | RestIO], LastCol, HasTrail};
print_map_pairs_list([K], AlignCol, _AlignStr, InData) ->
%% Odd last element (malformed map): emit alone.
{KIO, KCol} = print_node(K, AlignCol, InData),
KTrail = lfmt_fezzik_cst:trailing(K) =/= [],
{[KIO], KCol, KTrail}.
%% print_map_pairs_rest: emit remaining k-v pairs each preceded by \n+AlignStr.
-spec print_map_pairs_rest([lfmt_fezzik_cst:cst_node()],
non_neg_integer(), string(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_map_pairs_rest([K, V], AlignCol, AlignStr, InData) ->
{KIO, KCol} = print_node(K, AlignCol, InData),
{VIO, VCol} = print_node(V, KCol + 1, InData),
VTrail = lfmt_fezzik_cst:trailing(V) =/= [],
{["\n", AlignStr, KIO, " ", VIO], VCol, VTrail};
print_map_pairs_rest([K, V | Rest], AlignCol, AlignStr, InData) ->
{KIO, KCol} = print_node(K, AlignCol, InData),
{VIO, _VCol} = print_node(V, KCol + 1, InData),
{RestIO, LastCol, HasTrail} = print_map_pairs_rest(Rest, AlignCol, AlignStr, InData),
{["\n", AlignStr, KIO, " ", VIO | RestIO], LastCol, HasTrail};
print_map_pairs_rest([K], AlignCol, AlignStr, InData) ->
%% Odd last element.
{KIO, KCol} = print_node(K, AlignCol, InData),
KTrail = lfmt_fezzik_cst:trailing(K) =/= [],
{["\n", AlignStr, KIO], KCol, KTrail}.
%% render_clause: flat if trivial; list_head layout otherwise.
%% Directly dispatches to print_classified(list_head, …) to guarantee the
%% break regardless of what regime/2 would return for the clause's head.
-spec render_clause(lfmt_fezzik_cst:cst_node(), non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer()}.
render_clause(Clause, Col, InData) ->
case lfmt_fezzik_util:trivial_clause(Clause) of
true -> {lfmt_fezzik_util:flat_render(Clause), Col + lfmt_fezzik_util:flat_width(Clause)};
false ->
case lfmt_fezzik_cst:children(Clause) of
[] ->
print_broken(Clause, Col, InData);
[Head | Rest] ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Clause)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Clause)),
OpenLen = length(Open),
CloseLen = length(Close),
Dangling = lfmt_fezzik_cst:dangling(Clause),
Indent = Col + 2,
IndentStr = lists:duplicate(Indent, $\s),
CIndStr = lists:duplicate(Col, $\s),
print_classified(list_head, Head, Rest, Dangling,
Col, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData)
end
end.
%%====================================================================
%% Internal: classified broken rendering
%%====================================================================
-spec print_classified(head_class(),
lfmt_fezzik_cst:cst_node(), [lfmt_fezzik_cst:cst_node()],
[lfmt_fezzik_cst:trivia()],
non_neg_integer(), string(), non_neg_integer(),
string(), non_neg_integer(),
non_neg_integer(), string(), string(), boolean()) ->
{iolist(), non_neg_integer()}.
%% list_head: all elements aligned under the first at C+len(Open).
%%
%% Guard path (S3d): if RestChildren=[Guard|Body] where Guard is (when …)
%% and neither Pat (Head) nor Guard carries a comment, keep Pat+Guard on one
%% line and emit Body one per line at AlignCol. Falls back to element-per-line
%% when either has a comment (comment safety) or Head has a trailing comment.
%% Atom-pattern clauses (Pat is a symbol → funcall class) never reach here.
print_classified(list_head, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, _CloseLen,
Indent, IndentStr, CIndStr, InData) ->
AlignCol = C + OpenLen,
AlignStr = lists:duplicate(AlignCol, $\s),
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(Head), CIndStr),
{HeadIO, HeadCol} = print_node(Head, AlignCol, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Head), HeadCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(Head) =/= [],
UseGuard = case RestChildren of
[G | _] ->
lfmt_fezzik_util:is_when_form(G)
andalso not HeadHasTrail
andalso lfmt_fezzik_cst:leading(G) =:= []
andalso lfmt_fezzik_cst:trailing(G) =:= [];
_ -> false
end,
case {UseGuard, RestChildren} of
{true, [Guard | Body]} ->
%% Pat already printed; Guard on same line, Body below at AlignCol.
{GuardIO, GuardCol} = print_node(Guard, HTC + 1, InData),
case Body of
[] ->
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, false, GuardCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, " ", GuardIO, CloseIO], CloseCol};
_ ->
{BodyIO, LastCol, HasTrail} = print_rest_loop(Body, AlignCol,
AlignStr, true, InData),
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, " ", GuardIO, BodyIO, CloseIO], CloseCol}
end;
{false, []} ->
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HeadHasTrail, HTC,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, CloseIO], CloseCol};
{false, _} ->
{RestIO, LastCol, HasTrail} = print_rest_loop(RestChildren, AlignCol,
AlignStr, true, InData),
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, RestIO, CloseIO], CloseCol}
end;
%% specform N: distinguished args 1..N on head line; body at C+2.
%% N=0: head alone, all args at C+2.
%% defform (provisional = specform 1, refined in S2).
%% Falls back to body layout when:
%% • N=0 (always), OR
%% • HeadHasTrail (fix2: a trailing comment on the head ends the line — no
%% content may follow it on the head line), OR
%% • any distinguished arg has a leading/trailing comment (fix1-b).
%% Body=[] branch passes HeadHasTrail to close_section so (progn ; c) and
%% similar still break the close onto its own line (fix2).
print_classified({specform, N}, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, _CloseLen,
Indent, IndentStr, CIndStr, InData) ->
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(Head), CIndStr),
{HeadIO, HeadCol} = print_node(Head, C + OpenLen, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Head), HeadCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(Head) =/= [],
{DistIO, DistEndCol, Body} =
case N =:= 0 orelse HeadHasTrail of
true ->
{[], HTC, RestChildren};
false ->
NSplit = min(N, length(RestChildren)),
{DistPotential, BodyPotential} = lists:split(NSplit, RestChildren),
case lfmt_fezzik_util:any_dist_has_comment(DistPotential) of
true -> {[], HTC, RestChildren}; %% fall back: all to body
false ->
%% For let/let*: force-break binding list (one per line).
%% For flet/flet*/fletrec: force-break + defun-like per element.
{DIO, DCol} =
case lfmt_fezzik_util:is_let_head(Head) andalso DistPotential =/= [] of
true ->
[BindList] = DistPotential,
{BIO, BCol} = print_broken(BindList, HTC + 1, InData),
{BTrailIO, BTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(BindList), BCol),
{[" ", BIO, BTrailIO], BTC};
false ->
case lfmt_fezzik_util:is_flet_head(Head) andalso DistPotential =/= [] of
true ->
[BindList] = DistPotential,
{BIO, BCol} = print_flet_bindlist(
BindList, HTC + 1, InData),
{BTrailIO, BTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(BindList), BCol),
{[" ", BIO, BTrailIO], BTC};
false ->
print_distinguished(DistPotential, HTC, InData)
end
end,
{DIO, DCol, BodyPotential}
end
end,
case Body of
[] ->
%% No body args. HeadHasTrail forces the close onto its own line
%% (covers (progn ; c), (case ; c), and any head-trailing-no-args form).
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HeadHasTrail, DistEndCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, DistIO, CloseIO], CloseCol};
_ ->
IsCaseHead = lfmt_fezzik_util:is_clause_specform_head(Head, N),
IsReceiveHead = lfmt_fezzik_util:is_receive_head(Head),
IsTryHead = lfmt_fezzik_util:is_try_head(Head),
IsExportImportHead = lfmt_fezzik_util:is_export_import_head(Head),
IsDefunMatchHead =
lfmt_fezzik_util:is_defun_match_head(Head, N) andalso DistIO =/= [] andalso lfmt_fezzik_util:all_clauses(Body),
%% export/import use +1 indent (C+OpenLen); all others use the standard C+2.
EffIndent = case IsExportImportHead of true -> C + OpenLen; false -> Indent end,
EffIndStr = case IsExportImportHead of
true -> lists:duplicate(C + OpenLen, $\s);
false -> IndentStr
end,
%% export entries sorted alphabetically by {name, arity} (A7·S5b).
%% Sort only when head is "export", ALL items are (name arity) pairs,
%% AND no item has a leading comment (a commented item has intentional ordering).
HeadText = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Head)),
IsExportHead = IsExportImportHead andalso HeadText =:= "export",
IsImportHead = IsExportImportHead andalso HeadText =:= "import",
SortedBody =
case IsExportHead
andalso lists:all(fun lfmt_fezzik_util:is_export_entry/1, Body)
andalso not lists:any(fun lfmt_fezzik_util:entry_has_comment/1, Body) of
true -> lfmt_fezzik_util:sort_export_entries(Body);
false -> Body
end,
{BodyIO, LastCol, HasTrail} =
case {IsImportHead, IsExportImportHead, IsTryHead, IsReceiveHead,
IsCaseHead orelse IsDefunMatchHead} of
{true, _, _, _, _} -> print_import_body_loop(Body, EffIndent, EffIndStr,
true, InData);
{_, true, _, _, _} -> print_rest_loop(SortedBody, EffIndent, EffIndStr,
true, InData);
{_, _, true, _, _} -> print_try_body_loop(Body, Indent, IndentStr,
true, InData);
{_, _, _, true, _} -> print_receive_body_loop(Body, Indent, IndentStr,
true, InData);
{_, _, _, _, true} -> print_clause_loop(Body, Indent, IndentStr,
true, InData);
{_, _, _, _, false} -> print_rest_loop(Body, Indent, IndentStr,
true, InData)
end,
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
EffIndent, EffIndStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, DistIO, BodyIO, CloseIO], CloseCol}
end;
%% defform — dynamic N, delegating to specform (inherits full comment matrix).
%%
%% defun/defmacro:
%% N=2 when RestChildren=[_Name, Arg2|_] and lfmt_fezzik_util:is_arglist(Arg2)
%% → signature form: (defun name (args) body…)
%% N=1 otherwise
%% → match-clause form: (defun name clauses…)
%% any other defform → N=1 (name on head line, rest at C+2).
%%
%% Docstrings need no special case: the first body form (a string) lands at C+2.
print_classified(defform, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData) ->
N = lfmt_fezzik_util:defform_n(Head, RestChildren),
%% Rule (A7·S4a): def-forms are never alone on a line. When N=2 and the
%% distinguished args have a comment that would trigger the N=0 fallback
%% (keyword alone), fall back to N=1 instead so keyword + name share the
%% head line even when the arglist cannot.
EffN = case N > 1 of
false -> N;
true ->
NSplit = min(N, length(RestChildren)),
{DistPotential, _} = lists:split(NSplit, RestChildren),
case lfmt_fezzik_util:any_dist_has_comment(DistPotential) of
true -> 1;
false -> N
end
end,
print_classified({specform, EffN}, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData);
%% funcall: a1 on head line; a2..aN aligned under a1's column.
%% Align column = C + len(Open) + len(flat(head)) + 1.
%% Falls back to body layout when head has a trailing comment (fix2: nothing may
%% follow it on the head line) or when a1 has a leading comment (fix1).
print_classified(funcall, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, _CloseLen,
Indent, IndentStr, CIndStr, InData) ->
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(Head), CIndStr),
{HeadIO, HeadCol} = print_node(Head, C + OpenLen, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Head), HeadCol),
%% Head is always a symbol for funcall; use its text length for alignment.
HeadTextLen = length(lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Head))),
AlignCol = C + OpenLen + HeadTextLen + 1,
AlignStr = lists:duplicate(AlignCol, $\s),
HeadHasTrail = lfmt_fezzik_cst:trailing(Head) =/= [],
case RestChildren of
[] ->
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HeadHasTrail, HTC,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, CloseIO], CloseCol};
[A1 | RestArgs] ->
case HeadHasTrail orelse lfmt_fezzik_util:head_has_leading_comment(A1) of
true ->
%% Head trailing or a1 leading comment: all rest as body at C+2.
{AllIO, LastCol, HasTrail} = print_rest_loop(RestChildren, Indent,
IndentStr, true, InData),
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, AllIO, CloseIO], CloseCol};
false ->
{A1IO, A1Col} = print_node(A1, HTC + 1, InData),
{A1TrailIO, A1TC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(A1), A1Col),
A1HasTrail = lfmt_fezzik_cst:trailing(A1) =/= [],
case RestArgs of
[] ->
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, A1HasTrail, A1TC,
Indent, IndentStr, C, CIndStr,
Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO,
" ", A1IO, A1TrailIO, CloseIO], CloseCol};
_ ->
{RestIO, LastCol, HasTrail} = print_rest_loop(RestArgs, AlignCol,
AlignStr, true, InData),
{CloseIO, CloseCol} = lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
Indent, IndentStr, C, CIndStr,
Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO,
" ", A1IO, A1TrailIO, RestIO, CloseIO], CloseCol}
end
end
end.
%% print_distinguished: print distinguished args space-separated on the head line.
%% Each arg's leading is emitted via lfmt_fezzik_util:emit_head_leading(blanks dropped).
-spec print_distinguished([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
boolean()) ->
{iolist(), non_neg_integer()}.
print_distinguished([], Col, _InData) ->
{[], Col};
print_distinguished([D | Rest], Col, InData) ->
DLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(D), ""),
{DIO, DCol} = print_node(D, Col + 1, InData),
{DTrailIO, DTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(D), DCol),
{RestIO, LastCol} = print_distinguished(Rest, DTC, InData),
{[" ", DLeadIO, DIO, DTrailIO | RestIO], LastCol}.
%% print_rest_loop: emit children [c1..cN] each preceded by \n+Indent.
%% Returns {IO, LastCol, LastHasTrailing} where LastHasTrailing is true when
%% the final child carried a trailing comment (used by close_section fix1).
%% IsFirst=true suppresses the leading blank of the first rest child.
-spec print_rest_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
string(), boolean(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_rest_loop([Child | Rest], Indent, IndentStr, IsFirst, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(Child), IndentStr, IsFirst),
{ChildIO, ChildCol} = print_node(Child, Indent, InData),
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Child), ChildCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Child) =/= [],
{["\n", LeadIO, IndentStr, ChildIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} = print_rest_loop(Rest, Indent, IndentStr,
false, InData),
{["\n", LeadIO, IndentStr, ChildIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% print_local_fn_binding: render a single flet/fletrec binding (name args body…)
%% with forced {specform, N} classification (defun-like layout) rather than the
%% funcall/BP layout that a plain-symbol head would normally get.
-spec print_local_fn_binding(lfmt_fezzik_cst:cst_node(), non_neg_integer(),
boolean()) ->
{iolist(), non_neg_integer()}.
print_local_fn_binding(Binding, C, InData) ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Binding)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Binding)),
OpenLen = length(Open),
CloseLen = length(Close),
Dangling = lfmt_fezzik_cst:dangling(Binding),
Indent = C + 2,
IndentStr = lists:duplicate(Indent, $\s),
CIndStr = lists:duplicate(C, $\s),
N = lfmt_fezzik_util:local_fn_n(Binding),
case lfmt_fezzik_cst:children(Binding) of
[Head | RestChildren] ->
case N =:= 0 andalso RestChildren =/= [] andalso lfmt_fezzik_util:all_clauses(RestChildren) of
true ->
%% Match-clause local fn: name on head line, clauses via
%% render_clause at +2 (mirrors match-lambda / defun N=1 path).
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(Head), CIndStr),
{HeadIO, HeadCol} = print_node(Head, C + OpenLen, InData),
{HeadTrailIO, _} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Head), HeadCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(Head) =/= [],
{BodyIO, LastCol, HasTrail} =
print_clause_loop(RestChildren, Indent, IndentStr, true, InData),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, HasTrail orelse HeadHasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, BodyIO, CloseIO], CloseCol};
false ->
print_classified({specform, N}, Head, RestChildren, Dangling,
C, Open, OpenLen, Close, CloseLen,
Indent, IndentStr, CIndStr, InData)
end;
[] ->
{[Open, Close], C + OpenLen + CloseLen}
end.
%% print_flet_bindlist: render the binding list of an flet/fletrec form as a
%% force-broken container, with each binding rendered via print_local_fn_binding
%% (defun-like) rather than the generic BP/funcall path.
%% Geometry mirrors print_broken_container for the non-map canonical path, but
%% the element renderer is swapped.
-spec print_flet_bindlist(lfmt_fezzik_cst:cst_node(), non_neg_integer(),
boolean()) ->
{iolist(), non_neg_integer()}.
print_flet_bindlist(BindList, C, InData) ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(BindList)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(BindList)),
OpenLen = length(Open),
Bindings = lfmt_fezzik_cst:children(BindList),
Dangling = lfmt_fezzik_cst:dangling(BindList),
AlignCol = C + OpenLen,
AlignStr = lists:duplicate(AlignCol, $\s),
CIndStr = lists:duplicate(C, $\s),
case Bindings of
[] ->
{[Open, Close], C + OpenLen + length(Close)};
[First | Rest] ->
FirstLead = lfmt_fezzik_cst:leading(First),
FirstPrefixIO =
case lfmt_fezzik_util:has_comment_leading(FirstLead) of
true -> ["\n", lfmt_fezzik_util:emit_child_leading(FirstLead, AlignStr, false), AlignStr];
false -> []
end,
{FirstIO, FirstCol} = print_local_fn_binding(First, AlignCol, InData),
{FirstTrailIO, FirstTC} = lfmt_fezzik_util:emit_trailing(
lfmt_fezzik_cst:trailing(First), FirstCol),
HasFirstTrail = lfmt_fezzik_cst:trailing(First) =/= [],
{RestIO, LastCol, HasTrail} =
print_local_fn_bindings_loop(Rest, AlignCol, AlignStr,
FirstTC, HasFirstTrail, InData),
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, HasTrail, LastCol,
AlignCol, AlignStr, C, CIndStr, Close),
{[Open, FirstPrefixIO, FirstIO, FirstTrailIO, RestIO, CloseIO], CloseCol}
end.
%% print_local_fn_bindings_loop: render the 2nd-onward flet bindings, one per
%% line at AlignStr, each via print_local_fn_binding.
-spec print_local_fn_bindings_loop([lfmt_fezzik_cst:cst_node()],
non_neg_integer(), string(),
non_neg_integer(), boolean(),
boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_local_fn_bindings_loop([], _Indent, _IndStr, LastCol, HasTrail, _InData) ->
{[], LastCol, HasTrail};
print_local_fn_bindings_loop([B | Rest], Indent, IndStr, _PrevCol, _PrevTrail, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(B), IndStr, false),
{BIO, BCol} = print_local_fn_binding(B, Indent, InData),
{TrailIO, BTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(B), BCol),
HasTrail = lfmt_fezzik_cst:trailing(B) =/= [],
{RestIO, LastCol, LastHasTrail} =
print_local_fn_bindings_loop(Rest, Indent, IndStr, BTC, HasTrail, InData),
{["\n", LeadIO, IndStr, BIO, TrailIO | RestIO], LastCol, LastHasTrail}.
%% print_clause_loop: like print_rest_loop but uses render_clause for each child.
%% Used for case body clauses and for remaining cond clauses.
-spec print_clause_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
string(), boolean(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_clause_loop([Clause | Rest], Indent, IndentStr, IsFirst, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(Clause), IndentStr, IsFirst),
{ClauseIO, ClauseCol} = render_clause(Clause, Indent, InData),
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Clause), ClauseCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Clause) =/= [],
{["\n", LeadIO, IndentStr, ClauseIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} = print_clause_loop(Rest, Indent, IndentStr,
false, InData),
{["\n", LeadIO, IndentStr, ClauseIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% print_receive_body_loop: receive pattern clauses use render_clause, but the
%% (after timeout body...) section is not a clause and keeps generic rendering.
-spec print_receive_body_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
string(), boolean(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_receive_body_loop([Child | Rest], Indent, IndentStr, IsFirst, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(Child), IndentStr, IsFirst),
{ChildIO, ChildCol} =
case lfmt_fezzik_util:is_after_section(Child) of
true -> print_node(Child, Indent, InData);
false -> render_clause(Child, Indent, InData)
end,
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Child), ChildCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Child) =/= [],
{["\n", LeadIO, IndentStr, ChildIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} = print_receive_body_loop(Rest, Indent,
IndentStr, false, InData),
{["\n", LeadIO, IndentStr, ChildIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% print_try_body_loop: first child is the try body expr (print_node); subsequent
%% children are case/catch/after sections rendered via print_try_section.
-spec print_try_body_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
string(), boolean(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_try_body_loop([Child | Rest], Indent, IndentStr, IsFirst, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(Child), IndentStr, IsFirst),
{ChildIO, ChildCol} =
case IsFirst of
true -> print_node(Child, Indent, InData);
false -> print_try_section(Child, Indent, InData)
end,
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Child), ChildCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Child) =/= [],
{["\n", LeadIO, IndentStr, ChildIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} = print_try_body_loop(Rest, Indent, IndentStr,
false, InData),
{["\n", LeadIO, IndentStr, ChildIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% print_try_section: render a (case/catch/after …) section with the keyword alone
%% on the section line and contents at +2 below (case/catch via print_clause_loop;
%% after via print_rest_loop). Reachable only from print_try_body_loop.
-spec print_try_section(lfmt_fezzik_cst:cst_node(), non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer()}.
print_try_section(Section, C, InData) ->
case lfmt_fezzik_cst:type(Section) =:= list of
false ->
print_node(Section, C, InData);
true ->
case lfmt_fezzik_cst:children(Section) of
[] ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Section)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Section)),
{[Open, Close], C + length(Open) + length(Close)};
[SectionHead | Contents] ->
case lfmt_fezzik_cst:type(SectionHead) =:= symbol of
false ->
print_node(Section, C, InData);
true ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Section)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Section)),
OpenLen = length(Open),
Dangling = lfmt_fezzik_cst:dangling(Section),
Indent = C + 2,
IndentStr = lists:duplicate(Indent, $\s),
CIndStr = lists:duplicate(C, $\s),
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(
lfmt_fezzik_cst:leading(SectionHead), CIndStr),
{HeadIO, HeadCol} =
print_node(SectionHead, C + OpenLen, InData),
{HeadTrailIO, _} =
lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(SectionHead), HeadCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(SectionHead) =/= [],
{BodyIO, LastCol, HasTrail} =
case Contents of
[] ->
{[], HeadCol, false};
_ ->
case lfmt_fezzik_util:is_after_section(Section) of
true ->
print_rest_loop(Contents, Indent, IndentStr,
true, InData);
false ->
print_clause_loop(Contents, Indent, IndentStr,
true, InData)
end
end,
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, HasTrail orelse HeadHasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, BodyIO, CloseIO], CloseCol}
end
end
end.
%% print_import_body_loop: emit import clauses one-per-line via print_import_clause.
%% All children are clauses (from/rename/deprecated/other). Reachable only from
%% the import arm of the specform body router.
-spec print_import_body_loop([lfmt_fezzik_cst:cst_node()], non_neg_integer(),
string(), boolean(), boolean()) ->
{iolist(), non_neg_integer(), boolean()}.
print_import_body_loop([Child | Rest], Indent, IndentStr, IsFirst, InData) ->
LeadIO = lfmt_fezzik_util:emit_child_leading(lfmt_fezzik_cst:leading(Child), IndentStr, IsFirst),
{ChildIO, ChildCol} = print_import_clause(Child, Indent, InData),
{TrailIO, TrailCol} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Child), ChildCol),
case Rest of
[] ->
HasTrail = lfmt_fezzik_cst:trailing(Child) =/= [],
{["\n", LeadIO, IndentStr, ChildIO, TrailIO], TrailCol, HasTrail};
_ ->
{RestIO, LastCol, HasTrail} =
print_import_body_loop(Rest, Indent, IndentStr, false, InData),
{["\n", LeadIO, IndentStr, ChildIO, TrailIO | RestIO], LastCol, HasTrail}
end.
%% print_import_clause: render a single import clause.
%% (from M E…) and (rename M P…): keyword+module on head line; entries one-per-line
%% at C+OpenLen (+1); entries sorted (suppressed if any has a leading comment).
%% deprecated/other/non-list: render via print_node (generic at +1).
-spec print_import_clause(lfmt_fezzik_cst:cst_node(), non_neg_integer(), boolean()) ->
{iolist(), non_neg_integer()}.
print_import_clause(Clause, C, InData) ->
case lfmt_fezzik_cst:type(Clause) of
list ->
case lfmt_fezzik_cst:children(Clause) of
[ClauseHead, _Mod | _Entries] ->
case lfmt_fezzik_cst:type(ClauseHead) of
symbol ->
ClauseText = lfmt_fezzik_lexer:text(
lfmt_fezzik_cst:open(ClauseHead)),
case lists:member(ClauseText, ["from", "rename"]) of
true -> print_import_from_rename(Clause, C, InData);
false -> print_node(Clause, C, InData)
end;
_ ->
print_node(Clause, C, InData)
end;
_ ->
print_node(Clause, C, InData)
end;
_ ->
print_node(Clause, C, InData)
end.
%% print_import_from_rename: shared renderer for (from M E…) and (rename M P…).
%% Keyword and module on head line; entries one-per-line at C+OpenLen (+1).
-spec print_import_from_rename(lfmt_fezzik_cst:cst_node(), non_neg_integer(),
boolean()) -> {iolist(), non_neg_integer()}.
print_import_from_rename(Clause, C, InData) ->
Open = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(Clause)),
Close = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:close(Clause)),
OpenLen = length(Open),
Dangling = lfmt_fezzik_cst:dangling(Clause),
Indent = C + OpenLen,
IndentStr = lists:duplicate(Indent, $\s),
CIndStr = lists:duplicate(C, $\s),
[ClauseHead, Mod | Entries] = lfmt_fezzik_cst:children(Clause),
ClauseText = lfmt_fezzik_lexer:text(lfmt_fezzik_cst:open(ClauseHead)),
HeadLeadIO = lfmt_fezzik_util:emit_head_leading(lfmt_fezzik_cst:leading(ClauseHead), CIndStr),
{HeadIO, HeadCol} = print_node(ClauseHead, C + OpenLen, InData),
{HeadTrailIO, HTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(ClauseHead), HeadCol),
HeadHasTrail = lfmt_fezzik_cst:trailing(ClauseHead) =/= [],
ModLead = lfmt_fezzik_cst:leading(Mod),
HasModLeadComment = lfmt_fezzik_util:has_comment_leading(ModLead),
case HeadHasTrail orelse HasModLeadComment of
true ->
print_node(Clause, C, InData);
false ->
{ModIO, ModCol} = print_node(Mod, HTC + 1, InData),
{ModTrailIO, MTC} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Mod), ModCol),
ModHasTrail = lfmt_fezzik_cst:trailing(Mod) =/= [],
SortedEntries = lfmt_fezzik_util:sort_import_entries(ClauseText, Entries),
{BodyIO, LastCol, HasTrail} =
case Entries of
[] -> {[], MTC, false};
_ -> print_rest_loop(SortedEntries, Indent, IndentStr, true, InData)
end,
{CloseIO, CloseCol} =
lfmt_fezzik_util:close_section(Dangling, HasTrail orelse ModHasTrail, LastCol,
Indent, IndentStr, C, CIndStr, Close),
{[HeadLeadIO, Open, HeadIO, HeadTrailIO, " ", ModIO, ModTrailIO,
BodyIO, CloseIO], CloseCol}
end.