erlydtl_compiler.erl 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. %%%-------------------------------------------------------------------
  2. %%% File: erlydtl_compiler.erl
  3. %%% @author Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
  4. %%% @author Evan Miller <emmiller@gmail.com>
  5. %%% @author Andreas Stenius <kaos@astekk.se>
  6. %%% @copyright 2008 Roberto Saccon, Evan Miller
  7. %%% @copyright 2014 Andreas Stenius
  8. %%% @doc
  9. %%% ErlyDTL template compiler
  10. %%% @end
  11. %%%
  12. %%% The MIT License
  13. %%%
  14. %%% Copyright (c) 2007 Roberto Saccon, Evan Miller
  15. %%% Copyright (c) 2014 Andreas Stenius
  16. %%%
  17. %%% Permission is hereby granted, free of charge, to any person obtaining a copy
  18. %%% of this software and associated documentation files (the "Software"), to deal
  19. %%% in the Software without restriction, including without limitation the rights
  20. %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  21. %%% copies of the Software, and to permit persons to whom the Software is
  22. %%% furnished to do so, subject to the following conditions:
  23. %%%
  24. %%% The above copyright notice and this permission notice shall be included in
  25. %%% all copies or substantial portions of the Software.
  26. %%%
  27. %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  28. %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  29. %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  30. %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  31. %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  32. %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  33. %%% THE SOFTWARE.
  34. %%%
  35. %%% @since 2007-12-16 by Roberto Saccon, Evan Miller
  36. %%% @since 2014 by Andreas Stenius
  37. %%%-------------------------------------------------------------------
  38. -module(erlydtl_compiler).
  39. -author('rsaccon@gmail.com').
  40. -author('emmiller@gmail.com').
  41. -author('Andreas Stenius <kaos@astekk.se>').
  42. %% --------------------------------------------------------------------
  43. %% Definitions
  44. %% --------------------------------------------------------------------
  45. -export([compile_file/3, compile_template/3, compile_dir/3,
  46. format_error/1, default_options/0]).
  47. %% internal use
  48. -export([parse_file/2, parse_template/2, do_parse_template/2]).
  49. -import(erlydtl_compiler_utils,
  50. [add_filters/2, add_tags/2, call_extension/3,
  51. load_library/2, shorten_filename/1, get_current_file/1]).
  52. -include("erlydtl_ext.hrl").
  53. %% --------------------------------------------------------------------
  54. %% API
  55. %% --------------------------------------------------------------------
  56. default_options() -> [verbose, report].
  57. compile_template(Template, Module, Options) ->
  58. Context = process_opts(undefined, Module, Options),
  59. Bin = iolist_to_binary(Template),
  60. ?LOG_INFO("Compile template: ~32s~s~n",
  61. [Bin, if size(Bin) > 32 -> "...";
  62. true -> ""
  63. end],
  64. Context),
  65. compile(Context#dtl_context{ bin = Bin }).
  66. compile_file(File, Module, Options) ->
  67. Context = process_opts(File, Module, Options),
  68. ?LOG_INFO("Compile file: ~s~n", [File], Context),
  69. compile(Context).
  70. compile_dir(Dir, Module, Options) ->
  71. Context = process_opts({dir, Dir}, Module, Options),
  72. ?LOG_INFO("Compile directory: ~s~n", [Dir], Context),
  73. compile(Context).
  74. format_error({read_file, Error}) ->
  75. io_lib:format(
  76. "Failed to read file: ~s",
  77. [file:format_error(Error)]);
  78. format_error({read_file, File, Error}) ->
  79. io_lib:format(
  80. "Failed to include file ~s: ~s",
  81. [File, file:format_error(Error)]);
  82. format_error({deprecated_option, Opt, NewOpt}) ->
  83. io_lib:format(
  84. "Compile option '~s' has been deprecated. Use '~s' instead.",
  85. [Opt, NewOpt]);
  86. format_error(Error) ->
  87. erlydtl_compiler_utils:format_error(Error).
  88. %%====================================================================
  89. %% Internal functions
  90. %%====================================================================
  91. process_opts(File, Module, Options0) ->
  92. Options1 = proplists:normalize(
  93. update_defaults(Options0),
  94. [{aliases, deprecated_opts()}
  95. ]),
  96. Source0 = filename:absname(
  97. case File of
  98. undefined ->
  99. filename:join(
  100. [case proplists:get_value(out_dir, Options1, false) of
  101. false -> ".";
  102. OutDir -> OutDir
  103. end,
  104. Module]);
  105. {dir, Dir} ->
  106. Dir;
  107. _ ->
  108. File
  109. end),
  110. Source = shorten_filename(Source0),
  111. Options = [{compiler_options, [{source, Source}]}
  112. |compiler_opts(Options1, [])],
  113. Context =
  114. case File of
  115. {dir, _} ->
  116. init_context([], Source, Module, Options);
  117. _ ->
  118. init_context([Source], filename:dirname(Source), Module, Options)
  119. end,
  120. %% check original options here now that we have a context to
  121. %% process any warnings/errors generated.
  122. check_opts(Options0, Context).
  123. deprecated_opts() ->
  124. [{outdir, out_dir},
  125. {vars, default_vars},
  126. {blocktrans_fun, translation_fun},
  127. {blocktrans_locales, locales}].
  128. check_opts(Options, Context) ->
  129. lists:foldl(
  130. fun ({Opt, NewOpt}, Ctx) ->
  131. case proplists:get_value(Opt, Options) of
  132. undefined -> Ctx;
  133. _ -> ?WARN({deprecated_option, Opt, NewOpt}, Ctx)
  134. end
  135. end, Context, deprecated_opts()).
  136. compiler_opts([CompilerOption|Os], Acc)
  137. when
  138. CompilerOption =:= return;
  139. CompilerOption =:= return_warnings;
  140. CompilerOption =:= return_errors;
  141. CompilerOption =:= report;
  142. CompilerOption =:= report_warnings;
  143. CompilerOption =:= report_errors;
  144. CompilerOption =:= warnings_as_errors;
  145. CompilerOption =:= verbose;
  146. CompilerOption =:= debug_info ->
  147. compiler_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
  148. compiler_opts([O|Os], Acc) ->
  149. compiler_opts(Os, [O|Acc]);
  150. compiler_opts([], Acc) ->
  151. lists:reverse(Acc).
  152. update_defaults(Options) ->
  153. maybe_add_env_default_opts(Options).
  154. maybe_add_env_default_opts(Options) ->
  155. case proplists:get_bool(no_env, Options) of
  156. true -> Options;
  157. _ -> Options ++ env_default_opts()
  158. end.
  159. %% shamelessly borrowed from:
  160. %% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\
  161. %% lib/compiler/src/compile.erl#L128
  162. env_default_opts() ->
  163. Key = "ERLYDTL_COMPILER_OPTIONS",
  164. case os:getenv(Key) of
  165. false -> [];
  166. Str when is_list(Str) ->
  167. case erl_scan:string(Str) of
  168. {ok,Tokens,_} ->
  169. case erl_parse:parse_term(Tokens ++ [{dot, 1}]) of
  170. {ok,List} when is_list(List) -> List;
  171. {ok,Term} -> [Term];
  172. {error,_Reason} ->
  173. io:format("Ignoring bad term in ~s\n", [Key]),
  174. []
  175. end;
  176. {error, {_,_,_Reason}, _} ->
  177. io:format("Ignoring bad term in ~s\n", [Key]),
  178. []
  179. end
  180. end.
  181. compile(Context) ->
  182. Context1 = do_compile(Context),
  183. collect_result(Context1).
  184. collect_result(#dtl_context{
  185. module=Module,
  186. errors=#error_info{ list=[] },
  187. warnings=Ws }=Context) ->
  188. Info = case Ws of
  189. #error_info{ return=true, list=Warnings } ->
  190. [pack_error_list(Warnings)];
  191. _ ->
  192. []
  193. end,
  194. Res = case proplists:get_bool(binary, Context#dtl_context.all_options) of
  195. true ->
  196. [ok, Module, Context#dtl_context.bin | Info];
  197. false ->
  198. [ok, Module | Info]
  199. end,
  200. list_to_tuple(Res);
  201. collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
  202. if Es#error_info.return ->
  203. {error,
  204. pack_error_list(Es#error_info.list),
  205. case Ws of
  206. #error_info{ list=L } ->
  207. pack_error_list(L);
  208. _ ->
  209. []
  210. end};
  211. true -> error
  212. end.
  213. init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
  214. init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
  215. init_context(ParseTrail, DefDir, Module, Options) ->
  216. Ctx = #dtl_context{},
  217. Locales = lists:usort(
  218. lists:concat(
  219. [proplists:get_all_values(locale, Options),
  220. proplists:get_value(locales, Options, Ctx#dtl_context.trans_locales)]
  221. )),
  222. Context0 =
  223. #dtl_context{
  224. all_options = Options,
  225. auto_escape = case proplists:get_value(auto_escape, Options, true) of
  226. true -> [on];
  227. _ -> [off]
  228. end,
  229. parse_trail = ParseTrail,
  230. module = Module,
  231. doc_root = proplists:get_value(doc_root, Options, DefDir),
  232. libraries = proplists:get_value(libraries, Options, Ctx#dtl_context.libraries),
  233. custom_tags_dir = proplists:get_value(
  234. custom_tags_dir, Options,
  235. filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
  236. trans_fun = erlydtl_runtime:init_translation(
  237. proplists:get_value(translation_fun, Options, Ctx#dtl_context.trans_fun)),
  238. trans_locales = Locales,
  239. vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
  240. const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
  241. reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
  242. compiler_options = proplists:append_values(compiler_options, Options),
  243. binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
  244. force_recompile = proplists:get_bool(force_recompile, Options),
  245. verbose = length(proplists:get_all_values(verbose, Options)),
  246. is_compiling_dir = if ParseTrail == [] -> DefDir; true -> false end,
  247. extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
  248. scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
  249. record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
  250. || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
  251. errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
  252. warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options),
  253. lists_0_based = proplists:get_value(lists_0_based, Options, Ctx#dtl_context.lists_0_based),
  254. tuples_0_based = proplists:get_value(tuples_0_based, Options, Ctx#dtl_context.tuples_0_based),
  255. checks = proplists:substitute_negations(
  256. [{no_non_block_tag}],
  257. proplists:get_all_values(w, Options))
  258. },
  259. Context1 = load_libraries(proplists:get_value(default_libraries, Options, []), Context0),
  260. Context = default_checks(Ctx#dtl_context.checks, Context1),
  261. case call_extension(Context, init_context, [Context]) of
  262. {ok, C} when is_record(C, dtl_context) -> C;
  263. undefined -> Context
  264. end.
  265. default_checks([], Context) -> Context;
  266. default_checks([C|Cs], #dtl_context{ checks = Checks0 } = Context) ->
  267. Checks =
  268. case proplists:get_value(C, Checks0) of
  269. undefined -> [C|Checks0];
  270. _ -> Checks0
  271. end,
  272. default_checks(Cs, Context#dtl_context{ checks = Checks }).
  273. init_error_info(warnings, Ei, Options) ->
  274. case proplists:get_bool(warnings_as_errors, Options) of
  275. true -> warnings_as_errors;
  276. false ->
  277. init_error_info(get_error_info_opts(warnings, Options), Ei)
  278. end;
  279. init_error_info(Class, Ei, Options) ->
  280. init_error_info(get_error_info_opts(Class, Options), Ei).
  281. init_error_info([{return, true}|Flags], #error_info{ return = false }=Ei) ->
  282. init_error_info(Flags, Ei#error_info{ return = true });
  283. init_error_info([{report, true}|Flags], #error_info{ report = false }=Ei) ->
  284. init_error_info(Flags, Ei#error_info{ report = true });
  285. init_error_info([_|Flags], Ei) ->
  286. init_error_info(Flags, Ei);
  287. init_error_info([], Ei) -> Ei.
  288. get_error_info_opts(Class, Options) ->
  289. Flags = case Class of
  290. errors ->
  291. [return, report, {return_errors, return}, {report_errors, report}];
  292. warnings ->
  293. [return, report, {return_warnings, return}, {report_warnings, report}]
  294. end,
  295. [begin
  296. {Key, Value} = if is_atom(Flag) -> {Flag, Flag};
  297. true -> Flag
  298. end,
  299. {Value, proplists:get_bool(Key, Options)}
  300. end || Flag <- Flags].
  301. load_libraries([], #dtl_context{ all_options=Options }=Context) ->
  302. %% Load filters and tags passed using the old options
  303. Filters = proplists:get_value(custom_filters_modules, Options, []) ++ [erlydtl_filters],
  304. Tags = proplists:get_value(custom_tags_modules, Options, []),
  305. load_legacy_filters(Filters, load_legacy_tags(Tags, Context));
  306. load_libraries([Lib|Libs], Context) ->
  307. load_libraries(Libs, load_library(Lib, Context)).
  308. load_legacy_filters([], Context) -> Context;
  309. load_legacy_filters([Mod|Mods], Context) ->
  310. {Filters, Context1} = read_legacy_library(Mod, Context),
  311. load_legacy_filters(Mods, add_filters(Filters, Context1)).
  312. load_legacy_tags([], Context) -> Context;
  313. load_legacy_tags([Mod|Mods], Context) ->
  314. {Tags, Context1} = read_legacy_library(Mod, Context),
  315. load_legacy_tags(Mods, add_tags(Tags, Context1)).
  316. read_legacy_library(Mod, Context) ->
  317. case code:ensure_loaded(Mod) of
  318. {module, Mod} ->
  319. {[{Name, {Mod, Name}}
  320. || {Name, _} <- lists:ukeysort(1, Mod:module_info(exports)),
  321. Name =/= module_info
  322. ], Context};
  323. {error, Reason} ->
  324. {[], ?WARN({load_library, '(custom-legacy)', Mod, Reason}, Context)}
  325. end.
  326. is_up_to_date(_, #dtl_context{force_recompile = true}) ->
  327. false;
  328. is_up_to_date(CheckSum, Context) ->
  329. erlydtl_beam_compiler:is_up_to_date(CheckSum, Context).
  330. parse_file(File, Context) ->
  331. {M, F} = Context#dtl_context.reader,
  332. case catch M:F(File) of
  333. {ok, Data} ->
  334. parse_template(Data, Context);
  335. {error, Reason} ->
  336. {error, {read_file, File, Reason}}
  337. end.
  338. parse_template(Data, Context) ->
  339. CheckSum = binary_to_list(erlang:md5(Data)),
  340. case is_up_to_date(CheckSum, Context) of
  341. true -> up_to_date;
  342. false ->
  343. case do_parse_template(Data, Context) of
  344. {ok, Val} -> {ok, Val, CheckSum};
  345. Err -> Err
  346. end
  347. end.
  348. do_parse_template(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
  349. check_scan(
  350. apply(Scanner, scan, [Data]),
  351. Context).
  352. check_scan({ok, Tokens}, Context) ->
  353. Tokens1 = case call_extension(Context, post_scan, [Tokens]) of
  354. undefined -> Tokens;
  355. {ok, T} -> T
  356. end,
  357. check_parse(erlydtl_parser:parse(Tokens1), [], Context#dtl_context{ scanned_tokens=Tokens1 });
  358. check_scan({error, Err, State}, Context) ->
  359. case call_extension(Context, scan, [State]) of
  360. undefined ->
  361. {error, Err};
  362. {ok, NewState} ->
  363. check_scan(apply(Context#dtl_context.scanner_module, resume, [NewState]), Context);
  364. ExtRes ->
  365. ExtRes
  366. end;
  367. check_scan({error, _}=Error, _Context) ->
  368. Error.
  369. check_parse({ok, _}=Ok, [], _Context) -> Ok;
  370. check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed};
  371. check_parse({error, _}=Err, _, _Context) -> Err;
  372. check_parse({error, Err, State}, Acc, Context) ->
  373. {State1, Parsed} = reset_parse_state(State, Context),
  374. case call_extension(Context, parse, [State1]) of
  375. undefined ->
  376. {error, Err};
  377. {ok, ExtParsed} ->
  378. {ok, Acc ++ Parsed ++ ExtParsed};
  379. {error, ExtErr, ExtState} ->
  380. case reset_parse_state(ExtState, Context) of
  381. {_, []} ->
  382. %% todo: see if this is indeed a sensible ext error,
  383. %% or if we should rather present the original Err message
  384. {error, ExtErr};
  385. {State2, ExtParsed} ->
  386. check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context)
  387. end;
  388. ExtRes ->
  389. ExtRes
  390. end.
  391. %% backtrack up to the nearest opening tag, and keep the value stack parsed ok so far
  392. reset_parse_state([[{Tag, _, _}|_]=Ts, Tzr, _, _, Stack], Context)
  393. when Tag==open_tag; Tag==open_var ->
  394. %% reached opening tag, so the stack should be sensible here
  395. {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
  396. Tzr, 0, [], []], lists:flatten(Stack)};
  397. reset_parse_state([_, _, 0, [], []]=State, _Context) ->
  398. %% top of (empty) stack
  399. {State, []};
  400. reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context)
  401. when is_list(Parsed) ->
  402. %% top of good stack
  403. {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
  404. Tzr, 0, [], []], Parsed};
  405. reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) ->
  406. %% backtrack...
  407. reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context).
  408. reset_token_stream([T|_], [T|Ts]) -> [T|Ts];
  409. reset_token_stream(Ts, [_|S]) ->
  410. reset_token_stream(Ts, S).
  411. %% we should find the next token in the list of scanned tokens, or something is real fishy
  412. pack_error_list(Es) ->
  413. collect_error_info([], Es, []).
  414. collect_error_info([], [], Acc) ->
  415. lists:reverse(Acc);
  416. collect_error_info([{File, ErrorInfo}|Es], Rest, [{File, FEs}|Acc]) ->
  417. collect_error_info(Es, Rest, [{File, ErrorInfo ++ FEs}|Acc]);
  418. collect_error_info([E|Es], Rest, Acc) ->
  419. collect_error_info(Es, [E|Rest], Acc);
  420. collect_error_info([], Rest, Acc) ->
  421. case lists:reverse(Rest) of
  422. [E|Es] ->
  423. collect_error_info(Es, [], [E|Acc])
  424. end.
  425. do_compile(#dtl_context{ is_compiling_dir=false, bin=undefined }=Context) ->
  426. compile_output(parse_file(get_current_file(Context), Context), Context);
  427. do_compile(#dtl_context{ is_compiling_dir=false, bin=Template }=Context) ->
  428. compile_output(parse_template(Template, Context), Context);
  429. do_compile(#dtl_context{ is_compiling_dir=Dir }=Context) ->
  430. erlydtl_beam_compiler:compile_dir(Dir, Context).
  431. compile_output(up_to_date, Context) -> Context;
  432. compile_output({ok, DjangoParseTree, CheckSum}, Context) ->
  433. erlydtl_beam_compiler:compile(DjangoParseTree, CheckSum, Context#dtl_context{ bin=undefined });
  434. compile_output({error, Reason}, Context) -> ?ERR(Reason, Context).