Compare commits

16 Commits

Author SHA1 Message Date
niamtokik
2202b8ba27 try to create a new behavior 2023-11-30 14:21:43 +00:00
niamtokik
8496866b95 typo 2023-11-29 21:12:29 +00:00
niamtokik
6d6f648ec6 update 2023-11-29 21:10:40 +00:00
niamtokik
e22bb03cb4 update 2023-11-29 19:59:29 +00:00
niamtokik
551cf8d616 add few test data to generate pages 2023-11-29 19:36:44 +00:00
niamtokik
910fe1689e create new templates and variables features 2023-11-28 20:09:37 +00:00
niamtokik
25f12f4d2c just some example 2023-11-24 15:59:07 +00:00
niamtokik
b3cab0abc4 quick test. 2023-11-24 11:48:01 +00:00
niamtokik
846000c1e7 just add another mode. 2023-11-20 20:50:44 +00:00
niamtokik
448cf8f8d5 just few cleanup 2023-11-20 20:41:18 +00:00
niamtokik
96bf70c47c update gabarit_entities 2023-11-20 20:32:02 +00:00
niamtokik
90c66b5993 entities from XML. 2023-11-18 08:43:54 +00:00
niamtokik
5efa674d40 few modification and comments 2023-11-14 06:59:15 +00:00
niamtokik
bf03faf454 update 2023-11-13 20:50:12 +00:00
niamtokik
4ad8931d77 quick cleanup. 2023-11-13 20:38:36 +00:00
niamtokik
3b359dd9af create another way to create html and create a way to decode/encode entities. 2023-11-13 18:01:56 +00:00
26 changed files with 6160 additions and 0 deletions

13
examples/htmx/card.erml Normal file
View File

@@ -0,0 +1,13 @@
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => {card_image_url}, class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, {card_title}},
{'div', #{class => "card-subtitle text-gray"}, {card_subtitle}}
]},
{'div', #{class => "card-body"}, {card_body}},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, {card_button}}
]}
]}.

99
examples/htmx/index.erml Normal file
View File

@@ -0,0 +1,99 @@
{html, [
{head, [
{title, [<<"This is an example">>]},
{link, #{rel => "stylesheet", href => "https://unpkg.com/spectre.css/dist/spectre.min.css"}, []},
{link, #{rel => "stylesheet", href => "https://unpkg.com/spectre.css/dist/spectre-exp.min.css"}, []},
{link, #{rel => "stylesheet", href => "https://unpkg.com/spectre.css/dist/spectre-icons.min.css"}, []},
{script, #{src => "https://unpkg.com/htmx.org@1.9.9"}, []}
]},
{body, [
{'header', #{class => "navbar"}, [
{section, #{class => "navbar-section"}, [
{a, #{href => "#", class => "navbar-brand mr-2"}, <<"Gabarit HTML">>},
{a, #{href => "#", class => "btn btn-link"}, <<"Introduction">>},
{a, #{href => "#", class => "btn btn-link"}, <<"Docs">>}
]},
{section, #{class => "navbar-section"}, [
{'div', #{class => "input-group input-inline"}, [
{input, #{class => "form-input", type => "text", placeholder => "search"}, []},
{button, #{class => "btn btn-primary input-group-btn"}, <<"search">>}
]}
]}
]},
{'div', #{class => "container"}, [
{'div', #{class => "columns"}, [
% card 1
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => "https://picsum.photos/id/1/300/200", class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, [<<"card 1">>]},
{'div', #{class => "card-subtitle text-gray"}, [<<"This is card subtitle">>]}
]},
{'div', #{class => "card-body"}, [<<"This is the body!">>]},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, <<"refresh">>}
]}
]},
% card 2
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => "https://picsum.photos/id/2/300/200", class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, [<<"test">>]},
{'div', #{class => "card-subtitle text-gray"}, [<<"test">>]}
]},
{'div', #{class => "card-body"}, [<<"test">>]},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, <<"refresh">>}
]}
]},
% card 3
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => "https://picsum.photos/id/3/300/200", class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, [<<"test">>]},
{'div', #{class => "card-subtitle text-gray"}, [<<"test">>]}
]},
{'div', #{class => "card-body"}, [<<"test">>]},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, <<"refresh">>}
]}
]},
% card 4
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => "https://picsum.photos/id/4/300/200", class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, [<<"test">>]},
{'div', #{class => "card-subtitle text-gray"}, [<<"test">>]}
]},
{'div', #{class => "card-body"}, [<<"test">>]},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, <<"refresh">>}
]}
]},
% card 5
{'div', #{class => "card"}, [
{'div', #{class => "card-images"}, [
{img, #{src => "https://picsum.photos/id/5/300/200", class => "img-responsive"}}
]},
{'div', #{class => "card-header"}, [
{'div', #{class => "card-title h5"}, [<<"test">>]},
{'div', #{class => "card-subtitle text-gray"}, [<<"test">>]}
]},
{'div', #{class => "card-body"}, [<<"test">>]},
{'div', #{class => "card-footer"}, [
{button, #{class => "btn btn-primary"}, <<"refresh">>}
]}
]}
]}
]}
]}
]}.

10
priv/Makefile Normal file
View File

@@ -0,0 +1,10 @@
######################################################################
#
######################################################################
SOURCE ?= https://raw.githubusercontent.com/w3c/xml-entities/gh-pages/unicode.xml
unicode.xml:
curl -so $@ $(SOURCE)
clean:
rm unicode.xml

13
priv/example.erml Normal file
View File

