%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% The purpose of this pass is to aggregate list_item
%% blocks into proper lists. This involves building a
%% tree based on the rules for list items.
%%
%% The general rules are:
%%
%% - Any list item of different type/level than the
%% current list item is a child of the latter.
%%
%% - The level ultimately does not matter when building
%% the tree, * then **** then ** is accepted just fine.
%%
%% - Lists of the same type as a parent are not allowed.
%% On the other hand reusing a type in different parts
%% of the tree is not a problem.
%%
%% - Any literal paragraph following a list item is a
%% child of that list item. @todo
%%
%% - Any other block can be included as a child by using
%% list continuations.
-module(asciideck_lists_pass).
-export([run/1]).
run(AST) ->
list(AST, []).
list([], Acc) ->
lists:reverse(Acc);
%% Any trailing block continuation is ignored.
list([{list_item_continuation, _, _, _}], Acc) ->
lists:reverse(Acc);
%% The first list item contains the attributes for the list.
list([LI={list_item, Attrs, _, Ann}|Tail0], Acc) ->
{Items, Tail} = item(Tail0, LI, [type(Attrs)], []),
list(Tail, [{list, Attrs, Items, Ann}|Acc]);
list([Block|Tail], Acc) ->
list(Tail, [Block|Acc]).
%% Bulleted/numbered list item of the same type.
item([NextLI={list_item, #{type := T, level := L}, _, _}|Tail],
CurrentLI={list_item, #{type := T, level := L}, _, _}, Parents, Acc) ->
item(Tail, NextLI, Parents, [reverse_children(CurrentLI)|Acc]);
%% Labeled list item of the same type.
item([NextLI={list_item, #{type := T, separator := S}, _, _}|Tail],
CurrentLI={list_item, #{type := T, separator := S}, _, _}, Parents, Acc) ->
item(Tail, NextLI, Parents, [reverse_children(CurrentLI)|Acc]);
%% Other list items are either parent or children lists.
item(FullTail=[NextLI={list_item, Attrs, _, Ann}|Tail0], CurrentLI, Parents, Acc) ->
case lists:member(type(Attrs), Parents) of
%% We have a parent list item. This is the end of this child list.
true ->
{lists:reverse([reverse_children(CurrentLI)|Acc]), FullTail};
%% We have a child list item. This is the beginning of a new list.
false ->
{Items, Tail} = item(Tail0, NextLI, [type(Attrs)|Parents], []),
item(Tail, add_child(CurrentLI, {list, Attrs, Items, Ann}), Parents, Acc)
end;
%% Ignore multiple contiguous list continuations.
item([LIC={list_item_continuation, _, _, _},
{list_item_continuation, _, _, _}|Tail], CurrentLI, Parents, Acc) ->
item([LIC|Tail], CurrentLI, Parents, Acc);
%% Blocks that immediately follow list_item_continuation are children,
%% unless they are list_item themselves in which case it depends on the
%% type and level of the list item.
item([{list_item_continuation, _, _, _}, LI={list_item, _, _, _}|Tail], CurrentLI, Parents, Acc) ->
item([LI|Tail], CurrentLI, Parents, Acc);
item([{list_item_continuation, _, _, _}, Block|Tail], CurrentLI, Parents, Acc) ->
item(Tail, add_child(CurrentLI, Block), Parents, Acc);
%% Anything else is the end of the list.
item(Tail, CurrentLI, _, Acc) ->
{lists:reverse([reverse_children(CurrentLI)|Acc]), Tail}.
type(Attrs) ->
maps:with([type, level, separator], Attrs).
add_child({list_item, Attrs, Children, Ann}, Child) ->
{list_item, Attrs, [Child|Children], Ann}.
reverse_children({list_item, Attrs, Children, Ann}) ->
{list_item, Attrs, lists:reverse(Children), Ann}.
-ifdef(TEST).
list_test() ->
[{list, #{type := bulleted, level := 1}, [
{list_item, #{type := bulleted, level := 1},
[{paragraph, #{}, <<"Hello!">>, _}], #{line := 1}},
{list_item, #{type := bulleted, level := 1},
[{paragraph, #{}, <<"World!">>, _}], #{line := 2}}
], #{line := 1}}] = run([
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"World!">>, #{line => 2}}], #{line => 2}}
]),
ok.
list_of_list_test() ->
[{list, #{type := bulleted, level := 1}, [
{list_item, #{type := bulleted, level := 1}, [
{paragraph, #{}, <<"Hello!">>, _},
{list, #{type := bulleted, level := 2}, [
{list_item, #{type := bulleted, level := 2},
[{paragraph, #{}, <<"Cat!">>, _}], #{line := 2}},
{list_item, #{type := bulleted, level := 2},
[{paragraph, #{}, <<"Dog!">>, _}], #{line := 3}}
], #{line := 2}}
], #{line := 1}},
{list_item, #{type := bulleted, level := 1},
[{paragraph, #{}, <<"World!">>, _}], #{line := 4}}
], #{line := 1}}] = run([
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
{list_item, #{type => bulleted, level => 2},
[{paragraph, #{}, <<"Cat!">>, #{line => 2}}], #{line => 2}},
{list_item, #{type => bulleted, level => 2},
[{paragraph, #{}, <<"Dog!">>, #{line => 3}}], #{line => 3}},
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"World!">>, #{line => 4}}], #{line => 4}}
]),
ok.
list_continuation_test() ->
[{list, #{type := bulleted, level := 1}, [
{list_item, #{type := bulleted, level := 1}, [
{paragraph, #{}, <<"Hello!">>, _},
{listing_block, #{}, <<"hello() -> world.">>, #{line := 3}}
], #{line := 1}},
{list_item, #{type := bulleted, level := 1},
[{paragraph, #{}, <<"World!">>, _}], #{line := 6}}
], #{line := 1}}] = run([
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
{list_item_continuation, #{}, <<>>, #{line => 2}},
{listing_block, #{}, <<"hello() -> world.">>, #{line => 3}},
{list_item, #{type => bulleted, level => 1},
[{paragraph, #{}, <<"World!">>, #{line => 6}}], #{line => 6}}
]),
ok.
-endif.