sources_parser.erl 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. %% Author: dave
  2. %% Author: Sergey Prokhorov <me@seriyps.ru> (new/ext API)
  3. %% Created: Mar 1, 2010
  4. %% @doc:
  5. %% Parses source files and extracts translation directives on templates
  6. %% Examples:
  7. %% <pre>
  8. %% Tpl = <<"111"
  9. %% "{#Translators: btrans comment #}{%blocktrans%}btrns{%endblocktrans%}"
  10. %% "{%comment%} TRANSLATORS: trans comment {%endcomment%}222{%trans 'trns'%}"
  11. %% "333">>,
  12. %% Phrases = sources_parser:parse_content("filename.dtl", Tpl),
  13. %% Msgids = [sources_parser:phrase_info(msgid, P) || P <- Phrases].
  14. %% %% -> ["btrns", "trns"]
  15. %% InOldFormat = [begin
  16. %% [Str, File, Line, Col] = sources_parser:phrase_info([msgid, file, line, col], P),
  17. %% {Str, {File, Line, Col}}
  18. %% end || P <- Phrases].
  19. %% %% -> [{"btrns", {"filename.dtl", 1, 47}}, {"trns", {"filename.dtl", 1, 135}]
  20. %% </pre>
  21. -module(sources_parser).
  22. %%
  23. %% Exported Functions
  24. %%
  25. %% New API
  26. -export([parse_pattern/1, parse_file/1, parse_content/2, phrase_info/2]).
  27. %% Deprecated API
  28. -export([parse/0, parse/1, process_content/2]).
  29. -export_type([phrase/0, compat_phrase/0, field/0]).
  30. %%
  31. %% Include files
  32. %%
  33. -include("include/erlydtl_ext.hrl").
  34. -record(phrase, {msgid :: string(),
  35. msgid_plural :: string() | undefined,
  36. context :: string() | undefined,
  37. comment :: string() | undefined,
  38. file :: string(),
  39. line :: non_neg_integer(),
  40. col :: non_neg_integer()}).
  41. -record(state, {acc=[], translators_comment}).
  42. -opaque phrase() :: #phrase{}.
  43. -type compat_phrase() :: {string(), {string(), non_neg_integer(), non_neg_integer()}}.
  44. -type field() :: msgid | msgid_plural | context | comment | file | line | col.
  45. -define(bail(Fmt, Args),
  46. throw(lists:flatten(io_lib:format(Fmt, Args)))).
  47. -define(GET_FIELD(Key), phrase_info(Key, #phrase{ Key = Value }) -> Value).
  48. %%
  49. %% API Functions
  50. %%
  51. %% Old API
  52. parse() ->
  53. Parsed_Files = parse(["./views/*/*.html"]),
  54. io:format("Parsed files are ~p~n",[Parsed_Files]).
  55. parse(Pattern) ->
  56. to_compat(parse_pattern(Pattern)).
  57. process_content(Path, Content) ->
  58. to_compat(parse_content(Path, Content)).
  59. %% @doc convert new API output to old one.
  60. -spec to_compat([phrase()]) -> [compat_phrase()].
  61. to_compat(Phrases) ->
  62. [{Str, {File, Line, Col}}
  63. || #phrase{msgid=Str, file=File, line=Line, col=Col}
  64. <- Phrases].
  65. %% New API
  66. %% @doc extract info about phrase.
  67. %% See `field()' type for list of available info field names.
  68. -spec phrase_info([field()] | field(), phrase()) -> [Info] | Info when
  69. Info :: non_neg_integer() | string() | undefined.
  70. ?GET_FIELD(msgid);
  71. ?GET_FIELD(msgid_plural);
  72. ?GET_FIELD(context);
  73. ?GET_FIELD(comment);
  74. ?GET_FIELD(file);
  75. ?GET_FIELD(line);
  76. ?GET_FIELD(col);
  77. phrase_info(Fields, Phrase) when is_list(Fields) ->
  78. %% you may pass list of fields
  79. [phrase_info(Field, Phrase) || Field <- Fields].
  80. %% @doc list files, using wildcard and extract phrases from them
  81. -spec parse_pattern([string()]) -> [phrase()].
  82. parse_pattern(Pattern) ->
  83. %%We assume a basedir
  84. GetFiles = fun(Path,Acc) -> Acc ++ [F || F <- filelib:wildcard(Path), filelib:is_regular(F)] end,
  85. Files = lists:foldl(GetFiles,[],Pattern),
  86. io:format("Parsing files ~p~n",[Files]),
  87. ParsedFiles = [parse_file(File) || File <- Files],
  88. lists:flatten(ParsedFiles).
  89. %% @doc extract phrases from single file
  90. parse_file(Path) ->
  91. case file:read_file((Path)) of
  92. {ok, Content} ->
  93. parse_content(Path, Content);
  94. Error ->
  95. ?bail("Cannot read file ~s problem ~p~n", [Path, Error])
  96. end.
  97. %% @doc extract phrases from string / binary
  98. -spec parse_content(string(), binary()) -> [phrase()].
  99. parse_content(Path,Content)->
  100. case erlydtl_compiler:do_parse_template(Content, #dtl_context{}) of
  101. {ok, Data} ->
  102. try process_ast(Path, Data) of
  103. {ok, Result} -> Result
  104. catch
  105. Error:Reason ->
  106. io:format("~s: Template processing failed~nData: ~p~n", [Path, Data]),
  107. erlang:raise(Error, Reason, erlang:get_stacktrace())
  108. end;
  109. Error ->
  110. ?bail("Template parsing failed for template ~s, cause ~p~n", [Path, Error])
  111. end.
  112. %%
  113. %% Local Functions
  114. %%
  115. process_ast(Fname, Tokens) ->
  116. State = process_ast(Fname, Tokens, #state{}),
  117. {ok, State#state.acc}.
  118. process_ast(Fname, Tokens, State) when is_list(Tokens) ->
  119. lists:foldl(
  120. fun (Token, St) ->
  121. process_token(Fname, Token, St)
  122. end, State, Tokens);
  123. process_ast(Fname, Token, State) ->
  124. process_token(Fname, Token, State).
  125. %%Block are recursivelly processed, trans are accumulated and other tags are ignored
  126. process_token(Fname, {block,{identifier,{_Line,_Col},_Identifier},Children}, St) -> process_ast(Fname, Children, St);
  127. process_token(Fname, {trans,Text}, #state{acc=Acc, translators_comment=Comment}=St) ->
  128. {{Line, Col}, String} = trans(Text),
  129. Phrase = #phrase{msgid=unescape(String),
  130. comment=Comment,
  131. file=Fname,
  132. line=Line,
  133. col=Col},
  134. St#state{acc=[Phrase | Acc], translators_comment=undefined};
  135. process_token(Fname,
  136. {trans,Text,{string_literal, _, Context}},
  137. #state{acc=Acc, translators_comment=Comment}=St) ->
  138. {{Line, Col}, String} = trans(Text),
  139. Phrase = #phrase{msgid=unescape(String),
  140. context=unescape(Context),
  141. comment=Comment,
  142. file=Fname,
  143. line=Line,
  144. col=Col},
  145. St#state{acc=[Phrase | Acc], translators_comment=undefined};
  146. process_token(Fname, {blocktrans, Args, Contents, PluralContents}, #state{acc=Acc, translators_comment=Comment}=St) ->
  147. {Fname, Line, Col} = guess_blocktrans_lc(Fname, Args, Contents),
  148. Phrase = #phrase{msgid=unparse(Contents),
  149. msgid_plural=unparse(PluralContents),
  150. context=case proplists:get_value(context, Args) of
  151. {string_literal, _, String} ->
  152. erlydtl_compiler_utils:unescape_string_literal(String);
  153. undefined -> undefined
  154. end,
  155. comment=Comment,
  156. file=Fname,
  157. line=Line,
  158. col=Col},
  159. St#state{acc=[Phrase | Acc], translators_comment=undefined};
  160. process_token(_, {comment, Comment}, St) ->
  161. St#state{translators_comment=maybe_translators_comment(Comment)};
  162. process_token(_Fname, {comment_tag, _Pos, Comment}, St) ->
  163. St#state{translators_comment=translators_comment_text(Comment)};
  164. process_token(Fname, {_Instr, _Cond, Children}, St) -> process_ast(Fname, Children, St);
  165. process_token(Fname, {_Instr, _Cond, Children, Children2}, St) ->
  166. StModified = process_ast(Fname, Children, St),
  167. process_ast(Fname, Children2, StModified);
  168. process_token(_,_AST,St) -> St.
  169. trans({noop, Value}) ->
  170. trans(Value);
  171. trans({string_literal,Pos,String}) -> {Pos, String}.
  172. unescape(String) -> string:sub_string(String, 2, string:len(String) -1).
  173. unparse(undefined) -> undefined;
  174. unparse(Contents) -> erlydtl_unparser:unparse(Contents).
  175. %% hack to guess ~position of blocktrans
  176. guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
  177. %% guess by 1'st with
  178. {Fname, L, C - length("blocktrans with ")};
  179. guess_blocktrans_lc(Fname, _, [{string, {L, C}, _} | _]) ->
  180. %% guess by 1'st string
  181. {Fname, L, C - length("blocktrans %}")};
  182. guess_blocktrans_lc(Fname, _, [{variable, {identifier, {L, C}, _}} | _]) ->
  183. %% guess by 1'st {{...}}
  184. {Fname, L, C - length("blocktrans %}")};
  185. guess_blocktrans_lc(Fname, _, _) ->
  186. {Fname, -1, -1}.
  187. maybe_translators_comment([{string, _Pos, S}]) ->
  188. translators_comment_text(S);
  189. maybe_translators_comment(Other) ->
  190. %% smth like "{%comment%}Translators: Hey, {{var}} is variable substitution{%endcomment%}"
  191. Unparsed = erlydtl_unparser:unparse(Other),
  192. translators_comment_text(Unparsed).
  193. translators_comment_text(S) ->
  194. Stripped = string:strip(S, left),
  195. case "translators:" == string:to_lower(string:substr(Stripped, 1, 12)) of
  196. true -> S;
  197. false -> undefined
  198. end.