@@ -0,0 +1,13 @@
{html, [
{head, [
{apply, {module, function, args}},
{apply, {function, args},
{apply, fun() -> {ok, <<>>} end},
{apply, fun(Opts) -> {ok, <<>>} end},
% send
% {cast, {module, function, []}}
{script, #{ src => "https://unpkg.com/htmx.org@1.9.9"}, []}
]},
{body, [
]},
]}

323
src/gabarit_entities.erl Normal file
View File

@@ -0,0 +1,323 @@
%%%===================================================================
%%% @author Mathieu Kerjouan
%%% @doc
%%%
%%% This module extract all entities from unicode.xml file and convert
%%% them into indexed maps. The first one generated creates an index
%%% of ascii/utf8 chars, the second one is doing the opposite, by
%%% indexing directly the entity. That's quite dirty, but it works for
%%% now.
%%%
%%% The next step is to directly create a module template using
%%% `merl', where all entities/chars are being stored instead of
%%% starting a process.
%%%
%%% For now, everything is done inside a `gen_server':
%%%
%%% ```
%%% {ok, _} = gabarit_entities:start().
%%%
%%% {ok, #{ "8879-isolat1" => <<"&eacute;">>
%%% , "9573-2003-isolat1" => <<"&eacute;">>,
%%% , "xhtml1-lat1" => <<"&eacute;">>
%%% }
%%% } = gabarit_entities:char_to_entity(<<"é"/utf8>>).
%%%
%%% {ok,<<"é"/utf8>>}
%%% = gabarit_entities:entity_to_char(<<"&eacute;"/utf8>>).
%%%
%%% {ok,<<"&agrave;">>}
%%% = gabarit_entities:char_to_entity(<<"à"/utf8>>, "xhtml1-lat1").
%%% '''
%%%
%%% @end
%%%===================================================================
-module(gabarit_entities).
-behavior(gen_server).
-export([start/0, start_link/0]).
-export([entity_to_char/1, char_to_entity/1, char_to_entity/2]).
-export([get_entities/0, get_chars/0]).
-export([init/1]).
-export([handle_info/2, handle_cast/2, handle_call/3]).
-export([merl/0]).
-include_lib("xmerl/include/xmerl.hrl").
-record(?MODULE, { chars = #{} :: map()
, entities = #{} :: map()
, options = #{} :: map()
}).
%%--------------------------------------------------------------------
%% draft to generate a module.
%%--------------------------------------------------------------------
merl() ->
merl_entities().
merl_entities() ->
Entities = get_entities(),
Keys = maps:keys(Entities),
EntityToChar = merl_entities(Keys, Entities, [], length(Keys)+3),
[ {attribute,1,module,t}
, {attribute,2,export,[{entity_to_char,1}]}
, EntityToChar
].
merl_entities([], _, Buffer, _) ->
{function,3,entity_to_char,1, Buffer};
merl_entities([Key|Rest], Entities, Buffer, Counter) ->
#{ Key := Value} = Entities,
Pattern = {bin, Counter, [{bin_element, Counter, {string, Counter, binary_to_list(Key)}, default, default}]},
Return = {bin, Counter, [{bin_element, Counter, {string, Counter, binary_to_list(Value)}, default, default}]},
Clause = {clause,Counter,[Pattern, [], Return]},
merl_entities(Rest, Entities, [Clause|Buffer], Counter-1).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec start() -> {ok, pid()}.
start() ->
gen_server:start({local, ?MODULE}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec start_link() -> {ok, pid()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec entity_to_char(Entity) -> Return when
Entity :: binary(),
Return :: {ok, binary()}
| {error, {binary(), not_found}}
| timeout.
entity_to_char(<<Entity/binary>>) ->
gen_server:call(?MODULE, {entity_to_char, Entity}, 1000).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec char_to_entity(Entity) -> Return when
Entity :: binary(),
Return :: {ok, map()}
| {error, {binary(), not_found}}
| timeout.
char_to_entity(<<Char/binary>>) ->
gen_server:call(?MODULE, {char_to_entity, Char}, 1000).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec char_to_entity(Entity, Mode) -> Return when
Entity :: binary(),
Mode :: string(),
Return :: {ok, binary()}
| {error, {binary(), not_found}}
| timeout.
char_to_entity(<<Char/binary>>, Mode) ->
gen_server:call(?MODULE, {char_to_entity, Char, Mode}, 1000).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec get_entities() -> map().
get_entities() ->
gen_server:call(?MODULE, entities, 1000).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec get_chars() -> map().
get_chars() ->
gen_server:call(?MODULE, chars, 1000).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
init(Args) ->
{ok, {Chars, Entities}} = parse(),
State = #?MODULE{ chars = Chars
, entities = Entities
, options = Args
},
{ok, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
handle_call({char_to_entity, Char, Mode}, _, #?MODULE{ chars = Chars } = State) ->
case maps:get(Char, Chars, undefined) of
undefined ->
{reply, {error, {Char, not_found}}, State};
#{ Mode := Entity } ->
{reply, {ok, Entity}, State};
_ ->
{reply, {error, {Char, not_found}}, State}
end;
handle_call({char_to_entity, Char}, _, #?MODULE{ chars = Chars, options = #{ default := Default }} = State) ->
case maps:get(Char, Chars, undefined) of
undefined ->
{reply, {error, {Char, not_found}}, State};
Entity ->
case Entity of
#{ Default := Result } ->
{reply, {ok, Result}, State};
_ ->
{reply, {error, {Char, not_found, Default}}, State}
end
end;
handle_call({char_to_entity, Char}, _, #?MODULE{ chars = Chars } = State) ->
case maps:get(Char, Chars, undefined) of
undefined ->
{reply, {error, {Char, not_found}}, State};
Entity ->
{reply, {ok, Entity}, State}
end;
handle_call({entity_to_char, Entity}, _, #?MODULE{ entities = Entities} = State) ->
case maps:get(Entity, Entities, undefined) of
undefined ->
{reply, {error, {Entity, not_found}}, State};
Char ->
{reply, {ok, Char}, State}
end;
handle_call(entities, _, #?MODULE{ entities = Entities } = State) ->
{reply, Entities, State};
handle_call(chars, _, #?MODULE{ chars = Chars } = State) ->
{reply, Chars, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
handle_cast(_,State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
handle_info(_,State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
options() ->
[{event_fun, fun event/3}].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
parse() ->
Priv = code:priv_dir(gabarit),
Filename = filename:join(Priv, "unicode.xml"),
{ok, Content} = file:read_file(Filename),
stream(binary_to_list(Content)).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-spec stream(XML) -> Return when
XML :: binary() | string(),
Return :: {ok, {CharMap, EntityMap}},
CharMap :: map(),
EntityMap :: map().
stream(Content) ->
{ok, {charlist, Buffer}, _} = xmerl_sax_parser:stream(Content, options()),
list_to_map(Buffer, #{}, #{}).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
list_to_map(#{}, ByChars, ByEntities) -> {ok, {ByChars, ByEntities}};
list_to_map([], ByChars, ByEntities) -> {ok, {ByChars, ByEntities}};
list_to_map([#{ binary := Binary, entity := Entities }|Rest], ByChars, ByEntities) ->
EntitiesMap = entities_to_map(Entities, #{}),
EntityChar = entity_to_char(Binary, Entities, #{}),
list_to_map(Rest, ByChars#{ Binary => EntitiesMap }, maps:merge(ByEntities, EntityChar));
list_to_map([_|Rest], ByChars, ByEntities) ->
list_to_map(Rest, ByChars, ByEntities).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
entities_to_map([], Buffer) -> Buffer;
entities_to_map([#{ id := Id, set := Set }|Rest], Buffer) ->
entities_to_map(Rest, Buffer#{ Set => Id }).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
entity_to_char(_Binary, [], Buffer) -> Buffer;
entity_to_char(Binary, [#{ id := Id }|Rest], Buffer) ->
entity_to_char(Binary, Rest, Buffer#{ Id => Binary }).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
event(startDocument, _Location, State) -> State;
event({startElement, [], "unicode", _, Attributes}, _Location, State) ->
case get_attribute(Attributes, "unicode") of
"15.1" -> State;
Version -> throw({error, Version})
end;
event({startElement, [], "entitygroups", _, _Attributes}, _Location, State) ->
State;
event({startElement, [], "charlist", _, _Attributes}, _Location, _State) ->
{charlist, #{}};
event({startElement, [], "character",_,Attributes}, _Location, {charlist, Buffer}) ->
Id = get_attribute(Attributes, "id"),
Dec = get_attribute(Attributes, "dec"),
Mode = get_attribute(Attributes, "mode"),
Type = get_attribute(Attributes, "type"),
case decimal_to_utf8(Dec) of
{ok, FromDec} ->
Char = #{ id => Id
, dec => Dec
, mode => Mode
, type => Type
, binary => FromDec
},
{charlist, [Char|Buffer]};
{error, _} ->
{charlist, Buffer}
end;
event({startElement, [], "entity", _, Attributes}, _Location, {charlist, [Char|Buffer]}) ->
EntityId = get_attribute(Attributes, "id"),
Set = get_attribute(Attributes, "set"),
BinaryEntity = list_to_binary(EntityId),
Entity = #{ id => <<"&", BinaryEntity/binary, ";">>, set => Set },
case Char of
#{ entity := List } ->
{charlist, [Char#{ entity => [Entity|List]}|Buffer]};
_ ->
{charlist, [Char#{ entity => [Entity]}|Buffer]}
end;
event(_Event, _Location, State) ->
State.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
decimal_to_utf8(Int) ->
try
Integer = list_to_integer(Int),
{ok, <<Integer/utf8>>}
catch
_:_ -> {error, Int}
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
get_attribute(List, Key) -> get_attribute(List, Key, undefined).
get_attribute([], _, Default) -> Default;
get_attribute([{_,_,Key,Value}|_], Key, _Default) -> Value;
get_attribute([_Attribute|Rest], Key, Default) -> get_attribute(Rest, Key, Default).

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.

520
src/gabarit_html.erl Normal file
View File

@@ -0,0 +1,520 @@
%%%===================================================================
%%% @doc draft: Create html page using tuple, map and binaries.
%%%
%%% ```
%%% % create a simple page
%%% Title = {h1, <<"this is my title">>}.
%%% Paragraph1 = {p, <<"long time ago...">>}.
%%% Paragraph2 = {p, <<"that's all folks!">>}.
%%% Paragraph3 = fun(_Opts) -> {ok, {p, <<"end.">>}} end.
%%% Body = [Title, Paragraph1, Paragraph2, Paragraph3].
%%% gabarit_html:create({html, Body}).
%%% '''
%%%
%%% Will generate this page:
%%%
%%% ```
%%% <<"<html><h1>this is my title</h1>",
%%% "<p>long time ago...</p>",
%%% "<p>that&apos;s all folks!</p>",
%%% "<p>end.</p>",
%%% "</html>">>
%%% '''
%%%
%%% @todo convert this code as behavior.
%%% @todo when reading a template, compile it as module.
%%% @todo when a template includes dynamic code, allow rendering it
%%% with custom option in the mode.
%%%
%%% @end
%%%===================================================================
-module(gabarit_html).
-export([open/1, open/2]).
-export([create/1, create/2]).
-export([table/2]).
-export([join/1, join/2]).
-include_lib("eunit/include/eunit.hrl").
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
-type tag() :: atom() | list() | binary().
-type attributes() :: #{}.
-type content() :: [].
-type options() :: #{}.
-type element() :: {tag(), attributes()}
| {tag(), attributes(), content()}
| {tag(), attributes(), content(), options()}
| {apply, {atom(), list()}}
| {apply, {atom(), atom(), list()}}
| {include, list() | binary()}
| binary().
-type elements() :: [element()].
%%--------------------------------------------------------------------
%% @doc A demo function to generate table.
%% @end
%%--------------------------------------------------------------------
table([], _) ->
create({table, []});
table([Header|Rest], #{ header := true } = Opts) ->
Keys = maps:keys(Header),
Head = table_header(Keys, Header, Opts),
Body = table_rows(Rest, Keys, [], Opts),
{table, [Head, Body]};
table([Header|Rest] = Rows, Opts) ->
Keys = maps:keys(Header),
Body = table_rows(Rows, Keys, [], Opts),
{table, Body}.
table_header(Keys, Data, #{ extra := true }) ->
Th = [ {th, [{span, #{}, Key}
, <<" ">>
,{span, #{}, [<<"(">>, maps:get(Key, Data), <<")">>]}
]
}
|| Key <- Keys ],
{thead, {tr, #{}, Th}};
table_header(Keys, _, Opts) ->
{thead, {tr, #{}, [ {th, #{}, Key} || Key <- Keys ]}}.
table_rows([], _, Buffer, _) -> {tbody, Buffer};
table_rows([Last], Keys, Buffer, #{ footer := true }) ->
Footer = {tfooter, {tr, [ {td, maps:get(Last, Key)} || Key <- Keys ]}},
Body = {tbody, lists:reverse(Buffer)},
[Body, Footer];
table_rows([Row|Rest], Keys, Buffer, Opts) ->
table_rows(Rest, Keys, [{tr, [ {td, [maps:get(Key, Row)]} || Key <- Keys ]}|Buffer], Opts).
%%--------------------------------------------------------------------
%% @doc create a new HTML page from elements.
%% @end
%%--------------------------------------------------------------------
-spec create(Element) -> Return when
Element :: element() | elements(),
Return :: binary().
create(Element) ->
create(Element, #{}).
%%--------------------------------------------------------------------
%% @doc create a new HTML page from elements.
%% @end
%%--------------------------------------------------------------------
-spec create(Element, Opts) -> Return when
Element :: element() | elements(),
Opts :: options(),
Return :: binary().
create(Element, Opts) ->
Full = tags(Element, Opts),
doctype(Full, Opts).
%%--------------------------------------------------------------------
%% @doc create a new HTML page from elements.
%% @end
%%--------------------------------------------------------------------
open(Path) ->
open(Path, #{}).
open(Path, Opts) ->
{ok, Data} = file:consult(Path),
create(Data, Opts).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tags([], _) -> <<>>;
tags(Elements, Opts) ->
tags(Elements, <<>>, Opts).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tags([], Buffer, _Opts) -> Buffer;
tags(Element, Buffer, Opts)
when is_tuple(Element) ->
tags([Element], Buffer, Opts);
tags([Element|Elements], Buffer, Opts)
when is_tuple(Element) ->
Tag = tag(Element, Opts),
tags(Elements, <<Buffer/binary, Tag/binary>>, Opts);
tags([Element|Elements], Buffer, Opts)
when is_list(Element) ->
Tags = tags(Element, Opts),
tags(Elements, <<Buffer/binary, Tags/binary>>, Opts);
tags([Element|Elements], Buffer, Opts) ->
Encoded = text(Element, Opts),
tags(Elements, <<Buffer/binary, Encoded/binary>>, Opts);
tags(Element, Buffer, Opts) ->
Encoded = text(Element, Opts),
<<Buffer/binary, Encoded/binary>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
doctype(Buffer, #{ doctype := true }) ->
<<"<!DOCTYPE html>", Buffer/binary>>;
doctype(Buffer, _Opts) ->
Buffer.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
text(Content, Opts)
when is_integer(Content) ->
text(integer_to_binary(Content), Opts);
text(Content, Opts)
when is_atom(Content) ->
text(atom_to_binary(Content), Opts);
text(Content, #{ html_entities := false })
when is_binary(Content) ->
Content;
text(Content, _Opts)
when is_binary(Content) ->
gabarit_html_entities:encode(Content).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tag({include_raw, Path}, Opts) ->
case file:read_file(Path) of
{ok, Content} ->
gabarit_html_entities:encode(Content);
Elsewise ->
throw(Elsewise)
end;
tag({include_template, Path}, Opts) ->
case file:consult(Path) of
{ok, Content} ->
tags([Content], Opts);
Elsewise ->
throw(Elsewise)
end;
tag({include_template, Path, Variables}, Opts) ->
case file:consult(Path) of
{ok, Content} ->
tags([Content], #{ variables => Variables });
Elsewise ->
throw(Elsewise)
end;
% variable support
tag({Variable}, Opts) ->
Result = get_variable(Variable, Opts),
tags([Result], Opts);
tag({apply, {Function, Args}}, #{module := Module} = Opts)
when is_atom(Function), is_list(Args) ->
case apply(Module, Function, [Opts, Args]) of
{ok, Result} when is_binary(Result) ->
gabarit_html_entities:encode(Result);
{ok, Result} when is_list(Result) ->
tags(Result, Opts);
{ok, Result} when is_tuple(Result) ->
tag(Result, Opts)
end;
tag({apply, {Module, Function, Args}}, Opts)
when is_atom(Module), is_atom(Function), is_list(Args) ->
case apply(Module, Function, [Opts|Args]) of
{ok, Result} when is_binary(Result) ->
gabarit_html_entities:encode(Result);
{ok, Result} when is_list(Result) ->
tags(Result, Opts);
{ok, Result} when is_tuple(Result) ->
tag(Result, Opts)
end;
tag({apply, Fun}, Opts)
when is_function(Fun, 1) ->
case Fun(Opts) of
{ok, Result} when is_binary(Result) ->
gabarit_html_entities:encode(Result);
{ok, Result} when is_list(Result) ->
tags(Result, Opts);
{ok, Result} when is_tuple(Result) ->
tag(Result, Opts)
end;
tag({Element, Content}, Opts)
when is_binary(Content) ->
tag({Element, #{}, Content}, Opts);
% {html, []} should use default or empty attributes. The second
% element of the tuple is the inner content.
tag({Element, Content}, Opts)
when is_list(Content) ->
tag({Element, #{}, Content}, Opts);
tag({Element, Content}, Opts)
when is_tuple(Content) ->
tag({Element, #{}, Content}, Opts);
% {html, #{}} is a tag without content and using customer attributes.
tag({Element, Attributes}, Opts)
when is_map(Attributes) ->
tag({Element, Attributes, []}, Opts);
tag({Element, Attributes, _Content} = Tag, Opts) ->
tag1(Tag, <<>>, Opts);
tag(Element, Opts) when is_function(Element) ->
tag1(Element, <<>>, Opts).
tag_test() ->
% tag can be defined with a triplet
[?assertEqual(<<"<html></html>">>
,tag({html, #{}, []}, []))
% tag can be defined with a pair
,?assertEqual(<<"<html></html>">>
,tag({html, []}, []))
% tag can be defined with a pair
,?assertEqual(<<"<html>test</html>">>
,tag({html, [<<"test">>]}, []))
% tag can be defined with a pair
,?assertEqual(<<"<html>test</html>">>
,tag({html, <<"test">>}, []))
% tag's content can be a list containing binary
,?assertEqual(<<"<html>test</html>">>
,tag({html, #{}, [<<"test">>]}, []))
% tag's content can be a binary
,?assertEqual(<<"<html>test</html>">>
,tag({html, #{}, <<"test">>}, []))
% attribute's value can be an atom
,?assertEqual(<<"<html id=\"test\">test</html>">>
,tag({html, #{ id => test }, [<<"test">>]}, []))
% attribute's value can be a binary
,?assertEqual(<<"<html id=\"test\">test</html>">>
,tag({html, #{ id => <<"test">> }, [<<"test">>]}, []))
% attribute's value can be a string
,?assertEqual(<<"<html id=\"test\">test</html>">>
,tag({html, #{ id => "test" }, [<<"test">>]}, []))
% attribute's key can be a binary
,?assertEqual(<<"<html id=\"test\">test</html>">>
,tag({html, #{ <<"id">> => "test" }, [<<"test">>]}, []))
% attributes values must be encoded using html entities
,?assertEqual(<<"<html id=\"&amp;test\">test</html>">>
,tag({html, #{ <<"id">> => "&test" }, [<<"test">>]}, []))
% empty element support
,?assertEqual(<<"<html id=\"test\" />">>
,tag({{empty, html}, #{ id => "test"}}, []))
].
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
% support for empty tag
tag1({{empty, Element}, Attributes, Content}, Buffer, Opts)
when is_atom(Element) ->
NewElement = atom_to_binary(Element),
tag1({{empty, NewElement}, Attributes, Content}, Buffer, Opts);
tag1({{empty, Element}, Attributes, Content}, Buffer, Opts)
when is_binary(Element), is_map(Attributes) ->
tag_empty({Element, Attributes, Content, #{}}, Buffer, Opts);
% support for non-empty tag
tag1({Element, Attributes, Content}, Buffer, Opts)
when is_atom(Element) ->
NewElement = atom_to_binary(Element),
tag1({NewElement, Attributes, Content}, Buffer, Opts);
tag1({Element, Attributes, Content}, Buffer, Opts)
when is_binary(Element), is_map(Attributes) ->
Item = {Element, Attributes, Content, #{}},
% @todo add suppot for pre and code tags.
case Element of
<<"base">> -> tag_without_content(Item, Buffer, Opts);
<<"br">> -> tag_without_content(Item, Buffer, Opts);
<<"img">> -> tag_without_content(Item, Buffer, Opts);
<<"input">> -> tag_without_content(Item, Buffer, Opts);
<<"link">> -> tag_without_content(Item, Buffer, Opts);
<<"meta">> -> tag_without_content(Item, Buffer, Opts);
<<"source">> -> tag_without_content(Item, Buffer, Opts);
_ -> tag_with_content(Item, Buffer, Opts)
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tag_with_content({Element, Attributes, Content, _LocalOpts}, _Buffer, Opts) ->
NewAttributes = attributes(Attributes, Opts),
StartElement = bracket(Element, NewAttributes),
NewContent = tags(Content, Opts),
EndElement = bracket_end(Element),
<<StartElement/binary, NewContent/binary, EndElement/binary>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tag_without_content({Element, Attributes, _Content, _LocalOpts}, _Buffer, Opts) ->
NewAttributes = attributes(Attributes, Opts),
Item = bracket(Element, NewAttributes),
<<Item/binary>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
tag_empty({Element, Attributes, _Content, _LocalOpts}, _Buffer, Opts) ->
NewAttributes = attributes(Attributes, Opts),
Item = bracket_empty(Element, NewAttributes),
<<Item/binary>>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
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, ">">>.
%%--------------------------------------------------------------------
%% @hidden
%% @doc
%% @end
%%--------------------------------------------------------------------
attributes(Attributes, Opts)
when map_size(Attributes) =:= 0 ->
<<>>;
attributes(Attributes, Opts) ->
Keys = maps:keys(Attributes),
attributes(Attributes, Keys, [], Opts).
%%--------------------------------------------------------------------
%% @hidden
%% @doc
%% @end
%%--------------------------------------------------------------------
attributes(_Attributes, [], Buffer, Opts) ->
join(lists:reverse(Buffer), <<" ">>);
attributes(Attributes, [Key|Keys], Buffer, Opts) ->
Value = erlang:map_get(Key, Attributes),
Attribute = attribute(Key, Value, Opts),
attributes(Attributes, Keys, [Attribute|Buffer], Opts).
%%--------------------------------------------------------------------
%% @hidden
%% @doc
%% @todo cleanup the mess for htmx
%% @end
%%--------------------------------------------------------------------
attribute(Key, Value, Opts) ->
case key(Key, Opts) of
% css
{ok, <<"style", _binary>> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = single_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% this is a javascript like attribute
{ok, <<"on", _binary>> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = single_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% this is an htmx attribute, must be a valid json.
{ok, <<"hx-vals">> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = single_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% standard htmx attributes
{ok, <<"hx-", _/binary>> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = double_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% this is an htmx attribute
{ok, <<"htmx-", _/binary>> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = double_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
{ok, <<"href">> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = double_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% javascript element
{ok, <<"src">> = NewKey} ->
NewValue = value(Value, Opts),
Quoted = single_quote(NewValue),
<<NewKey/binary,"=", Quoted/binary>>;
% that's another kind of key
{ok, NewKey} ->
NewValue = value(Value, Opts),
Encoded = gabarit_html_entities:encode(NewValue),
Quoted = double_quote(Encoded),
<<NewKey/binary, "=", Quoted/binary>>
end.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
join(Binaries) -> join(Binaries, <<" ">>).
join(Binaries, Sep) -> join(Binaries, Sep, <<>>).
join([Binary], _, Buffer) ->
<<Binary/binary, Buffer/binary>>;
join([Binary|Rest], Sep, Buffer) ->
join(Rest, Sep, <<Sep/binary, Binary/binary, Buffer/binary>>).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
get_variable(Variable, #{variables := Variables} = Opts) ->
case maps:get(Variable, Variables, '$undefined') of
'$undefined' ->
throw({error, {undefined, Variable}});
Content -> Content
end;
get_variable(Variable, Opts) ->
throw({error, {unset, variables}}).
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
value({Variable}, Opts) ->
get_variable(Variable, Opts);
value(Value, _Opts) when is_list(Value) ->
list_to_binary(Value);
value(Value, _Opts) when is_atom(Value) ->
atom_to_binary(Value);
value(Value, _Opts) when is_integer(Value) ->
integer_to_binary(Value);
value(Value, _Opts) when is_binary(Value) ->
Value.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
key(Key, _Opts) when is_list(Key) ->
{ok, list_to_binary(Key)};
key(Key, _Opts) when is_atom(Key) ->
{ok, atom_to_binary(Key)};
key(Key, _Opts) when is_integer(Key) ->
{ok, integer_to_binary(Key)};
key(Key, _Opts) when is_binary(Key) ->
{ok, Key};
key(Key, Opts) ->
{error, Key}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
double_quote(Value) ->
<<$\", Value/binary, $\">>.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
single_quote(Value) ->
<<$\', Value/binary, $\'>>.

File diff suppressed because it is too large Load Diff

489
src/gabarit_html_tag.erl Normal file
View File

@@ -0,0 +1,489 @@
%%%===================================================================
%%% @doc draft module to convert gabarit html into behavior.
%%%
%%% The idea behind this module is to offer a comprehensive, flexible
%%% and easy interface to generate optimized HTML code. When
%%% evaluated, a template should produce two kind of data:
%%%
%%% 1. if the page is static, a `binary' term is produced containing
%%% the whole document
%%%
%%% 2. if the page is dynamic (including some function call), a list
%%% containing binary and function is then produced.
%%%
%%% == Requirement ==
%%%
%%% - Reusable HTML part
%%%
%%% - Optimized HTML
%%%
%%% - Flexible HTML generator (include, function, module call...)
%%%
%%% - Flexible HTML entities (standard, strict, custom...)
%%%
%%% - Compatible with spectre css by default (see:
%%% https://picturepan2.github.io/spectre/)
%%%
%%% - Compatible with htmx by default (see: https://htmx.org/)
%%%
%%% == More ==
%%%
%%% More tests will be required to evaluate the performance of the
%%% whole implementation, and if it's required to create dedicated
%%% 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/5]).
-export([content/3]).
%%--------------------------------------------------------------------
%% some type definition.
%%--------------------------------------------------------------------
-type state() :: term().
-type data() :: binary() | function().
-type return_ok() :: {ok, data(), state()}.
-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
%%--------------------------------------------------------------------
-spec tag(Tag, Opts, State) -> Return when
Tag :: term(),
Opts :: map(),
State :: term(),
Return :: return().
%---------------------------------------------------------------------
% add variable support (template only). A variable can be any Erlang
% term but MUST be present in variables key from Opts
%---------------------------------------------------------------------
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} = 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} = 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)
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) to execute on the same module.
%---------------------------------------------------------------------
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)
when is_function(Function, 0) ->
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)
when is_function(Function, 1) ->
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} = 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:
% - base
% - br
% - img
% - input
% - link
% - meta
% - source
%--------------------------------------------------------------------
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_binary(Content) ->
Begin = bracket(<<"code">>, <<>>),
End = bracket_end(<<"code">>),
{ok, <<Begin/binary, Content/binary, End/binary>>, 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_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)
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)
when is_list(Tags) ->
{stop, {todo, Tags, Opts}, State};
%---------------------------------------------------------------------
% All tags defined as atom, list or numbers are converted into binary
% by default. It offers a flexible way to define tags for the developer.
%---------------------------------------------------------------------
tag({Tag, Content}, Opts, State) ->
tag({Tag, #{}, Content}, Opts, State);
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)
when is_list(Tag) ->
NewTag = list_to_binary(Tag),
tag({NewTag, 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)
when is_float(Tag) ->
NewTag = float_to_binary(Tag),
tag({NewTag, 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); 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, {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(Tag, Key, Value, Opts, State) -> Return when
Tag :: binary(),
Key :: term(),
Value :: term(),
Opts :: map(),
State :: term(),
Return :: return().
% 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 content (inner text).
%% @end
%%--------------------------------------------------------------------
-spec content(Content, Opts, State) -> Return when
Content :: term(),
Opts :: map(),
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) ->
{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, ">">>.

View File

@@ -0,0 +1,17 @@
{html, [
{head, [
{apply, {module, function, args}},
{apply, {function, args}},
{apply, fun(_Opts) -> {ok, <<>>} end},
]},
{body, [
{'div', [
{h1, <<"title">>},
{p, <<"paragraph1">>},
{p, <<"paragraph2">>},
{p, [{span, <<"test">>},
<<"paragraph1">>]},
]},
{include, "/path/to/paragraph.erlm" }
]}
]}.

View File

@@ -0,0 +1,10 @@
%%%===================================================================
%%% Simple code template.
%%%===================================================================
{code, [
<<"struct proc *p;">>,
<<"struct process *pr;">>,
<<"struct pdevinit *pdev;">>,
<<"extern struct pdevinit pdevinit[];">>,
<<"extern void disk_init(void);">>,
]}.

View File

@@ -0,0 +1,10 @@
%%%===================================================================
%%% A form with select/option.
%%%===================================================================
{'div', #{class => "form-group"}, [
{select, #{class => "form-select"}, [
{option, [<<"Joe Armstrong">>]},
{option, [<<"Robert Virding">>]},
{option, [<<"Mike Williams">>]}
]}
]}.

View File

@@ -0,0 +1,13 @@
%%%===================================================================
%%% A simple form.
%%%===================================================================
{'div', #{class => "form-group"}, [
{label, #{ class => "form-label"
, for => "input-example-1", ["Name"]
}, []},
{input, #{ class => "form-input"
, type => "text"
, id => "input-example-1"
, placeholder => "Name"
}, []}
]}.

View File

@@ -0,0 +1,9 @@
%%%===================================================================
%%% A simple hero page.
%%%===================================================================
{'div', #{class => "hero bg-gray"}, [
{'div', #{class => "hero-body"}, [
{h1, <<"Hero title">>},
{p, <<"This is a hero example">>}
]}
]}.

View File

@@ -0,0 +1,9 @@
%%%===================================================================
%%% A static list.
%%%===================================================================
{ul, [
{li, <<"In order to handle failure, you need two machines.">>},
{li, <<"Shared memory is evil.">>},
{li, <<"Erlang is like Meccano. Meccano is very good.">>},
{li, <<"Bad ideas in computer science and anywhere are sticky.">>}
]}.

View File

@@ -0,0 +1,4 @@
%%%===================================================================
%%% A template list.
%%%===================================================================
{ul, {list}}.

View File

@@ -0,0 +1,19 @@
%%%===================================================================
%%% A more complex page calling other tag.
%%%===================================================================
{html, [
{head, [
{title, <<"Lorem Ipsum">>}
]},
{body, [
% calling a module
{'div', #{id => "module"}, [
{apply, {gabarit_test_module, random_paragraph, []}}
]},
% include a raw text
% including a template with variable
{'div', #{id => "template"}, [
{include_template, "quote_template", #{quote => <<"test">>, author => <<"test">>}}
]}
]}
]}.

View File

@@ -0,0 +1,25 @@
%%%===================================================================
%%% A more complex page calling other tag.
%%%===================================================================
{html, [
{head, [
{title, <<"Include test">>}
]},
{body, [
{'div', #{id => "ascii"}, [
{include_raw, "raw_ascii.txt"}
]},
{'div', #{id => "html"}, [
{include_raw, "raw_html.txt"}
]},
{'div', #{id => "chinese"}, [
{include_raw, "raw_chinese.txt"}
]},
{'div', #{id => "japanese"}, [
{include_raw, "raw_japanese.txt"}
]},
{'div', #{id => "russian"}, [
{include_raw, "raw_russian.txt"}
]}
]}
]}.

View File

@@ -0,0 +1,53 @@
%%%===================================================================
%%% This is a simple template page containing Lorem Ipsum content.
%%%===================================================================
{html, [
{head, [
{title, <<"Lorem Ipsum">>}
]},
{body, [
{h1, <<"Lorem Ipsum">>},
{h2, <<"Neque porro quisquam est qui dolorem ipsum quia dolor sit"
"amet, consectetur, adipisci velit...">>},
{p, <<"Lorem ipsum dolor sit amet, consectetur adipiscing"
"elit. Aenean vel lobortis augue, sit amet mollis"
"tortor. Etiam ante tortor, rutrum sit amet dolor vel,"
"maximus commodo lacus. Donec eu ex sed diam ullamcorper"
"facilisis. Integer felis quam, rhoncus nec fermentum vel,"
"aliquam eu libero. Donec lectus dui, mollis vitae elit sit"
"amet, consectetur placerat nibh. Maecenas sit amet elit eu"
"libero auctor volutpat ut sit amet metus. Nunc consequat"
"facilisis dolor, non scelerisque diam luctus eu. Nunc"
"bibendum lectus non nisi rhoncus convallis. Proin"
"dignissim, nulla at mattis venenatis, quam massa ornare"
"massa, dictum eleifend nisi sapien at quam. Donec lacinia"
"dapibus augue vel consectetur. Nulla facilisi. Mauris non"
"blandit lectus, imperdiet sollicitudin quam. Fusce semper"
"nisi eu enim convallis ullamcorper. Aenean velit arcu,"
"dictum ut arcu vitae, rutrum condimentum metus. Ut nulla"
"ante, ultricies et urna eu, hendrerit ullamcorper"
"massa. Vestibulum vestibulum volutpat tempus. ">>},
{p, <<"Pellentesque interdum augue eu blandit"
"molestie. Pellentesque iaculis mauris eget odio sagittis, a"
"sagittis tortor molestie. Cras vestibulum leo vitae lorem"
"feugiat, ac pretium tellus dictum. Integer tristique erat"
"lectus, pellentesque mollis tortor tincidunt non. Sed et"
"diam et dui dapibus egestas. Phasellus eu elementum lectus,"
"ac pretium enim. Donec tempor, ipsum sit amet rhoncus"
"rutrum, ante nibh volutpat nibh, eu consectetur mauris"
"metus id tellus. Donec a mattis ex. Quisque aliquet est non"
"erat porttitor, eget ultrices erat dignissim. Vestibulum"
"ante ipsum primis in faucibus orci luctus et ultrices"
"posuere cubilia curae;">>},
{p, <<"Nunc sodales urna tortor. Vestibulum dui sem, venenatis non"
"tellus vel, tristique tempor mauris. Mauris consectetur"
"mauris a nisl suscipit pretium. Fusce diam odio,"
"sollicitudin ac diam et, auctor gravida tellus. Vivamus est"
"nisl, venenatis sit amet accumsan ac, sagittis id"
"risus. Sed nec eros purus. Vestibulum vulputate nulla ac"
"pellentesque tempus. Cras vehicula eros eu nulla mollis"
"ornare. Pellentesque et tellus viverra, egestas diam et,"
"iaculis dolor. Quisque facilisis turpis quis nibh bibendum"
"posuere.">>}
]}
]}.

View File

@@ -0,0 +1,7 @@
%%%===================================================================
%%% A simple quote using blockquote and cite tags.
%%%===================================================================
{blockquote, [
{p, <<"Four languages to learn: C, Prolog, Erlang, Javascript.">>},
{cite, <<"Joe Armstrong">>}
]}.

View File

@@ -0,0 +1,7 @@
%%%===================================================================
%%% A templating quote using blockquote and cite tags.
%%%===================================================================
{blockquote, [
{p, [{quote}]},
{cite, [{author}]}
]}.

View File

@@ -0,0 +1,49 @@
Lorem Ipsum
Neque porro quisquam est qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit...
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce non
velit iaculis, congue risus non, posuere ante. Nunc gravida aliquet
pellentesque. Nullam euismod sed dui elementum efficitur. Pellentesque
elementum diam eget consequat rhoncus. Nunc nunc nisi, egestas sit
amet feugiat vel, tristique eget risus. Nunc eget dui et libero
placerat consectetur. Maecenas iaculis dui tincidunt mattis
congue. Duis odio ligula, eleifend et ullamcorper eu, consectetur ac
nisi. Pellentesque ut risus odio. Integer feugiat, tellus nec ultrices
sollicitudin, nisi ex ornare tortor, in venenatis nulla ipsum cursus
tortor.
Nunc molestie luctus convallis. Cras bibendum lorem convallis sapien
iaculis feugiat. Maecenas porta dui ac elementum bibendum. Maecenas
lobortis elit ex, vel auctor arcu ornare vitae. In hac habitasse
platea dictumst. Morbi vitae leo sit amet lacus auctor faucibus. Nam
bibendum ipsum turpis, sit amet posuere elit viverra eu. Maecenas
tempus lacinia lectus, eu ultrices orci maximus nec.
Vivamus dignissim feugiat diam. Nunc vulputate vestibulum
tempus. Donec semper id sapien in consectetur. Class aptent taciti
sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos. Nullam blandit tempus libero id lobortis. Etiam convallis
vehicula ligula at auctor. Nullam et porta turpis, a porttitor
odio. Aliquam imperdiet sit amet magna eu tempor. Nunc finibus porta
ante nec ornare.
Interdum et malesuada fames ac ante ipsum primis in
faucibus. Vestibulum ante ipsum primis in faucibus orci luctus et
ultrices posuere cubilia curae; Quisque sit amet ex urna. Pellentesque
habitant morbi tristique senectus et netus et malesuada fames ac
turpis egestas. Quisque quis massa vel ante vestibulum rutrum. Cras eu
sem facilisis dolor molestie ultrices. Donec luctus ante a lorem
gravida tincidunt. Duis scelerisque quis tortor non fringilla. Morbi
in efficitur nisl. Ut et felis at nunc tincidunt varius sit amet id
lectus. Praesent ut nulla sed tortor volutpat imperdiet. Duis euismod
velit et tincidunt suscipit. Nullam vitae tristique purus. Curabitur
ut mi ipsum. Phasellus ultricies risus non odio fermentum porttitor.
Pellentesque non augue dolor. Ut euismod odio sit amet sapien
fringilla sollicitudin. Vivamus vel erat quis eros tincidunt fringilla
ac et metus. Aliquam odio justo, gravida eget aliquet feugiat,
accumsan semper tortor. Donec pharetra massa erat, sed ullamcorper
augue ultricies eget. Nam sit amet metus hendrerit nunc posuere porta
non nec odio. Mauris pulvinar erat in massa dapibus mattis.

View File

@@ -0,0 +1 @@
坏人活着是为了吃与喝,而好人却是为了活着才吃与喝。

View File

@@ -0,0 +1 @@
<p>this is a test.</p>

View File

@@ -0,0 +1,3 @@
佛道をならふといふは 自己をならふ也 自己をならふといふは 自己をわする
るなり 自己をわするるといふは 萬法に證せらるるなり 萬法に證せらるると
いふは 自己の身心および他己の身心をして脱落せしむるなり

View File

@@ -0,0 +1,10 @@
Желать вам всякого добра — я желаю, о мужи афиняне, и люблю вас, а
слушаться буду скорее бога[комм. 1], чем вас, и, пока есть во мне
дыхание и способность, не перестану философствовать, уговаривать и
убеждать всякого из вас, кого только встречу, говоря то самое, что
обыкновенно говорю: о лучший из мужей, гражданин города Афин,
величайшего из городов и больше всех прославленного за мудрость и
силу, не стыдно ли тебе, что ты заботишься о деньгах, чтобы их у тебя
было как можно больше, о славе и о почестях, а о разумности, об истине
и о душе своей, чтобы она была как можно лучше, — не заботишься и не
помышляешь?