%%%% LFE source formatter — public entry + document orchestration.
%%%% Pipeline: lfmt_fezzik_lexer -> lfmt_fezzik_cst -> render -> iolist.
-module(lfmt_fezzik).
-behaviour(lfmt_engine).
-export([format/1, format/2]).
%% regime/2 exported for unit testing only (re-exported from lfmt_fezzik_util).
-ifdef(TEST).
-export([regime/2]).
regime(Node, InData) -> lfmt_fezzik_util:regime(Node, InData).
-endif.
%%====================================================================
%% Exported API
%%====================================================================
%% Dialyzer infers a concrete nested-list type for the iolist return; suppress
%% the underspecs warning since iolist() is the correct public abstraction.
-dialyzer({no_underspecs, format/1}).
-spec format(binary() | string()) -> {ok, iolist()} | {error, term()}.
format(Input) ->
case lfmt_fezzik_lexer:tokens(Input) of
{ok, Tokens} ->
case lfmt_fezzik_cst:parse(Tokens) of
{ok, Doc} -> {ok, render_document(Doc)};
{error, _} = Err -> Err
end;
{error, _} = Err ->
Err
end.
%% lfmt_engine behaviour callback. 0.4.0 opts carry only the engine selector
%% (consumed by lfmt:format/2 before dispatch), so there is no fezzik-specific
%% option to read yet. format/1's domain is binary()|string() (the lexer's), so
%% normalise the generic chardata() the behaviour accepts to a UTF-8 binary
%% first — this keeps fezzik honest about the contract without touching the
%% engine internals. (When fezzik honours an option, e.g. width, read it here.)
-spec format(lfmt:opts(), unicode:chardata()) -> {ok, iolist()} | {error, term()}.
format(_Opts, Source) ->
case unicode:characters_to_binary(Source) of
Bin when is_binary(Bin) -> format(Bin);
_ -> {error, {invalid_encoding, Source}}
end.
%%====================================================================
%% Internal: document-level layout
%%====================================================================
-spec render_document(lfmt_fezzik_cst:cst_document()) -> iolist().
render_document(Doc) ->
Nodes = lfmt_fezzik_cst:document_children(Doc),
DangItems = lfmt_fezzik_cst:document_dangling(Doc),
Parts = render_toplevel(Nodes, true, [], false),
DangIO = lfmt_fezzik_util:emit_toplevel_dangling(DangItems),
lists:reverse([DangIO | Parts]).
%% render_toplevel: emit each top-level node with its leading trivia and final \n.
-spec render_toplevel([lfmt_fezzik_cst:cst_node()], boolean(), iolist(),
boolean()) -> iolist().
render_toplevel([], _IsFirst, Acc, _InData) ->
Acc;
render_toplevel([Node | Rest], IsFirst, Acc, InData) ->
LeadIO = lfmt_fezzik_util:emit_leading_trivia(lfmt_fezzik_cst:leading(Node), "", IsFirst),
{NodeIO, NodeCol} = lfmt_fezzik_render:print_node(Node, 0, InData),
{TrailIO, _Col} = lfmt_fezzik_util:emit_trailing(lfmt_fezzik_cst:trailing(Node), NodeCol),
Part = [LeadIO, NodeIO, TrailIO, "\n"],
render_toplevel(Rest, false, [Part | Acc], InData).