├── ebin
└── .gitignore
├── examples
├── unescaped.mustache
├── nonl.mustache
├── simple.mustache
├── nonl.erl
├── unescaped.erl
├── complex.mustache
├── simple.erl
└── complex.erl
├── .gitignore
├── rebar
├── Emakefile
├── .travis.yml
├── Makefile
├── rebar.config
├── src
├── mustache.app.src
├── mustache_ctx.erl
└── mustache.erl
├── benchmarks
└── bench.erl
├── LICENSE
├── test
├── mustache_ctx_tests.erl
└── mustache_tests.erl
└── README.md
/ebin/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/unescaped.mustache:
--------------------------------------------------------------------------------
1 |
{{{title}}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.dump
3 | ebin
4 | .eunit
5 | deps/
6 |
--------------------------------------------------------------------------------
/rebar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mojombo/mustache.erl/master/rebar
--------------------------------------------------------------------------------
/examples/nonl.mustache:
--------------------------------------------------------------------------------
1 | Hello {{name}} you {{#in_ca}} totally {{/in_ca}} win {{value}}!
--------------------------------------------------------------------------------
/Emakefile:
--------------------------------------------------------------------------------
1 | % -*- mode: erlang -*-
2 | {["src/*"],
3 | [{i, "include"},
4 | {outdir, "ebin"},
5 | debug_info]
6 | }.
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | otp_release:
3 | - R14B02
4 | - R15B03
5 | - R16B
6 | - 17.5
7 | - 17.4
8 | - 17.3
9 |
--------------------------------------------------------------------------------
/examples/simple.mustache:
--------------------------------------------------------------------------------
1 | Hello {{name}}
2 | You have just won ${{value}}!
3 | {{#in_ca}}
4 | Well, ${{ taxed_value }}, after taxes.
5 | {{/in_ca}}
--------------------------------------------------------------------------------
/examples/nonl.erl:
--------------------------------------------------------------------------------
1 | -module(nonl).
2 | -compile(export_all).
3 |
4 | name() ->
5 | "Tom".
6 |
7 | value() ->
8 | "10000".
9 |
10 | taxed_value() ->
11 | integer_to_list(value() - (value() * 0.4)).
12 |
13 | in_ca() ->
14 | true.
15 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ERL ?= erl
2 | EBIN_DEPS_DIRS := $(wildcard deps/*/ebin)
3 |
4 | all: deps compile
5 |
6 | compile:
7 | @./rebar compile
8 |
9 | test:
10 | @./rebar eunit skip_deps=true
11 |
12 | deps:
13 | @./rebar get-deps
14 |
15 | run:
16 | @$(ERL) -pa ebin/ -pa $(EBIN_DEPS_DIRS)
17 |
18 | .PHONY: test
19 |
--------------------------------------------------------------------------------
/examples/unescaped.erl:
--------------------------------------------------------------------------------
1 | -module(unescaped).
2 | -compile(export_all).
3 |
4 | title() ->
5 | "Bear > Shark".
6 |
7 | %%---------------------------------------------------------------------------
8 |
9 | start() ->
10 | code:add_patha(".."),
11 | Output = mustache:render(unescaped, "unescaped.mustache"),
12 | io:format(Output, []).
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
2 | %% ex: ts=4 sw=4 et
3 |
4 | {cover_enabled, true}.
5 |
6 | {erl_opts, [debug_info, fail_on_warning]}.
7 |
8 | {lib_dirs, ["deps"]}.
9 | {deps, [
10 | {meck, ".*",{git, "https://github.com/eproxus/meck.git", {branch, "master"}}}
11 | ]}.
12 |
13 |
--------------------------------------------------------------------------------
/examples/complex.mustache:
--------------------------------------------------------------------------------
1 | {{header}}
2 | {{#list}}
3 |
4 | {{#item}}
5 | {{#current}}
6 | - {{name}}
7 | {{/current}}
8 | {{#link}}
9 | - {{name}}
10 | {{/link}}
11 | {{/item}}
12 |
13 | {{/list}}
14 | {{#empty}}
15 | The list is empty.
16 | {{/empty}}
--------------------------------------------------------------------------------
/src/mustache.app.src:
--------------------------------------------------------------------------------
1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
2 | %% ex: ts=4 sw=4 et
3 |
4 | {application, mustache,
5 | [
6 | {description, "Mustache Templates Rendering Library"},
7 | {vsn, "0.1.1"},
8 | {registered, []},
9 | {applications, [
10 | kernel,
11 | stdlib
12 | ]},
13 | {modules, [mustache]},
14 | {env, []}
15 | ]}.
16 |
--------------------------------------------------------------------------------
/examples/simple.erl:
--------------------------------------------------------------------------------
1 | -module(simple).
2 | -compile(export_all).
3 |
4 | name() ->
5 | "Tom".
6 |
7 | value() ->
8 | 10000.
9 |
10 | taxed_value() ->
11 | value() - (value() * 0.4).
12 |
13 | in_ca() ->
14 | true.
15 |
16 | %%---------------------------------------------------------------------------
17 |
18 | start() ->
19 | code:add_patha(".."),
20 | Ctx = dict:from_list([{name, "TPW"}]),
21 | Output = mustache:render(simple, "simple.mustache", Ctx),
22 | io:format(Output, []).
--------------------------------------------------------------------------------
/examples/complex.erl:
--------------------------------------------------------------------------------
1 | -module(complex).
2 | -compile(export_all).
3 |
4 | header() ->
5 | "Colors".
6 |
7 | item() ->
8 | A = dict:from_list([{name, "red"}, {current, true}, {url, "#Red"}]),
9 | B = dict:from_list([{name, "green"}, {current, false}, {url, "#Green"}]),
10 | C = dict:from_list([{name, "blue"}, {current, false}, {url, "#Blue"}]),
11 | [A, B, C].
12 |
13 | link(Ctx) ->
14 | mustache:get(current, Ctx).
15 |
16 | list() ->
17 | length(item()) =/= 0.
18 |
19 | empty() ->
20 | length(item()) =:= 0.
21 |
22 | %%---------------------------------------------------------------------------
23 |
24 | start() ->
25 | code:add_patha(".."),
26 | Output = mustache:render(complex, "complex.mustache"),
27 | io:format(Output, []).
--------------------------------------------------------------------------------
/benchmarks/bench.erl:
--------------------------------------------------------------------------------
1 | -module(bench).
2 | -export([run/0]).
3 |
4 | -define(COUNT, 500).
5 |
6 | run() ->
7 | Ctx0 = dict:from_list([{header, "Chris"}, {empty, false}, {list, true}]),
8 | A = dict:from_list([{name, "red"}, {current, true}, {url, "#Red"}]),
9 | B = dict:from_list([{name, "green"}, {current, false}, {url, "#Green"}]),
10 | C = dict:from_list([{name, "blue"}, {current, false}, {url, "#Blue"}]),
11 | Ctx1 = dict:store(item, [A, B, C], Ctx0),
12 | % Ctx1 = dict:new(),
13 | CT = mustache:compile(complex, "../examples/complex.mustache"),
14 | T0 = erlang:now(),
15 | render(CT, Ctx1, ?COUNT),
16 | T1 = erlang:now(),
17 | Diff = timer:now_diff(T1, T0),
18 | Mean = Diff / ?COUNT,
19 | io:format("~nTotal time: ~.2fs~n", [Diff / 1000000]),
20 | io:format("Mean render time: ~.2fms~n", [Mean / 1000]).
21 |
22 | render(_CT, _Ctx, 0) ->
23 | ok;
24 | render(CT, Ctx, N) ->
25 | Out = mustache:render(complex, CT, Ctx),
26 | % io:format(Out, []),
27 | 158 = length(Out),
28 | % io:format(".", []),
29 | render(CT, Ctx, N - 1).
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2009 Tom Preston-Werner
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
13 | all 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
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/src/mustache_ctx.erl:
--------------------------------------------------------------------------------
1 | %% The MIT License
2 | %%
3 | %% Copyright (c) 2009 Tom Preston-Werner
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
13 | %% all 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
21 | %% THE SOFTWARE.
22 |
23 | %% See the README at http://github.com/mojombo/mustache.erl for additional
24 | %% documentation and usage examples.
25 |
26 | -module(mustache_ctx).
27 |
28 | -define(MODULE_KEY, '__mod__').
29 | -define(NEW_EXIT(Data), exit({improper_ctx, Data})).
30 |
31 | -export([ new/0, new/1, to_list/1 ]).
32 | -export([ merge/2 ]).
33 | -export([ module/1, module/2 ]).
34 | -export([ get/2 ]).
35 |
36 | -ifdef(EUNIT).
37 | -compile(export_all).
38 | -endif.
39 |
40 | %% ===================================================================
41 | %% Create new context
42 | %% ===================================================================
43 |
44 | new() -> new([]).
45 |
46 | new(List) when is_list(List) ->
47 | try dict:from_list(List)
48 | catch
49 | _:_ -> ?NEW_EXIT(List)
50 | end;
51 | new(Data) when is_tuple(Data) ->
52 | case erlang:element(1, Data) of
53 | dict -> Data;
54 | _ -> ?NEW_EXIT(Data)
55 | end;
56 | new(Data) ->
57 | ?NEW_EXIT(Data).
58 |
59 | to_list(Ctx) ->
60 | List = dict:to_list(Ctx),
61 | lists:keydelete(?MODULE_KEY, 1, List).
62 |
63 | %% ===================================================================
64 | %% Merge
65 | %% ===================================================================
66 |
67 | merge(Ctx1, Ctx2) ->
68 | dict:merge(fun(_, Value1, _) -> Value1 end, Ctx1, Ctx2).
69 |
70 |
71 | %% ===================================================================
72 | %% Dynamic data module
73 | %% ===================================================================
74 |
75 | module(Ctx) ->
76 | case dict:find(?MODULE_KEY, Ctx) of
77 | {ok, Module} -> {ok, Module};
78 | error -> {error, module_not_set}
79 | end.
80 |
81 | module(Module, Ctx) ->
82 | dict:store(?MODULE_KEY, Module, Ctx).
83 |
84 | %% ===================================================================
85 | %% Module
86 | %% ===================================================================
87 |
88 | get(Key, Ctx) ->
89 | case dict:find(Key, Ctx) of
90 | {ok, Value} -> {ok, Value};
91 | error ->
92 | get_from_module(Key, Ctx)
93 | end.
94 |
95 | get_from_module(Key, Ctx) ->
96 | FunList = case module(Ctx) of
97 | {error, _} -> [];
98 | {ok, Module} -> [
99 | fun() -> Module:Key(Ctx) end,
100 | fun() -> Module:Key() end
101 | ]
102 | end,
103 | get_from_module(FunList).
104 |
105 | get_from_module([]) -> {error, not_found};
106 | get_from_module([ Fun | Rest ]) ->
107 | try Value = Fun(),
108 | {ok, Value}
109 | catch
110 | _:_ ->
111 | get_from_module(Rest)
112 | end.
113 |
114 |
--------------------------------------------------------------------------------
/test/mustache_ctx_tests.erl:
--------------------------------------------------------------------------------
1 | -module(mustache_ctx_tests).
2 | -compile(export_all).
3 |
4 | -include_lib("eunit/include/eunit.hrl").
5 |
6 | new_ctx_empty_test() ->
7 | Ctx = mustache_ctx:new(),
8 | CtxList = mustache_ctx:to_list(Ctx),
9 | ?assertEqual([], CtxList).
10 |
11 | new_ctx_from_proplist_test() ->
12 | List = [{k,v}],
13 | Ctx = mustache_ctx:new(List),
14 | CtxList = mustache_ctx:to_list(Ctx),
15 | ?assertEqual(List, CtxList).
16 |
17 | new_ctx_from_dict_test() ->
18 | List = [{k,v}],
19 | Dict = dict:from_list(List),
20 | CtxFromList = mustache_ctx:new(List),
21 | CtxFromDict = mustache_ctx:new(Dict),
22 | ?assertEqual(CtxFromList, CtxFromDict).
23 |
24 | new_ctx_from_improper_data_test() ->
25 | ?assertExit(_, mustache_ctx:new([{k,v}, other])),
26 | ?assertExit(_, mustache_ctx:new({other, tuple})),
27 | ?assertExit(_, mustache_ctx:new(other)).
28 |
29 | module_not_set_test() ->
30 | Ctx = mustache_ctx:new(),
31 | ?assertEqual({error, module_not_set}, mustache_ctx:module(Ctx)).
32 |
33 | module_set_and_get_test() ->
34 | Module = module_name,
35 | Ctx = mustache_ctx:new(),
36 | Ctx1 = mustache_ctx:module(Module, Ctx),
37 | ?assertEqual({ok, Module}, mustache_ctx:module(Ctx1)).
38 |
39 | ctx_to_list_with_module_test() ->
40 | Module = module_name,
41 | Ctx = mustache_ctx:new(),
42 | Ctx1 = mustache_ctx:module(Module, Ctx),
43 | CtxList = mustache_ctx:to_list(Ctx1),
44 | ?assertEqual([], CtxList).
45 |
46 | get_from_empty_test() ->
47 | Ctx = mustache_ctx:new(),
48 | ?assertEqual({error, not_found}, mustache_ctx:get(key, Ctx)).
49 |
50 | get_not_found_test() ->
51 | Ctx = mustache_ctx:new([{k,v}]),
52 | ?assertEqual({error, not_found}, mustache_ctx:get(key, Ctx)).
53 |
54 | get_found_test() ->
55 | Ctx = mustache_ctx:new([{key,value}]),
56 | ?assertEqual({ok, value}, mustache_ctx:get(key, Ctx)).
57 |
58 | get_from_module_not_found_test() ->
59 | Ctx0 = mustache_ctx:new(),
60 | Ctx1 = mustache_ctx:module(mock_module, Ctx0),
61 | ?assertEqual({error, not_found}, mustache_ctx:get(key, Ctx1)).
62 |
63 | get_from_module_test_() ->
64 | {foreach,
65 | fun() -> ok = meck:new(mock_module,[non_strict]) end,
66 | fun(_) -> ok = meck:unload(mock_module) end,
67 | [
68 | {"fun/1", fun get_from_module_fun_1_/0},
69 | {"fun/0", fun get_from_module_fun_0_/0},
70 | {"function call order", fun get_from_module_call_order_/0}
71 | ]}.
72 |
73 | get_from_module_fun_1_() ->
74 | ok = meck:expect(mock_module, key, fun(_) -> value end),
75 | Ctx0 = mustache_ctx:new(),
76 | Ctx1 = mustache_ctx:module(mock_module, Ctx0),
77 | ?assertEqual({ok, value}, mustache_ctx:get(key, Ctx1)).
78 |
79 | get_from_module_fun_0_() ->
80 | ok = meck:expect(mock_module, key, fun() -> value end),
81 | Ctx0 = mustache_ctx:new(),
82 | Ctx1 = mustache_ctx:module(mock_module, Ctx0),
83 | ?assertEqual({ok, value}, mustache_ctx:get(key, Ctx1)).
84 |
85 | get_from_module_call_order_() ->
86 | ok = meck:expect(mock_module, key, fun(_) -> value_1 end),
87 | ok = meck:expect(mock_module, key, fun() -> value_0 end),
88 | Ctx0 = mustache_ctx:new(),
89 | Ctx1 = mustache_ctx:module(mock_module, Ctx0),
90 | ?assertEqual({ok, value_1}, mustache_ctx:get(key, Ctx1)).
91 |
92 | merge_disjoin_test() ->
93 | Ctx1 = mustache_ctx:new([{k1,v1}]),
94 | Ctx2 = mustache_ctx:new([{k2,v2}]),
95 | Ctx = mustache_ctx:merge(Ctx1, Ctx2),
96 | ?assertEqual({ok, v1}, mustache_ctx:get(k1, Ctx)),
97 | ?assertEqual({ok, v2}, mustache_ctx:get(k2, Ctx)).
98 |
99 | merge_intersecting_test() ->
100 | Ctx1 = mustache_ctx:new([{k0, v1}, {k1,v1}]),
101 | Ctx2 = mustache_ctx:new([{k0, v2}, {k2,v2}]),
102 | Ctx = mustache_ctx:merge(Ctx1, Ctx2),
103 | ?assertEqual({ok, v1}, mustache_ctx:get(k0, Ctx)),
104 | ?assertEqual({ok, v1}, mustache_ctx:get(k1, Ctx)),
105 | ?assertEqual({ok, v2}, mustache_ctx:get(k2, Ctx)).
106 |
107 |
--------------------------------------------------------------------------------
/test/mustache_tests.erl:
--------------------------------------------------------------------------------
1 | %% The MIT License
2 | %%
3 | %% Copyright (c) 2009 Tom Preston-Werner
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
13 | %% all 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
21 | %% THE SOFTWARE.
22 |
23 | %% See the README at http://github.com/mojombo/mustache.erl for additional
24 | %% documentation and usage examples.
25 |
26 | -module(mustache_tests).
27 |
28 | -compile(export_all).
29 |
30 | -include_lib("eunit/include/eunit.hrl").
31 |
32 | simple_test() ->
33 | Ctx = dict:from_list([{name, "world"}]),
34 | Result = mustache:render("Hello {{name}}!", Ctx),
35 | ?assertEqual("Hello world!", Result).
36 |
37 | integer_values_too_test() ->
38 | Ctx = dict:from_list([{name, "Chris"}, {value, 10000}]),
39 | Result = mustache:render("Hello {{name}}~nYou have just won ${{value}}!", Ctx),
40 | ?assertEqual("Hello Chris~nYou have just won $10000!", Result).
41 |
42 | specials_test() ->
43 | Ctx = dict:from_list([{name, "Chris"}, {value, 10000}]),
44 | Result = mustache:render("\'Hello\n\"{{name}}\"~nYou \"have\" ju\0st\\ won\b\r\"${{value}}!\"\t", Ctx),
45 | ?assertEqual("\'Hello\n\"Chris\"~nYou \"have\" ju\0st\\ won\b\r\"$10000!\"\t", Result).
46 |
47 | %% ===================================================================
48 | %% basic tag types
49 | %% ===================================================================
50 |
51 | tag_type_variable_empty_test() ->
52 | test_helper("{{name}}", "", []).
53 |
54 | tag_type_variable_string_test() ->
55 | test_helper("{{name}}", "NAME", [{name, "NAME"}]).
56 |
57 | tag_type_variable_integer_test() ->
58 | test_helper("{{name}}", "1", [{name, 1}]).
59 |
60 | tag_type_variable_atom_test() ->
61 | test_helper("{{name}}", "atom", [{name, atom}]).
62 |
63 | tag_type_varibale_escaped_test() ->
64 | test_helper("{{name}}", ">&do<it>", [{name, ">&do"}]).
65 |
66 | tag_type_variabel_unescaped_test() ->
67 | test_helper("{{{name}}}", ">dont&do", [{name, ">dont&do"}]).
68 |
69 | tag_type_variable_unescaped_with_ampersand_test() ->
70 | test_helper("{{&name}}", ">dont&do", [{name, ">dont&do"}]).
71 |
72 |
73 | tag_type_section_empty_test() ->
74 | test_helper("{{#name}}section{{/name}}", "", []).
75 |
76 | tag_type_section_false_test() ->
77 | test_helper("{{#name}}section{{/name}}", "", [{name, false}]).
78 |
79 | tag_type_section_true_test() ->
80 | test_helper("{{#name}}section{{/name}}", "section", [{name, true}]).
81 |
82 | tag_type_section_empty_list_test() ->
83 | test_helper("{{#name}}section{{/name}}", "", [{name, []}]).
84 |
85 | tag_type_section_nonempty_list_test() ->
86 | CtxList = [{name, [ dict:new() || _ <- lists:seq(1,3) ]}],
87 | test_helper("{{#name}}section{{/name}}", "sectionsectionsection", CtxList).
88 |
89 |
90 | tag_type_inverted_section_empty_test() ->
91 | test_helper("{{^name}}section{{/name}}", "section", []).
92 |
93 | tag_type_inverted_section_false_test() ->
94 | test_helper("{{^name}}section{{/name}}", "section", [{name, false}]).
95 |
96 | tag_type_inverted_section_true_test() ->
97 | test_helper("{{^name}}section{{/name}}", "", [{name, true}]).
98 |
99 | tag_type_inverted_section_empty_list_test() ->
100 | test_helper("{{^name}}section{{/name}}", "section", [{name, []}]).
101 |
102 | tag_type_inverted_section_nonempty_list_test() ->
103 | CtxList = [{name, [ dict:new() || _ <- lists:seq(1,3) ]}],
104 | test_helper("{{^name}}section{{/name}}", "", CtxList).
105 |
106 |
107 | tag_type_comment_test() ->
108 | test_helper("{{!comment}}", "", []).
109 |
110 | tag_type_comment_empty_test() ->
111 | test_helper("{{! }}", "", []).
112 |
113 | tag_type_comment_multiline_test() ->
114 | test_helper("{{!\ncomment\ncomment\ncomment\n\n}}", "", []).
115 |
116 |
117 | test_helper(Template, Expected, CtxList) ->
118 | Ctx = dict:from_list(CtxList),
119 | ?assertEqual(Expected, mustache:render(Template, Ctx)).
120 |
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Mustache for Erlang [](http://travis-ci.org/mojombo/mustache.erl)
2 | ===================
3 |
4 | An Erlang port of [Mustache for Ruby][1]. Mustache is a framework-agnostic
5 | templating system that enforces separation of view logic from the template
6 | file. Indeed, it is not even possible to embed logic in the template. This
7 | allows templates to be reused across language boundaries and for other
8 | language independent uses.
9 |
10 | This project uses [Semantic Versioning](http://semver.org) for release
11 | numbering.
12 |
13 | Working with Mustache means dealing with templates, views, and contexts.
14 | Templates contain HTML (or some other format) and Mustache tags that specify
15 | what data to pull in. A template can be either a string or a file (usually
16 | ending in .mustache). Views are Erlang modules that can define functions that
17 | are called and provide the data for the template tags. A context is an Erlang
18 | dict that contains the current context from which tags can pull data. A few
19 | examples will clarify how these items interact.
20 |
21 | NOTE: This is alpha software. Do not use it in production without extensive
22 | testing. The API may change at any time. It still lacks some of the features
23 | of Mustache for Ruby and the performance (even with compiled templates) is not
24 | yet where I'd like it to be.
25 |
26 |
27 | Installation
28 | ------------
29 |
30 | To compile the code, navigate to the Mustache.erl project root and issue:
31 |
32 | make
33 |
34 | This will produce a `mustache.beam` file in the `ebin` directory that must be
35 | included in the code path of projects that need it.
36 |
37 |
38 | The Simplest Example
39 | --------------------
40 |
41 | The simplest example involves using a string template and a context from the
42 | REPL.
43 |
44 | 1> Ctx = dict:from_list([{planet, "World!"}]).
45 | {dict,1,16,16,8,80,48,...}
46 |
47 | 2> mustache:render("Hello {{planet}}", Ctx).
48 | "Hello World!"
49 |
50 | In line 1 we created a context that contains a value bound to the `planet`
51 | tag. In line 2 we render a string template by passing in the template and the
52 | context.
53 |
54 |
55 | Two-File Example
56 | ----------------
57 |
58 | A more complex example consists of two files: the view and the template. The
59 | view (logic) file is an Erlang module (simple.erl):
60 |
61 | -module(simple).
62 | -compile(export_all).
63 |
64 | name() ->
65 | "Tom".
66 |
67 | value() ->
68 | 10000.
69 |
70 | taxed_value() ->
71 | value() - (value() * 0.4).
72 |
73 | in_ca() ->
74 | true.
75 |
76 | In the view we define functions that will be called by the template. The names
77 | of the functions correspond to the tag names that will be used in the
78 | template. Some functions reference others, some return values, and some return
79 | only booleans.
80 |
81 | The template file (simple.mustache) looks like so:
82 |
83 | Hello {{name}}
84 | You have just won ${{value}}!
85 | {{#in_ca}}
86 | Well, ${{ taxed_value }}, after taxes.
87 | {{/in_ca}}
88 |
89 | Notice that the template references the functions in the view module. The
90 | return values from the view dictate how the template will be rendered. To get
91 | the HTML output, make sure the `simple.beam` bytecode file is in your code
92 | path and call the following function:
93 |
94 | mustache:render(simple)
95 |
96 | This tells Mustache to use the `simple` view and to look for a template named
97 | `simple.mustache` in the same directory as the `simple.beam` bytecode file. If
98 | all goes well, it returns the rendered HTML:
99 |
100 | Hello Tom
101 | You have just won $10000!
102 | Well, $6000.00, after taxes.
103 |
104 |
105 | Compiled Templates (for speed)
106 | ------------------------------
107 |
108 | In order to boost performance for templates that will be called many times in
109 | the lifetime of a runtime, Mustache allows you to compile a template and then
110 | provide that to the render function (instead of having to implicitly recompile
111 | the template on each call).
112 |
113 | 1> TFun = mustache:compile(simple).
114 | 2> mustache:render(simple, TFun).
115 |
116 | Now, each call to render will use the compiled template (TFun) instead of
117 | compiling the template on its own.
118 |
119 |
120 | The Power of Context
121 | --------------------
122 |
123 | You will often want to provide additional data to your template and view. You
124 | can do this by passing in an initial context to the render function. During
125 | rendering, tag lookups always hit the context first before looking for a view
126 | function. In this way, the context can be used to override view functions.
127 | Using the same template and view as above, we can replace the name tag with
128 | different data by constructing a context and passing it to `render`:
129 |
130 |
131 | 1> Ctx = dict:from_list([{name, "Chris"}]).
132 | 1> TFun = mustache:compile(simple).
133 | 2> mustache:render(simple, TFun, Ctx).
134 |
135 | This will produce the following output:
136 |
137 | Hello Chris
138 | You have just won $10000!
139 | Well, $6000.00, after taxes.
140 |
141 | The context is also accessible from view functions, making it easy to pass in
142 | initialization data. Consider a case where we want to pass in a user ID:
143 |
144 | Ctx = dict:from_list([{id, 42}])
145 |
146 | View functions can get access to the context by accepting a single argument:
147 |
148 | name(Ctx) ->
149 | ...
150 |
151 | Now when this function is called, it will be handed the context. In order to
152 | fetch data from the context, use `mustache:get/2`:
153 |
154 | name(Ctx) ->
155 | Id = mustache:get(id, Ctx),
156 | ...
157 |
158 | If the requested key does not exist in the context, the empty list `[]` will
159 | be returned.
160 |
161 |
162 | Tag Types
163 | ---------
164 |
165 | Tags are indicated by the double mustaches. `{{name}}` is a tag. Let's talk
166 | about the different types of tags.
167 |
168 | ### Variables
169 |
170 | The most basic tag is the variable. A `{{name}}` tag in a basic template will
171 | try to call the `name` function on your view. By default a variable "miss"
172 | returns an empty string.
173 |
174 | All variables are HTML escaped by default. If you want to return unescaped
175 | HTML, use the triple mustache: `{{{name}}}`.
176 |
177 | ### Boolean Sections
178 |
179 | A section begins with a pound and ends with a slash. That is,
180 | `{{#person}}` begins a "person" section while `{{/person}}` ends it.
181 |
182 | If the `person` method exists and calling it returns `false`, the HTML
183 | between the pound and slash will not be displayed.
184 |
185 | If the `person` method exists and calling it returns `true`, the HTML
186 | between the pound and slash will be rendered and displayed.
187 |
188 | ### List Sections
189 |
190 | List sections are syntactically identical to boolean sections in that they
191 | begin with a pound and end with a slash. The difference, however, is in the
192 | view: if the function called returns a list, the section is repeated as the
193 | list is iterated over.
194 |
195 | Each item in the enumerable is expected to be a dict that will then become the
196 | context of the corresponding iteration. In this way we can construct loops.
197 |
198 | For example, imagine this template:
199 |
200 | {{#repo}}
201 | {{name}}
202 | {{/repo}}
203 |
204 | And this view code:
205 |
206 | repo() ->
207 | [dict:from_list([{name, Name}]) || Name <- ["Tom", "Chris", "PJ"]].
208 |
209 | When rendered, our view will contain a list of each of the names in the source
210 | list.
211 |
212 | ### Comments
213 |
214 | Comments begin with a bang and are ignored. The following template:
215 |
216 | Today{{! ignore me }}.
217 |
218 | Will render as follows:
219 |
220 | Today.
221 |
222 |
223 | TODO
224 | ----
225 |
226 | * Support partials
227 | * Learn some things from erlydtl (speed improvments, perhaps)
228 |
229 |
230 | Meta
231 | ----
232 |
233 | * Code: `git clone git://github.com/mojombo/mustache.erl.git`
234 |
235 | [1]: http://github.com/defunkt/mustache.git
--------------------------------------------------------------------------------
/src/mustache.erl:
--------------------------------------------------------------------------------
1 | %% The MIT License
2 | %%
3 | %% Copyright (c) 2009 Tom Preston-Werner
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
13 | %% all 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
21 | %% THE SOFTWARE.
22 |
23 | %% See the README at http://github.com/mojombo/mustache.erl for additional
24 | %% documentation and usage examples.
25 |
26 | -module(mustache). %% v0.1.1
27 | -author("Tom Preston-Werner").
28 | -export([compile/1, compile/2, render/1, render/2, render/3, get/2, get/3, escape/1, start/1]).
29 |
30 | -record(mstate, {mod = undefined,
31 | section_re = undefined,
32 | tag_re = undefined}).
33 |
34 | -define(MUSTACHE_CTX, mustache_ctx).
35 | -define(MUSTACHE_CTX_STR, "mustache_ctx").
36 | -define(MUSTACHE_STR, "mustache").
37 |
38 | compile(Body) when is_list(Body) ->
39 | State = #mstate{},
40 | CompiledTemplate = pre_compile(Body, State),
41 | % io:format("~p~n~n", [CompiledTemplate]),
42 | % io:format(CompiledTemplate ++ "~n", []),
43 | {ok, Tokens, _} = erl_scan:string(CompiledTemplate),
44 | {ok, [Form]} = erl_parse:parse_exprs(Tokens),
45 | Bindings = erl_eval:new_bindings(),
46 | {value, Fun, _} = erl_eval:expr(Form, Bindings),
47 | Fun;
48 | compile(Mod) ->
49 | TemplatePath = template_path(Mod),
50 | compile(Mod, TemplatePath).
51 |
52 | compile(Mod, File) ->
53 | code:purge(Mod),
54 | {module, _} = code:load_file(Mod),
55 | {ok, TemplateBin} = file:read_file(File),
56 | Template = re:replace(TemplateBin, "\"", "\\\\\"", [global, {return,list}]),
57 | State = #mstate{mod = Mod},
58 | CompiledTemplate = pre_compile(Template, State),
59 | % io:format("~p~n~n", [CompiledTemplate]),
60 | % io:format(CompiledTemplate ++ "~n", []),
61 | {ok, Tokens, _} = erl_scan:string(CompiledTemplate),
62 | {ok, [Form]} = erl_parse:parse_exprs(Tokens),
63 | Bindings = erl_eval:new_bindings(),
64 | {value, Fun, _} = erl_eval:expr(Form, Bindings),
65 | Fun.
66 |
67 | render(Mod) ->
68 | TemplatePath = template_path(Mod),
69 | render(Mod, TemplatePath).
70 |
71 | render(Body, Ctx) when is_list(Body) ->
72 | TFun = compile(Body),
73 | render(undefined, TFun, Ctx);
74 | render(Mod, File) when is_list(File) ->
75 | render(Mod, File, []);
76 | render(Mod, CompiledTemplate) ->
77 | render(Mod, CompiledTemplate, []).
78 |
79 | render(Mod, File, Ctx) when is_list(File) ->
80 | CompiledTemplate = compile(Mod, File),
81 | render(Mod, CompiledTemplate, Ctx);
82 | render(Mod, CompiledTemplate, CtxData) ->
83 | Ctx0 = ?MUSTACHE_CTX:new(CtxData),
84 | Ctx1 = ?MUSTACHE_CTX:module(Mod, Ctx0),
85 | lists:flatten(CompiledTemplate(Ctx1)).
86 |
87 | pre_compile(T, State) ->
88 | SectionRE = "{{(#|\\^)([^}]*)}}\\s*(.+?){{/\\2}}\\s*",
89 | {ok, CompiledSectionRE} = re:compile(SectionRE, [dotall]),
90 | TagRE = "{{(#|=|!|<|>|{|&)?(.+?)\\1?}}+",
91 | {ok, CompiledTagRE} = re:compile(TagRE, [dotall]),
92 | State2 = State#mstate{section_re = CompiledSectionRE, tag_re = CompiledTagRE},
93 | "fun(Ctx) -> " ++
94 | compiler(T, State2) ++ " end.".
95 |
96 | compiler(T, State) ->
97 | Res = re:run(T, State#mstate.section_re),
98 | case Res of
99 | {match, [{M0, M1}, {K0, K1}, {N0, N1}, {C0, C1}]} ->
100 | Front = string:substr(T, 1, M0),
101 | Back = string:substr(T, M0 + M1 + 1),
102 | Kind = string:substr(T, K0 + 1, K1),
103 | Name = string:substr(T, N0 + 1, N1),
104 | Content = string:substr(T, C0 + 1, C1),
105 | "[" ++ compile_tags(Front, State) ++
106 | " | [" ++ compile_section(Kind, Name, Content, State) ++
107 | " | [" ++ compiler(Back, State) ++ "]]]";
108 | nomatch ->
109 | compile_tags(T, State)
110 | end.
111 |
112 | compile_section("#", Name, Content, State) ->
113 | Mod = State#mstate.mod,
114 | Result = compiler(Content, State),
115 | "fun() -> " ++
116 | "case " ++ ?MUSTACHE_STR ++ ":get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++
117 | "\"true\" -> " ++ Result ++ "; " ++
118 | "\"false\" -> []; " ++
119 | "List when is_list(List) -> " ++
120 | "[fun(Ctx) -> " ++ Result ++ " end(" ++ ?MUSTACHE_CTX_STR ++ ":merge(SubCtx, Ctx)) || SubCtx <- List]; " ++
121 | "Else -> " ++
122 | "throw({template, io_lib:format(\"Bad context for ~p: ~p\", [" ++ Name ++ ", Else])}) " ++
123 | "end " ++
124 | "end()";
125 | compile_section("^", Name, Content, State) ->
126 | Mod = State#mstate.mod,
127 | Result = compiler(Content, State),
128 | "fun() -> " ++
129 | "case " ++ ?MUSTACHE_STR ++ ":get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++
130 | "\"false\" -> " ++ Result ++ "; " ++
131 | "[] -> " ++ Result ++ "; " ++
132 | "_ -> [] "
133 | "end " ++
134 | "end()".
135 |
136 | compile_tags(T, State) ->
137 | Res = re:run(T, State#mstate.tag_re),
138 | case Res of
139 | {match, [{M0, M1}, K, {C0, C1}]} ->
140 | Front = string:substr(T, 1, M0),
141 | Back = string:substr(T, M0 + M1 + 1),
142 | Content = string:substr(T, C0 + 1, C1),
143 | Kind = tag_kind(T, K),
144 | Result = compile_tag(Kind, Content, State),
145 | "[\"" ++ escape_special(Front) ++
146 | "\" | [" ++ Result ++
147 | " | " ++ compile_tags(Back, State) ++ "]]";
148 | nomatch ->
149 | "[\"" ++ escape_special(T) ++ "\"]"
150 | end.
151 |
152 | tag_kind(_T, {-1, 0}) ->
153 | none;
154 | tag_kind(T, {K0, K1}) ->
155 | string:substr(T, K0 + 1, K1).
156 |
157 | compile_tag(none, Content, State) ->
158 | compile_escaped_tag(Content, State);
159 | compile_tag("&", Content, State) ->
160 | compile_unescaped_tag(Content, State);
161 | compile_tag("{", Content, State) ->
162 | compile_unescaped_tag(Content, State);
163 | compile_tag("!", _Content, _State) ->
164 | "[]".
165 |
166 | compile_escaped_tag(Content, State) ->
167 | Mod = State#mstate.mod,
168 | ?MUSTACHE_STR ++ ":escape(" ++ ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ "))".
169 |
170 | compile_unescaped_tag(Content, State) ->
171 | Mod = State#mstate.mod,
172 | ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ ")".
173 |
174 | template_dir(Mod) ->
175 | DefaultDirPath = filename:dirname(code:which(Mod)),
176 | case application:get_env(mustache, templates_dir) of
177 | {ok, DirPath} when is_list(DirPath) ->
178 | case filelib:ensure_dir(DirPath) of
179 | ok -> DirPath;
180 | _ -> DefaultDirPath
181 | end;
182 | _ ->
183 | DefaultDirPath
184 | end.
185 | template_path(Mod) ->
186 | DirPath = template_dir(Mod),
187 | Basename = atom_to_list(Mod),
188 | filename:join(DirPath, Basename ++ ".mustache").
189 |
190 | get(Key, Ctx, Mod) ->
191 | get(Key, ?MUSTACHE_CTX:module(Mod, Ctx)).
192 |
193 | get(Key, Ctx) when is_list(Key) ->
194 | get(list_to_atom(Key), Ctx);
195 | get(Key, Ctx) ->
196 | case ?MUSTACHE_CTX:get(Key, Ctx) of
197 | {ok, Value} -> to_s(Value);
198 | {error, _} -> []
199 | end.
200 |
201 |
202 | to_s(Val) when is_integer(Val) ->
203 | integer_to_list(Val);
204 | to_s(Val) when is_float(Val) ->
205 | io_lib:format("~.2f", [Val]);
206 | to_s(Val) when is_atom(Val) ->
207 | atom_to_list(Val);
208 | to_s(Val) ->
209 | Val.
210 |
211 | escape(HTML) ->
212 | escape(HTML, []).
213 |
214 | escape([], Acc) ->
215 | lists:reverse(Acc);
216 | escape([$< | Rest], Acc) ->
217 | escape(Rest, lists:reverse("<", Acc));
218 | escape([$> | Rest], Acc) ->
219 | escape(Rest, lists:reverse(">", Acc));
220 | escape([$& | Rest], Acc) ->
221 | escape(Rest, lists:reverse("&", Acc));
222 | escape([X | Rest], Acc) ->
223 | escape(Rest, [X | Acc]).
224 |
225 | escape_special(String) ->
226 | lists:flatten([escape_char(Char) || Char <- String]).
227 |
228 | escape_char($\0) -> "\\0";
229 | escape_char($\n) -> "\\n";
230 | escape_char($\t) -> "\\t";
231 | escape_char($\b) -> "\\b";
232 | escape_char($\r) -> "\\r";
233 | escape_char($') -> "\\'";
234 | escape_char($") -> "\\\"";
235 | escape_char($\\) -> "\\\\";
236 | escape_char(Char) -> Char.
237 |
238 | %%---------------------------------------------------------------------------
239 |
240 | start([T]) ->
241 | Out = render(list_to_atom(T)),
242 | io:format(Out ++ "~n", []).
243 |
--------------------------------------------------------------------------------