Updated Branches: refs/heads/1994-merge-rcouch 4005c46a5 -> 8cb6e7fdf
add missing files Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/8cb6e7fd Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/8cb6e7fd Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/8cb6e7fd Branch: refs/heads/1994-merge-rcouch Commit: 8cb6e7fdf4368dd055ab148d84523f76533e08df Parents: 4005c46 Author: Benoit Chesneau <[email protected]> Authored: Thu Jan 9 23:58:48 2014 +0100 Committer: Benoit Chesneau <[email protected]> Committed: Thu Jan 9 23:58:48 2014 +0100 ---------------------------------------------------------------------- .gitignore | 5 +- src/test/etap/etap.erl | 612 ++++++++++++++++++++++++++++++++++++++++ src/test/etap/mustache.erl | 228 +++++++++++++++ 3 files changed, 843 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb/blob/8cb6e7fd/.gitignore ---------------------------------------------------------------------- diff --git a/.gitignore b/.gitignore index b2d1ede..e08a2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -161,5 +161,6 @@ src/share/server/main.js src/test/etap/test_cfg_register src/test/out -src/test/etap -src/test/etap +src/test/etap/*.beam +src/test/etap/*.o +src/test/etap/temp.* http://git-wip-us.apache.org/repos/asf/couchdb/blob/8cb6e7fd/src/test/etap/etap.erl ---------------------------------------------------------------------- diff --git a/src/test/etap/etap.erl b/src/test/etap/etap.erl new file mode 100644 index 0000000..82e0cfe --- /dev/null +++ b/src/test/etap/etap.erl @@ -0,0 +1,612 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <[email protected]> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines <[email protected]> [http://socklabs.com/] +%% @author Jeremy Wall <[email protected]> +%% @version 0.3.4 +%% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @todo Finish implementing the skip directive. +%% @todo Document the messages handled by this receive loop. +%% @todo Explain in documentation why we use a process to handle test input. +%% @doc etap is a TAP testing module for Erlang components and applications. +%% This module allows developers to test their software using the TAP method. +%% +%% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p> +%% TAP, the Test Anything Protocol, is a simple text-based interface between +%% testing modules in a test harness. TAP started life as part of the test +%% harness for Perl but now has implementations in C/C++, Python, PHP, Perl +%% and probably others by the time you read this. +%% </p></blockquote> +%% +%% The testing process begins by defining a plan using etap:plan/1, running +%% a number of etap tests and then calling eta:end_tests/0. Please refer to +%% the Erlang modules in the t directory of this project for example tests. +-module(etap). +-vsn("0.3.4"). + +-export([ + ensure_test_server/0, + start_etap_server/0, + test_server/1, + msg/1, msg/2, + diag/1, diag/2, + expectation_mismatch_message/3, + plan/1, + end_tests/0, + not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, + fun_is/3, expect_fun/3, expect_fun/4, + is_greater/3, + skip/1, skip/2, + datetime/1, + skip/3, + bail/0, bail/1, + test_state/0, failure_count/0 +]). + +-export([ + contains_ok/3, + is_before/4 +]). + +-export([ + is_pid/2, + is_alive/2, + is_mfa/3 +]). + +-export([ + loaded_ok/2, + can_ok/2, can_ok/3, + has_attrib/2, is_attrib/3, + is_behaviour/2 +]). + +-export([ + dies_ok/2, + lives_ok/2, + throws_ok/3 +]). + + +-record(test_state, { + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" +}). + +%% @spec plan(N) -> Result +%% N = unknown | skip | {skip, string()} | integer() +%% Result = ok +%% @doc Create a test plan and boot strap the test server. +plan(unknown) -> + ensure_test_server(), + etap_server ! {self(), plan, unknown}, + ok; +plan(skip) -> + io:format("1..0 # skip~n"); +plan({skip, Reason}) -> + io:format("1..0 # skip ~s~n", [Reason]); +plan(N) when is_integer(N), N > 0 -> + ensure_test_server(), + etap_server ! {self(), plan, N}, + ok. + +%% @spec end_tests() -> ok +%% @doc End the current test plan and output test results. +%% @todo This should probably be done in the test_server process. +end_tests() -> + case whereis(etap_server) of + undefined -> self() ! true; + _ -> etap_server ! {self(), state} + end, + State = receive X -> X end, + if + State#test_state.planned == -1 -> + io:format("1..~p~n", [State#test_state.count]); + true -> + ok + end, + case whereis(etap_server) of + undefined -> ok; + _ -> etap_server ! done, ok + end. + +bail() -> + bail(""). + +bail(Reason) -> + etap_server ! {self(), diag, "Bail out! " ++ Reason}, + etap_server ! done, ok, + ok. + +%% @spec test_state() -> Return +%% Return = test_state_record() | {error, string()} +%% @doc Return the current test state +test_state() -> + etap_server ! {self(), state}, + receive + X when is_record(X, test_state) -> X + after + 1000 -> {error, "Timed out waiting for etap server reply.~n"} + end. + +%% @spec failure_count() -> Return +%% Return = integer() | {error, string()} +%% @doc Return the current failure count +failure_count() -> + case test_state() of + #test_state{fail=FailureCount} -> FailureCount; + X -> X + end. + +%% @spec msg(S) -> ok +%% S = string() +%% @doc Print a message in the test output. +msg(S) -> etap_server ! {self(), diag, S}, ok. + +%% @spec msg(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a message in the test output. +%% Function arguments are passed through io_lib:format/2. +msg(Format, Data) -> msg(io_lib:format(Format, Data)). + +%% @spec diag(S) -> ok +%% S = string() +%% @doc Print a debug/status message related to the test suite. +diag(S) -> msg("# " ++ S). + +%% @spec diag(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a debug/status message related to the test suite. +%% Function arguments are passed through io_lib:format/2. +diag(Format, Data) -> diag(io_lib:format(Format, Data)). + +%% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok +%% Got = any() +%% Expected = any() +%% Desc = string() +%% @doc Print an expectation mismatch message in the test output. +expectation_mismatch_message(Got, Expected, Desc) -> + msg(" ---"), + msg(" description: ~p", [Desc]), + msg(" found: ~p", [Got]), + msg(" wanted: ~p", [Expected]), + msg(" ..."), + ok. + +% @spec evaluate(Pass, Got, Expected, Desc) -> Result +%% Pass = true | false +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Evaluate a test statement, printing an expectation mismatch message +%% if the test failed. +evaluate(Pass, Got, Expected, Desc) -> + case mk_tap(Pass, Desc) of + false -> + expectation_mismatch_message(Got, Expected, Desc), + false; + true -> + true + end. + +%% @spec ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is true. +ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). + +%% @spec not_ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is false. +not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). + +%% @spec is_ok(Expr, Desc) -> Result +%% Expr = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). + +%% @spec is(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). + +%% @spec isnt(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are not the same. +isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). + +%% @spec is_greater(ValueA, ValueB, Desc) -> Result +%% ValueA = number() +%% ValueB = number() +%% Desc = string() +%% Result = true | false +%% @doc Assert that an integer is greater than another. +is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> + mk_tap(ValueA > ValueB, Desc). + +%% @spec any(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is in a list. +any(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), true, Desc); +any(Got, Items, Desc) -> + is(lists:member(Got, Items), true, Desc). + +%% @spec none(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is not in a list. +none(Got, Items, Desc) when is_function(Got) -> + is(lists:any(Got, Items), false, Desc); +none(Got, Items, Desc) -> + is(lists:member(Got, Items), false, Desc). + +%% @spec fun_is(Fun, Expected, Desc) -> Result +%% Fun = function() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match. +fun_is(Fun, Expected, Desc) when is_function(Fun) -> + is(Fun(Expected), true, Desc). + +%% @spec expect_fun(ExpectFun, Got, Desc) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc) -> + evaluate(ExpectFun(Got), Got, ExpectFun, Desc). + +%% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result +%% ExpectFun = function() +%% Got = any() +%% Desc = string() +%% ExpectStr = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match, using actual +%% value as the argument to the function. +expect_fun(ExpectFun, Got, Desc, ExpectStr) -> + evaluate(ExpectFun(Got), Got, ExpectStr, Desc). + +%% @equiv skip(TestFun, "") +skip(TestFun) when is_function(TestFun) -> + skip(TestFun, ""). + +%% @spec skip(TestFun, Reason) -> ok +%% TestFun = function() +%% Reason = string() +%% @doc Skip a test. +skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + begin_skip(Reason), + catch TestFun(), + end_skip(), + ok. + +%% @spec skip(Q, TestFun, Reason) -> ok +%% Q = true | false | function() +%% TestFun = function() +%% Reason = string() +%% @doc Skips a test conditionally. The first argument to this function can +%% either be the 'true' or 'false' atoms or a function that returns 'true' or +%% 'false'. +skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> + case QFun() of + true -> begin_skip(Reason), TestFun(), end_skip(); + _ -> TestFun() + end, + ok; + +skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> + begin_skip(Reason), + TestFun(), + end_skip(), + ok; + +skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + TestFun(), + ok. + +%% @private +begin_skip(Reason) -> + etap_server ! {self(), begin_skip, Reason}. + +%% @private +end_skip() -> + etap_server ! {self(), end_skip}. + +%% @spec contains_ok(string(), string(), string()) -> true | false +%% @doc Assert that a string is contained in another string. +contains_ok(Source, String, Desc) -> + etap:isnt( + string:str(Source, String), + 0, + Desc + ). + +%% @spec is_before(string(), string(), string(), string()) -> true | false +%% @doc Assert that a string comes before another string within a larger body. +is_before(Source, StringA, StringB, Desc) -> + etap:is_greater( + string:str(Source, StringB), + string:str(Source, StringA), + Desc + ). + +%% @doc Assert that a given variable is a pid. +is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); +is_pid(_, Desc) -> etap:ok(false, Desc). + +%% @doc Assert that a given process/pid is alive. +is_alive(Pid, Desc) -> + etap:ok(erlang:is_process_alive(Pid), Desc). + +%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. +is_mfa(Pid, MFA, Desc) -> + etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). + +%% @spec loaded_ok(atom(), string()) -> true | false +%% @doc Assert that a module has been loaded successfully. +loaded_ok(M, Desc) when is_atom(M) -> + etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). + +%% @spec can_ok(atom(), atom()) -> true | false +%% @doc Assert that a module exports a given function. +can_ok(M, F) when is_atom(M), is_atom(F) -> + Matches = [X || {X, _} <- M:module_info(exports), X == F], + etap:ok(Matches > 0, lists:concat([M, " can ", F])). + +%% @spec can_ok(atom(), atom(), integer()) -> true | false +%% @doc Assert that a module exports a given function with a given arity. +can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> + Matches = [X || X <- M:module_info(exports), X == {F, A}], + etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). + +%% @spec has_attrib(M, A) -> true | false +%% M = atom() +%% A = atom() +%% @doc Asserts that a module has a given attribute. +has_attrib(M, A) when is_atom(M), is_atom(A) -> + etap:isnt( + proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), + 'asdlkjasdlkads', + lists:concat([M, " has attribute ", A]) + ). + +%% @spec has_attrib(M, A. V) -> true | false +%% M = atom() +%% A = atom() +%% V = any() +%% @doc Asserts that a module has a given attribute with a given value. +is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> + etap:is( + proplists:get_value(A, M:module_info(attributes)), + [V], + lists:concat([M, "'s ", A, " is ", V]) + ). + +%% @spec is_behavior(M, B) -> true | false +%% M = atom() +%% B = atom() +%% @doc Asserts that a given module has a specific behavior. +is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> + is_attrib(M, behaviour, B). + +%% @doc Assert that an exception is raised when running a given function. +dies_ok(F, Desc) -> + case (catch F()) of + {'EXIT', _} -> etap:ok(true, Desc); + _ -> etap:ok(false, Desc) + end. + +%% @doc Assert that an exception is not raised when running a given function. +lives_ok(F, Desc) -> + etap:is(try_this(F), success, Desc). + +%% @doc Assert that the exception thrown by a function matches the given exception. +throws_ok(F, Exception, Desc) -> + try F() of + _ -> etap:ok(nok, Desc) + catch + _:E -> + etap:is(E, Exception, Desc) + end. + +%% @private +%% @doc Run a function and catch any exceptions. +try_this(F) when is_function(F, 0) -> + try F() of + _ -> success + catch + throw:E -> {throw, E}; + error:E -> {error, E}; + exit:E -> {exit, E} + end. + +%% @private +%% @doc Start the etap_server process if it is not running already. +ensure_test_server() -> + case whereis(etap_server) of + undefined -> + proc_lib:start(?MODULE, start_etap_server,[]); + _ -> + diag("The test server is already running.") + end. + +%% @private +%% @doc Start the etap_server loop and register itself as the etap_server +%% process. +start_etap_server() -> + catch register(etap_server, self()), + proc_lib:init_ack(ok), + etap:test_server(#test_state{ + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }). + + +%% @private +%% @doc The main etap_server receive/run loop. The etap_server receive loop +%% responds to seven messages apperatining to failure or passing of tests. +%% It is also used to initiate the testing process with the {_, plan, _} +%% message that clears the current test state. +test_server(State) -> + NewState = receive + {_From, plan, unknown} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + State#test_state{ + planned = -1, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, plan, N} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + io:format("1..~p~n", [N]), + State#test_state{ + planned = N, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, begin_skip, Reason} -> + State#test_state{ + skip = 1, + skip_reason = Reason + }; + {_From, end_skip} -> + State#test_state{ + skip = 0, + skip_reason = "" + }; + {_From, pass, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + pass = State#test_state.pass + 1 + }; + + {_From, fail, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + fail = State#test_state.fail + 1 + }; + {From, state} -> + From ! State, + State; + {_From, diag, Message} -> + io:format("~s~n", [Message]), + State; + {From, count} -> + From ! State#test_state.count, + State; + {From, is_skip} -> + From ! State#test_state.skip, + State; + done -> + exit(normal) + end, + test_server(NewState). + +%% @private +%% @doc Process the result of a test and send it to the etap_server process. +mk_tap(Result, Desc) -> + IsSkip = lib:sendw(etap_server, is_skip), + case [IsSkip, Result] of + [_, true] -> + etap_server ! {self(), pass, Desc}, + true; + [1, _] -> + etap_server ! {self(), pass, Desc}, + true; + _ -> + etap_server ! {self(), fail, Desc}, + false + end. + +%% @private +%% @doc Format a date/time string. +datetime(DateTime) -> + {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, + io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). + +%% @private +%% @doc Craft an output message taking skip/todo into consideration. +skip_diag(Message, 0, _) -> + Message; +skip_diag(_Message, 1, "") -> + " # SKIP"; +skip_diag(_Message, 1, Reason) -> + " # SKIP : " ++ Reason. http://git-wip-us.apache.org/repos/asf/couchdb/blob/8cb6e7fd/src/test/etap/mustache.erl ---------------------------------------------------------------------- diff --git a/src/test/etap/mustache.erl b/src/test/etap/mustache.erl new file mode 100644 index 0000000..f6963cd --- /dev/null +++ b/src/test/etap/mustache.erl @@ -0,0 +1,228 @@ +%% The MIT License +%% +%% Copyright (c) 2009 Tom Preston-Werner <[email protected]> +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +%% THE SOFTWARE. + +%% See the README at http://github.com/mojombo/mustache.erl for additional +%% documentation and usage examples. + +-module(mustache). %% v0.1.0 +-author("Tom Preston-Werner"). +-export([compile/1, compile/2, render/1, render/2, render/3, get/2, get/3, escape/1, start/1]). + +-record(mstate, {mod = undefined, + section_re = undefined, + tag_re = undefined}). + +compile(Body) when is_list(Body) -> + State = #mstate{}, + CompiledTemplate = pre_compile(Body, State), + % io:format("~p~n~n", [CompiledTemplate]), + % io:format(CompiledTemplate ++ "~n", []), + {ok, Tokens, _} = erl_scan:string(CompiledTemplate), + {ok, [Form]} = erl_parse:parse_exprs(Tokens), + Bindings = erl_eval:new_bindings(), + {value, Fun, _} = erl_eval:expr(Form, Bindings), + Fun; +compile(Mod) -> + TemplatePath = template_path(Mod), + compile(Mod, TemplatePath). + +compile(Mod, File) -> + code:purge(Mod), + {module, _} = code:load_file(Mod), + {ok, TemplateBin} = file:read_file(File), + Template = re:replace(TemplateBin, "\"", "\\\\\"", [global, {return,list}]), + State = #mstate{mod = Mod}, + CompiledTemplate = pre_compile(Template, State), + % io:format("~p~n~n", [CompiledTemplate]), + % io:format(CompiledTemplate ++ "~n", []), + {ok, Tokens, _} = erl_scan:string(CompiledTemplate), + {ok, [Form]} = erl_parse:parse_exprs(Tokens), + Bindings = erl_eval:new_bindings(), + {value, Fun, _} = erl_eval:expr(Form, Bindings), + Fun. + +render(Mod) -> + TemplatePath = template_path(Mod), + render(Mod, TemplatePath). + +render(Body, Ctx) when is_list(Body) -> + TFun = compile(Body), + render(undefined, TFun, Ctx); +render(Mod, File) when is_list(File) -> + render(Mod, File, dict:new()); +render(Mod, CompiledTemplate) -> + render(Mod, CompiledTemplate, dict:new()). + +render(Mod, File, Ctx) when is_list(File) -> + CompiledTemplate = compile(Mod, File), + render(Mod, CompiledTemplate, Ctx); +render(Mod, CompiledTemplate, Ctx) -> + Ctx2 = dict:store('__mod__', Mod, Ctx), + lists:flatten(CompiledTemplate(Ctx2)). + +pre_compile(T, State) -> + SectionRE = "\{\{\#([^\}]*)}}\s*(.+?){{\/\\1\}\}\s*", + {ok, CompiledSectionRE} = re:compile(SectionRE, [dotall]), + TagRE = "\{\{(#|=|!|<|>|\{)?(.+?)\\1?\}\}+", + {ok, CompiledTagRE} = re:compile(TagRE, [dotall]), + State2 = State#mstate{section_re = CompiledSectionRE, tag_re = CompiledTagRE}, + "fun(Ctx) -> " ++ + "CFun = fun(A, B) -> A end, " ++ + compiler(T, State2) ++ " end.". + +compiler(T, State) -> + Res = re:run(T, State#mstate.section_re), + case Res of + {match, [{M0, M1}, {N0, N1}, {C0, C1}]} -> + Front = string:substr(T, 1, M0), + Back = string:substr(T, M0 + M1 + 1), + Name = string:substr(T, N0 + 1, N1), + Content = string:substr(T, C0 + 1, C1), + "[" ++ compile_tags(Front, State) ++ + " | [" ++ compile_section(Name, Content, State) ++ + " | [" ++ compiler(Back, State) ++ "]]]"; + nomatch -> + compile_tags(T, State) + end. + +compile_section(Name, Content, State) -> + Mod = State#mstate.mod, + Result = compiler(Content, State), + "fun() -> " ++ + "case mustache:get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++ + "\"true\" -> " ++ + Result ++ "; " ++ + "\"false\" -> " ++ + "[]; " ++ + "List when is_list(List) -> " ++ + "[fun(Ctx) -> " ++ Result ++ " end(dict:merge(CFun, SubCtx, Ctx)) || SubCtx <- List]; " ++ + "Else -> " ++ + "throw({template, io_lib:format(\"Bad context for ~p: ~p\", [" ++ Name ++ ", Else])}) " ++ + "end " ++ + "end()". + +compile_tags(T, State) -> + Res = re:run(T, State#mstate.tag_re), + case Res of + {match, [{M0, M1}, K, {C0, C1}]} -> + Front = string:substr(T, 1, M0), + Back = string:substr(T, M0 + M1 + 1), + Content = string:substr(T, C0 + 1, C1), + Kind = tag_kind(T, K), + Result = compile_tag(Kind, Content, State), + "[\"" ++ Front ++ + "\" | [" ++ Result ++ + " | " ++ compile_tags(Back, State) ++ "]]"; + nomatch -> + "[\"" ++ T ++ "\"]" + end. + +tag_kind(_T, {-1, 0}) -> + none; +tag_kind(T, {K0, K1}) -> + string:substr(T, K0 + 1, K1). + +compile_tag(none, Content, State) -> + Mod = State#mstate.mod, + "mustache:escape(mustache:get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ "))"; +compile_tag("{", Content, State) -> + Mod = State#mstate.mod, + "mustache:get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ ")"; +compile_tag("!", _Content, _State) -> + "[]". + +template_dir(Mod) -> + DefaultDirPath = filename:dirname(code:which(Mod)), + case application:get_env(mustache, templates_dir) of + {ok, DirPath} when is_list(DirPath) -> + case filelib:ensure_dir(DirPath) of + ok -> DirPath; + _ -> DefaultDirPath + end; + _ -> + DefaultDirPath + end. +template_path(Mod) -> + DirPath = template_dir(Mod), + Basename = atom_to_list(Mod), + filename:join(DirPath, Basename ++ ".mustache"). + +get(Key, Ctx) when is_list(Key) -> + {ok, Mod} = dict:find('__mod__', Ctx), + get(list_to_atom(Key), Ctx, Mod); +get(Key, Ctx) -> + {ok, Mod} = dict:find('__mod__', Ctx), + get(Key, Ctx, Mod). + +get(Key, Ctx, Mod) when is_list(Key) -> + get(list_to_atom(Key), Ctx, Mod); +get(Key, Ctx, Mod) -> + case dict:find(Key, Ctx) of + {ok, Val} -> + % io:format("From Ctx {~p, ~p}~n", [Key, Val]), + to_s(Val); + error -> + case erlang:function_exported(Mod, Key, 1) of + true -> + Val = to_s(Mod:Key(Ctx)), + % io:format("From Mod/1 {~p, ~p}~n", [Key, Val]), + Val; + false -> + case erlang:function_exported(Mod, Key, 0) of + true -> + Val = to_s(Mod:Key()), + % io:format("From Mod/0 {~p, ~p}~n", [Key, Val]), + Val; + false -> + [] + end + end + end. + +to_s(Val) when is_integer(Val) -> + integer_to_list(Val); +to_s(Val) when is_float(Val) -> + io_lib:format("~.2f", [Val]); +to_s(Val) when is_atom(Val) -> + atom_to_list(Val); +to_s(Val) -> + Val. + +escape(HTML) -> + escape(HTML, []). + +escape([], Acc) -> + lists:reverse(Acc); +escape(["<" | Rest], Acc) -> + escape(Rest, lists:reverse("<", Acc)); +escape([">" | Rest], Acc) -> + escape(Rest, lists:reverse(">", Acc)); +escape(["&" | Rest], Acc) -> + escape(Rest, lists:reverse("&", Acc)); +escape([X | Rest], Acc) -> + escape(Rest, [X | Acc]). + +%%--------------------------------------------------------------------------- + +start([T]) -> + Out = render(list_to_atom(T)), + io:format(Out ++ "~n", []).
