aboutsummaryrefslogblamecommitdiffstats
path: root/src/asciideck_lists_pass.erl
blob: efb8e87518f67a95f6b116bf19d1a44b991525ea (plain) (tree)


























































































































































                                                                                                   
%% 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.