try to create a new behavior

This commit is contained in:
niamtokik
2023-11-30 14:21:43 +00:00
parent 8496866b95
commit 2202b8ba27
2 changed files with 411 additions and 83 deletions

104
src/gabarit_generator.erl Normal file
View File

@@ -0,0 +1,104 @@
%%%===================================================================
%%% @doc draft.
%%% @end
%%%===================================================================
-module(gabarit_generator).
-export([compile/3, compile/4]).
-export([flatten/2]).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
compile(Module, Args, Data) ->
compile(Module, Args, Data, #{}).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
compile(Module, Args, Data, Opts) ->
init_loop(Module, Args, Data, Opts).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
init_loop(Module, Args, Data, Opts) ->
try Module:init(Args) of
{ok, State} ->
loop(Module, Data, Opts, State, [], [])
catch
E:R:S ->
{error, {E,R,S}}
end.
%%--------------------------------------------------------------------
%% custom function to flatten an improper list containing binaries and
%% other terms.
%%--------------------------------------------------------------------
flatten([], Buffer)
when is_binary(Buffer) -> Buffer;
flatten([], Buffer)
when is_list(Buffer) -> lists:reverse(Buffer);
flatten([H|T], Buffer)
when is_binary(H), is_binary(Buffer) ->
flatten(T, <<Buffer/binary, H/binary>>);
flatten([H|T], Buffer)
when is_binary(Buffer) ->
flatten(T, [H|[Buffer]]);
flatten([H|T], [Last|Rest])
when is_binary(Last), is_binary(H) ->
flatten(T, [<<Last/binary, H/binary>>|Rest]);
flatten([H|T], Buffer)
when is_list(Buffer) ->
flatten(T, [H|Buffer]).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
loop(Module, Data, Opts, State, LBuffer, RBuffer) ->
case elements(Module, Data, Opts, State, LBuffer, RBuffer) of
{ok, L, R, _State} ->
List = lists:flatten([L,R]),
Flatten = flatten(List, <<>>),
{ok, Flatten};
Elsewise ->
Elsewise
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
elements(Module, [], Opts, State, LB, RB) ->
{ok, LB, RB, State};
elements(Module, [Element|Elements], Opts, State, LB, RB) ->
case element(Module, Element, Opts, State) of
{ok, Content, NewState} ->
elements(Module, Elements, Opts, NewState, [LB, Content], RB);
{ok, Begin, End, NewState} ->
elements(Module, Elements, Opts, NewState, [LB, Begin], [End,RB]);
Elsewise ->
Elsewise
end;
elements(Module, Element, Opts, State, LB, RB) ->
case element(Module, Element, Opts, State) of
{ok, Content, NewState} ->
{ok, [LB, Content], RB, NewState};
{ok, Begin, End, NewState} ->
{ok, [LB, Begin], [End,RB], NewState};
Elsewise ->
Elsewise
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
element(Module, Element, Opts, State) ->
case Module:tag(Element, Opts, State) of
{ok, Content, NewState} ->
{ok, Content, NewState};
{ok, Begin, End, NewState} ->
{ok, Begin, End, NewState};
{ok, Begin, End, Inner, NewState} ->
elements(Module, Inner, Opts, NewState, [Begin], [End]);
Elsewise ->
Elsewise
end.

View File

@@ -33,17 +33,55 @@
%%% module when compiling. Different models can be used:
%%%
%%% - Store templates into ETS (in memory)
%%%
%%%
%%% - Compile a module with common interface
%%%
%%% - Store static page in cache with versionning and dynamic one on
%%% another storage layer.
%%%
%%% == Notes ==
%%%
%%% ```
%%% {}: null
%%% {Variable}: variable
%%% {Tag, Content}: tag without attribute
%%% {Tag, Attribute, Content}: full tag
%%% {Tag, Attribute, Content, Opts}: full tag with custom options
%%% {apply, Module, Function, Args}: special tag
%%% {apply, Function}
%%% {apply, Function, Args}
%%% '''
%%%
%%% Static template produce (a binary):
%%%
%%% ```
%%% <<"<body><head>test</head></body>">>
%%% '''
%%%
%%% Dynamic template production (a list of binaries and tuples):
%%%
%%% ```
%%% [ <<"<body><head>"
%%% , {apply, Module, Function, Args}
%%% , "</head></body>">>
%%% ]
%%% '''
%%%
%%% ```
%%% 1: [{body, #{}, {head, #{}, []}}].
%%% 2: {body, #{}, {head, #{}, []}}.
%%% 3: ["<body>", {head, #{}, []} ,"</body>"]
%%% 4: ["<body>", "<head>", "</head>", "</body>"]
%%% 5: "<body><head></head></body>"
%%% '''
%%%
%%% @end
%%%===================================================================
-module(gabarit_html_tag).
-export([create/1, create/2]).
-export([init/1]).
-export([tag/3]).
-export([attribute/4, key/3, value/3]).
-export([attribute/5]).
-export([content/3]).
%%--------------------------------------------------------------------
@@ -55,6 +93,22 @@
-type return_stop() :: {stop, term(), state()}.
-type return() :: return_ok() | return_stop().
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
create(Data) -> create(Data, #{}).
create(Data, Opts) -> gabarit_generator:compile(?MODULE, Opts, Data).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec init(Args) -> Return when
Args :: term(),
Return :: term().
init(Args) ->
{ok, []}.
%%--------------------------------------------------------------------
%% @doc main function used to generate html using gabarit tags.
%% @end
@@ -69,69 +123,120 @@
% add variable support (template only). A variable can be any Erlang
% term but MUST be present in variables key from Opts
%---------------------------------------------------------------------
tag({Variable}, Opts, State) ->
{ok, todo, State};
tag({Variable} = Tag, Opts, State) ->
{stop, {todo, Tag, Opts}, State};
%--------------------------------------------------------------------
% explicit content with specific options
%--------------------------------------------------------------------
tag({content, Content} = Tag, Opts, State) ->
content(Content, Opts, State);
tag({content, Content, LocalOpts} = Tag, Opts, State) ->
content(Content, maps:merge(Opts, LocalOpts), State);
%---------------------------------------------------------------------
% add raw include support. A raw include can use default parameters
% from Opts but can also use local parameters, useful if someone needs
% to insert a script or stylecheat.
%---------------------------------------------------------------------
tag({include_raw, Path}, Opts, State) ->
{ok, todo, State};
tag({include_raw, Path, LocalOpts}, Opts, State) ->
{ok, todo, State};
tag({include_raw, Path} = Tag, Opts, State) ->
{stop, {todo, Tag, Opts}, State};
tag({include_raw, Path, LocalOpts} = Tag, Opts, State) ->
{stop, {todo, Tag, Opts}, State};
%---------------------------------------------------------------------
% add template include support. A template is an erml file or a list
% of term containing gabarit html tags.
%---------------------------------------------------------------------
tag({include_template, Path}, Opts, State) ->
{ok, todo, State};
tag({include_template, Path, LocalOpts}, Opts, State) ->
{ok, todo, State};
tag({include_template, Path} = Tag, Opts, State) ->
{stop, {todo, Tag, Opts}, State};
tag({include_template, Path, LocalOpts} = Tag, Opts, State) ->
{stop, {todo, Tag, Opts}, State};
%---------------------------------------------------------------------
% call a standard behaviors and integrate their answer in the
% template. this will create a dynamic template. This part of the code
% is managed by the generator or the final renderer.
%---------------------------------------------------------------------
% default call to gen_server
tag({call, Pid, Message}, Opts, State) ->
tag({gen_server, call, Pid, Message, 1000}, Opts, State);
tag({call, Pid, Message, Timeout}, Opts, State) ->
tag({gen_server, call, Pid, Message, Timeout}, Opts, State);
% gen_server support
tag({gen_server, call, Pid, Message}, Opts, State) ->
tag({gen_server, call, Pid, Message, 1000}, Opts, State);
tag({gen_server, call, Pid, Message, Timeout} = Call, Opts, State) ->
{ok, Call, State};
% statem support
tag({gen_statem, call, Pid, Message}, Opts, State) ->
tag({gen_statem, call, Pid, Message, 1000}, Opts, State);
tag({gen_statem, call, Pid, Message, Timeout} = Call, Opts, State) ->
{ok, Call, State};
%---------------------------------------------------------------------
% add MFA support. This part of the code add a dynamic layer to the
% page. Every time the page is called, MFA defined is called and then
% generate a new page.
%---------------------------------------------------------------------
tag({apply, {Module, Function, Arguments}}, Opts, State) ->
{ok, todo, State};
tag({apply, Module, Function, Arguments}, Opts, State)
when is_atom(Module), is_atom(Function), is_list(Arguments) ->
try apply(Module, Function, Arguments) of
{ok, Result} when is_binary(Result) ->
{ok, Result, State}
catch
E:R:S -> {E,R,S}
end;
%---------------------------------------------------------------------
% add local function support (helper) from gabarit. Same as MFA.
% add local function support (helper) to execute on the same module.
%---------------------------------------------------------------------
tag({apply, {Function, Arguments}}, Opts, State) ->
{ok, todo, State};
tag({apply, Function, Arguments}, Opts, State)
when is_atom(Function), is_list(Arguments) ->
try Function(Arguments) of
{ok, Result} when is_binary(Result) ->
{ok, Result, State}
catch
E:R:S -> {E,R,S}
end;
%---------------------------------------------------------------------
% add support for function generator (without argument). Same as MFA.
%---------------------------------------------------------------------
tag({apply, Function}, Opts, State)
tag({apply, Function}, Opts, State)
when is_function(Function, 0) ->
{ok, todo, State};
try Function() of
{ok, Result} when is_binary(Result) ->
{ok, Result, State}
catch
E:R:S -> {E,R,S}
end;
%---------------------------------------------------------------------
% add support for function with opts. Same as MFA.
%---------------------------------------------------------------------
tag({apply, Function}, Opts, State)
tag({apply, Function}, Opts, State)
when is_function(Function, 1) ->
{ok, todo, State};
try Function(Opts) of
{ok, Result} when is_binary(Result) ->
{ok, Result, State}
catch
E:R:S -> {E,R,S}
end;
%---------------------------------------------------------------------
% add support for empty tag. Empty tags are special tags without
% content.
%---------------------------------------------------------------------
tag({empty, Tag}, Opts, State) ->
{ok, todo, State};
tag({{empty, Tag}, Attributes}, Opts, State) ->
{ok, todo, State};
tag({empty, Tag} = T, Opts, State) ->
{stop, {todo, T, Opts}, State};
tag({{empty, Tag} = T, Attributes}, Opts, State) ->
{stop, {todo, T, Opts}, State};
%--------------------------------------------------------------------
% some tags will behave differently:
% - code
% - pre
% - base
% - br
% - img
@@ -140,33 +245,67 @@ tag({{empty, Tag}, Attributes}, Opts, State) ->
% - meta
% - source
%--------------------------------------------------------------------
tag({<<"code">>, Content}, Opts, State) ->
tag({<<"code">>, #{}, Content}, Opts, State);
tag({<<"code">>, Attributes, [Integer|_] = Content} = Tag, Opts, State)
when is_list(Content), is_integer(Integer), Integer>0 ->
Result = list_to_binary(Content),
tag({<<"code">>, Attributes, Result}, Opts, State);
tag({<<"code">>, Attributes, [List|_] = Content} = Tag, Opts, State)
when is_list(Content), is_list(List) ->
Result = list_to_binary(string:join(Content, "\n")),
tag({<<"code">>, Attributes, Result}, Opts, State);
tag({<<"code">>, Attributes, [Binary|_] = Content} = Tag, Opts, State)
when is_list(Content), is_binary(Binary) ->
Result = join(Content, <<"\n">>),
tag({<<"code">>, Attributes, Result}, Opts, State);
tag({<<"code">>, Attributes, Content}, Opts, State)
when is_list(Content) ->
{ok, todo, State};
when is_binary(Content) ->
Begin = bracket(<<"code">>, <<>>),
End = bracket_end(<<"code">>),
{ok, <<Begin/binary, Content/binary, End/binary>>, State};
tag({<<"pre">>, Content}, Opts, State) ->
tag({<<"pre">>, #{}, Content}, Opts, State);
tag({<<"pre">>, Attributes, [Integer|_] = Content} = Tag, Opts, State)
when is_list(Content), is_integer(Integer), Integer>0 ->
Result = list_to_binary(Content),
tag({<<"pre">>, Attributes, Result}, Opts, State);
tag({<<"pre">>, Attributes, [List|_] = Content} = Tag, Opts, State)
when is_list(Content), is_list(List) ->
Result = list_to_binary(string:join(Content, "\n")),
tag({<<"pre">>, Attributes, Result}, Opts, State);
tag({<<"pre">>, Attributes, [Binary|_] = Content} = Tag, Opts, State)
when is_list(Content), is_binary(Binary) ->
Result = join(Content, <<"\n">>),
tag({<<"pre">>, Attributes, Result}, Opts, State);
tag({<<"pre">>, Attributes, Content}, Opts, State)
when is_list(Content) ->
{ok, todo, state};
when is_binary(Content) ->
Begin = bracket(<<"pre">>, <<>>),
End = bracket_end(<<"pre">>),
{ok, <<Begin/binary, Content/binary, End/binary>>, State};
%---------------------------------------------------------------------
% add support for regular tag
%---------------------------------------------------------------------
tag({Tag, Content}, Opts, State) ->
tag({Tag, #{}, Content}, Opts, State);
tag({Tag, Attributes, Content}, Opts, State) ->
{ok, todo, State};
tag({Tag, Attributes, Content}, Opts, State)
when is_binary(Tag) ->
case attributes(Tag, Attributes, Opts, State) of
{ok, <<>>, NewState} ->
Begin = <<"<", Tag/binary,">">>,
End = <<"</",Tag/binary,">">>,
{ok, Begin, End, Content, State};
{ok, Serialized, NewState} ->
Begin = <<"<", Tag/binary," ", Serialized/binary, ">">>,
End = <<"</",Tag/binary,">">>,
{ok, Begin, End, Content, State}
end;
%---------------------------------------------------------------------
% add support when a list is present. If a list is present, we assume
% this is a list of tags and we should treat them one by one.
%---------------------------------------------------------------------
tag(Tags, Opts, State)
tag(Tags, Opts, State)
when is_list(Tags) ->
{ok, todo, State};
{stop, {todo, Tags, Opts}, State};
%---------------------------------------------------------------------
% All tags defined as atom, list or numbers are converted into binary
@@ -178,15 +317,15 @@ tag({Tag, Attributes, Content}, Opts, State)
when is_atom(Tag) ->
NewTag = atom_to_binary(Tag),
tag({NewTag, Attributes, Content}, Opts, State);
tag({Tag, Attributes, Content}, Opts, State)
tag({Tag, Attributes, Content}, Opts, State)
when is_list(Tag) ->
NewTag = list_to_binary(Tag),
tag({NewTag, Attributes, Content}, Opts, State);
tag({Tag, Attributes, Content}, Opts, State)
tag({Tag, Attributes, Content}, Opts, State)
when is_integer(Tag) ->
NewTag = integer_to_binary(Tag),
tag({NewTag, Attributes, Content}, Opts, State);
tag({Tag, Attributes, Content}, Opts, State)
tag({Tag, Attributes, Content}, Opts, State)
when is_float(Tag) ->
NewTag = float_to_binary(Tag),
tag({NewTag, Attributes, Content}, Opts, State);
@@ -195,64 +334,88 @@ tag({Tag, Attributes, Content}, Opts, State)
% we assume binary, numbers, and atoms are text. these tags must be
% protected and encoded with html entities.
%--------------------------------------------------------------------
tag(Text, Opts, State)
when is_binary(Text) ->
{ok, todo, State};
tag(Text, Opts, State)
when is_atom(Text) ->
{ok, todo, State};
tag(Text, Opts, State)
when is_number(Text) ->
{ok, todo, State};
tag(Text, Opts, State)
when is_binary(Text); is_atom(Text);
is_integer(Text); is_float(Text) ->
content(Text, Opts, State);
%---------------------------------------------------------------------
% If unsupported tags are present, we should stop.
%---------------------------------------------------------------------
tag(Unsupported, Opts, State) ->
{stop, Unsupported, State}.
{stop, {todo, Unsupported, Opts}, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
attributes(Tag, Attributes, Opts, State) ->
attributes(Tag, Attributes, maps:keys(Attributes), [], Opts, State).
attributes(Tag, Attributes, [], Buffer, Opts, State) ->
{ok, join(lists:reverse(Buffer)), State};
attributes(Tag, Attributes, [Key|Keys], Buffer, Opts, State) ->
Value = maps:get(Key, Attributes),
case attribute(Tag, Key, Value, Opts, State) of
{ok, K, V, NewState} ->
Pair = <<K/binary,"=",V/binary>>,
attributes(Tag, Attributes, Keys, [Pair|Buffer], Opts, NewState);
Elsewise ->
Elsewise
end.
%%--------------------------------------------------------------------
%% @doc main function used to generate html using gabarit tags.
%% @end
%%--------------------------------------------------------------------
-spec attribute(Key, Value, Opts, State) -> Return when
-spec attribute(Tag, Key, Value, Opts, State) -> Return when
Tag :: binary(),
Key :: term(),
Value :: term(),
Opts :: map(),
State :: term(),
Return :: return().
attribute(Key, Value, Opts, State) ->
{ok, todo, State}.
% convert keys to binary
attribute(Tag, Key, Value, Opts, State)
when is_atom(Key) ->
attribute(Tag, atom_to_binary(Key), Value, Opts, State);
attribute(Tag, Key, Value, Opts, State)
when is_list(Key) ->
attribute(Tag, list_to_binary(Key), Value, Opts, State);
% convert values to binary
attribute(Tag, Key, Value, Opts, State)
when is_atom(Value) ->
attribute(Tag, Key, atom_to_binary(Value), Opts, State);
attribute(Tag, Key, Value, Opts, State)
when is_list(Value) ->
attribute(Tag, Key, list_to_binary(Value), Opts, State);
% some example of specific attributes
attribute(<<"a">>, <<"href">> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(<<"img">>, <<"src">> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(_Tag, <<"style">>, Value, Opts, State) ->
{ok, <<"style">>, Value, State};
attribute(_Tag, <<"on", _/binary>> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(_Tag, <<"hx-vals">> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(_Tag, <<"hx-", _binary/binary>> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(_Tag, <<"htmx-", _binary/binary>> = Key, Value, Opts, State) ->
{ok, Key, Value, State};
attribute(_Tag, Key, Value, Opts, State)
when is_binary(Key), is_binary(Value) ->
{ok, Key, quote(Value, $"), State};
% global attributes, by default, not supported
attribute(Tag, Key, Value, Opts, State) ->
{stop, {todo, Tag, Key, Value, Opts, State}, State}.
%%--------------------------------------------------------------------
%% @doc main function to check attribute keys.
%% @end
%%--------------------------------------------------------------------
-spec key(Key, Opts, State) -> Return when
Key :: term(),
Opts :: map(),
State :: term(),
Return :: return().
key(Key, Opts, State) ->
{ok, todo, State}.
%%--------------------------------------------------------------------
%% @doc main function to check attribute values.
%% @end
%%--------------------------------------------------------------------
-spec value(Value, Opts, State) -> Return when
Value :: term(),
Opts :: map(),
State :: term(),
Return :: return().
value(Value, Opts, State) ->
{ok, todo, State}.
%%--------------------------------------------------------------------
%% @doc main function to check content.
%% @doc main function to check content (inner text).
%% @end
%%--------------------------------------------------------------------
-spec content(Content, Opts, State) -> Return when
@@ -261,5 +424,66 @@ value(Value, Opts, State) ->
State :: term(),
Return :: return().
content(Content, Opts, State)
when is_atom(Content) ->
content(atom_to_binary(Content), Opts, State);
content(Content, Opts, State)
when is_integer(Content) ->
content(integer_to_binary(Content), Opts, State);
content(Content, Opts, State)
when is_float(Content) ->
content(float_to_binary(Content), Opts, State);
content(Content, Opts, State)
when is_list(Content) ->
content(list_to_binary(Content), Opts, State);
content(Content, #{ entities := false } = _Opts, State)
when is_binary(Content) ->
{ok, Content, State};
content(Content, Opts, State)
when is_binary(Content) ->
{ok, gabarit_html_entities:encode(Content), State};
content(Content, Opts, State) ->
{ok, todo, State}.
{stop, {todo, content, Content, Opts, State}, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
join([]) -> <<>>;
join(Binaries) -> join(Binaries, <<" ">>).
join([], _) -> <<>>;
join(Binaries, Sep) -> join(Binaries, Sep, <<>>).
join([], _, Buffer) -> Buffer;
join([Binary], _, Buffer) ->
<<Binary/binary, Buffer/binary>>;
join([Binary|Rest], Sep, Buffer) ->
join(Rest, Sep, <<Sep/binary, Binary/binary, Buffer/binary>>).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
quote(Value, Char) ->
<<Char, Value/binary, Char>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
bracket_empty(Element, <<>>) ->
<<"<", Element/binary, " />">>;
bracket_empty(Element, Attributes) ->
<<"<", Element/binary, " ", Attributes/binary, " />">>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
bracket(Element, <<>>) ->
<<"<", Element/binary, ">">>;
bracket(Element, Attributes) ->
<<"<", Element/binary, " ", Attributes/binary, ">">>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
bracket_end(Element) ->
<<"</", Element/binary, ">">>.