%% Copyright (c) 2022, Madalin Grigore-Enescu %% %% 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, <>) 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).