├── 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 | 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 [![Build Status](https://secure.travis-ci.org/mojombo/mustache.erl.png?branch=master)](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 | --------------------------------------------------------------------------------