Much like Elixir has EEx, Erlang has EEl, or Embedded Erlang. With EEl we can embed and evaluate Erlang inside strings.
The Erlang code it's written between section punctuations called markers
.
In a nutshell, an IO data or a file can be evaluated
1> eel:eval(<<"Hello, <%= Name .%>!">>, #{'Name' => <<"World">>}).
["Hello, ",<<"World">>,"!"]
a file compiled to a module
1> eel:to_module(<<"Hello, <%= Name .%>!">>, foo).
{ok,foo}
2> foo:eval(#{'Name' => <<"World">>}).
["Hello, ",<<"World">>,"!"]
or a set of functions used in a module, for example, given this module
-module(foo).
-export([render/1, render/2]).
% Including the header will transform the `eel:compile` to AST
% by evaluating it in te compile time, boosting the performance.
-include("eel.hrl").
render(Bindings) ->
{ok, Snapshot} = eel:compile(<<
"<html>"
"<head>"
"<title><%= Title .%></title>"
"</head>"
"<body>"
"Hello, <%= Name .%>!"
"</body>"
"</html>"
>>),
render(Bindings, Snapshot).
render(Bindings, Snapshot) ->
{ok, RenderSnapshot} = eel_renderer:render(Bindings, Snapshot),
{eel_evaluator:eval(RenderSnapshot), RenderSnapshot}.
now run
rebar3 shell
and type this in the Erlang shell
1> {_, Snapshot} = foo:render(#{'Title' => <<"Hey!">>, 'Name' => <<"World">>}).
{["<html><head><title>",<<"Hey!">>,
"</title></head><body>Hello, ",<<"World">>,
"!</body></html>"],
{snapshot,[{1,{{1,1},"<html><head><title>"}},
{3,{{1,33},"</title></head><body>Hello, "}},
{5,{{1,73},"!</body></html>"}}],
[{2,{{1,20},<<"Hey!">>}},{4,{{1,61},<<"World">>}}],
[{2,
{{1,20},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[{...}]}]}}]}]}},
{4,
{{1,61},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[...]}]}}]}]}}],
#{'Name' => <<"World">>,'Title' => <<"Hey!">>},
[{2,['Title']},{4,['Name']}],
[{2,<<"Hey!">>},{4,<<"World">>}]}}
2> {IoData, _} = foo:render(#{'Name' => <<"Erlang">>}, Snapshot).
{["<html><head><title>",<<"Hey!">>,
"</title></head><body>Hello, ",<<"Erlang">>,
"!</body></html>"],
{snapshot,[{1,{{1,1},"<html><head><title>"}},
{3,{{1,33},"</title></head><body>Hello, "}},
{5,{{1,73},"!</body></html>"}}],
[{2,{{1,20},<<"Hey!">>}},{4,{{1,61},<<"Erlang">>}}],
[{2,
{{1,20},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[{...}]}]}}]}]}},
{4,
{{1,61},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[...]}]}}]}]}}],
#{'Name' => <<"Erlang">>,'Title' => <<"Hey!">>},
[{2,['Title']},{4,['Name']}],
[{4,<<"Erlang">>}]}}
3> erlang:iolist_to_binary(IoData).
<<"<html><head><title>Hey!</title></head><body>Hello, Erlang!</body></html>">>
Looking at the pattern matched results, the first tuple element contains the evaluated value
["<html><head><title>",<<"Hey!">>,
"</title></head><body>Hello, ",<<"World">>,
"!</body></html>"],
and the second a metadata called snapshot
{snapshot,[{1,{{1,1},"<html><head><title>"}},
{3,{{1,33},"</title></head><body>Hello, "}},
{5,{{1,73},"!</body></html>"}}],
[{2,{{1,20},<<"Hey!">>}},{4,{{1,61},<<"World">>}}],
[{2,
{{1,20},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[{...}]}]}}]}]}},
{4,
{{1,61},
[{call,1,
{remote,1,{atom,1,eel_converter},{atom,1,to_string}},
[{'fun',1,{clauses,[{clause,1,[],[],[...]}]}}]}]}}],
#{'Name' => <<"World">>,'Title' => <<"Hey!">>},
[{2,['Title']},{4,['Name']}],
[{2,<<"Hey!">>},{4,<<"World">>}]}
The line 1
will evaluate the bindings Title
and Name
, but the line 2
will only eval the Name
variable, because it uses the snapshot of the previous
render. It only eval the changes and does not need to compile the binary again, unless the expression contains the global Bindings
variable (see below),
because the snapshot
includes the required information.
The var Bindings
is a reserved one and can be used to get values in a conditional way, checking if the variable exists in the template, e.g.:
<%= maps:get('Foo', Bindings, bar) .%>
The Bindings
should contains the unbound/required variables of the template. The syntax it's a map with keys as atoms starting with upper case, e.g:
#{'Foo' => <<"foo">>, 'FooBar' => bar}
or the same in lower case when passing the option #{snake_case => true} to the render function, e.g.:
eel_renderer:render(Bindings, Snapshot, #{snake_case => true})
Passing the snake_case option the Bindings
above you must write the keys as
#{foo => <<"foo">>, foo_bar => bar}
Including Bindings
to the expression makes it to be always evaluated by the render function.
The default engine is the eel_smart_engine
.
You can implement your own engine using the behavior eel_engine
.
The eel_smart_engine
markers are:
<%=
starts an expression;.%>
indicates that the expression ends;%>
indicates that the expression continues;<%
continues the last expression if it ends with%>
;<%%
starts a comment;%%>
ends a comment.
The template should have the .eel
extension and a structure like this:
If you use VSCode, you can get highlighting by installing the Embedded Erlang (EEl) extension.
- Explain how this lib works
- Better explanation about the API
- Improve the code
- Functions documentations
- Functions specs
- Test everything
If you like this tool, please consider sponsoring me.
I'm thankful for your never-ending support ❤️
I also accept coffees ☕
Feels free to submit an issue on Github.
# Clone this repo
git clone git@github.com:williamthome/eel.git
# Navigate to the project root
cd eel
# Compile (ensure you have rebar3 installed)
rebar3 compile