-module(gurka_parser). -include("gurka.hrl"). -export([parse/1, tokens/1]). -spec parse(File) -> {ok, Feature} | {error, Reason} when File :: file:name(), Feature :: feature(), Reason :: file:posix() | badarg | terminated | system_limit. parse(File) -> case file:read_file(File) of {ok, Binary} -> {_, Lines} = lists:foldl(fun(Line, {Row, AccIn}) -> {Row + 1, [{Row, Line, tokens(Line)} | AccIn]} end, {1, []}, binary:split(Binary, [<<$\n>>, <<$\r>>], [global, trim])), {ok, inherit_tags(compact(process(undefined, undefined, lists:reverse(Lines))))}; {error, Reason} -> {error, Reason} end. -spec process(Phase, Action, Lines) -> Feature when Phase :: phase(), Action :: action(), Lines :: lines(), Feature :: feature(). process(_Phase, _Action, []) -> []; process(Phase, Action, [{_Row, _Line, []} | Lines]) -> process(Phase, Action, Lines); process(Phase, Action, [{Row, _Line, Tags = [<<$@, _/binary>> | _]} | Lines]) -> [#step{phase = Phase, action = tags, tokens = Tags, row = Row} | process(Phase, Action, Lines)]; process(Phase, Action, [{Row, Line, [<<"|">> | Tokens]} | Lines]) -> {Lines2, Table} = build_table([{Row, Line, [<<"|">> | Tokens]} | Lines], []), [#step{phase = Phase, action = table, tokens = Table, row = Row} | process(Phase, Action, Lines2)]; process(Phase, Action, [{Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines]) -> {Lines2, Docstring} = build_docstring(Lines, []), [#step{phase = Phase, action = docstring, tokens = Docstring, row = Row} | process(Phase, Action, Lines2)]; process(Phase, Action, [{Row, _Line, [FirstToken | Tokens]} | Lines]) -> case normalize_token(FirstToken) of "and" -> [#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)]; "but" -> [#step{phase = Phase, action = Action, tokens = Tokens, row = Row} | process(Phase, Action, Lines)]; "given" -> [#step{phase = Phase, action = given, tokens = Tokens, row = Row} | process(Phase, given, Lines)]; "when" -> [#step{phase = Phase, action = 'when', tokens = Tokens, row = Row} | process(Phase, 'when', Lines)]; "then" -> [#step{phase = Phase, action = then, tokens = Tokens, row = Row} | process(Phase, then, Lines)]; "examples" -> [#step{phase = Phase, action = examples, tokens = Tokens, row = Row} | process(Phase, examples, Lines)]; "feature" -> [#step{phase = feature, action = start, tokens = Tokens, row = Row} | process(feature, undefined, Lines)]; "background" -> [#step{phase = background, action = start, tokens = Tokens, row = Row} | process(background, undefined, Lines)]; "scenario" -> case normalize_token(hd(Tokens)) of "outline" -> [#step{phase = scenario_outline, action = start, tokens = tl(Tokens), row = Row} | process(scenario_outline, undefined, Lines)]; _ -> [#step{phase = scenario, action = start, tokens = Tokens, row = Row} | process(scenario, undefined, Lines)] end; _ -> [#step{phase = Phase, action = desc, tokens = [FirstToken | Tokens], row = Row} | process(Phase, Action, Lines)] end. compact(Feature) -> compact(lists:reverse(Feature), []). compact([], Acc) -> Acc; compact([#step{action = docstring, tokens = Docstring} | [Step | Steps]], Acc) -> compact([Step#step{tokens = Step#step.tokens ++ [{docstring, Docstring}]} | Steps], Acc); compact([#step{action = table, tokens = Table} | [Step = #step{action = examples} | Steps]], Acc) -> compact([Step#step{tokens = Table} | Steps], Acc); compact([#step{action = table, tokens = Table} | [Step | Steps]], Acc) -> compact([Step#step{tokens = Step#step.tokens ++ [{table, Table}]} | Steps], Acc); compact([Step = #step{phase = Phase, action = start} | [#step{action = tags, tokens = Tags} | Steps]], Acc) when Phase == feature; Phase == scenario; Phase == scenario_outline -> compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc); compact([Step = #step{phase = scenario_outline, action = examples} | [#step{action = tags, tokens = Tags} | Steps]], Acc) -> compact([Step#step{meta = [{tags, [Tag || <<$@, Tag/binary>> <- Tags]}]} | Steps], Acc); compact([#step{action = tags} | Steps], Acc) -> compact(Steps, Acc); compact([Step | Steps], Acc) -> compact(Steps, [Step | Acc]). inherit_tags(Feature) -> inherit_tags(Feature, {[], []}). inherit_tags([], _) -> []; inherit_tags([Step = #step{phase = feature, action = start} | Steps], _) -> FeatureTags = proplists:get_value(tags, Step#step.meta, []), NewMeta = [{merged_tags, FeatureTags} | Step#step.meta], [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, []})]; inherit_tags([Step = #step{phase = Phase, action = start} | Steps], {FeatureTags, _}) when Phase == scenario; Phase == scenario_outline -> ScenarioTags = proplists:get_value(tags, Step#step.meta, []), NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags} | Step#step.meta], [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})]; inherit_tags([Step = #step{phase = scenario_outline, action = examples} | Steps], {FeatureTags, ScenarioTags}) -> ExamplesTags = proplists:get_value(tags, Step#step.meta, []), NewMeta = [{merged_tags, FeatureTags ++ ScenarioTags ++ ExamplesTags} | Step#step.meta], [Step#step{meta = NewMeta} | inherit_tags(Steps, {FeatureTags, ScenarioTags})]; inherit_tags([Step | Steps], Tags) -> [Step | inherit_tags(Steps, Tags)]. build_table([], Table) -> {[], lists:reverse(Table)}; build_table([{_Row, Line, [<<"|">> | _Tokens]} | Lines], Table) -> Cells = [list_to_binary(string:strip(Field)) || Field <- string:tokens(string:strip(binary_to_list(Line)), "|")], build_table(Lines, [Cells | Table]); build_table(Lines, Table) -> {Lines, lists:reverse(Table)}. build_docstring([], Docstring) -> {[], Docstring}; build_docstring([{_Row, _Line, [<<"\"\"\"">> | _Tokens]} | Lines], Docstring) -> {Lines, list_to_binary(Docstring)}; build_docstring([{_Row, Line, _Tokens} | Lines], []) -> build_docstring(Lines, string:strip(binary_to_list(Line))); build_docstring([{_Row, Line, _Tokens} | Lines], Docstring) -> build_docstring(Lines, Docstring ++ "\n" ++ string:strip(binary_to_list(Line))). normalize_token(Token) -> hd(string:tokens(string:to_lower(binary_to_list(Token)), ":")). tokens(Line) -> [strip_quotes(Token) || Token <- tokens(Line, [], <<>>)]. tokens(<<>>, Tokens, <<>>) -> Tokens; tokens(<<>>, [<<$", $">>], <<$">>) -> [<<$", $", $">>]; tokens(<<>>, Tokens, Buffer) -> Tokens ++ [Buffer]; tokens(<<$\s, Line/binary>>, Tokens, <<>>) -> tokens(Line, Tokens, <<>>); tokens(<<$\s, Line/binary>>, Tokens, <<$", Buffer/binary>>) -> tokens(Line, Tokens, <<$", Buffer/binary, $\s>>); tokens(<<$\s, Line/binary>>, Tokens, Buffer) -> tokens(<<$\s, Line/binary>>, Tokens ++ [Buffer], <<>>); tokens(<<$\", Line/binary>>, Tokens, <<>>) -> tokens(Line, Tokens, <<$">>); tokens(<<$\", Line/binary>>, Tokens, <<$", Buffer/binary>>) -> tokens(Line, Tokens ++ [<<$", Buffer/binary, $">>], <<>>); tokens(<<$\", Line/binary>>, Tokens, Buffer) -> tokens(Line, Tokens ++ [Buffer], <<$">>); tokens(<>, Tokens, Buffer) -> tokens(Line, Tokens, <>). strip_quotes(<<>>) -> <<>>; strip_quotes(<<$", $", $">>) -> <<$", $", $">>; strip_quotes(<<$", Token/binary>>) -> case binary:last(Token) of $" -> binary:part(Token, {0, byte_size(Token) - 1}); _ -> Token end; strip_quotes(Token) -> Token.