diff --git a/README.md b/README.md index 460cf14..5b8c1a0 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ mustache =========== Mustache template engine for Erlang/OTP. -OTP17 (or later) - ## What is Mustach ? A logic-less templates. - [{{mustache}}](http://mustache.github.io/) @@ -11,11 +9,31 @@ A logic-less templates. ## Overview - Binary and map base. - Do not use a regular expression !! +- Support an associative array for the before R17. ## Usage +### Use as a library +Add the following settings. + +```erlang +%% rebar.config + +%% If you want to use a map is necessary +{erl_opts, [ + {platform_define, "^[0-9]+", namespaced_types} + ]}. + +{deps, + [ + {mustache, ".*", {git, "git://github.com/soranoba/mustache.git", {branch, "master"}}} + ]}. +``` + +### How to use simple Mustache - [Mastache Manual](http://mustache.github.io/mustache.5.html) - Support all of syntax ! +Map (R17 or later) ```erlang 1> mustache:render(<<"{{name}}">>, #{"name" => "hoge"}). <<"hoge">> @@ -31,6 +49,22 @@ A logic-less templates. <<"hoge">> ``` +Associative array +```erlang +1> mustache:render(<<"{{name}}">>, [{"name", "hoge"}]). +<<"hoge">> + +2> Template1 = mustache:parse_binary(<<"{{name}}">>). +... +3> mustache:compile(Template1, [{"name", "hoge"}]). +<<"hoge">> + +4> Template2 = mustache:parse_file(<<"./hoge.mustache">>). +... +5> mustache:compile(Template2, [{"name", "hoge"}]). +<<"hoge">> +``` + ## Attention - The number of new line. - New line in the template has left all. diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..4589465 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,11 @@ + + +# The mustache application # + + +## Modules ## + + + +
mustache
+ diff --git a/doc/mustache.md b/doc/mustache.md new file mode 100644 index 0000000..0765fc2 --- /dev/null +++ b/doc/mustache.md @@ -0,0 +1,131 @@ + + +# Module mustache # +* [Description](#description) +* [Data Types](#types) +* [Function Index](#index) +* [Function Details](#functions) + + +Mustach template engine for Erlang/OTP. +Copyright (c) 2015 Hinagiku Soranoba All Rights Reserved. + + + + +## Data Types ## + + + + +### assoc_data() ### + + + +

+assoc_data() = [{data_key(), data_value()}]
+
+ + + + + +### data() ### + + + +

+data() = assoc_data()
+
+ + + + + +### data_key() ### + + + +

+data_key() = string()
+
+ + + + + +### data_value() ### + + + +

+data_value() = data() | iodata() | fun((data(), function()) -> iodata())
+
+ + + + + +### template() ### + + +__abstract datatype__: `template()` + + + +## Function Index ## + + +
compile/2Embed the data in the template.
parse_binary/1Create a template/0 from a binary.
parse_file/1Create a template/0 from a file.
render/2Equivalent to compile(parse_binary(Bin), Data).
+ + + + +## Function Details ## + + + +### compile/2 ### + + +

+compile(Mustache::template(), Data::data()) -> binary()
+
+
+ +Embed the data in the template. + + +### parse_binary/1 ### + + +

