Skip to main content

src/lfmt_fezzik.erl

%%%% 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).