Skip to content

Commit

Permalink
Merge pull request #40 from soranoba/feature/top_level_dot
Browse files Browse the repository at this point in the history
(re-make) Allow {{.}} to appear on the top level
  • Loading branch information
soranoba authored May 23, 2019
2 parents 5b28302 + 5c32ac1 commit ccede8d
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 90 deletions.
34 changes: 19 additions & 15 deletions doc/bbmustache.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@ Please see [this](../benchmarks/README.md) for a list of features that bbmustach



### <a name="type-assoc_data">assoc_data()</a> ###


<pre><code>
assoc_data() = [{atom(), <a href="#type-data_value">data_value()</a>}] | [{binary(), <a href="#type-data_value">data_value()</a>}] | [{string(), <a href="#type-data_value">data_value()</a>}]
</code></pre>




### <a name="type-compile_option">compile_option()</a> ###


Expand All @@ -53,20 +43,24 @@ compile_option() = {key_type, atom | binary | string} | raise_on_context_miss |


<pre><code>
data() = <a href="#type-assoc_data">assoc_data()</a>
data() = term()
</code></pre>

Beginners should consider [`data/0`](#data-0) as [`recursive_data/0`](#recursive_data-0).
By specifying options, the type are greatly relaxed and equal to `term/0`.



### <a name="type-data_value">data_value()</a> ###
### <a name="type-data_key">data_key()</a> ###


<pre><code>
data_value() = <a href="#type-data">data()</a> | iodata() | number() | atom() | fun((<a href="#type-data">data()</a>, function()) -&gt; iodata())
data_key() = atom() | binary() | string()
</code></pre>

Function is intended to support a lambda expression.
You can choose one from these as the type of key in [`recursive_data/0`](#recursive_data-0).
The default is `string/0`.
If you want to change this, you need to specify `key_type` in [`compile_option/0`](#compile_option-0).



Expand All @@ -92,6 +86,16 @@ parse_option() = raise_on_partial_miss



### <a name="type-recursive_data">recursive_data()</a> ###


<pre><code>
recursive_data() = [{<a href="#type-data_key">data_key()</a>, term()}]
</code></pre>




### <a name="type-render_option">render_option()</a> ###


Expand Down Expand Up @@ -216,7 +220,7 @@ render(Bin::binary(), Data::<a href="#type-data">data()</a>) -&gt; binary()

Equivalent to [`render(Bin, Data, [])`](#render-3).

__See also:__ [compile/2](#compile-2), [compile_option/0](#compile_option-0), [parse_binary/1](#parse_binary-1), [parse_file/1](#parse_file-1), [parse_option/0](#parse_option-0), [render/2](#render-2).
__See also:__ [compile/2](#compile-2), [compile_option/0](#compile_option-0), [compile_option/0](#compile_option-0), [parse_binary/1](#parse_binary-1), [parse_file/1](#parse_file-1), [parse_option/0](#parse_option-0), [render/2](#render-2).

<a name="render-3"></a>

Expand Down
137 changes: 68 additions & 69 deletions src/bbmustache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
-export_type([
template/0,
data/0,
recursive_data/0,
option/0, % deprecated
compile_option/0,
parse_option/0,
Expand Down Expand Up @@ -112,14 +113,6 @@
}).
-type state() :: #state{}.

-type data_key() :: atom() | binary() | string().
%% You can choose one from these as the type of key in {@link data/0}.

-type data_value() :: data() | iodata() | number() | atom() | fun((data(), function()) -> iodata()).
%% Function is intended to support a lambda expression.

-type assoc_data() :: [{atom(), data_value()}] | [{binary(), data_value()}] | [{string(), data_value()}].

-type parse_option() :: raise_on_partial_miss.
%% - raise_on_partial_miss: If the template used in partials does not found, it will throw an exception (error).

Expand All @@ -140,15 +133,24 @@
%% This type has been deprecated since 1.6.0. It will remove in 2.0.0.
%% @see compile_option/0

-type data() :: term().
%% Beginners should consider {@link data/0} as {@link recursive_data/0}.
%% By specifying options, the type are greatly relaxed and equal to `term/0'.
%%
%% @see render/2
%% @see compile/2

-type data_key() :: atom() | binary() | string().
%% You can choose one from these as the type of key in {@link recursive_data/0}.
%% The default is `string/0'.
%% If you want to change this, you need to specify `key_type' in {@link compile_option/0}.

-ifdef(namespaced_types).
-type maps_data() :: #{atom() => data_value()} | #{binary() => data_value()} | #{string() => data_value()}.
-type data() :: maps_data() | assoc_data().
-type recursive_data() :: #{data_key() => term()} | [{data_key(), term()}].
-else.
-type data() :: assoc_data().
-type recursive_data() :: [{data_key(), term()}].
-endif.
%% All keys MUST be same type.
%% @see render/2
%% @see compile/2
%% It is a part of {@link data/0} that can have child elements.

-type endtag() :: {endtag, {state(), [key()], LastTagSize :: non_neg_integer(), Rest :: binary(), Result :: [tag()]}}.

Expand Down Expand Up @@ -215,12 +217,8 @@ compile(Template, Data) ->
%% All keys MUST be same type.
-spec compile(template(), data(), [compile_option()]) -> binary().
compile(#?MODULE{data = Tags} = T, Data, Options) ->
case check_data_type(Data) of
false -> error(function_clause, [T, Data]);
_ ->
Ret = compile_impl(Tags, Data, [], T#?MODULE{options = Options, data = []}),
iolist_to_binary(lists:reverse(Ret))
end.
Ret = compile_impl(Tags, Data, [], T#?MODULE{options = Options, data = []}),
iolist_to_binary(lists:reverse(Ret)).

%% @doc Default value serializer for templtated values
-spec default_value_serializer(number() | binary() | string() | atom()) -> iodata().
Expand All @@ -247,57 +245,57 @@ default_value_serializer(X) ->
-spec compile_impl(Template :: [tag()], data(), Result :: iodata(), template()) -> iodata().
compile_impl([], _, Result, _) ->
Result;
compile_impl([{n, Keys} | T], Map, Result, State) ->
compile_impl([{n, Keys} | T], Data, Result, State) ->
ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1),
Value = iolist_to_binary(ValueSerializer(get_data_recursive(Keys, Map, <<>>, State))),
Value = iolist_to_binary(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State))),
EscapeFun = proplists:get_value(escape_fun, State#?MODULE.options, fun escape/1),
compile_impl(T, Map, ?ADD(EscapeFun(Value), Result), State);
compile_impl([{'&', Keys} | T], Map, Result, State) ->
compile_impl(T, Data, ?ADD(EscapeFun(Value), Result), State);
compile_impl([{'&', Keys} | T], Data, Result, State) ->
ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1),
compile_impl(T, Map, ?ADD(ValueSerializer(get_data_recursive(Keys, Map, <<>>, State)), Result), State);
compile_impl([{'#', Keys, Tags, Source} | T], Map, Result, State) ->
Value = get_data_recursive(Keys, Map, false, State),
NestedState = State#?MODULE{context_stack = [Map | State#?MODULE.context_stack]},
case check_data_type(Value) of
true ->
compile_impl(T, Map, compile_impl(Tags, Value, Result, NestedState), State);
_ when is_list(Value) ->
compile_impl(T, Map, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc, NestedState) end,
compile_impl(T, Data, ?ADD(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State)), Result), State);
compile_impl([{'#', Keys, Tags, Source} | T], Data, Result, State) ->
Value = get_data_recursive(Keys, Data, false, State),
NestedState = State#?MODULE{context_stack = [Data | State#?MODULE.context_stack]},
case is_recursive_data(Value) of
true ->
compile_impl(T, Data, compile_impl(Tags, Value, Result, NestedState), State);
_ when is_list(Value) ->
compile_impl(T, Data, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc, NestedState) end,
Result, Value), State);
_ when Value =:= false ->
compile_impl(T, Map, Result, State);
_ when is_function(Value, 2) ->
Ret = Value(Source, fun(Text) -> render(Text, Map, State#?MODULE.options) end),
compile_impl(T, Map, ?ADD(Ret, Result), State);
_ ->
compile_impl(T, Map, compile_impl(Tags, Map, Result, State), State)
_ when Value =:= false ->
compile_impl(T, Data, Result, State);
_ when is_function(Value, 2) ->
Ret = Value(Source, fun(Text) -> render(Text, Data, State#?MODULE.options) end),
compile_impl(T, Data, ?ADD(Ret, Result), State);
_ ->
compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State)
end;
compile_impl([{'^', Keys, Tags} | T], Map, Result, State) ->
Value = get_data_recursive(Keys, Map, false, State),
compile_impl([{'^', Keys, Tags} | T], Data, Result, State) ->
Value = get_data_recursive(Keys, Data, false, State),
case Value =:= [] orelse Value =:= false of
true -> compile_impl(T, Map, compile_impl(Tags, Map, Result, State), State);
false -> compile_impl(T, Map, Result, State)
true -> compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State);
false -> compile_impl(T, Data, Result, State)
end;
compile_impl([{'>', Key, Indent} | T], Map, Result0, #?MODULE{partials = Partials} = State) ->
compile_impl([{'>', Key, Indent} | T], Data, Result0, #?MODULE{partials = Partials} = State) ->
case proplists:get_value(Key, Partials) of
undefined ->
case ?RAISE_ON_CONTEXT_MISS_ENABLED(State#?MODULE.options) of
true -> error(?CONTEXT_MISSING_ERROR({?FILE_ERROR, Key}));
false -> compile_impl(T, Map, Result0, State)
false -> compile_impl(T, Data, Result0, State)
end;
PartialT ->
Indents = State#?MODULE.indents ++ [Indent],
Result1 = compile_impl(PartialT, Map, [Indent | Result0], State#?MODULE{indents = Indents}),
compile_impl(T, Map, Result1, State)
Result1 = compile_impl(PartialT, Data, [Indent | Result0], State#?MODULE{indents = Indents}),
compile_impl(T, Data, Result1, State)
end;
compile_impl([B1 | [_|_] = T], Map, Result, #?MODULE{indents = Indents} = State) when Indents =/= [] ->
compile_impl([B1 | [_|_] = T], Data, Result, #?MODULE{indents = Indents} = State) when Indents =/= [] ->
%% NOTE: indent of partials
case byte_size(B1) > 0 andalso binary:last(B1) of
$\n -> compile_impl(T, Map, [Indents, B1 | Result], State);
_ -> compile_impl(T, Map, [B1 | Result], State)
$\n -> compile_impl(T, Data, [Indents, B1 | Result], State);
_ -> compile_impl(T, Data, [B1 | Result], State)
end;
compile_impl([Bin | T], Map, Result, State) ->
compile_impl(T, Map, [Bin | Result], State).
compile_impl([Bin | T], Data, Result, State) ->
compile_impl(T, Data, [Bin | Result], State).

%% @doc Parse remaining partials in State. It returns {@link template/0}.
-spec parse_remaining_partials(state(), template(), [parse_option()]) -> template().
Expand Down Expand Up @@ -630,7 +628,7 @@ get_data_recursive_impl([], Data, _) ->
get_data_recursive_impl([<<".">>], Data, _) ->
{ok, Data};
get_data_recursive_impl([Key | RestKey] = Keys, Data, #?MODULE{context_stack = Stack} = State) ->
case check_data_type(Data) =:= true andalso find_data(convert_keytype(Key, State), Data) of
case is_recursive_data(Data) andalso find_data(convert_keytype(Key, State), Data) of
{ok, ChildData} ->
get_data_recursive_impl(RestKey, ChildData, State#?MODULE{context_stack = []});
_ when Stack =:= [] ->
Expand All @@ -639,34 +637,35 @@ get_data_recursive_impl([Key | RestKey] = Keys, Data, #?MODULE{context_stack = S
get_data_recursive_impl(Keys, hd(Stack), State#?MODULE{context_stack = tl(Stack)})
end.

%% @doc find the value of the specified key from {@link data/0}
-spec find_data(data_key(), data() | term()) -> {ok, Value ::term()} | error.
%% @doc find the value of the specified key from {@link recursive_data/0}
-spec find_data(data_key(), recursive_data() | term()) -> {ok, Value :: term()} | error.
-ifdef(namespaced_types).
find_data(Key, Map) when is_map(Map) ->
maps:find(Key, Map);
find_data(Key, AssocList) ->
find_data(Key, AssocList) when is_list(AssocList) ->
case proplists:lookup(Key, AssocList) of
none -> error;
{_, V} -> {ok, V}
end.
end;
find_data(_, _) ->
error.
-else.
find_data(Key, AssocList) ->
case proplists:lookup(Key, AssocList) of
none -> error;
{_, V} -> {ok, V}
end.
end;
find_data(_, _) ->
error.
-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.
%% @doc When the value is {@link recursive_data/0}, it returns true. Otherwise it returns false.
-spec is_recursive_data(recursive_data() | term()) -> boolean().
-ifdef(namespaced_types).
check_data_type([]) -> maybe;
check_data_type([Tuple | _]) when is_tuple(Tuple) -> true;
check_data_type(Map) -> is_map(Map).
is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true;
is_recursive_data(V) when is_map(V) -> true;
is_recursive_data(_) -> false.
-else.
check_data_type([]) -> maybe;
check_data_type([Tuple | _]) when is_tuple(Tuple) -> true;
check_data_type(_) -> false.
is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true;
is_recursive_data(_) -> false.
-endif.
25 changes: 19 additions & 6 deletions test/bbmustache_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,32 @@ assoc_list_render_test_() ->
end}
].

top_level_context_render_test_() ->
[
{"top-level binary",
?_assertEqual(<<"hello world">>, bbmustache:render(<<"hello {{.}}">>, <<"world">>))},
{"top-level string",
?_assertEqual(<<"hello world">>, bbmustache:render(<<"hello {{.}}">>, "world"))},
{"top-level integer",
?_assertEqual(<<"1">>, bbmustache:render(<<"{{.}}">>, 1))},
{"top-level float",
?_assertEqual(<<"1.5">>, bbmustache:render(<<"{{.}}">>, 1.5))},
{"top-level atom",
?_assertEqual(<<"atom">>, bbmustache:render(<<"{{.}}">>, atom))},
{"top-level array",
?_assertEqual(<<"1, 2, 3, ">>, bbmustache:render(<<"{{#.}}{{.}}, {{/.}}">>, [1, 2, 3]))},
{"top-level map",
?_assertEqual(<<"yes">>, bbmustache:render(<<"{{.}}">>, #{"a" => "1"}, [{value_serializer, fun(#{"a" := "1"}) -> <<"yes">> end}]))}
].

atom_and_binary_key_test_() ->
[
{"atom key",
fun() ->
F = fun(Text, Render) -> ["<b>", Render(Text), "</b>"] end,
?assertEqual(<<"<b>Willy is awesome.</b>">>,
bbmustache:render(<<"{{#wrapped}}{{name}} is awesome.{{dummy_atom}}{{/wrapped}}">>,
[{name, "Willy"}, {wrapped, F}], [{key_type, atom}])),
[{name, "Willy"}, {wrapped, F}], [{key_type, atom}])),
?assertError(_, binary_to_existing_atom(<<"dummy_atom">>, utf8))
end},
{"binary key",
Expand All @@ -139,11 +157,6 @@ atom_and_binary_key_test_() ->
end}
].

unsupported_data_test_() ->
[
{"dict", ?_assertError(function_clause, bbmustache:render(<<>>, dict:new()))}
].

raise_on_context_miss_test_() ->
[
{"It raise an exception, if the key of escape tag does not exist",
Expand Down

0 comments on commit ccede8d

Please sign in to comment.