+parse_binary(Bin::binary()) -> template()
+
+
+ +Create a [`template/0`](#template-0) from a binary. + + +### parse_file/1 ### + + +

+parse_file(Filename::file:filename()) -> template()
+
+
+ +Create a [`template/0`](#template-0) from a file. + + +### render/2 ### + + +

+render(Bin::binary(), Data::data()) -> binary()
+
+
+ +Equivalent to [`compile(parse_binary(Bin), Data)`](#compile-2). + +__See also:__ [compile/2](#compile-2), [parse_binary/1](#parse_binary-1), [parse_file/1](#parse_file-1), [render/2](#render-2). diff --git a/rebar.config b/rebar.config index 398f38c..6b9a057 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,7 @@ %% vim: set filetype=erlang : -*- erlang -*- {erl_opts, [ + {platform_define, "^[0-9]+", namespaced_types}, warnings_as_errors, warn_export_all, warn_untyped_record diff --git a/src/mustache.erl b/src/mustache.erl index 8ba75a2..0efa42d 100644 --- a/src/mustache.erl +++ b/src/mustache.erl @@ -50,19 +50,29 @@ -opaque template() :: #?MODULE{}. %% @see parse_binary/1 %% @see parse_file/1 --type data() :: #{string() => data() | iodata() | fun((data(), function()) -> iodata())}. + +-type data_key() :: string(). +-type data_value() :: data() | iodata() | fun((data(), function()) -> iodata()). +-type assoc_data() :: [{data_key(), data_value()}]. + +-ifdef(namespaced_types). +-type data() :: #{data_key() => data_value()} | assoc_data(). +-else. +-type data() :: assoc_data(). +-endif. %% @see render/2 %% @see compile/2 + -type partial() :: {partial, {state(), EndTag :: binary(), LastTagSize :: non_neg_integer(), Rest :: binary(), [tag()]}}. %%---------------------------------------------------------------------------------------------------------------------- %% Exported Functions %%---------------------------------------------------------------------------------------------------------------------- -%% @equiv compile(parse_binary(Bin), Map) +%% @equiv compile(parse_binary(Bin), Data) -spec render(binary(), data()) -> binary(). -render(Bin, Map) -> - compile(parse_binary(Bin), Map). +render(Bin, Data) -> + compile(parse_binary(Bin), Data). %% @doc Create a {@link template/0} from a binary. -spec parse_binary(binary()) -> template(). @@ -79,8 +89,11 @@ parse_file(Filename) -> %% @doc Embed the data in the template. -spec compile(template(), data()) -> binary(). -compile(#?MODULE{data = Tags}, Map) when is_map(Map) -> - iolist_to_binary(lists:reverse(compile_impl(Tags, Map, []))). +compile(#?MODULE{data = Tags} = T, Data) -> + case check_data_type(Data) of + false -> error(function_clause, [T, Data]); + _ -> iolist_to_binary(lists:reverse(compile_impl(Tags, Data, []))) + end. %%---------------------------------------------------------------------------------------------------------------------- %% Internal Function @@ -93,21 +106,21 @@ compile(#?MODULE{data = Tags}, Map) when is_map(Map) -> compile_impl([], _, Result) -> Result; compile_impl([{n, Key} | T], Map, Result) -> - compile_impl(T, Map, [escape(to_binary(maps:get(binary_to_list(Key), Map, <<>>))) | Result]); + compile_impl(T, Map, [escape(to_binary(data_get(binary_to_list(Key), Map, <<>>))) | Result]); compile_impl([{'&', Key} | T], Map, Result) -> - compile_impl(T, Map, [to_binary(maps:get(binary_to_list(Key), Map, <<>>)) | Result]); + compile_impl(T, Map, [to_binary(data_get(binary_to_list(Key), Map, <<>>)) | Result]); compile_impl([{'#', Key, Tags, Source} | T], Map, Result) -> - Value = maps:get(binary_to_list(Key), Map, undefined), - if - is_list(Value) -> compile_impl(T, Map, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc) end, - Result, Value)); - Value =:= false; Value =:= undefined -> compile_impl(T, Map, Result); - is_function(Value, 2) -> compile_impl(T, Map, [Value(Source, fun(Text) -> render(Text, Map) end) | Result]); - is_map(Value) -> compile_impl(T, Map, compile_impl(Tags, Value, Result)); - true -> compile_impl(T, Map, compile_impl(Tags, Map, Result)) + Value = data_get(binary_to_list(Key), Map, undefined), + case check_data_type(Value) of + true -> compile_impl(T, Map, compile_impl(Tags, Value, Result)); + _ when is_list(Value) -> compile_impl(T, Map, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc) end, + Result, Value)); + _ when Value =:= false; Value =:= undefined -> compile_impl(T, Map, Result); + _ when is_function(Value, 2) -> compile_impl(T, Map, [Value(Source, fun(Text) -> render(Text, Map) end) | Result]); + _ -> compile_impl(T, Map, compile_impl(Tags, Map, Result)) end; compile_impl([{'^', Key, Tags} | T], Map, Result) -> - Value = maps:get(binary_to_list(Key), Map, undefined), + Value = data_get(binary_to_list(Key), Map, undefined), case Value =:= undefined orelse Value =:= [] orelse Value =:= false of true -> compile_impl(T, Map, compile_impl(Tags, Map, Result)); false -> compile_impl(T, Map, Result) @@ -270,3 +283,29 @@ escape_char($&) -> <<"&">>; escape_char($") -> <<""">>; escape_char($') -> <<"'">>; escape_char(C) -> <>. + +%% @doc fetch the value of the specified key from {@link data/0} +-spec data_get(data_key(), data(), Default :: term()) -> term(). +-ifdef(namespaced_types). +data_get(Key, Map, Default) when is_map(Map) -> + maps:get(Key, Map, Default); +data_get(Key, AssocList, Default) -> + proplists:get_value(Key, AssocList, Default). +-else. +data_get(Key, AssocList, Default) -> + proplists:get_value(Key, AssocList, Default). +-endif. + +%% @doc check whether the type of {@link data/0} +%% +%% maybe: There is also the possibility of iolist +-spec check_data_type(data() | term()) -> boolean() | maybe. +-ifdef(namespaced_types). +check_data_type([]) -> maybe; +check_data_type([{_, _} | _]) -> true; +check_data_type(Map) -> is_map(Map). +-else. +check_data_type([]) -> maybe; +check_data_type([{_, _} | _]) -> true; +check_data_type(_) -> false. +-endif. diff --git a/test/mustache_tests.erl b/test/mustache_tests.erl index 2466f64..f5d1458 100644 --- a/test/mustache_tests.erl +++ b/test/mustache_tests.erl @@ -74,6 +74,7 @@ parse_binary_test_() -> -define(PATH(File), <<"../test/test_data/", File/binary>>). %% TestData Path +-ifdef(namespaced_types). manual_test_() -> [ {"Variables", @@ -95,10 +96,14 @@ manual_test_() -> Template = mustache:parse_file(?PATH(<<"non-empty.mustache">>)), {ok, File} = file:read_file(?PATH(<<"non-empty.result">>)), ?assertEqual(File, mustache:compile(Template, #{ "repo" => [ - #{ "name" => "resque" }, + [{"name", "resque"}], #{ "name" => "hub" }, #{ "name" => "rip" } - ]})) + ]})), + ?assertEqual(File, mustache:compile(Template, [{"repo", [ [{"name", "resque"}], + #{"name" => "hub"}, + [{"name", "rip"}] + ]}])) end}, {"Sections : Lamdas", fun() -> @@ -155,3 +160,85 @@ render_test_() -> #{"i" => 1, "f" => 1.5, "b" => <<"hoge">>, "s" => "fugo"})) end} ]. +-endif. + +assoc_list_manual_test_() -> + [ + {"Variables", + fun() -> + Template = mustache:parse_file(?PATH(<<"variables.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"variables.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"name", "Chris"}, {"company", "GitHub"}])) + end}, + {"Sections : False Values or Empty Lists", + fun() -> + Template = mustache:parse_file(?PATH(<<"false_values.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"false_values.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"person", false}])), + ?assertEqual(File, mustache:compile(Template, [{"person", []}])), + ?assertEqual(File, mustache:compile(Template, [])) + end}, + {"Sections : Non-Empty Lists", + fun() -> + Template = mustache:parse_file(?PATH(<<"non-empty.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"non-empty.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"repo", [ [{"name", "resque"}], + [{"name", "hub"}], + [{"name", "rip"}] + ]} + ])) + end}, + {"Sections : Lamdas", + fun() -> + Template = mustache:parse_file(?PATH(<<"lamdas.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"lamdas.result">>)), + + F = fun(Text, Render) -> ["", Render(Text), ""] end, + ?assertEqual(File, mustache:compile(Template, [{"name", "Willy"}, {"wrapped", F}])) + end}, + {"Sections : Non-False Values", + fun() -> + Template = mustache:parse_file(?PATH(<<"non-false.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"non-false.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"person?", [{"name", "Jon"}]}])) + end}, + {"Inverted Sections", + fun() -> + Template = mustache:parse_file(?PATH(<<"invarted.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"invarted.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"repo", []}])) + end}, + {"Comments", + fun() -> + Template = mustache:parse_file(?PATH(<<"comment.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"comment.result">>)), + ?assertEqual(File, mustache:compile(Template, [])) + end}, + {"Partials", + fun() -> + Template = mustache:parse_file(?PATH(<<"partial.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"partial.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"names", [ [{"name", "alice"}], + [{"name", "bob"}] + ]}])) + end}, + {"Set Delimiter", + fun() -> + Template = mustache:parse_file(?PATH(<<"delimiter.mustache">>)), + {ok, File} = file:read_file(?PATH(<<"delimiter.result">>)), + ?assertEqual(File, mustache:compile(Template, [{"default_tags", "tag1"}, + {"erb_style_tags", "tag2"}, + {"default_tags_again", "tag3"} + ])) + end} + ]. + +assoc_list_render_test_() -> + [ + {"integer, float, binary, string", + fun() -> + ?assertEqual(<<"1, 1.5, hoge, fugo">>, + mustache:render(<<"{{i}}, {{f}}, {{b}}, {{s}}">>, + [{"i", 1}, {"f", 1.5}, {"b", <<"hoge">>}, {"s", "fugo"}])) + end} + ].