1%% @copyright 2015 Hinagiku Soranoba All Rights Reserved. 2%% 3%% @doc Binary pattern match Based Mustach template engine for Erlang/OTP. 4%% 5%% Please refer to [the man page](http://mustache.github.io/mustache.5.html) and [the spec](https://github.com/mustache/spec) of mustache as the need arises.<br /> 6%% 7%% Please see [this](../benchmarks/README.md) for a list of features that bbmustache supports. 8%% 9 10-module(bbmustache). 11 12%%---------------------------------------------------------------------------------------------------------------------- 13%% Exported API 14%%---------------------------------------------------------------------------------------------------------------------- 15-export([ 16 render/2, 17 render/3, 18 parse_binary/1, 19 parse_binary/2, 20 parse_file/1, 21 parse_file/2, 22 compile/2, 23 compile/3, 24 default_value_serializer/1, 25 default_partial_file_reader/2 26 ]). 27 28-ifdef(bbmustache_escriptize). 29-export([main/1]). 30-endif. 31 32-export_type([ 33 key/0, 34 template/0, 35 data/0, 36 recursive_data/0, 37 option/0, % deprecated 38 compile_option/0, 39 parse_option/0, 40 render_option/0 41 ]). 42 43%%---------------------------------------------------------------------------------------------------------------------- 44%% Defines & Records & Types 45%%---------------------------------------------------------------------------------------------------------------------- 46 47-define(PARSE_ERROR, incorrect_format). 48-define(FILE_ERROR, file_not_found). 49-define(CONTEXT_MISSING_ERROR(Msg), {context_missing, Msg}). 50 51-define(IIF(Cond, TValue, FValue), 52 case Cond of true -> TValue; false -> FValue end). 53 54-define(ADD(X, Y), ?IIF(X =:= <<>>, Y, [X | Y])). 55-define(START_TAG, <<"{{">>). 56-define(STOP_TAG, <<"}}">>). 57 58-define(RAISE_ON_CONTEXT_MISS_ENABLED(Options), 59 proplists:get_bool(raise_on_context_miss, Options)). 60-define(RAISE_ON_PARTIAL_MISS_ENABLED(Options), 61 proplists:get_bool(raise_on_partial_miss, Options)). 62 63-define(PARSE_OPTIONS, [ 64 partial_file_reader, 65 raise_on_partial_miss 66 ]). 67 68-type key() :: binary(). 69%% Key MUST be a non-whitespace character sequence NOT containing the current closing delimiter. <br /> 70%% 71%% In addition, `.' have a special meaning. <br /> 72%% (1) `parent.child' ... find the child in the parent. <br /> 73%% (2) `.' ... It means this. However, the type of correspond is only `[integer() | float() | binary() | string() | atom()]'. Otherwise, the behavior is undefined. 74%% 75 76-type source() :: binary(). 77%% If you use lamda expressions, the original text is necessary. 78%% 79%% ``` 80%% e.g. 81%% template: 82%% {{#lamda}}a{{b}}c{{/lamda}} 83%% parse result: 84%% {'#', <<"lamda">>, [<<"a">>, {'n', <<"b">>}, <<"c">>], <<"a{{b}}c">>} 85%% ''' 86%% 87%% NOTE: 88%% Since the binary reference is used internally, it is not a capacitively large waste. 89%% However, the greater the number of tags used, it should use the wasted memory. 90 91-type tag() :: {n, [key()]} 92 | {'&', [key()]} 93 | {'#', [key()], [tag()], source()} 94 | {'^', [key()], [tag()]} 95 | {'>', key(), Indent :: source()} 96 | binary(). % plain text 97 98-record(?MODULE, 99 { 100 data :: [tag()], 101 102 partials = [] :: [{key(), [tag()]} | key()], 103 %% The `{key(), [tag()]}` indicates that `key()` already parsed and `[tag()]` is the result of parsing. 104 %% The `key()` indicates that the file did not exist. 105 106 options = [] :: [compile_option()], 107 indents = [] :: [binary()], 108 context_stack = [] :: [data()] 109 }). 110 111-opaque template() :: #?MODULE{}. 112%% @see parse_binary/1 113%% @see parse_file/1 114 115-record(state, 116 { 117 dirname = <<>> :: file:filename_all(), 118 start = ?START_TAG :: binary(), 119 stop = ?STOP_TAG :: binary(), 120 partials = [] :: [key()], 121 standalone = true :: boolean() 122 }). 123-type state() :: #state{}. 124 125-type parse_option() :: {partial_file_reader, fun((Dirname :: binary(), key()) -> Data :: binary())} 126 | raise_on_partial_miss. 127%% |key |description | 128%% |:-- |:---------- | 129%% |partial_file_reader | When you specify this, it delegate reading of file to the function by `partial'.<br/> This can be used when you want to read from files other than local files.| 130%% |raise_on_partial_miss| If the template used in partials does not found, it will throw an exception (error). | 131 132-type compile_option() :: {key_type, atom | binary | string} 133 | raise_on_context_miss 134 | {escape_fun, fun((binary()) -> binary())} 135 | {value_serializer, fun((any()) -> iodata())}. 136%% |key |description | 137%% |:-- |:---------- | 138%% |key_type | Specify the type of the key in {@link data/0}. Default value is `string'. | 139%% |raise_on_context_miss| If key exists in template does not exist in data, it will throw an exception (error).| 140%% |escape_fun | Specify your own escape function. | 141%% |value_serializer | specify how terms are converted to iodata when templating. | 142 143-type render_option() :: compile_option() | parse_option(). 144%% @see compile_option/0 145%% @see parse_option/0 146 147-type option() :: compile_option(). 148%% This type has been deprecated since 1.6.0. It will remove in 2.0.0. 149%% @see compile_option/0 150 151-type data() :: term(). 152%% Beginners should consider {@link data/0} as {@link recursive_data/0}. 153%% By specifying options, the type are greatly relaxed and equal to `term/0'. 154%% 155%% @see render/2 156%% @see compile/2 157 158-type data_key() :: atom() | binary() | string(). 159%% You can choose one from these as the type of key in {@link recursive_data/0}. 160%% The default is `string/0'. 161%% If you want to change this, you need to specify `key_type' in {@link compile_option/0}. 162 163-ifdef(namespaced_types). 164-type recursive_data() :: #{data_key() => term()} | [{data_key(), term()}]. 165-else. 166-type recursive_data() :: [{data_key(), term()}]. 167-endif. 168%% It is a part of {@link data/0} that can have child elements. 169 170-type endtag() :: {endtag, {state(), [key()], LastTagSize :: non_neg_integer(), Rest :: binary(), Result :: [tag()]}}. 171 172%%---------------------------------------------------------------------------------------------------------------------- 173%% Exported Functions 174%%---------------------------------------------------------------------------------------------------------------------- 175 176%% @equiv render(Bin, Data, []) 177-spec render(binary(), data()) -> binary(). 178render(Bin, Data) -> 179 render(Bin, Data, []). 180 181%% @equiv compile(parse_binary(Bin), Data, Options) 182-spec render(binary(), data(), [render_option()]) -> binary(). 183render(Bin, Data, Options) -> 184 {ParseOptions, CompileOptions} 185 = lists:partition(fun(X) -> 186 lists:member(?IIF(is_tuple(X), element(1, X), X), ?PARSE_OPTIONS) 187 end, Options), 188 compile(parse_binary(Bin, ParseOptions), Data, CompileOptions). 189 190%% @equiv parse_binary(Bin, []) 191-spec parse_binary(binary()) -> template(). 192parse_binary(Bin) when is_binary(Bin) -> 193 parse_binary(Bin, []). 194 195%% @doc Create a {@link template/0} from a binary. 196-spec parse_binary(binary(), [parse_option()]) -> template(). 197parse_binary(Bin, Options) -> 198 {State, Data} = parse(#state{}, Bin), 199 parse_remaining_partials(State, #?MODULE{data = Data}, Options). 200 201%% @equiv parse_file(Filename, []) 202-spec parse_file(file:filename_all()) -> template(). 203parse_file(Filename) -> 204 parse_file(Filename, []). 205 206%% @doc Create a {@link template/0} from a file. 207-spec parse_file(file:filename_all(), [parse_option()]) -> template(). 208parse_file(Filename, Options) -> 209 State = #state{dirname = filename:dirname(Filename)}, 210 case file:read_file(Filename) of 211 {ok, Bin} -> 212 {State1, Data} = parse(State, Bin), 213 Template = case to_binary(filename:extension(Filename)) of 214 <<".mustache">> = Ext -> #?MODULE{partials = [{filename:basename(Filename, Ext), Data}], data = Data}; 215 _ -> #?MODULE{data = Data} 216 end, 217 parse_remaining_partials(State1, Template, Options); 218 _ -> 219 error(?FILE_ERROR, [Filename, Options]) 220 end. 221 222%% @equiv compile(Template, Data, []) 223-spec compile(template(), data()) -> binary(). 224compile(Template, Data) -> 225 compile(Template, Data, []). 226 227%% @doc Embed the data in the template. 228%% 229%% ``` 230%% 1> Template = bbmustache:parse_binary(<<"{{name}}">>). 231%% 2> bbmustache:compile(Template, #{"name" => "Alice"}). 232%% <<"Alice">> 233%% ''' 234%% Data support an associative array or a map. <br /> 235%% All keys MUST be same type. 236-spec compile(template(), data(), [compile_option()]) -> binary(). 237compile(#?MODULE{data = Tags} = T, Data, Options) -> 238 Ret = compile_impl(Tags, Data, [], T#?MODULE{options = Options, data = []}), 239 iolist_to_binary(lists:reverse(Ret)). 240 241%% @doc Default value serializer for templtated values 242-spec default_value_serializer(number() | binary() | string() | atom()) -> iodata(). 243default_value_serializer(Integer) when is_integer(Integer) -> 244 list_to_binary(integer_to_list(Integer)); 245default_value_serializer(Float) when is_float(Float) -> 246 %% NOTE: It is the same behaviour as io_lib:format("~p", [Float]), but it is fast than. 247 %% http://www.cs.indiana.edu/~dyb/pubs/FP-Printing-PLDI96.pdf 248 io_lib_format:fwrite_g(Float); 249default_value_serializer(Atom) when is_atom(Atom) -> 250 list_to_binary(atom_to_list(Atom)); 251default_value_serializer(X) when is_map(X); is_tuple(X) -> 252 error(unsupported_term, [X]); 253default_value_serializer(X) -> 254 X. 255 256%% @doc Default partial file reader 257-spec default_partial_file_reader(binary(), binary()) -> {ok, binary()} | {error, Reason :: term()}. 258default_partial_file_reader(Dirname, Key) -> 259 Filename0 = <<Key/binary, ".mustache">>, 260 Filename = ?IIF(Dirname =:= <<>>, Filename0, filename:join([Dirname, Filename0])), 261 file:read_file(Filename). 262 263%%---------------------------------------------------------------------------------------------------------------------- 264%% Internal Function 265%%---------------------------------------------------------------------------------------------------------------------- 266 267%% @doc {@link compile/2} 268%% 269%% ATTENTION: The result is a list that is inverted. 270-spec compile_impl(Template :: [tag()], data(), Result :: iodata(), template()) -> iodata(). 271compile_impl([], _, Result, _) -> 272 Result; 273compile_impl([{n, Keys} | T], Data, Result, State) -> 274 ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1), 275 Value = iolist_to_binary(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State))), 276 EscapeFun = proplists:get_value(escape_fun, State#?MODULE.options, fun escape/1), 277 compile_impl(T, Data, ?ADD(EscapeFun(Value), Result), State); 278compile_impl([{'&', Keys} | T], Data, Result, State) -> 279 ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1), 280 compile_impl(T, Data, ?ADD(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State)), Result), State); 281compile_impl([{'#', Keys, Tags, Source} | T], Data, Result, State) -> 282 Value = get_data_recursive(Keys, Data, false, State), 283 NestedState = State#?MODULE{context_stack = [Data | State#?MODULE.context_stack]}, 284 case is_recursive_data(Value) of 285 true -> 286 compile_impl(T, Data, compile_impl(Tags, Value, Result, NestedState), State); 287 _ when is_list(Value) -> 288 compile_impl(T, Data, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc, NestedState) end, 289 Result, Value), State); 290 _ when Value =:= false -> 291 compile_impl(T, Data, Result, State); 292 _ when is_function(Value, 2) -> 293 Ret = Value(Source, fun(Text) -> render(Text, Data, State#?MODULE.options) end), 294 compile_impl(T, Data, ?ADD(Ret, Result), State); 295 _ -> 296 compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State) 297 end; 298compile_impl([{'^', Keys, Tags} | T], Data, Result, State) -> 299 Value = get_data_recursive(Keys, Data, false, State), 300 case Value =:= [] orelse Value =:= false of 301 true -> compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State); 302 false -> compile_impl(T, Data, Result, State) 303 end; 304compile_impl([{'>', Key, Indent} | T], Data, Result0, #?MODULE{partials = Partials} = State) -> 305 case proplists:get_value(Key, Partials) of 306 undefined -> 307 case ?RAISE_ON_CONTEXT_MISS_ENABLED(State#?MODULE.options) of 308 true -> error(?CONTEXT_MISSING_ERROR({?FILE_ERROR, Key})); 309 false -> compile_impl(T, Data, Result0, State) 310 end; 311 PartialT -> 312 Indents = State#?MODULE.indents ++ [Indent], 313 Result1 = compile_impl(PartialT, Data, [Indent | Result0], State#?MODULE{indents = Indents}), 314 compile_impl(T, Data, Result1, State) 315 end; 316compile_impl([B1 | [_|_] = T], Data, Result, #?MODULE{indents = Indents} = State) when Indents =/= [] -> 317 %% NOTE: indent of partials 318 case byte_size(B1) > 0 andalso binary:last(B1) of 319 $\n -> compile_impl(T, Data, [Indents, B1 | Result], State); 320 _ -> compile_impl(T, Data, [B1 | Result], State) 321 end; 322compile_impl([Bin | T], Data, Result, State) -> 323 compile_impl(T, Data, [Bin | Result], State). 324 325%% @doc Parse remaining partials in State. It returns {@link template/0}. 326-spec parse_remaining_partials(state(), template(), [parse_option()]) -> template(). 327parse_remaining_partials(#state{partials = []}, Template = #?MODULE{}, _Options) -> 328 Template; 329parse_remaining_partials(State = #state{partials = [P | PartialKeys]}, Template = #?MODULE{partials = Partials}, Options) -> 330 case proplists:is_defined(P, Partials) of 331 true -> parse_remaining_partials(State#state{partials = PartialKeys}, Template, Options); 332 false -> 333 FileReader = proplists:get_value(partial_file_reader, Options, fun default_partial_file_reader/2), 334 Dirname = State#state.dirname, 335 case FileReader(Dirname, P) of 336 {ok, Input} -> 337 {State1, Data} = parse(State, Input), 338 parse_remaining_partials(State1, Template#?MODULE{partials = [{P, Data} | Partials]}, Options); 339 {error, Reason} -> 340 case ?RAISE_ON_PARTIAL_MISS_ENABLED(Options) of 341 true -> error({?FILE_ERROR, P, Reason}); 342 false -> parse_remaining_partials(State#state{partials = PartialKeys}, 343 Template#?MODULE{partials = [P | Partials]}, Options) 344 end 345 end 346 end. 347 348%% @doc Analyze the syntax of the mustache. 349-spec parse(state(), binary()) -> {#state{}, [tag()]}. 350parse(State0, Bin) -> 351 case parse1(State0, Bin, []) of 352 {endtag, {_, Keys, _, _, _}} -> 353 error({?PARSE_ERROR, {section_is_incorrect, binary_join(Keys, <<".">>)}}); 354 {#state{partials = Partials} = State, Tags} -> 355 {State#state{partials = lists:usort(Partials), start = ?START_TAG, stop = ?STOP_TAG}, 356 lists:reverse(Tags)} 357 end. 358 359%% @doc Part of the `parse/1' 360%% 361%% ATTENTION: The result is a list that is inverted. 362-spec parse1(state(), Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag(). 363parse1(#state{start = Start} = State, Bin, Result) -> 364 case binary:match(Bin, [Start, <<"\n">>]) of 365 nomatch -> {State, ?ADD(Bin, Result)}; 366 {S, L} -> 367 Pos = S + L, 368 B2 = binary:part(Bin, Pos, byte_size(Bin) - Pos), 369 case binary:at(Bin, S) of 370 $\n -> parse1(State#state{standalone = true}, B2, ?ADD(binary:part(Bin, 0, Pos), Result)); % \n 371 _ -> parse2(State, split_tag(State, Bin), Result) 372 end 373 end. 374 375%% @doc Part of the `parse/1' 376%% 377%% ATTENTION: The result is a list that is inverted. 378-spec parse2(state(), iolist(), Result :: [tag()]) -> {state(), [tag()]} | endtag(). 379parse2(State, [B1, B2, B3], Result) -> 380 case remove_space_from_head(B2) of 381 <<T, Tag/binary>> when T =:= $&; T =:= ${ -> 382 parse1(State#state{standalone = false}, B3, [{'&', keys(Tag)} | ?ADD(B1, Result)]); 383 <<T, Tag/binary>> when T =:= $#; T =:= $^ -> 384 parse_loop(State, ?IIF(T =:= $#, '#', '^'), keys(Tag), B3, [B1 | Result]); 385 <<"=", Tag0/binary>> -> 386 Tag1 = remove_space_from_tail(Tag0), 387 Size = byte_size(Tag1) - 1, 388 case Size >= 0 andalso Tag1 of 389 <<Tag2:Size/binary, "=">> -> parse_delimiter(State, Tag2, B3, [B1 | Result]); 390 _ -> error({?PARSE_ERROR, {unsupported_tag, <<"=", Tag0/binary>>}}) 391 end; 392 <<"!", _/binary>> -> 393 parse3(State, B3, [B1 | Result]); 394 <<"/", Tag/binary>> -> 395 EndTagSize = byte_size(B2) + byte_size(State#state.start) + byte_size(State#state.stop), 396 {endtag, {State, keys(Tag), EndTagSize, B3, [B1 | Result]}}; 397 <<">", Tag/binary>> -> 398 parse_jump(State, filename_key(Tag), B3, [B1 | Result]); 399 Tag -> 400 parse1(State#state{standalone = false}, B3, [{n, keys(Tag)} | ?ADD(B1, Result)]) 401 end; 402parse2(_, _, _) -> 403 error({?PARSE_ERROR, unclosed_tag}). 404 405%% @doc Part of the `parse/1' 406%% 407%% it is end processing of tag that need to be considered the standalone. 408-spec parse3(#state{}, binary(), [tag()]) -> {state(), [tag()]} | endtag(). 409parse3(State0, Post0, [Tag | Result0]) when is_tuple(Tag) -> 410 {State1, _, Post1, Result1} = standalone(State0, Post0, Result0), 411 parse1(State1, Post1, [Tag | Result1]); 412parse3(State0, Post0, Result0) -> 413 {State1, _, Post1, Result1} = standalone(State0, Post0, Result0), 414 parse1(State1, Post1, Result1). 415 416%% @doc Loop processing part of the `parse/1' 417%% 418%% `{{# Tag}}' or `{{^ Tag}}' corresponds to this. 419-spec parse_loop(state(), '#' | '^', [key()], Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag(). 420parse_loop(State0, Mark, Keys, Input0, Result0) -> 421 {State1, _, Input1, Result1} = standalone(State0, Input0, Result0), 422 case parse1(State1, Input1, []) of 423 {endtag, {State2, Keys, LastTagSize, Rest0, LoopResult0}} -> 424 {State3, _, Rest1, LoopResult1} = standalone(State2, Rest0, LoopResult0), 425 case Mark of 426 '#' -> Source = binary:part(Input1, 0, byte_size(Input1) - byte_size(Rest0) - LastTagSize), 427 parse1(State3, Rest1, [{'#', Keys, lists:reverse(LoopResult1), Source} | Result1]); 428 '^' -> parse1(State3, Rest1, [{'^', Keys, lists:reverse(LoopResult1)} | Result1]) 429 end; 430 {endtag, {_, OtherKeys, _, _, _}} -> 431 error({?PARSE_ERROR, {section_is_incorrect, binary_join(OtherKeys, <<".">>)}}); 432 _ -> 433 error({?PARSE_ERROR, {section_end_tag_not_found, <<"/", (binary_join(Keys, <<".">>))/binary>>}}) 434 end. 435 436%% @doc Endtag part of the `parse/1' 437-spec parse_jump(state(), Tag :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag(). 438parse_jump(State0, Tag, NextBin0, Result0) -> 439 {State1, Indent, NextBin1, Result1} = standalone(State0, NextBin0, Result0), 440 State2 = State1#state{partials = [Tag | State1#state.partials]}, 441 parse1(State2, NextBin1, [{'>', Tag, Indent} | Result1]). 442 443%% @doc Update delimiter part of the `parse/1' 444%% 445%% ParseDelimiterBin :: e.g. `{{=%% %%=}}' -> `%% %%' 446-spec parse_delimiter(state(), ParseDelimiterBin :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag(). 447parse_delimiter(State0, ParseDelimiterBin, NextBin, Result) -> 448 case binary:match(ParseDelimiterBin, <<"=">>) of 449 nomatch -> 450 case [X || X <- binary:split(ParseDelimiterBin, <<" ">>, [global]), X =/= <<>>] of 451 [Start, Stop] -> parse3(State0#state{start = Start, stop = Stop}, NextBin, Result); 452 _ -> error({?PARSE_ERROR, delimiters_may_not_contain_whitespaces}) 453 end; 454 _ -> 455 error({?PARSE_ERROR, delimiters_may_not_contain_equals}) 456 end. 457 458%% @doc Split by the tag, it returns a list of the split binary. 459%% 460%% e.g. 461%% ``` 462%% 1> split_tag(State, <<"...{{hoge}}...">>). 463%% [<<"...">>, <<"hoge">>, <<"...">>] 464%% 465%% 2> split_tag(State, <<"...{{hoge ...">>). 466%% [<<"...">>, <<"hoge ...">>] 467%% 468%% 3> split_tag(State, <<"...">>) 469%% [<<"...">>] 470%% ''' 471-spec split_tag(state(), binary()) -> [binary(), ...]. 472split_tag(#state{start = StartDelimiter, stop = StopDelimiter}, Bin) -> 473 case binary:match(Bin, StartDelimiter) of 474 nomatch -> 475 [Bin]; 476 {StartPos, StartDelimiterLen} -> 477 PosLimit = byte_size(Bin) - StartDelimiterLen, 478 ShiftNum = while({true, StartPos + 1}, 479 fun(Pos) -> 480 ?IIF(Pos =< PosLimit 481 andalso binary:part(Bin, Pos, StartDelimiterLen) =:= StartDelimiter, 482 {true, Pos + 1}, {false, Pos}) 483 end) - StartPos - 1, 484 {PreTag, X} = split_binary(Bin, StartPos + ShiftNum), 485 Tag0 = part(X, StartDelimiterLen, 0), 486 case binary:split(Tag0, StopDelimiter) of 487 [_] -> [PreTag, Tag0]; % not found. 488 [Tag, Rest] -> 489 IncludeStartDelimiterTag = binary:part(X, 0, byte_size(Tag) + StartDelimiterLen), 490 E = ?IIF(repeatedly_binary(StopDelimiter, $}), 491 ?IIF(byte_size(Rest) > 0 andalso binary:first(Rest) =:= $}, 1, 0), 492 ?IIF(byte_size(Tag) > 0 andalso binary:last(Tag) =:= $}, -1, 0)), 493 S = ?IIF(repeatedly_binary(StartDelimiter, ${), 494 ?IIF(ShiftNum > 0, -1, 0), 495 ?IIF(byte_size(Tag) > 0 andalso binary:first(Tag) =:= ${, 1, 0)), 496 case E =:= 0 orelse S =:= 0 of 497 true -> % {{ ... }} 498 [PreTag, Tag, Rest]; 499 false -> % {{{ ... }}} 500 [part(PreTag, 0, min(0, S)), 501 part(IncludeStartDelimiterTag, max(0, S) + StartDelimiterLen - 1, min(0, E)), 502 part(Rest, max(0, E), 0)] 503 end 504 end 505 end. 506 507%% @doc if it is standalone line, remove spaces from edge. 508-spec standalone(#state{}, binary(), [tag()]) -> {#state{}, StashPre :: binary(), Post :: binary(), [tag()]}. 509standalone(#state{standalone = false} = State, Post, [Pre | Result]) -> 510 {State, <<>>, Post, ?ADD(Pre, Result)}; 511standalone(#state{standalone = false} = State, Post, Result) -> 512 {State, <<>>, Post, Result}; 513standalone(State, Post0, Result0) -> 514 {Pre, Result1} = case Result0 =/= [] andalso hd(Result0) of 515 Pre0 when is_binary(Pre0) -> {Pre0, tl(Result0)}; 516 _ -> {<<>>, Result0} 517 end, 518 case remove_indent_from_head(Pre) =:= <<>> andalso remove_indent_from_head(Post0) of 519 <<"\r\n", Post1/binary>> -> 520 {State, Pre, Post1, Result1}; 521 <<"\n", Post1/binary>> -> 522 {State, Pre, Post1, Result1}; 523 <<>> -> 524 {State, Pre, <<>>, Result1}; 525 _ -> 526 {State#state{standalone = false}, <<>>, Post0, ?ADD(Pre, Result1)} 527 end. 528 529%% @doc If the binary is repeatedly the character, return true. Otherwise, return false. 530-spec repeatedly_binary(binary(), byte()) -> boolean(). 531repeatedly_binary(<<X, Rest/binary>>, X) -> 532 repeatedly_binary(Rest, X); 533repeatedly_binary(<<>>, _) -> 534 true; 535repeatedly_binary(_, _) -> 536 false. 537 538%% @doc During the first element of the tuple is true, to perform the function repeatedly. 539-spec while({boolean(), term()}, fun((term()) -> {boolean(), term()})) -> term(). 540while({true, Value}, Fun) -> 541 while(Fun(Value), Fun); 542while({false, Value}, _Fun) -> 543 Value. 544 545%% @equiv binary:part(X, Start, byte_size(X) - Start + End) 546-spec part(binary(), non_neg_integer(), 0 | neg_integer()) -> binary(). 547part(X, Start, End) when End =< 0 -> 548 binary:part(X, Start, byte_size(X) - Start + End). 549 550%% @doc binary to keys 551-spec keys(binary()) -> [key()]. 552keys(Bin0) -> 553 Bin1 = << <<X:8>> || <<X:8>> <= Bin0, X =/= $ >>, 554 case Bin1 =:= <<>> orelse Bin1 =:= <<".">> of 555 true -> [Bin1]; 556 false -> [X || X <- binary:split(Bin1, <<".">>, [global]), X =/= <<>>] 557 end. 558 559%% @doc binary to filename key 560-spec filename_key(binary()) -> key(). 561filename_key(Bin) -> 562 remove_space_from_tail(remove_space_from_head(Bin)). 563 564%% @doc Function for binary like the `string:join/2'. 565-spec binary_join(BinaryList :: [binary()], Separator :: binary()) -> binary(). 566binary_join([], _) -> 567 <<>>; 568binary_join(Bins, Sep) -> 569 [Hd | Tl] = [ [Sep, B] || B <- Bins ], 570 iolist_to_binary([tl(Hd) | Tl]). 571 572%% @doc Remove the space from the head. 573-spec remove_space_from_head(binary()) -> binary(). 574remove_space_from_head(<<" ", Rest/binary>>) -> remove_space_from_head(Rest); 575remove_space_from_head(Bin) -> Bin. 576 577%% @doc Remove the indent from the head. 578-spec remove_indent_from_head(binary()) -> binary(). 579remove_indent_from_head(<<X:8, Rest/binary>>) when X =:= $\t; X =:= $ -> 580 remove_indent_from_head(Rest); 581remove_indent_from_head(Bin) -> 582 Bin. 583 584%% @doc Remove the space from the tail. 585-spec remove_space_from_tail(binary()) -> binary(). 586remove_space_from_tail(<<>>) -> <<>>; 587remove_space_from_tail(Bin) -> 588 PosList = binary:matches(Bin, <<" ">>), 589 LastPos = remove_space_from_tail_impl(lists:reverse(PosList), byte_size(Bin)), 590 binary:part(Bin, 0, LastPos). 591 592%% @see remove_space_from_tail/1 593-spec remove_space_from_tail_impl([{non_neg_integer(), pos_integer()}], non_neg_integer()) -> non_neg_integer(). 594remove_space_from_tail_impl([{X, Y} | T], Size) when Size =:= X + Y -> 595 remove_space_from_tail_impl(T, X); 596remove_space_from_tail_impl(_, Size) -> 597 Size. 598 599%% @doc string or binary to binary 600-spec to_binary(binary() | [byte()]) -> binary(). 601to_binary(Bin) when is_binary(Bin) -> 602 Bin; 603to_binary(Bytes) when is_list(Bytes) -> 604 list_to_binary(Bytes). 605 606%% @doc HTML Escape 607-spec escape(binary()) -> binary(). 608escape(Bin) -> 609 << <<(escape_char(X))/binary>> || <<X:8>> <= Bin >>. 610 611%% @doc escape a character if needed. 612-spec escape_char(byte()) -> <<_:8, _:_*8>>. 613escape_char($<) -> <<"<">>; 614escape_char($>) -> <<">">>; 615escape_char($&) -> <<"&">>; 616escape_char($") -> <<""">>; 617escape_char(C) -> <<C:8>>. 618 619%% @doc convert to {@link data_key/0} from binary. 620-spec convert_keytype(key(), template()) -> data_key(). 621convert_keytype(KeyBin, #?MODULE{options = Options}) -> 622 case proplists:get_value(key_type, Options, string) of 623 atom -> 624 try binary_to_existing_atom(KeyBin, utf8) of 625 Atom -> Atom 626 catch 627 _:_ -> <<" ">> % It is not always present in data/0 628 end; 629 string -> binary_to_list(KeyBin); 630 binary -> KeyBin 631 end. 632 633%% @doc fetch the value of the specified `Keys' from {@link data/0} 634%% 635%% - If `Keys' is `[<<".">>]', it returns `Data'. 636%% - If raise_on_context_miss enabled, it raise an exception when missing `Keys'. Otherwise, it returns `Default'. 637-spec get_data_recursive([key()], data(), Default :: term(), template()) -> term(). 638get_data_recursive(Keys, Data, Default, Template) -> 639 case get_data_recursive_impl(Keys, Data, Template) of 640 {ok, Term} -> Term; 641 error -> 642 case ?RAISE_ON_CONTEXT_MISS_ENABLED(Template#?MODULE.options) of 643 true -> error(?CONTEXT_MISSING_ERROR({key, binary_join(Keys, <<".">>)})); 644 false -> Default 645 end 646 end. 647 648%% @see get_data_recursive/4 649-spec get_data_recursive_impl([key()], data(), template()) -> {ok, term()} | error. 650get_data_recursive_impl([], Data, _) -> 651 {ok, Data}; 652get_data_recursive_impl([<<".">>], Data, _) -> 653 {ok, Data}; 654get_data_recursive_impl([Key | RestKey] = Keys, Data, #?MODULE{context_stack = Stack} = State) -> 655 case is_recursive_data(Data) andalso find_data(convert_keytype(Key, State), Data) of 656 {ok, ChildData} -> 657 get_data_recursive_impl(RestKey, ChildData, State#?MODULE{context_stack = []}); 658 _ when Stack =:= [] -> 659 error; 660 _ -> 661 get_data_recursive_impl(Keys, hd(Stack), State#?MODULE{context_stack = tl(Stack)}) 662 end. 663 664%% @doc find the value of the specified key from {@link recursive_data/0} 665-spec find_data(data_key(), recursive_data() | term()) -> {ok, Value :: term()} | error. 666-ifdef(namespaced_types). 667find_data(Key, Map) when is_map(Map) -> 668 maps:find(Key, Map); 669find_data(Key, AssocList) when is_list(AssocList) -> 670 case proplists:lookup(Key, AssocList) of 671 none -> error; 672 {_, V} -> {ok, V} 673 end; 674find_data(_, _) -> 675 error. 676-else. 677find_data(Key, AssocList) -> 678 case proplists:lookup(Key, AssocList) of 679 none -> error; 680 {_, V} -> {ok, V} 681 end; 682find_data(_, _) -> 683 error. 684-endif. 685 686%% @doc When the value is {@link recursive_data/0}, it returns true. Otherwise it returns false. 687-spec is_recursive_data(recursive_data() | term()) -> boolean(). 688-ifdef(namespaced_types). 689is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true; 690is_recursive_data(V) when is_map(V) -> true; 691is_recursive_data(_) -> false. 692-else. 693is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true; 694is_recursive_data(_) -> false. 695-endif. 696 697%%---------------------------------------------------------------------------------------------------------------------- 698%% Escriptize 699%%---------------------------------------------------------------------------------------------------------------------- 700 701-ifdef(bbmustache_escriptize). 702 703%% escript entry point 704-spec main([string()]) -> ok. 705main(Args) -> 706 %% Load the application to be able to access its information 707 %% (e.g. --version option) 708 _ = application:load(bbmustache), 709 try case getopt:parse(option_spec_list(), Args) of 710 {ok, {Options, Commands}} -> process_commands(Options, Commands); 711 {error, Reason} -> throw(getopt:format_error(option_spec_list(), Reason)) 712 end catch 713 throw:ThrowReason -> 714 ok = io:format(standard_error, "ERROR: ~s~n", [ThrowReason]), 715 halt(1) 716 end. 717 718%% Processes command-line commands 719-spec process_commands([getopt:option()], [string()]) -> ok. 720process_commands(Opts, Cmds) -> 721 HasHelp = proplists:is_defined(help, Opts), 722 HasVersion = proplists:is_defined(version, Opts), 723 if 724 HasHelp -> print_help(standard_io); 725 HasVersion -> print_version(); 726 Opts =:= [], Cmds =:= [] -> print_help(standard_error); 727 true -> process_render(Opts, Cmds) 728 end. 729 730%% Returns command-line options. 731-spec option_spec_list() -> [getopt:option_spec()]. 732option_spec_list() -> 733[ 734 %% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg} 735 {help, $h, "help", undefined, "Show this help information."}, 736 {version, $v, "version", undefined, "Output the current bbmustache version."}, 737 {key_type, $k, "key-type", atom, "Key type (atom | binary | string)."}, 738 {data_file, $d, "data-file", string, "Erlang terms file."} 739]. 740 741%% Processes render 742-spec process_render([getopt:option()], [string()]) -> ok. 743process_render(Opts, TemplateFileNames) -> 744 DataFileNames = proplists:get_all_values(data_file, Opts), 745 Data = lists:foldl(fun(Filename, Acc) -> read_data_files(Filename) ++ Acc end, [], DataFileNames), 746 KeyType = proplists:get_value(key_type, Opts, string), 747 RenderOpts = [{key_type, KeyType}], 748 lists:foreach(fun(TemplateFileName) -> 749 try parse_file(TemplateFileName) of 750 Template -> io:format(compile(Template, Data, RenderOpts)) 751 catch 752 error:?FILE_ERROR -> 753 throw(io_lib:format("~s is unable to read.", [TemplateFileName])) 754 end 755 end, TemplateFileNames). 756 757%% Prints usage/help. 758-spec print_help(getopt:output_stream()) -> ok. 759print_help(OutputStream) -> 760 getopt:usage(option_spec_list(), escript:script_name(), "template_files ...", OutputStream). 761 762%% Prints version. 763-spec print_version() -> ok. 764print_version() -> 765 Vsn = case application:get_key(bbmustache, vsn) of 766 undefined -> throw("vsn can not read from bbmustache.app"); 767 {ok, Vsn0} -> Vsn0 768 end, 769 AdditionalVsn = case application:get_env(bbmustache, git_vsn) of 770 {ok, {_Tag, Count, [$g | GitHash]}} -> "+build." ++ Count ++ ".ref" ++ GitHash; 771 _ -> "" 772 end, 773 %% e.g. bbmustache v1.9.0+build.5.ref90a0afd4f2 774 io:format("bbmustache v~s~s~n", [Vsn, AdditionalVsn]). 775 776%% Read the data-file and return terms. 777-spec read_data_files(file:filename_all()) -> [term()]. 778read_data_files(Filename) -> 779 case file:consult(Filename) of 780 {ok, [Map]} when is_map(Map) -> 781 maps:to_list(Map); 782 {ok, Terms0} when is_list(Terms0) -> 783 Terms = case Terms0 of 784 [Term] when is_list(Term) -> Term; 785 _ -> Terms0 786 end, 787 lists:foldl(fun(Term, Acc) when is_tuple(Term) -> 788 [Term | Acc]; 789 (InclusionFilename, Acc) when is_list(InclusionFilename) -> 790 Path = filename:join(filename:dirname(Filename), InclusionFilename), 791 read_data_files(Path) ++ Acc; 792 (Term, _Acc) -> 793 throw(io_lib:format("~s have unsupported format terms. (~p)", [Filename, Term])) 794 end, [], Terms); 795 {error, Reason} -> 796 throw(io_lib:format("~s is unable to read. (~p)", [Filename, Reason])) 797 end. 798 799-endif. 800