src/erlte_renderer.erl

%% Copyright (c) 2022, Madalin Grigore-Enescu <github@ergenius.com> <www.ergenius.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(erlte_renderer).
-author("Madalin Grigore-Enescu").

-include("../include/erlte.hrl").

-export([render/2]).
-export([render_to_compiled/2]).
-export([render_to_file/2, render_to_file/3]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% render
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%     

-spec render(Compiled, Variables) -> {ok, Result} | {error, Error} when
    Compiled :: erlte:compiled(),
    Variables :: list(),
    Result :: binary(),
    Error :: term().
%% @doc Render the compiled template replacing the specified variables
%% There is no error return case now but some maybe added in the future so for compatibility
%% reasons you should handle a generic error case too.
render(#erlte_compiled{fragments = Fragments}, Variables) when Variables =:= undefined; Variables =:= [] -> 
    NewFragments = render_missing(Fragments),
    {ok, erlang:iolist_to_binary(lists:reverse(NewFragments))};
render(Compiled, Variables) -> render(Compiled, lists:reverse(Variables), 0).

render(Compiled = #erlte_compiled{
    format = Format, 
    fragments = Fragments}, [H|T], Iterations) ->
    NewFragments = variable_render(Format, H, Fragments),
    render(Compiled#erlte_compiled{fragments = NewFragments}, T, Iterations+1);

render(#erlte_compiled{fragments = Fragments}, _, Iterations) -> 
    NewFragments = render_missing(Fragments),
    % Save another one lists:reverse
    Remainder = (Iterations+1) rem 2,
    case Remainder of
        1 -> {ok, erlang:iolist_to_binary(lists:reverse(NewFragments))};
        _ -> {ok, erlang:iolist_to_binary(NewFragments)}
    end.

render_missing(Fragments) -> render_missing(Fragments, []).
render_missing([{v, Name}|T], Acum) -> render_missing(T, [Name|Acum]);
render_missing([H|T], Acum) -> render_missing(T, [H|Acum]);
render_missing([], Acum) -> Acum.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% render_to_compiled
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%    

-spec render_to_compiled(Compiled, Variables) -> {ok, NewCompiled} | {error, Error} when
    Compiled :: erlte:compiled(),
    Variables :: list(),
    NewCompiled :: erlte:compiled(),
    Error :: term().
%% @doc Render the compiled template replacing the specified variables and return a new compiled template
%% There is no error return case now but some maybe added in the future so for compatibility
%% reasons you should handle a generic error case too.
render_to_compiled(Compiled, Variables) when Variables =:= undefined; Variables =:= [] ->  {ok, Compiled};
render_to_compiled(Compiled, Variables) -> render_to_compiled(Compiled, lists:reverse(Variables), 0).

render_to_compiled(Compiled = #erlte_compiled{
    format = Format,
    fragments = Fragments}, [H|T], Iterations) ->
    NewFragments = variable_render(Format, H, Fragments),
    render_to_compiled(Compiled#erlte_compiled{fragments = NewFragments}, T, Iterations+1);

render_to_compiled(Compiled = #erlte_compiled{fragments = Fragments}, _, Iterations) -> 
    % Save another one lists:reverse
    Remainder = Iterations rem 2,
    case Remainder of
        1 -> {ok, render_to_compiled_collapse(Compiled#erlte_compiled{fragments = lists:reverse(Fragments)})};
        _ -> {ok, render_to_compiled_collapse(Compiled)}
    end.

render_to_compiled_collapse(Compiled) -> render_to_compiled_collapse(Compiled, [], []).
render_to_compiled_collapse(Compiled = #erlte_compiled{
    fragments = [H|T]}, TempAcum, FragmentsAcum) when erlang:is_binary(H) ->
    render_to_compiled_collapse(Compiled#erlte_compiled{
        fragments = T}, [TempAcum, H], FragmentsAcum);

render_to_compiled_collapse(Compiled = #erlte_compiled{
        fragments = [H|T]}, TempAcum, FragmentsAcum) ->
    NewTempAcum = erlang:iolist_to_binary(TempAcum),
    NewFragmentsAcum = [FragmentsAcum, NewTempAcum, H],
    render_to_compiled_collapse(Compiled#erlte_compiled{
            fragments = T}, [], NewFragmentsAcum);
render_to_compiled_collapse(Compiled = #erlte_compiled{
        fragments = []}, TempAcum, FragmentsAcum) ->
    case TempAcum of
        [] -> Compiled#erlte_compiled{fragments = lists:flatten(FragmentsAcum)};
        _ -> 
            NewTempAcum = erlang:iolist_to_binary(TempAcum),
            Compiled#erlte_compiled{fragments = lists:flatten([FragmentsAcum, NewTempAcum])}
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% render_to_file
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%      

-spec render_to_file(Filename, Compiled) -> {ok, Result} | {error, Reason} when
    Filename :: file:name_all(),
    Compiled :: erlte:compiled(),
    Result :: binary(),    
    Reason :: file:posix() | badarg | terminated | system_limit.
%% @doc Render compiled template to the specified file
render_to_file(File, Compiled) -> render_to_file(File, Compiled, undefined).

-spec render_to_file(Filename, Compiled, Variables) -> {ok, Result} | {error, Reason} when
    Filename :: file:name_all(),
    Compiled :: erlte:compiled(),
    Variables :: undefined | list(),
    Result :: binary(),
    Reason :: file:posix() | badarg | terminated | system_limit.
%% @doc Render compiled template replacing the specified variables and save the result into the specified file
render_to_file(Filename, Compiled, Variables) ->
    case render(Compiled, Variables) of
        {ok, Result} -> 
            BOM = unicode:encoding_to_bom(utf8),
            case file:write_file(Filename, <<BOM/binary, Result/binary>>) of 
                ok -> {ok, Result};
                WriteFileError -> WriteFileError
            end;
        Error -> Error
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% variable_render
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

% @doc Render variable
variable_render(Format, {Name, Value}, Fragments) -> 
    BinaryName = variable_name_to_binary(Name),
    BinaryValue = variable_value_to_binary(Format, BinaryName, Value),
    variable_render_all(BinaryName, BinaryValue, Fragments, []).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% variable_render_all
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%         

variable_render_all(Name, BinaryValue, [{v,Name}|T], Acum) ->
    NewAcum = [BinaryValue|Acum],
    variable_render_all(Name, BinaryValue, T, NewAcum);
variable_render_all(Name, BinaryValue, [H|T], Acum) ->
    NewAcum = [H|Acum],
    variable_render_all(Name, BinaryValue, T, NewAcum);
variable_render_all(_Name, _BinaryValue, [], Acum) -> 
    % We don't reverse acumulator here, 
    % we do this later when all variables where iterated.
    % This way we save a reverse on each variable iteration.
    Acum.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% variable_name_to_binary
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

% @doc Convert variable name to binary
variable_name_to_binary(Name) when erlang:is_binary(Name) -> Name;
variable_name_to_binary(Name) when erlang:is_list(Name) -> unicode:characters_to_binary(Name, utf8, utf8);
variable_name_to_binary(Name) when erlang:is_atom(Name) -> erlang:atom_to_binary(Name, utf8);
variable_name_to_binary(Name) when erlang:is_integer(Name) -> erlang:integer_to_binary(Name).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% variable_value_to_binary
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

% @doc Sanitize the variable value according to the specified template file format
variable_value_to_binary(Format, Name, {Value, Options}) -> variable_value_to_binary_options(Format, Name, Value, Options);
variable_value_to_binary(_Format, Name, Value) when erlang:is_integer(Value) -> 
    variable_value_to_binary(_Format, Name, erlang:integer_to_list(Value));
variable_value_to_binary(_Format, Name, Value) when erlang:is_float(Value) -> 
    variable_value_to_binary(_Format, Name, erlang:float_to_list(Value));
variable_value_to_binary(_Format, Name, Value) when erlang:is_atom(Value) -> 
    variable_value_to_binary(_Format, Name, erlang:atom_to_list(Value));
variable_value_to_binary(_Format, Name, Value) when erlang:is_binary(Value) -> 
    variable_value_to_binary(_Format, Name, unicode:characters_to_list(Value, utf8));
variable_value_to_binary(Format, _Name, Value) when erlang:is_list(Value)-> 
    variable_sanitize(Format, Value).

variable_value_to_binary_options(Format, Name, Value, Options) when 
    erlang:is_float(Value); erlang:is_list(Options) ->
    variable_value_to_binary(Format, Name, erlang:float_to_list(Value, Options));
variable_value_to_binary_options(Format, Name, Value, {f, Module, Function, Arg}) when 
    erlang:is_atom(Module); erlang:is_atom(Function) ->
    Result = erlang:apply(Module, Function, [Format, Name, Value, Arg]),
    true = erlang:is_binary(Result),
    Result.

variable_sanitize(html, Value) -> unicode:characters_to_binary(erlte_utils:html_entities_encode(Value));
variable_sanitize(_, Value) -> unicode:characters_to_binary(Value, utf8, utf8).