├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── aihtml.d
├── erlang.mk
├── examples
├── complex.erl
├── complex.mustache
├── dom_render.erl
└── shared
│ ├── item.mustache
│ ├── level.mustache
│ └── user.mustache
└── src
├── ai_dom_node.erl
├── ai_dom_render.erl
├── ai_mustache.erl
├── ai_mustache_loader.erl
├── ai_mustache_parser.erl
├── ai_mustache_runner.erl
├── aihtml_app.erl
└── aihtml_sup.erl
/.gitignore:
--------------------------------------------------------------------------------
1 | .rebar3
2 | _rel
3 | .eunit
4 | *.o
5 | *.beam
6 | *.plt
7 | *.swp
8 | *.swo
9 | .erlang.cookie
10 | ebin
11 | log
12 | erl_crash.dump
13 | .rebar
14 | logs
15 | _build
16 | .idea
17 | *.iml
18 | rebar3.crashdump
19 | log
20 | .erlang.mk
21 | deps
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2018, David.Gao
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT = aihtml
2 | PROJECT_DESCRIPTION = html tool for productions from ailink.io
3 | PROJECT_VERSION = 0.3.7
4 |
5 | ERLC_OPTS = -Werror +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard
6 | DEPS = ailib
7 |
8 | dep_ailib = git https://github.com/DavidAlphaFox/ailib.git v0.4.5
9 |
10 | include erlang.mk
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aihtml
2 |
3 | A simple html render libary more than Mustache Template Complier
4 |
5 | ## Erlang Mustache Template
6 |
7 | Mustache is a framework-agnostic templating system that enforces separation of view logic from the template
8 | file. Indeed, it is not even possible to embed logic in the template. This
9 | allows templates to be reused across language boundaries and for other
10 | language independent uses.
11 |
12 | Working with Mustache means dealing with templates, views, and contexts.
13 | Templates contain HTML (or some other format) and Mustache tags that specify
14 | what data to pull in. A template can be either a string or a file (usually
15 | ending in .mustache). Views are Erlang modules that can define functions that
16 | are called and provide the data for the template tags. A context is an Erlang
17 | dict that contains the current context from which tags can pull data. A few
18 | examples will clarify how these items interact.
19 |
20 |
21 | ## Installation
22 |
23 | aihtml uses erlang.mk as its building tool. So currently, it only support erlang.mk.
24 |
25 |
26 | ## Difference between bbmustache
27 |
28 | The target of aihtml is to help user to build a simple view engine in the Erlang. And aihtml uses a modified version muatche compiler from [bbmustache](https://github.com/soranoba/bbmustache). But there are some difference between [bbmustache](https://github.com/soranoba/bbmustache).
29 |
30 | bbmustahce:
31 |
32 | - It supports the standards mustache sytanx.
33 | - Very light, it won't create any process or ets.
34 | - It can compile mustache file or render directly.
35 | - It can render mustache string directly.
36 |
37 | aihtml:
38 |
39 | - It also supports the standards mustache sytanx.
40 | - It adds lamda section on mustache sytanx.
41 | - Very heavy, it will create a process and use an ets to store some information.
42 | - It must compile mustache file before rendering, and store the compile result in the ets for resusing.
43 | - It can't render mustache string directly.
44 |
45 | ## How to use
46 |
47 | ### Incompatible Changes
48 |
49 | In v0.3.5 we start using the atom key to replace the binary key of tags.
50 | So when using v0.3.5 or above, please use atom keys in `context` to render the templates.
51 |
52 | ### Bootstrap
53 |
54 | aihtml has to bootstrap before rendering mustache files.
55 |
56 | It boostraps using function `ai_mustache:bootstrap`, it will using `views` directory as default directory where the mustache files are stored. And will compile all mustache files with the suffix `.mustache` into IR code and store them in ets.
57 |
58 | ```erlang
59 | bootstrap()-> ai_mustache_loader:bootstrap().
60 | ```
61 |
62 | And there is a function which can accept one params settings to change default settings.
63 |
64 | ```erlang
65 | bootstrap(Settings) -> ai_mustache_loader:bootstrap(Settings).
66 | Settings :: #{
67 | views := binary(),
68 | suffix := binary()
69 | }.
70 | ```
71 |
72 | ### Render Templates
73 |
74 | #### Context
75 |
76 | aihtml only support `maps` as context params when it render a mustache file. And the key must be a `binary`.
77 |
78 | ```erlang
79 | #{
80 | <<"user">> => #{
81 | <<"name">> => "David Gao",
82 | <<"level">> => 1
83 | },
84 | <<"stars">> => 10
85 | }
86 | ```
87 |
88 | #### Partials
89 |
90 | aihtml supports partials, and it will auto load the partial mustache file from `views` directory which can be modified by bootstrap.
91 |
92 | ```erlang
93 | {{> shared/user }}
94 | ```
95 |
96 | If we use the default settings, aihtml will load `user.mustache` from `views/shared` directory auto.
97 |
98 | #### Sections
99 |
100 | Section in context is a `list`
101 |
102 | friends.mustache
103 | ```mustache
104 |
105 | {{# friends }}
106 | -
107 |
108 | {{ friends.name }}
109 |
110 | {{/ friends }}
111 |
112 | ```
113 | friends_context.erl
114 | ```erlang
115 | Context = #{
116 | <<"friends">> => [
117 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
118 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
119 | ]
120 | }
121 | ```
122 |
123 |
124 | Section in context is `fun/2`
125 | The first param of function will be the rendered binary inside the section.
126 |
127 | friends.mustache
128 | ```mustache
129 | {{# warpped }}
130 |
131 | {{# friends }}
132 | -
133 |
134 | {{ friends.name }}
135 |
136 | {{/ friends }}
137 |
138 | {{/ warpped}}
139 | ```
140 |
141 | friends_context.erl
142 | ```erlang
143 | warpped(Acc,Context) -> <<" ",Acc/binary,"
" >>.
144 | Context = #{
145 | <<"friends">> => [
146 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
147 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
148 | ],
149 | <<"warpped">> => fun warpped/2
150 | }
151 | ```
152 |
153 | #### Inverted Sections
154 |
155 | Inverted section in context is a `list` or not exsist
156 |
157 | friends.mustache
158 | ```mustache
159 | {{^ friends }}
160 | Want to know some new friends ?
161 | {{/ friends }}
162 | ```
163 |
164 | friends_context.erl
165 | ```erlang
166 | Context = #{
167 | <<"friends">> => []
168 | }
169 | ```
170 | or
171 | ```erlang
172 | Context = #{}
173 | ```
174 |
175 | #### Has Section
176 |
177 | Has section in context is `map`
178 |
179 | navbar.mustache
180 | ```mustache
181 | {{+ user }}
182 |
183 | {{user.name}}
184 | {{user.level}}
185 |
186 | {{/ user }}
187 | ```
188 |
189 | navbar_context.erl
190 | ```erlang
191 | #{
192 | <<"user">> => #{
193 | <<"name">> => "David Gao",
194 | <<"level">> => 1
195 | }
196 | }
197 | ```
198 |
199 | Has section in context is `bool` or `binary`
200 |
201 | friends.mustache
202 | ```mustache
203 | {{+ has_friends }}
204 |
205 | {{# friends }}
206 | -
207 |
208 | {{ friends.name }}
209 |
210 | {{/ friends }}
211 |
212 | {{/ has_friends}}
213 | ```
214 |
215 | friends_context.erl
216 | ```erlang
217 | Context = #{
218 | <<"friends">> => [
219 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
220 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
221 | ],
222 | <<"has_friends">> => true
223 | }
224 | ```
225 |
226 | Has section in context is `fun/1`
227 |
228 | friends.mustache
229 | ```mustache
230 | {{+ has_friends }}
231 |
232 | {{# friends }}
233 | -
234 |
235 | {{ friends.name }}
236 |
237 | {{/ friends }}
238 |
239 | {{/ has_friends}}
240 | ```
241 |
242 | friends_context.erl
243 | ```erlang
244 | has_friends(Context) ->
245 | case maps:get(<<"friends>>,Context, undefined) of
246 | undefined -> false;
247 | _ -> true
248 | end.
249 | Context = #{
250 | <<"friends">> => [
251 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
252 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
253 | ],
254 | <<"has_friends">> => fun has_friends/1
255 | }
256 | ```
257 |
258 | #### Inverted Has Section
259 |
260 | Inverted has section in context is `bool`
261 |
262 | friends.mustache
263 | ```mustache
264 | {+ has_friends }}
265 |
266 | {{# friends }}
267 | -
268 |
269 | {{ friends.name }}
270 |
271 | {{/ friends }}
272 |
273 | {{/ has_friends}}
274 | {{- has_friends }}
275 | Want to know some new friends ?
276 | {{/ has_friends }}
277 | ```
278 |
279 | friends_context.erl
280 | ```erlang
281 | Context = #{
282 | <<"friends">> => [
283 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
284 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
285 | ],
286 | <<"has_friends">> => true
287 | }
288 | ```
289 |
290 | Inverted has section in context is `fun/1`
291 |
292 | friends.mustache
293 | ```mustache
294 | {{+ has_friends }}
295 |
296 | {{# friends }}
297 | -
298 |
299 | {{ friends.name }}
300 |
301 | {{/ friends }}
302 |
303 | {{/ has_friends}}
304 | {{- has_friends }}
305 | Want to know some new friends ?
306 | {{/ has_friends }}
307 | ```
308 |
309 | friends_context.erl
310 | ```erlang
311 | has_friends(Context) ->
312 | case maps:get(<<"friends>>,Context, undefined) of
313 | undefined -> false;
314 | _ -> true
315 | end.
316 | Context = #{
317 | <<"friends">> => [
318 | #{<<"name">> => "Jane", <<"avatar">> => "/images/avatar/ jane.png" },
319 | #{<<"name">> => "David", <<"avatar">> => "/images/avatar/ David.png" },
320 | ],
321 | <<"has_friends">> => fun has_friends/1
322 | }
323 | ```
324 |
325 | #### lambda
326 |
327 | This is an extends of aihtml on mustach syntax.
328 |
329 | Lambda in context is `fun/1`
330 | layout.mustache
331 | ```mustache
332 | {{* yield}}
333 | ```
334 |
335 | layout_context.erl
336 | ```erlang
337 | yield(Context) -> ......
338 |
339 | Context = #{
340 | <<"yield">> => fun yield/1
341 | }
342 | ```
343 |
344 | Lambda in context is `fun/2` and a value
345 |
346 | layout.mustache
347 | ```mustache
348 | {{* yield}}
349 | ```
350 |
351 | layout_context.erl
352 | ```erlang
353 | yield(Template,Context)->
354 | ai_mustache:render(Template,Context).
355 | render(Template,State) ->
356 | Context = maps:get(context,State,#{}),
357 | Layout = maps:get(layout,State,<<"layout/default">>),
358 | LayoutContext = Context#{ <<"yield">> => [fun yield/2,Template] },
359 | ai_mustache:render(Layout,LayoutContext).
360 | ```
361 |
362 | ## Projects who use this
363 |
364 | - [aiwiki](https://github.com/DavidAlphaFox/aiwiki) a very simple blog.
365 |
--------------------------------------------------------------------------------
/aihtml.d:
--------------------------------------------------------------------------------
1 |
2 | COMPILE_FIRST +=
3 |
--------------------------------------------------------------------------------
/examples/complex.erl:
--------------------------------------------------------------------------------
1 | -module(complex).
2 | -compile(export_all).
3 |
4 | -define(COUNT, 500).
5 | yield(Name,Ctx)->
6 | Header = maps:get(header,Ctx),
7 | <>.
8 |
9 | context()->
10 | A = maps:from_list([{name, "red"}, {current, true}, {url, "#Red"}]),
11 | B = maps:from_list([{name, "green"}, {current, true}, {url, "#Green"}]),
12 | C = maps:from_list([{name, "blue"}, {current, false}, {url, "#Blue"}]),
13 | #{
14 | items => [A,B,C],
15 | header => <<"Colors">>,
16 | list => true,
17 | empty => false,
18 | user => #{ name => <<"David Gao">>},
19 | level => #{ name => <<"VIP User">> },
20 | yield => [fun yield/2,<<"Test">>]
21 | }.
22 |
23 |
24 |
25 | %%---------------------------------------------------------------------------
26 |
27 | start() ->
28 | code:add_patha("../ebin"),
29 | code:add_patha("../deps/ailib/ebin"),
30 | application:start(ailib),
31 | application:start(aihtml),
32 |
33 | {ok,CWD} = file:get_cwd(),
34 | ai_mustache:bootstrap(#{views => CWD}),
35 |
36 | Output = ai_mustache:render("complex",context()),
37 | io:format("~ts~n",[Output]),
38 | T0 = os:timestamp(),
39 | render(context(), ?COUNT),
40 | T1 = os:timestamp(),
41 | Diff = timer:now_diff(T1, T0),
42 | Mean = Diff / ?COUNT,
43 | io:format("~nTotal time: ~.2fs~n", [Diff / 1000000]),
44 | io:format("Mean render time: ~.2fms~n", [Mean / 1000]).
45 |
46 |
47 | render(_Ctx,0) ->
48 | ok;
49 | render(Ctx,N) ->
50 | ai_mustache:render("complex",Ctx),
51 | render(Ctx,N - 1).
52 |
--------------------------------------------------------------------------------
/examples/complex.mustache:
--------------------------------------------------------------------------------
1 | {{header}}
2 |
3 | {{# items}}
4 | {{> shared/item}}
5 | {{/ items}}
6 |
7 | {{> shared/user}}
8 | |
9 | {{#boolean}}
10 | {{/boolean}}
11 | |
12 | {{*yield}}
13 |
--------------------------------------------------------------------------------
/examples/dom_render.erl:
--------------------------------------------------------------------------------
1 | -module(dom_render).
2 | -compile(export_all).
3 |
4 | build()->
5 | Tree = ai_dom_node:new(html),
6 | Tree0 = ai_dom_node:insert_attributes(#{<<"lang">> => <<"zh_cn">>},Tree),
7 | Header = ai_dom_node:new(header),
8 | Body = ai_dom_node:new(body),
9 | Table = ai_dom_node:new(table),
10 | Tr = ai_dom_node:new(tr),
11 | Thead = ai_dom_node:new(thead),
12 | Ths = lists:map(fun(El)->
13 | ai_dom_node:set_value(ai_dom_node:id(El),El)
14 | end,[ai_dom_node:new(ID,th) || ID
15 | <- lists:seq(1,10)]),
16 | Tr0 = ai_dom_node:append_children(Ths,Tr),
17 | Thead0 = ai_dom_node:append_child(Tr0,Thead),
18 | Table0 = ai_dom_node:append_child(Thead0,Table),
19 | Body0 = ai_dom_node:append_child(Table0,Body),
20 | ai_dom_node:append_children([Header,Body0],Tree0).
21 |
22 | start() ->
23 | code:add_patha("../ebin"),
24 | code:add_patha("../deps/ailib/ebin"),
25 | application:start(ailib),
26 | application:start(aihtml),
27 | Tree = build(),
28 | R = ai_dom_render:render(Tree),
29 | io:format("~ts~n",[R]).
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/shared/item.mustache:
--------------------------------------------------------------------------------
1 | {{+ items.current }}
2 | {{ items.name }}
3 | 中文
4 | {{!这里是注释}}
5 | {{/ items.current }}
6 |
7 | {{+ link}}
8 | {{name}}
9 | {{- not_found}}
10 | 此处应该显示
11 | {{/ not_found}}
12 | {{+ not_found}}
13 | 此处应不该显示
14 | {{/ not_found}}
15 | {{/ link}}
16 |
17 |
--------------------------------------------------------------------------------
/examples/shared/level.mustache:
--------------------------------------------------------------------------------
1 | {{#level}}
2 | {{level.name}}
3 | {{/level}}
4 |
--------------------------------------------------------------------------------
/examples/shared/user.mustache:
--------------------------------------------------------------------------------
1 | {{#user}}
2 | {{user.name}}
3 | {{/user}}
--------------------------------------------------------------------------------
/src/ai_dom_node.erl:
--------------------------------------------------------------------------------
1 | -module(ai_dom_node).
2 |
3 | -export([new/0,new/1,new/2]).
4 | -export([insert_child/3,append_child/2,remove_child/2,
5 | append_children/2,remove_children/1,children/1]).
6 | -export([insert_attribute/3,insert_attributes/2,remove_attribute/2,
7 | attribute/2,remove_attributes/1, attributes/1]).
8 | -export([set_value/2,set_id/2,set_tag/2,value/1,id/1,tag/1]).
9 | -export([set_opening/2,opening/1,set_slash/2,slash/1]).
10 |
11 | -record(ai_dom_node,{
12 | id :: binary(), %% 节点ID
13 | tag :: atom(), %% 节点名字
14 | attributes :: maps:maps(), %% 节点属性
15 | children :: [], %% 子节点
16 | value :: term(), %% 节点值
17 | opening :: boolean(), %% 开放标签
18 | slash :: boolean() %% 开放标签后面是否有slash
19 | }).
20 |
21 | -opaque ai_dom_node() :: #ai_dom_node{}.
22 | -export_type([ai_dom_node/0]).
23 |
24 | new()-> new(undefined,undefined).
25 | new(Tag)-> new(undefined,Tag).
26 | new(ID,Tag)->
27 | #ai_dom_node{
28 | id = ID,tag = Tag,
29 | attributes = maps:new(),children = [],
30 | value = undefined,
31 | opening = false,
32 | slash = false
33 | }.
34 | insert_child(Index,Child,Node)->
35 | case Node#ai_dom_node.children of
36 | [] when Index == 0 ->
37 | Node#ai_dom_node{children = [Child]};
38 | [] ->
39 | throw({error,out_of_range});
40 | Children ->
41 | {_N,Children0} =
42 | lists:foldl(fun(C,{N,Acc})->
43 | if N == Index -> {N + 2,[C,Child|Acc]};
44 | true -> {N+1,[C|Acc]}
45 | end
46 | end,{0,[]},Children),
47 | Node#ai_dom_node{children = lists:reverse(Children0)}
48 | end.
49 |
50 | append_child(Child,Node)->
51 | Node#ai_dom_node{
52 | children = lists:append(Node#ai_dom_node.children,[Child])
53 | }.
54 | remove_child(Index,Node)->
55 | {CHead,[_I|CTail]} = lists:split(Index,Node#ai_dom_node.children),
56 | Node#ai_dom_node{
57 | children = lists:append(CHead,CTail)
58 | }.
59 | remove_children(Node)-> Node#ai_dom_node{children = []}.
60 | append_children(Children,Node)->
61 | Node#ai_dom_node{
62 | children = lists:append(Node#ai_dom_node.children,Children)
63 | }.
64 | children(Node)-> Node#ai_dom_node.children.
65 |
66 | insert_attribute(Name,Value,Node)->
67 | Node#ai_dom_node{
68 | attributes = maps:put(Name,Value,Node#ai_dom_node.attributes)
69 | }.
70 | remove_attribute(Name,Node)->
71 | Node#ai_dom_node{
72 | attributes = maps:remove(Name,Node#ai_dom_node.attributes)
73 | }.
74 | insert_attributes(Attributes,Node)->
75 | Node#ai_dom_node{
76 | attributes = maps:merge(Node#ai_dom_node.attributes,Attributes)
77 | }.
78 | remove_attributes(Node)->
79 | Node#ai_dom_node{
80 | attributes = maps:new()
81 | }.
82 | attribute(Name,Node)->
83 | Attributes = Node#ai_dom_node.attributes,
84 | maps:get(Name,Attributes,undefined).
85 |
86 | attributes(Node)->Node#ai_dom_node.attributes.
87 |
88 |
89 | set_value(Value,Node)-> Node#ai_dom_node{value = Value}.
90 | set_tag(Tag,Node)->Node#ai_dom_node{tag = Tag}.
91 | set_id(ID,Node)-> Node#ai_dom_node{id = ID}.
92 | value(Node)-> Node#ai_dom_node.value.
93 | tag(Node)-> Node#ai_dom_node.tag.
94 | id(Node)-> Node#ai_dom_node.id.
95 |
96 | set_opening(Value,Node)-> Node#ai_dom_node{opening = Value}.
97 | set_slash(Value,Node)-> Node#ai_dom_node{slash = Value}.
98 | opening(Node)-> Node#ai_dom_node.opening.
99 | slash(Node)-> Node#ai_dom_node.slash.
--------------------------------------------------------------------------------
/src/ai_dom_render.erl:
--------------------------------------------------------------------------------
1 | -module(ai_dom_render).
2 | -export([render/1]).
3 |
4 | render(Node)-> render([Node],[],<<>>).
5 | render(undefined,Node)-> ai_string:to_string(ai_dom_node:value(Node));
6 | render(Tag,Node) ->
7 | TagBin = ai_string:to_string(Tag),
8 | Attributes = attributes(Node),
9 | case ai_dom_node:opening(Tag) of
10 | false ->
11 | Value =
12 | case ai_dom_node:value(Node) of
13 | undefined -> <<"">>;
14 | NV -> ai_string:to_string(NV)
15 | end,
16 | <<"<",TagBin/binary,Attributes/binary,">",Value/binary,"",TagBin/binary,">">>;
17 | true ->
18 | EndTag =
19 | case ai_dom_node:slash(Node) of
20 | true -> <<" />">>;
21 | false -> <<" >">>
22 | end,
23 | <<"<",TagBin/binary,Attributes/binary,EndTag/binary>>
24 | end.
25 | render([],[],Acc)-> Acc;
26 | render([],[{Parent,Rest,OldAcc}|Stack],Acc)->
27 | TagBin = ai_string:to_string(ai_dom_node:tag(Parent)),
28 | Attributes = attributes(Parent),
29 | Acc1 = <<"<",TagBin/binary,Attributes/binary,">",Acc/binary,"",TagBin/binary,">">>,
30 | render(Rest,Stack,<>);
31 | render([El|Rest],Stack,Acc) ->
32 | case ai_dom_node:children(El) of
33 | []->
34 | NodeBin = render(ai_dom_node:tag(El),El),
35 | render(Rest,Stack,<>);
36 | Children -> render(Children,[{El,Rest,Acc}|Stack],<<>>)
37 | end.
38 |
39 | attributes(Node)->
40 | Attributes = ai_dom_node:attributes(Node),
41 | Attributes0 =
42 | case ai_dom_node:id(Node) of
43 | undefined -> Attributes;
44 | ID -> maps:put(id,ID,Attributes)
45 | end,
46 | lists:foldl(fun({K,V},Acc)->
47 | Attr = ai_string:to_string(K),
48 | case V of
49 | ture -> <>;
50 | _->
51 | AttrValue = ai_string:to_string(V),
52 | <>
53 | end
54 | end,<<" ">>,maps:to_list(Attributes0)).
55 |
56 |
--------------------------------------------------------------------------------
/src/ai_mustache.erl:
--------------------------------------------------------------------------------
1 | -module(ai_mustache).
2 | -export([bootstrap/0,bootstrap/1]).
3 | -export([reload/0]).
4 | -export([render/2]).
5 |
6 | %% 定义如下规则
7 |
8 |
9 | %% Partils中包含的是子模板的代码
10 | %% Partils是一个map
11 | %% 结构为 #{path => IRCode}
12 |
13 | %% Ctx 全局共享
14 | %% 对于mustach中 {{ shared.name }} 的变量会自动去寻找
15 | %% #{ "shared" => #{"name" => name} }
16 |
17 | render(Template,Ctx)->
18 | Run =
19 | case erlang:get(Template) of
20 | undefined ->
21 | Code = ai_mustache_loader:template(Template),
22 | erlang:put(Template,Code),
23 | Code;
24 | Cache ->
25 | Cache
26 | end,
27 | ai_mustache_runner:render(Run,Ctx).
28 |
29 | bootstrap()-> ai_mustache_loader:bootstrap().
30 | bootstrap(Settings) -> ai_mustache_loader:bootstrap(Settings).
31 | reload()-> ai_mustache_loader:reload().
32 |
--------------------------------------------------------------------------------
/src/ai_mustache_loader.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% @author
3 | %%% @copyright (C) 2018,
4 | %%% @doc
5 | %%% 模板加载器,单写入ets
6 | %%% @end
7 | %%% Created : 19 Dec 2018 by
8 | %%%-------------------------------------------------------------------
9 | -module(ai_mustache_loader).
10 |
11 | -behaviour(gen_server).
12 |
13 | %% API
14 | -export([start_link/0]).
15 |
16 | %% gen_server callbacks
17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
18 | terminate/2, code_change/3, format_status/2]).
19 |
20 | -export([template/1]).
21 | -export([bootstrap/0,bootstrap/1]).
22 | -export([reload/0]).
23 |
24 | -define(SERVER, ?MODULE).
25 |
26 | -record(state, {view_path,suffix = <<".mustache">>}).
27 |
28 | %%%===================================================================
29 | %%% API
30 | %%%===================================================================
31 | reload() ->
32 | gen_server:call(?SERVER, reload).
33 |
34 | template(Template)->
35 | Name = erlang:binary_to_atom(ai_string:to_string(Template), utf8),
36 | TKey = template_key(Name),
37 | CKey = code_key(Name),
38 | Match = ets:lookup(ai_mustache,TKey),
39 | case Match of
40 | [] ->
41 | ok = load(Name),
42 | template(Name);
43 | [{TKey,Partials}]->
44 | PK = template_partial(Partials),
45 | M1 = lists:foldl(fun({I,V},Acc)->
46 | [{I,IR}] = ets:lookup(ai_mustache,I),
47 | maps:put(V,IR,Acc)
48 | end,#{},maps:to_list(PK)),
49 | [{CKey,TIR}] = ets:lookup(ai_mustache,CKey),
50 | {TIR,M1}
51 | end.
52 |
53 | template_partial(Partials)->
54 | lists:foldl(
55 | fun(P,Acc) ->
56 | TKey = template_key(P),
57 | CKey = code_key(P),
58 | [{TKey,PPartials}] = ets:lookup(ai_mustache,TKey),
59 | NewPartials = template_partial(PPartials),
60 | maps:merge(Acc#{CKey => P},NewPartials)
61 | end,#{},Partials).
62 |
63 | load(Template)-> gen_server:call(?SERVER,{load,Template}).
64 | bootstrap()-> gen_server:call(?SERVER,{bootstrap,undefined,undefined}).
65 | bootstrap(Settings)->
66 | ViewPath = maps:get(views,Settings,undefined),
67 | Suffix = maps:get(suffix,Settings,undefined),
68 | gen_server:call(?SERVER,{bootstrap,ViewPath,Suffix}).
69 |
70 |
71 | %%--------------------------------------------------------------------
72 | %% @doc
73 | %% Starts the server
74 | %% @end
75 | %%--------------------------------------------------------------------
76 | -spec start_link() -> {ok, Pid :: pid()} |
77 | {error, Error :: {already_started, pid()}} |
78 | {error, Error :: term()} |
79 | ignore.
80 | start_link() ->
81 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
82 |
83 | %%%===================================================================
84 | %%% gen_server callbacks
85 | %%%===================================================================
86 |
87 | %%--------------------------------------------------------------------
88 | %% @private
89 | %% @doc
90 | %% Initializes the server
91 | %% @end
92 | %%--------------------------------------------------------------------
93 | -spec init(Args :: term()) -> {ok, State :: term()} |
94 | {ok, State :: term(), Timeout :: timeout()} |
95 | {ok, State :: term(), hibernate} |
96 | {stop, Reason :: term()} |
97 | ignore.
98 | init([]) ->
99 | ai_mustache = ets:new(ai_mustache,[set,named_table,protected,
100 | {write_concurrency,false},{read_concurrency,true}]),
101 | process_flag(trap_exit, true),
102 | State = #state{},
103 | {ok,CWD} = file:get_cwd(),
104 | ViewPath = filename:join(CWD,"views"),
105 | {ok,State#state{view_path = ai_string:to_string(ViewPath)}}.
106 |
107 | %%--------------------------------------------------------------------
108 | %% @private
109 | %% @doc
110 | %% Handling call messages
111 | %% @end
112 | %%--------------------------------------------------------------------
113 | -spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
114 | {reply, Reply :: term(), NewState :: term()} |
115 | {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
116 | {reply, Reply :: term(), NewState :: term(), hibernate} |
117 | {noreply, NewState :: term()} |
118 | {noreply, NewState :: term(), Timeout :: timeout()} |
119 | {noreply, NewState :: term(), hibernate} |
120 | {stop, Reason :: term(), Reply :: term(), NewState :: term()} |
121 | {stop, Reason :: term(), NewState :: term()}.
122 | handle_call({load,Template},_From,#state{view_path = ViewPath,suffix = Suffix} = State)->
123 | Reply =
124 | try
125 | TKey = template_key(ai_string:to_string(Template)),
126 | case ets:lookup(ai_mustache,TKey) of
127 | [] -> load(Template,ViewPath,Suffix);
128 | _ -> ok
129 | end
130 | catch
131 | _Error:Reason -> {error,Reason}
132 | end,
133 | {reply,Reply,State};
134 | handle_call(reload,_From,#state{suffix = Suffix, view_path = ViewPath} = State)->
135 | Reply =
136 | try
137 | ets:delete_all_objects(ai_mustache),
138 | bootstrap_load(ViewPath,Suffix)
139 | catch
140 | _Error:Reason -> { error,Reason }
141 | end,
142 | {reply,Reply,State};
143 |
144 | handle_call({bootstrap,ViewPath0,Suffix0},_From,#state{suffix = Suffix, view_path = ViewPath} = State)->
145 | ViewPath1 =
146 | if ViewPath0 == undefined -> ViewPath;
147 | true -> ai_string:to_string(ViewPath0)
148 | end,
149 | Suffix1 =
150 | if Suffix0 == undefined -> Suffix;
151 | true -> ai_string:to_string(Suffix0)
152 | end,
153 | Reply =
154 | try
155 | bootstrap_load(ViewPath1,Suffix1)
156 | catch
157 | _Error:Reason -> {error,Reason}
158 | end,
159 | {reply,Reply,
160 | State#state{view_path = ViewPath1,suffix = Suffix1}
161 | };
162 |
163 | handle_call(_Request, _From, State) ->
164 | Reply = ok,
165 | {reply, Reply, State}.
166 |
167 | %%--------------------------------------------------------------------
168 | %% @private
169 | %% @doc
170 | %% Handling cast messages
171 | %% @end
172 | %%--------------------------------------------------------------------
173 | -spec handle_cast(Request :: term(), State :: term()) ->
174 | {noreply, NewState :: term()} |
175 | {noreply, NewState :: term(), Timeout :: timeout()} |
176 | {noreply, NewState :: term(), hibernate} |
177 | {stop, Reason :: term(), NewState :: term()}.
178 | handle_cast(_Request, State) ->
179 | {noreply, State}.
180 |
181 | %%--------------------------------------------------------------------
182 | %% @private
183 | %% @doc
184 | %% Handling all non call/cast messages
185 | %% @end
186 | %%--------------------------------------------------------------------
187 | -spec handle_info(Info :: timeout() | term(), State :: term()) ->
188 | {noreply, NewState :: term()} |
189 | {noreply, NewState :: term(), Timeout :: timeout()} |
190 | {noreply, NewState :: term(), hibernate} |
191 | {stop, Reason :: normal | term(), NewState :: term()}.
192 | handle_info(_Info, State) ->
193 | {noreply, State}.
194 |
195 | %%--------------------------------------------------------------------
196 | %% @private
197 | %% @doc
198 | %% This function is called by a gen_server when it is about to
199 | %% terminate. It should be the opposite of Module:init/1 and do any
200 | %% necessary cleaning up. When it returns, the gen_server terminates
201 | %% with Reason. The return value is ignored.
202 | %% @end
203 | %%--------------------------------------------------------------------
204 | -spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
205 | State :: term()) -> any().
206 | terminate(_Reason, _State) ->
207 | ok.
208 |
209 | %%--------------------------------------------------------------------
210 | %% @private
211 | %% @doc
212 | %% Convert process state when code is changed
213 | %% @end
214 | %%--------------------------------------------------------------------
215 | -spec code_change(OldVsn :: term() | {down, term()},
216 | State :: term(),
217 | Extra :: term()) -> {ok, NewState :: term()} |
218 | {error, Reason :: term()}.
219 | code_change(_OldVsn, State, _Extra) ->
220 | {ok, State}.
221 |
222 | %%--------------------------------------------------------------------
223 | %% @private
224 | %% @doc
225 | %% This function is called for changing the form and appearance
226 | %% of gen_server status when it is returned from sys:get_status/1,2
227 | %% or when it appears in termination error logs.
228 | %% @end
229 | %%--------------------------------------------------------------------
230 | -spec format_status(Opt :: normal | terminate,
231 | Status :: list()) -> Status :: term().
232 | format_status(_Opt, Status) ->
233 | Status.
234 |
235 | %%%===================================================================
236 | %%% Internal functions
237 | %%%===================================================================
238 | template_key(Name)->{t,Name}.
239 | code_key(Name)->{c,Name}.
240 |
241 | remove_suffix(Name,Suffix)->
242 | if
243 | erlang:byte_size(Suffix) > 0 -> binary:replace(Name,Suffix,<<"">>);
244 | true -> Name
245 | end.
246 | has_suffix(Name,Suffix)->
247 | if
248 | erlang:byte_size(Suffix) > 0 ->
249 | case string:find(Name,Suffix,trailing) of
250 | nomatch -> false;
251 | _ -> true
252 | end;
253 | true -> true
254 | end.
255 |
256 | load(Template,ViewPath,Suffix)->
257 | File = filename:join(ViewPath,Template),
258 | Name = erlang:binary_to_atom(remove_suffix(Template,Suffix),utf8),
259 | case file:read_file(File) of
260 | {ok,Body}->
261 | {IR,Partials} = ai_mustache_parser:parse(Body),
262 | TKey = template_key(Name),
263 | CKey = code_key(Name),
264 | ok = load_partial(Partials,ViewPath,Suffix),
265 | ets:insert(ai_mustache,{CKey,IR}),
266 | ets:insert(ai_mustache,{TKey,Partials}),
267 | ok;
268 | Error -> Error
269 | end.
270 |
271 |
272 | load_partial([],_ViewPath,_Suffix)-> ok;
273 | load_partial([H|T],ViewPath,Suffix)->
274 | File = ai_string:to_string(H),
275 | Name = erlang:binary_to_atom(remove_suffix(File,Suffix),utf8),
276 | TKey = template_key(Name),
277 | case ets:lookup(ai_mustache,TKey) of
278 | [] -> load(File,ViewPath,Suffix);
279 | _ -> ok
280 | end,
281 | load_partial(T,ViewPath,Suffix).
282 |
283 |
284 | %% 找出特定目录下所有的文件
285 | recursive_dir(Dir) ->
286 | recursive_dir(Dir, true). % default value of FilesOnly is true
287 |
288 | recursive_dir(Dir, FilesOnly) ->
289 | case filelib:is_file(Dir) of
290 | true ->
291 | case filelib:is_dir(Dir) of
292 | true -> {ok, recursive_dir([Dir], FilesOnly, [])};
293 | false -> {error, enotdir}
294 | end;
295 | false -> {error, enoent}
296 | end.
297 |
298 |
299 |
300 | recursive_dir([], _FilesOnly, Acc) -> Acc;
301 | recursive_dir([Path|Paths], FilesOnly, Acc) ->
302 | case filelib:is_dir(Path) of
303 | false -> recursive_dir(Paths,FilesOnly,[Path | Acc]);
304 | true ->
305 | {ok, Listing} = file:list_dir(Path),
306 | SubPaths = [filename:join(Path, Name) || Name <- Listing],
307 | Acc0 = case FilesOnly of
308 | true -> Acc;
309 | false -> [Path | Acc]
310 | end,
311 | recursive_dir(Paths ++ SubPaths, FilesOnly,Acc0)
312 | end.
313 |
314 |
315 | bootstrap_templates(Files, Prefix,Suffix) ->
316 | lists:foldl(
317 | fun(I0,Acc)->
318 | I = ai_string:to_string(I0),
319 | case has_suffix(I,Suffix) of
320 | false -> Acc;
321 | _->
322 | T0 = string:prefix(I,Prefix),
323 | [T0|Acc]
324 | end
325 | end,[],Files).
326 |
327 | bootstrap_load(ViewPath,Suffix)->
328 | MaybeFiles = recursive_dir(ViewPath),
329 | case MaybeFiles of
330 | {error,_} -> MaybeFiles;
331 | {ok,Files}->
332 | Prefix0 = filename:join(ViewPath,<<"./">>),
333 | Prefix = <>,
334 | Templates = bootstrap_templates(Files,Prefix,Suffix),
335 | lists:foldl(
336 | fun(Template,Acc)->
337 | case Acc of
338 | ok -> load(Template,ViewPath,Suffix);
339 | _ -> Acc
340 | end
341 | end,ok,Templates)
342 | end.
343 |
--------------------------------------------------------------------------------
/src/ai_mustache_parser.erl:
--------------------------------------------------------------------------------
1 | %% The MIT License (MIT)
2 | %%
3 | %% Copyright (c) 2015 Hinagiku Soranoba
4 | %%
5 | %% Permission is hereby granted, free of charge, to any person obtaining a copy
6 | %% of this software and associated documentation files (the "Software"), to deal
7 | %% in the Software without restriction, including without limitation the rights
8 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | %% copies of the Software, and to permit persons to whom the Software is
10 | %% furnished to do so, subject to the following conditions:
11 | %%
12 | %% The above copyright notice and this permission notice shall be included in all
13 | %% copies or substantial portions of the Software.
14 | %%
15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | %% SOFTWARE.
22 | %% @copyright 2015 Hinagiku Soranoba All Rights Reserved.
23 | %%
24 | %% @doc Binary pattern match Based Mustach template engine for Erlang/OTP.
25 | %%
26 | %% Please refer to [the man page](http://mustache.github.io/mustache.5.html) and [the spec](https://github.com/mustache/spec) of mustache as the need arises.
27 | %%
28 | %% Please see [this](../benchmarks/README.md) for a list of features that bbmustache supports.
29 | %%
30 |
31 | -module(ai_mustache_parser).
32 |
33 | -export([parse/1]).
34 |
35 | -define(PARSE_ERROR, incorrect_format).
36 |
37 | -define(IIF(Cond, TValue, FValue),
38 | case Cond of true -> TValue; false -> FValue end).
39 | -define(ADD(X, Y), ?IIF(X =:= <<>>, Y, [{binary,X} | Y])).
40 |
41 | -define(START_TAG, <<"{{">>).
42 | -define(STOP_TAG, <<"}}">>).
43 |
44 | -type key() :: atom().
45 | -type tag() :: {tag,none,[key()]}
46 | | {tag,partial,[key()]}
47 | | {tag,raw,[key()]}
48 | | {section, [key()], [tag()],boolean()}
49 | | binary(). % plain text
50 | -record(state,{
51 | start = ?START_TAG :: binary(),
52 | stop = ?STOP_TAG :: binary(),
53 | partials = [],
54 | standalone = true :: boolean()
55 | }).
56 | -type state() :: #state{}.
57 | -type endtag() :: {endtag, {state(), [key()], LastTagSize :: non_neg_integer(), Rest :: binary(), Result :: [tag()]}}.
58 |
59 | parse(Body)->
60 | {IR,State} = parse(#state{},Body),
61 | IR0 = remove_empty_section(IR),
62 | {merge_continuous_binary(IR0),State#state.partials}.
63 |
64 | -spec parse(state(),binary()) -> {[tag()],#state{}}.
65 | parse(State0,Bin) ->
66 | case parse1(State0,Bin,[]) of
67 | {endtag, {_, Keys, _, _, _}} ->
68 | error({?PARSE_ERROR, {section_is_incorrect, binary_join(Keys, <<".">>)}});
69 | {State,Tags} ->
70 | {lists:reverse(Tags),State}
71 | end.
72 |
73 | %% pase的第一阶段
74 | %% 找出StartTag或者该分段中的文本
75 | -spec parse1(state(),Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
76 | parse1(#state{start = StartTag} = State,Bin, Result) ->
77 | case binary:match(Bin, [StartTag, <<"\n">>]) of %% 找StartTag,或者\n
78 | nomatch -> {State, ?ADD(Bin, Result)}; %% 整个就是个binary
79 | {S, L} -> %% 找到StartTag或者\n了
80 | case binary:at(Bin, S) of
81 | $\n -> %% 此处进行优化,只有是换行符号的时候,才需要进行计算
82 | Pos = S + L, %%binary的切开点,Pos是未匹配字串的第一个字符
83 | B2 = binary:part(Bin, Pos, erlang:byte_size(Bin) - Pos),
84 | parse1(State#state{standalone = true}, B2,
85 | ?ADD(binary:part(Bin, 0, Pos), Result)); % \n,\n前面是个文本,此处切割出来的字符串包含\n
86 | _ ->
87 | parse2(State, split_tag(State, Bin), Result) %% 找到标签了,整个文本向前找标签
88 | end
89 | end.
90 | %% @doc Part of the `parse/1'
91 | %%
92 | %% ATTENTION: The result is a list that is inverted.
93 | -spec parse2(state(), iolist(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
94 | parse2(State, [B1, B2, B3], Result) ->
95 | case remove_space_from_head(B2) of %% 清除\s和\t,然后匹配tag类型
96 | <> when T =:= $&; T =:= ${ ->
97 | parse1(State#state{standalone = false}, B3, [{tag,raw,keys(Tag)} | ?ADD(B1, Result)]);
98 | <> when T =:= $#; T =:= $^ ->
99 | parse_section(State, T, keys(Tag), B3, ?ADD(B1, Result));
100 | <> when T =:= $+; T =:= $- ->
101 | parse_has(State#state{standalone = false},T,keys(Tag),B3,?ADD(B1,Result));
102 | <<"*",Tag/binary>> ->
103 | parse1(State#state{standalone = false},B3,[{lambda,keys(Tag)}| ?ADD(B1,Result)]);
104 | <<"=", Tag0/binary>> ->
105 | Tag1 = remove_space_from_tail(Tag0),
106 | Size = erlang:byte_size(Tag1) - 1,
107 | case Size >= 0 andalso Tag1 of
108 | <> ->
109 | parse_delimiter(State, Tag2, B3, ?ADD(B1,Result));
110 | _ ->
111 | error({?PARSE_ERROR, {unsupported_tag, <<"=", Tag0/binary>>}})
112 | end;
113 | <<"!", _/binary>> ->
114 | parse3(State, B3,?ADD(B1, Result));
115 | <<"/", Tag/binary>> ->
116 | EndTagSize = byte_size(B2) + byte_size(State#state.start) + byte_size(State#state.stop),
117 | {endtag, {State, keys(Tag), EndTagSize, B3, ?ADD(B1,Result)}};
118 | <<">", Tag/binary>> ->
119 | parse_partial(State, keys(Tag), B3, ?ADD(B1,Result));
120 | Tag ->
121 | parse1(State#state{standalone = false}, B3, [{tag,none,keys(Tag)} | ?ADD(B1, Result)])
122 | end;
123 | parse2(_, _, _) -> error({?PARSE_ERROR, unclosed_tag}).
124 |
125 | %% @doc Part of the `parse/1'
126 | %%
127 | %% it is end processing of tag that need to be considered the standalone.
128 | -spec parse3(#state{}, binary(), [tag()]) -> {state(), [tag()]} | endtag().
129 | parse3(State0, Post0, [Tag | Result0]) when is_tuple(Tag) ->
130 | {State1,_,Post1, Result1} = standalone(State0, Post0, Result0),
131 | parse1(State1, Post1, [Tag | Result1]);
132 | parse3(State0, Post0, Result0) ->
133 | {State1, _,Post1, Result1} = standalone(State0, Post0, Result0),
134 | parse1(State1, Post1, Result1).
135 |
136 |
137 | %% {{+ Tag}} or {{- Tag}}
138 | parse_has(State0,Mark,Keys,Input0,Result0)->
139 | {State1,_, Input1, Result1} = standalone(State0, Input0, Result0),
140 | case parse1(State1, Input1, []) of
141 | {endtag, {State2, Keys, _LastTagSize, Rest0, LoopResult0}} ->
142 | {State3, _, Rest1, LoopResult1} = standalone(State2, Rest0, LoopResult0),
143 | case Mark of
144 | $+ ->
145 | parse1(State3, Rest1, [{has, Keys, lists:reverse(LoopResult1),true} | Result1]);
146 | $- ->
147 | parse1(State3, Rest1, [{has, Keys, lists:reverse(LoopResult1),false} | Result1])
148 | end;
149 | {endtag, {_, OtherKeys, _, _, _}} ->
150 | error({?PARSE_ERROR, {has_is_incorrect, binary_join(OtherKeys, <<".">>)}});
151 | _ ->
152 | error({?PARSE_ERROR, {has_end_tag_not_found, <<"/", (binary_join(Keys, <<".">>))/binary>>}})
153 | end.
154 |
155 | %% @doc Loop processing part of the `parse/1'
156 | %%
157 | %% `{{# Tag}}' or `{{^ Tag}}' corresponds to this.
158 | -spec parse_section(state(), '#' | '^', [key()], Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
159 | parse_section(State0, Mark, Keys, Input0, Result0) ->
160 | {State1,_, Input1, Result1} = standalone(State0, Input0, Result0),
161 | case parse1(State1, Input1, []) of
162 | {endtag, {State2, Keys, _LastTagSize, Rest0, LoopResult0}} ->
163 | {State3, _, Rest1, LoopResult1} = standalone(State2, Rest0, LoopResult0),
164 | case Mark of
165 | $# ->
166 | parse1(State3, Rest1, [{section, Keys, lists:reverse(LoopResult1),true} | Result1]);
167 | $^ ->
168 | parse1(State3, Rest1, [{section, Keys, lists:reverse(LoopResult1),false} | Result1])
169 | end;
170 | {endtag, {_, OtherKeys, _, _, _}} ->
171 | error({?PARSE_ERROR, {section_is_incorrect, binary_join(OtherKeys, <<".">>)}});
172 | _ ->
173 | error({?PARSE_ERROR, {section_end_tag_not_found, <<"/", (binary_join(Keys, <<".">>))/binary>>}})
174 | end.
175 |
176 | -spec parse_partial(state(), Tag :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
177 | parse_partial(State0, [Tag], NextBin0, Result0) ->
178 | {State1, Indent, NextBin1, Result1} = standalone(State0, NextBin0, Result0),
179 | Partials = State1#state.partials,
180 | parse1(State1#state{partials = [Tag|Partials]}, NextBin1, [{tag,partial, Tag}| ?ADD(Indent,Result1)]).
181 |
182 | %% ParseDelimiterBin :: e.g. `{{=%% %%=}}' -> `%% %%'
183 | -spec parse_delimiter(state(), ParseDelimiterBin :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
184 | parse_delimiter(State0, ParseDelimiterBin, NextBin, Result) ->
185 | case binary:match(ParseDelimiterBin, <<"=">>) of
186 | nomatch ->
187 | case [X || X <- binary:split(ParseDelimiterBin, <<" ">>, [global]), X =/= <<>>] of
188 | [Start, Stop] -> parse3(State0#state{start = Start, stop = Stop}, NextBin, Result);
189 | _ -> error({?PARSE_ERROR, delimiters_may_not_contain_whitespaces})
190 | end;
191 | _ ->
192 | error({?PARSE_ERROR, delimiters_may_not_contain_equals})
193 | end.
194 | %% @doc Split by the tag, it returns a list of the split binary.
195 | %%
196 | %% e.g.
197 | %% ```
198 | %% 1> split_tag(State, <<"...{{hoge}}...">>).
199 | %% [<<"...">>, <<"hoge">>, <<"...">>]
200 | %%
201 | %% 2> split_tag(State, <<"...{{hoge ...">>).
202 | %% [<<"...">>, <<"hoge ...">>]
203 | %%
204 | %% 3> split_tag(State, <<"...">>)
205 | %% [<<"...">>]
206 | %% '''
207 | -spec split_tag(state(), binary()) -> [binary(), ...].
208 | split_tag(#state{start = StartTag, stop = StopTag}, Bin) ->
209 | case binary:match(Bin, StartTag) of
210 | nomatch -> [Bin]; %% 未找到开始标签
211 | {StartPos, StartTagLen} ->
212 | PosLimit = byte_size(Bin) - StartTagLen,
213 | ShiftNum = ai_function:while(
214 | {true, StartPos + 1}, %% 在下一个StartTag之前一直向前推进
215 | fun(Pos) -> %% {{{ ,startPos 0, StartDelimiterLen = 2 ShitNum = 1
216 | ?IIF(Pos =< PosLimit
217 | andalso binary:part(Bin, Pos, StartTagLen) =:= StartTag,
218 | {true, Pos + 1}, {false, Pos})
219 | end) - StartPos - 1,
220 | %% PreTag是StartTag之前的文本,X是包含StarTag的文本
221 | {PreTag, X} = erlang:split_binary(Bin, StartPos + ShiftNum),
222 | Tag0 = part(X, StartTagLen, 0), %% 去掉StartTag
223 | case binary:split(Tag0, StopTag) of
224 | [_] -> [PreTag, Tag0]; % 这段文本里面没有StopTag
225 | [TagName, Rest] -> %% 找到TagName了
226 | IncludeStartTag = binary:part(X, 0, byte_size(TagName) + StartTagLen),%%切出包含StartTag的tag
227 | E = ?IIF(repeatedly_binary(StopTag, $}), %% 判断是否是重复的}}
228 | ?IIF(byte_size(Rest) > 0 andalso binary:first(Rest) =:= $}, 1, 0),%% 检查剩余部分第一个元素是否是}
229 | ?IIF(byte_size(TagName) > 0 andalso binary:last(TagName) =:= $}, -1, 0)),%% 检查tag最后的一个元素是否是}
230 | S = ?IIF(repeatedly_binary(StartTag, ${), %% 判断是否是重复的{{
231 | ?IIF(ShiftNum > 0, -1, 0),%% 找到{{之前的ShiftNum大于0就为-1否则为0
232 | ?IIF(byte_size(TagName) > 0 andalso binary:first(TagName) =:= ${, 1, 0)),%% 判断第一个元素是否是{
233 | case E =:= 0 orelse S =:= 0 of %% 如果 S = 0 代表{{之前没有东西或者Tag中第一个元素不是{,
234 | true -> % {{ ... }}
235 | [PreTag, TagName, Rest]; %% 这个是一个存粹的Tag
236 | false -> % {{{ ... }}}
237 | [part(PreTag, 0, erlang:min(0, S)), %% 去掉 {
238 | part(IncludeStartTag, erlang:max(0, S) + StartTagLen - 1,
239 | erlang:min(0, E)), %% 获取真正的tag,其中包含了类型{tag
240 | part(Rest, max(0, E), 0)] %% 去掉 }
241 | end
242 | end
243 | end.
244 |
245 | -spec keys(binary()) -> [key()].
246 | keys(Bin0) ->
247 | Bin1 = << <> || <> <= Bin0, X =/= $ >>,
248 | case Bin1 =:= <<>> orelse Bin1 =:= <<".">> of
249 | true -> [erlang:binary_to_atom(Bin1,utf8)];
250 | false -> [erlang:binary_to_atom(X,utf8) || X <- binary:split(Bin1, <<".">>, [global]), X =/= <<>>]
251 | end.
252 | -spec remove_space_from_head(binary()) -> binary().
253 | remove_space_from_head(<>)
254 | when X =:= $\t; X =:= $ ->
255 | remove_space_from_head(Rest);
256 | remove_space_from_head(Bin) -> Bin.
257 |
258 | -spec remove_space_from_tail(binary()) -> binary().
259 | remove_space_from_tail(<<>>) -> <<>>;
260 | remove_space_from_tail(Bin) ->
261 | PosList = binary:matches(Bin, <<" ">>),
262 | LastPos = remove_space_from_tail_impl(lists:reverse(PosList), byte_size(Bin)),
263 | binary:part(Bin, 0, LastPos).
264 |
265 | -spec remove_space_from_tail_impl([{non_neg_integer(), pos_integer()}], non_neg_integer()) -> non_neg_integer().
266 | remove_space_from_tail_impl([{X, Y} | T], Size) when Size =:= X + Y -> remove_space_from_tail_impl(T, X);
267 | remove_space_from_tail_impl(_, Size) ->Size.
268 |
269 | %% standalone模式, standalone并不影响一般的tag,只影响section和comment
270 | %% 如果\r\n{{ ... }}\r\n,tag后面的\r\n被视为tag的一部分
271 | %% 如果\s{{ ... }}\n,\s{{ ... }}\r\n tag后面的\n和\r\n被视为tag的一部分
272 | %% 如果是partials,Ident需要被保留
273 | -spec standalone(#state{}, binary(), [tag()]) -> {#state{}, StashPre :: binary(), Post :: binary(), [tag()]}.
274 | standalone(#state{standalone = false} = State, Post, [Pre | Result]) ->
275 | case Pre of
276 | {binary,PreBin}->
277 | {State, <<>>, Post, ?ADD(PreBin, Result)};
278 | _ ->
279 | {State, <<>>, Post, [Pre|Result]}
280 | end;
281 | standalone(#state{standalone = false} = State, Post, Result) ->
282 | {State, <<>>, Post, Result};
283 | standalone(State, Post0, Result0) ->
284 | {Pre, Result1} =
285 | case Result0 =/= [] andalso hd(Result0) of
286 | {binary,Pre0} -> {Pre0, tl(Result0)};
287 | Pre0 when is_binary(Pre0) -> {Pre0, tl(Result0)};
288 | _ -> {<<>>, Result0}
289 | end,
290 | case remove_space_from_head(Pre) =:= <<>> andalso remove_space_from_head(Post0) of
291 | <<"\r\n", Post1/binary>> ->
292 | {State, Pre, Post1, Result1};
293 | <<"\n", Post1/binary>> ->
294 | {State, Pre, Post1, Result1};
295 | <<>> ->
296 | {State, Pre, <<>>, Result1};
297 | _ ->
298 | {State#state{standalone = false}, <<>>, Post0, ?ADD(Pre, Result1)}
299 | end.
300 |
301 |
302 | %% @doc If the binary is repeatedly the character, return true. Otherwise, return false.
303 | -spec repeatedly_binary(binary(), byte()) -> boolean().
304 | repeatedly_binary(<>, X) -> repeatedly_binary(Rest, X);
305 | repeatedly_binary(<<>>, _) -> true;
306 | repeatedly_binary(_, _) -> false.
307 |
308 | %% @equiv binary:part(X, Start, byte_size(X) - Start + End)
309 | -spec part(binary(), non_neg_integer(), 0 | neg_integer()) -> binary().
310 | part(X, Start, End) when End =< 0 -> binary:part(X, Start, byte_size(X) - Start + End).
311 |
312 | -spec binary_join(BinaryList :: [binary()], Separator :: binary()) -> binary().
313 | binary_join([], _) -> <<>>;
314 | binary_join(Bins, Sep) ->
315 | [Hd | Tl] = [ [Sep, B] || B <- Bins ],
316 | erlang:iolist_to_binary([erlang:tl(Hd) | Tl]).
317 |
318 | merge_continuous_binary(IR)->
319 | MergeFun =
320 | fun(NeedMerge,Acc,I)->
321 | case NeedMerge of
322 | [] -> {Acc ++ [I],NeedMerge};
323 | _ ->
324 | MergeBinary = lists:foldl(
325 | fun(Bin,BinAcc)->
326 | <>
327 | end,<<>>,NeedMerge),
328 | {Acc ++ [{binary,MergeBinary},I],[]}
329 | end
330 | end,
331 | {L1,Rest} =
332 | lists:foldl(
333 | fun(I,{Acc,NeedMerge})->
334 | case I of
335 | {section,Keys,IR1,Expect}->
336 | IR2 = merge_continuous_binary(IR1),
337 | MergeFun(NeedMerge,Acc,{section,Keys,IR2,Expect});
338 | {binary,Bin}->
339 | {Acc,NeedMerge ++ [Bin]};
340 | _ ->
341 | MergeFun(NeedMerge,Acc,I)
342 | end
343 | end,{[],[]},IR),
344 | case Rest of
345 | [] -> L1;
346 | _ ->
347 | MergeBinary =
348 | lists:foldl(
349 | fun(Bin,BinAcc)->
350 | <>
351 | end,<<>>,Rest),
352 | L1 ++ [{binary,MergeBinary}]
353 | end.
354 |
355 | remove_empty_section(IR)->
356 | lists:foldl(
357 | fun(I,Acc)->
358 | case I of
359 | {section,_Keys,IR1,_Expect}->
360 | case IR1 of
361 | [] -> Acc;
362 | [<<>>] -> Acc;
363 | _ -> Acc ++ [I]
364 | end;
365 | _->
366 | Acc ++ [I]
367 | end
368 | end,[],IR).
369 |
--------------------------------------------------------------------------------
/src/ai_mustache_runner.erl:
--------------------------------------------------------------------------------
1 | -module(ai_mustache_runner).
2 | -export([render/2,render/3]).
3 | %% runner是一个栈式执行机器
4 | %% section可以被认为是分支预测工具
5 | -spec render({[term()],map()},map())-> binary().
6 | render({IR,Partials},Ctx)->run(<<>>,IR,[],Partials,Ctx).
7 |
8 | -spec render([term()],map(),map())-> binary().
9 | render(IR,Partials,Ctx)-> run(<<>>,IR,[],Partials,Ctx).
10 |
11 | run(Acc,[],[],_Partials,_Ctx)-> Acc;
12 |
13 | run(Acc,[],[H|Stack],Partials,Ctx)->
14 | run(Acc,H,Stack,Partials,Ctx);
15 |
16 | run(Acc,[{binary,Value}|IR],Stack,Partials,Ctx)->
17 | run(<>,IR,Stack,Partials,Ctx);
18 | %% 正常的字符串
19 | run(Acc,[{tag,none,Name}|IR],Stack,Partials,Ctx)->
20 | Value = ai_string:html_escape(ai_maps:get(Name,Ctx,<<"">>)),
21 | run(<>,IR,Stack,Partials,Ctx);
22 | %% 不需要转换的原生字符串
23 | run(Acc,[{tag,raw,Name}|IR],Stack,Partials,Ctx)->
24 | Value = ai_string:to_string(ai_maps:get(Name,Ctx,<<"">>)),
25 | run(<>,IR,Stack,Partials,Ctx);
26 | %% 局部模版
27 | run(Acc,[{tag,partial,Name }|IR],Stack,Partials,Ctx)->
28 | case maps:get(Name,Partials,undefined) of
29 | undefined -> run(Acc,IR,Stack,Partials,Ctx);
30 | NewIR -> run(Acc,NewIR,[IR|Stack],Partials,Ctx)
31 | end;
32 |
33 | %% lamda 扩展,非标准
34 | run(Acc,[{lambda,Name}| IR],Stack,Partials,Ctx)->
35 | case ai_maps:get(Name,Ctx,undefined) of
36 | undefined -> run(Acc,IR,Stack,Partials,Ctx);
37 | [Fun,Value] when erlang:is_function(Fun,2)->
38 | Acc0 = Fun(Value,Ctx),
39 | run(<>,IR,Stack,Partials,Ctx);
40 | Fun when erlang:is_function(Fun, 1)->
41 | Acc0 = Fun(Ctx),
42 | run(<>,IR,Stack,Partials,Ctx)
43 | end;
44 |
45 | %% has 扩展,非标准
46 | run(Acc,[{has,Name,SectionIR,false}| IR],Stack,Partials,Ctx)->
47 | case ai_maps:get(Name,Ctx,undefined) of
48 | undefined -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
49 | false -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
50 | %% 实现简单的if false操作,当一个函数返回false的时候,里面的section就会执行
51 | F when erlang:is_function(F,1) ->
52 | Hide = F(Ctx),
53 | if
54 | Hide == true -> run(Acc,IR,Stack,Partials,Ctx);
55 | true -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx)
56 | end;
57 | _ -> run(Acc,IR,Stack,Partials,Ctx)
58 | end;
59 |
60 | run(Acc,[{has,Name,SectionIR,true}|IR],Stack,Partials,Ctx)->
61 | case ai_maps:get(Name,Ctx,undefined) of
62 | true -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
63 | %% 如果是一个maps,就是表示内部代码块需要执行
64 | M when erlang:is_map(M)-> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
65 | [] -> run(Acc,IR,Stack,Partials,Ctx);
66 | L when erlang:is_list(L)-> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
67 | %% 此处是扩展,当一个函数返回true的时候,里面的section会执行
68 | %% 实现简单if true 操作
69 | F when erlang:is_function(F,1) ->
70 | Run = F(Ctx),
71 | if
72 | Run == true -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
73 | true -> run(Acc,IR,Stack,Partials,Ctx)
74 | end;
75 | %% 扩展,非标准,如果是一个binary,等同于true
76 | B when erlang:is_binary(B)-> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
77 | _-> run(Acc,IR,Stack,Partials,Ctx)
78 | end;
79 | %% section
80 | run(Acc,[{section,Name,SectionIR,false}|IR],Stack,Partials,Ctx)->
81 | case ai_maps:get(Name,Ctx,undefined) of
82 | [] -> run(Acc,SectionIR,[IR|Stack],Partials,Ctx);
83 | _ -> run(Acc,IR,Stack,Partials,Ctx)
84 | end;
85 | run(Acc,[{section,Name,SectionIR,true}|IR],Stack,Partials,Ctx)->
86 | case ai_maps:get(Name,Ctx,undefined) of
87 | %% 如果是一个maps,就是表示内部代码块需要执行
88 | [] -> run(Acc,IR,Stack,Partials,Ctx);
89 | L when erlang:is_list(L)->
90 | run_section(Acc,SectionIR,L,[IR|Stack],Partials,Ctx,Name);
91 | F when erlang:is_function(F,2) ->
92 | Acc0 =
93 | case SectionIR of
94 | [] -> <<>>;
95 | _ -> run(<<>>,SectionIR,[],Partials,Ctx)
96 | end,
97 | Acc1 = F(Acc0,Ctx),
98 | run(<>,IR,Stack,Partials,Ctx);
99 | _-> run(Acc,IR,Stack,Partials,Ctx)
100 | end.
101 |
102 | run_section(Acc,_SectionIR,[],[IR|Stack],Partials,Ctx,_Name)->
103 | run(Acc,IR,Stack,Partials,Ctx);
104 | run_section(Acc,SectionIR,[H|T],Stack,Partials,Ctx,Name)->
105 | SectionCtx = maps:merge(Ctx,ai_maps:put(Name,H,#{})),
106 | Acc0 = run(Acc,SectionIR,[],Partials,SectionCtx),
107 | run_section(Acc0,SectionIR,T,Stack,Partials,Ctx,Name).
108 |
--------------------------------------------------------------------------------
/src/aihtml_app.erl:
--------------------------------------------------------------------------------
1 | -module(aihtml_app).
2 | -behaviour(application).
3 |
4 | -export([start/2]).
5 | -export([stop/1]).
6 |
7 | start(_Type, _Args) ->
8 | application:start(crypto),
9 | application:start(ailib),
10 | aihtml_sup:start_link().
11 |
12 | stop(_State) ->
13 | ok.
14 |
--------------------------------------------------------------------------------
/src/aihtml_sup.erl:
--------------------------------------------------------------------------------
1 | -module(aihtml_sup).
2 | -behaviour(supervisor).
3 |
4 | -export([start_link/0]).
5 | -export([init/1]).
6 |
7 | start_link() ->
8 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
9 |
10 | init([]) ->
11 |
12 | SupFlags = #{strategy => one_for_one,
13 | intensity => 1,
14 | period => 5},
15 |
16 | TemplateLoader
17 | = #{id => ai_mustache_loader,
18 | start => {ai_mustache_loader, start_link, []},
19 | restart => transient,
20 | shutdown => 5000,
21 | type => worker,
22 | modules => [ai_mustache_loader]},
23 |
24 | {ok, {SupFlags,[TemplateLoader]}}.
25 |
--------------------------------------------------------------------------------