123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- %%%-------------------------------------------------------------------
- %%% File: erlydtl_compiler.erl
- %%% @author Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
- %%% @author Evan Miller <emmiller@gmail.com>
- %%% @author Andreas Stenius <kaos@astekk.se>
- %%% @copyright 2008 Roberto Saccon, Evan Miller
- %%% @copyright 2014 Andreas Stenius
- %%% @doc
- %%% ErlyDTL template compiler
- %%% @end
- %%%
- %%% The MIT License
- %%%
- %%% Copyright (c) 2007 Roberto Saccon, Evan Miller
- %%% Copyright (c) 2014 Andreas Stenius
- %%%
- %%% 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.
- %%%
- %%% @since 2007-12-16 by Roberto Saccon, Evan Miller
- %%% @since 2014 by Andreas Stenius
- %%%-------------------------------------------------------------------
- -module(erlydtl_compiler).
- -author('rsaccon@gmail.com').
- -author('emmiller@gmail.com').
- -author('Andreas Stenius <kaos@astekk.se>').
- %% --------------------------------------------------------------------
- %% Definitions
- %% --------------------------------------------------------------------
- -export([compile_file/3, compile_template/3, compile_dir/3,
- format_error/1, default_options/0]).
- %% internal use
- -export([parse_file/2, parse_template/2, do_parse_template/2]).
- -import(erlydtl_compiler_utils,
- [add_filters/2, add_tags/2, call_extension/3,
- load_library/2]).
- -include("erlydtl_ext.hrl").
- %% --------------------------------------------------------------------
- %% API
- %% --------------------------------------------------------------------
- default_options() -> [verbose, report].
- compile_template(Template, Module, Options) ->
- Context = process_opts(undefined, Module, Options),
- compile(Context#dtl_context{ bin = iolist_to_binary(Template) }).
- compile_file(File, Module, Options) ->
- Context = process_opts(File, Module, Options),
- ?LOG_INFO("Compile template: ~s~n", [File], Context),
- compile(Context).
- compile_dir(Dir, Module, Options) ->
- Context = process_opts({dir, Dir}, Module, Options),
- ?LOG_INFO("Compile directory: ~s~n", [Dir], Context),
- compile(Context).
- format_error({read_file, Error}) ->
- io_lib:format(
- "Failed to read file: ~s",
- [file:format_error(Error)]);
- format_error({read_file, File, Error}) ->
- io_lib:format(
- "Failed to include file ~s: ~s",
- [File, file:format_error(Error)]);
- format_error(Error) ->
- erlydtl_compiler_utils:format_error(Error).
- %%====================================================================
- %% Internal functions
- %%====================================================================
- process_opts(File, Module, Options0) ->
- Options1 = proplists:normalize(
- update_defaults(Options0),
- [{aliases, [{outdir, out_dir}]}
- ]),
- Source0 = filename:absname(
- case File of
- undefined ->
- filename:join(
- [case proplists:get_value(out_dir, Options1, false) of
- false -> ".";
- OutDir -> OutDir
- end,
- Module]);
- {dir, Dir} ->
- Dir;
- _ ->
- File
- end),
- Source = shorten_filename(Source0),
- Options = [{compiler_options, [{source, Source}]}
- |compiler_opts(Options1, [])],
- case File of
- {dir, _} ->
- init_context([], Source, Module, Options);
- _ ->
- init_context([Source], filename:dirname(Source), Module, Options)
- end.
- compiler_opts([CompilerOption|Os], Acc)
- when
- CompilerOption =:= return;
- CompilerOption =:= return_warnings;
- CompilerOption =:= return_errors;
- CompilerOption =:= report;
- CompilerOption =:= report_warnings;
- CompilerOption =:= report_errors;
- CompilerOption =:= warnings_as_errors;
- CompilerOption =:= verbose;
- CompilerOption =:= debug_info ->
- compiler_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
- compiler_opts([O|Os], Acc) ->
- compiler_opts(Os, [O|Acc]);
- compiler_opts([], Acc) ->
- lists:reverse(Acc).
- update_defaults(Options) ->
- maybe_add_env_default_opts(Options).
- maybe_add_env_default_opts(Options) ->
- case proplists:get_bool(no_env, Options) of
- true -> Options;
- _ -> Options ++ env_default_opts()
- end.
- %% shamelessly borrowed from:
- %% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\
- %% lib/compiler/src/compile.erl#L128
- env_default_opts() ->
- Key = "ERLYDTL_COMPILER_OPTIONS",
- case os:getenv(Key) of
- false -> [];
- Str when is_list(Str) ->
- case erl_scan:string(Str) of
- {ok,Tokens,_} ->
- case erl_parse:parse_term(Tokens ++ [{dot, 1}]) of
- {ok,List} when is_list(List) -> List;
- {ok,Term} -> [Term];
- {error,_Reason} ->
- io:format("Ignoring bad term in ~s\n", [Key]),
- []
- end;
- {error, {_,_,_Reason}, _} ->
- io:format("Ignoring bad term in ~s\n", [Key]),
- []
- end
- end.
- %% shorten_filename/1 copied from Erlang/OTP lib/compiler/src/compile.erl
- shorten_filename(Name0) ->
- {ok,Cwd} = file:get_cwd(),
- case lists:prefix(Cwd, Name0) of
- false -> Name0;
- true ->
- case lists:nthtail(length(Cwd), Name0) of
- "/"++N -> N;
- N -> N
- end
- end.
- compile(Context) ->
- Context1 = do_compile(Context),
- collect_result(Context1).
- collect_result(#dtl_context{
- module=Module,
- errors=#error_info{ list=[] },
- warnings=Ws }=Context) ->
- Info = case Ws of
- #error_info{ return=true, list=Warnings } ->
- [pack_error_list(Warnings)];
- _ ->
- []
- end,
- Res = case proplists:get_bool(binary, Context#dtl_context.all_options) of
- true ->
- [ok, Module, Context#dtl_context.bin | Info];
- false ->
- [ok, Module | Info]
- end,
- list_to_tuple(Res);
- collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
- if Es#error_info.return ->
- {error,
- pack_error_list(Es#error_info.list),
- case Ws of
- #error_info{ list=L } ->
- pack_error_list(L);
- _ ->
- []
- end};
- true -> error
- end.
- init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
- init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
- init_context(ParseTrail, DefDir, Module, Options) ->
- Ctx = #dtl_context{},
- Locale = proplists:get_value(locale, Options),
- BlocktransLocales = proplists:get_value(blocktrans_locales, Options),
- TransLocales = case {Locale, BlocktransLocales} of
- {undefined, undefined} -> Ctx#dtl_context.trans_locales;
- {undefined, Val} -> Val;
- {Val, undefined} -> [Val];
- _ -> lists:usort([Locale | BlocktransLocales])
- end,
- Context0 =
- #dtl_context{
- all_options = Options,
- auto_escape = case proplists:get_value(auto_escape, Options, true) of
- true -> on;
- _ -> off
- end,
- parse_trail = ParseTrail,
- module = Module,
- doc_root = proplists:get_value(doc_root, Options, DefDir),
- libraries = proplists:get_value(libraries, Options, Ctx#dtl_context.libraries),
- custom_tags_dir = proplists:get_value(
- custom_tags_dir, Options,
- filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
- trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
- trans_locales = TransLocales,
- vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
- reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
- compiler_options = proplists:append_values(compiler_options, Options),
- binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
- force_recompile = proplists:get_bool(force_recompile, Options),
- verbose = length(proplists:get_all_values(verbose, Options)),
- is_compiling_dir = ParseTrail == [],
- extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
- scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
- record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
- || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
- errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
- warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options)
- },
- Context = load_libraries(proplists:get_value(default_libraries, Options, []), Context0),
- case call_extension(Context, init_context, [Context]) of
- {ok, C} when is_record(C, dtl_context) -> C;
- undefined -> Context
- end.
- init_error_info(warnings, Ei, Options) ->
- case proplists:get_bool(warnings_as_errors, Options) of
- true -> warnings_as_errors;
- false ->
- init_error_info(get_error_info_opts(warnings, Options), Ei)
- end;
- init_error_info(Class, Ei, Options) ->
- init_error_info(get_error_info_opts(Class, Options), Ei).
- init_error_info([{return, true}|Flags], #error_info{ return = false }=Ei) ->
- init_error_info(Flags, Ei#error_info{ return = true });
- init_error_info([{report, true}|Flags], #error_info{ report = false }=Ei) ->
- init_error_info(Flags, Ei#error_info{ report = true });
- init_error_info([_|Flags], Ei) ->
- init_error_info(Flags, Ei);
- init_error_info([], Ei) -> Ei.
- get_error_info_opts(Class, Options) ->
- Flags = case Class of
- errors ->
- [return, report, {return_errors, return}, {report_errors, report}];
- warnings ->
- [return, report, {return_warnings, return}, {report_warnings, report}]
- end,
- [begin
- {Key, Value} = if is_atom(Flag) -> {Flag, Flag};
- true -> Flag
- end,
- {Value, proplists:get_bool(Key, Options)}
- end || Flag <- Flags].
- load_libraries([], #dtl_context{ all_options=Options }=Context) ->
- %% Load filters and tags passed using the old options
- Filters = proplists:get_value(custom_filters_modules, Options, []) ++ [erlydtl_filters],
- Tags = proplists:get_value(custom_tags_modules, Options, []),
- load_legacy_filters(Filters, load_legacy_tags(Tags, Context));
- load_libraries([Lib|Libs], Context) ->
- load_libraries(Libs, load_library(Lib, Context)).
- load_legacy_filters([], Context) -> Context;
- load_legacy_filters([Mod|Filters], Context) ->
- load_legacy_filters(Filters, add_filters(read_legacy_library(Mod), Context)).
- load_legacy_tags([], Context) -> Context;
- load_legacy_tags([Mod|Tags], Context) ->
- load_legacy_tags(Tags, add_tags(read_legacy_library(Mod), Context)).
- read_legacy_library(Mod) ->
- [{Name, {Mod, Name}}
- || {Name, _} <- lists:ukeysort(1, Mod:module_info(exports)),
- Name =/= module_info
- ].
- is_up_to_date(_, #dtl_context{force_recompile = true}) ->
- false;
- is_up_to_date(CheckSum, Context) ->
- erlydtl_beam_compiler:is_up_to_date(CheckSum, Context).
- parse_file(File, Context) ->
- {M, F} = Context#dtl_context.reader,
- case catch M:F(File) of
- {ok, Data} ->
- parse_template(Data, Context);
- {error, Reason} ->
- {error, {read_file, File, Reason}}
- end.
- parse_template(Data, Context) ->
- CheckSum = binary_to_list(erlang:md5(Data)),
- case is_up_to_date(CheckSum, Context) of
- true -> up_to_date;
- false ->
- case do_parse_template(Data, Context) of
- {ok, Val} -> {ok, Val, CheckSum};
- Err -> Err
- end
- end.
- do_parse_template(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
- check_scan(
- apply(Scanner, scan, [Data]),
- Context).
- check_scan({ok, Tokens}, Context) ->
- Tokens1 = case call_extension(Context, post_scan, [Tokens]) of
- undefined -> Tokens;
- {ok, T} -> T
- end,
- check_parse(erlydtl_parser:parse(Tokens1), [], Context#dtl_context{ scanned_tokens=Tokens1 });
- check_scan({error, Err, State}, Context) ->
- case call_extension(Context, scan, [State]) of
- undefined ->
- {error, Err};
- {ok, NewState} ->
- check_scan(apply(Context#dtl_context.scanner_module, resume, [NewState]), Context);
- ExtRes ->
- ExtRes
- end;
- check_scan({error, _}=Error, _Context) ->
- Error.
- check_parse({ok, _}=Ok, [], _Context) -> Ok;
- check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed};
- check_parse({error, _}=Err, _, _Context) -> Err;
- check_parse({error, Err, State}, Acc, Context) ->
- {State1, Parsed} = reset_parse_state(State, Context),
- case call_extension(Context, parse, [State1]) of
- undefined ->
- {error, Err};
- {ok, ExtParsed} ->
- {ok, Acc ++ Parsed ++ ExtParsed};
- {error, ExtErr, ExtState} ->
- case reset_parse_state(ExtState, Context) of
- {_, []} ->
- %% todo: see if this is indeed a sensible ext error,
- %% or if we should rather present the original Err message
- {error, ExtErr};
- {State2, ExtParsed} ->
- check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context)
- end;
- ExtRes ->
- ExtRes
- end.
- %% backtrack up to the nearest opening tag, and keep the value stack parsed ok so far
- reset_parse_state([[{Tag, _, _}|_]=Ts, Tzr, _, _, Stack], Context)
- when Tag==open_tag; Tag==open_var ->
- %% reached opening tag, so the stack should be sensible here
- {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
- Tzr, 0, [], []], lists:flatten(Stack)};
- reset_parse_state([_, _, 0, [], []]=State, _Context) ->
- %% top of (empty) stack
- {State, []};
- reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context)
- when is_list(Parsed) ->
- %% top of good stack
- {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
- Tzr, 0, [], []], Parsed};
- reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) ->
- %% backtrack...
- reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context).
- reset_token_stream([T|_], [T|Ts]) -> [T|Ts];
- reset_token_stream(Ts, [_|S]) ->
- reset_token_stream(Ts, S).
- %% we should find the next token in the list of scanned tokens, or something is real fishy
- pack_error_list(Es) ->
- collect_error_info([], Es, []).
- collect_error_info([], [], Acc) ->
- lists:reverse(Acc);
- collect_error_info([{File, ErrorInfo}|Es], Rest, [{File, FEs}|Acc]) ->
- collect_error_info(Es, Rest, [{File, ErrorInfo ++ FEs}|Acc]);
- collect_error_info([E|Es], Rest, Acc) ->
- collect_error_info(Es, [E|Rest], Acc);
- collect_error_info([], Rest, Acc) ->
- case lists:reverse(Rest) of
- [E|Es] ->
- collect_error_info(Es, [], [E|Acc])
- end.
- do_compile(#dtl_context{ is_compiling_dir=true, parse_trail=[Dir] }=Context) ->
- erlydtl_beam_compiler:compile_dir(Dir, Context);
- do_compile(#dtl_context{ bin=undefined, parse_trail=[File] }=Context) ->
- compile_output(parse_file(File, Context), Context);
- do_compile(#dtl_context{ bin=Template }=Context) ->
- compile_output(parse_template(Template, Context), Context).
- compile_output(up_to_date, Context) -> Context;
- compile_output({ok, DjangoParseTree, CheckSum}, Context) ->
- erlydtl_beam_compiler:compile(DjangoParseTree, CheckSum, Context#dtl_context{ bin=undefined });
- compile_output({error, Reason}, Context) -> ?ERR(Reason, Context).
|