├── .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 | 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 | 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 | 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 | 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 | 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 | 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 | 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,"">>; 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,"">>, 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 | <>}}) 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 | --------------------------------------------------------------------------------