aboutsummaryrefslogblamecommitdiffstats
path: root/src/asciideck_tables_pass.erl
blob: fdda6ef48f20e3125671a73d1bccc33c3ba5ca3a (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.

%% This pass parses and builds a table from the contents
%% of a table block.
%%
%% Asciidoc User Guide 23
%%
%% @todo Rows and cells are currently not annotated.
-module(asciideck_tables_pass).

-export([run/1]).

-define(IS_WS(C), (C =:= $\s) or (C =:= $\t) or (C =:= $\n).

run([]) ->
	[];
run([Table={table, _, _, _}|Tail]) ->
	[table(Table)|run(Tail)];
run([Block|Tail]) ->
	[Block|run(Tail)].

table({table, Attrs, Contents, Ann}) ->
	{Cells, NumCols} = parse_table(Contents, Attrs),
	Children = rows(Cells, NumCols),
	{table, Attrs, Children, Ann}.

-ifdef(TEST).
table_test() ->
	{table, _, [
		{row, _, [
			{cell, _, <<"1">>, _},
			{cell, _, <<"2">>, _},
			{cell, _, <<"A">>, _}
		], _},
		{row, _, [
			{cell, _, <<"3">>, _},
			{cell, _, <<"4">>, _},
			{cell, _, <<"B">>, _}
		], _},
		{row, _, [
			{cell, _, <<"5">>, _},
			{cell, _, <<"6">>, _},
			{cell, _, <<"C">>, _}
		], _}
	], _} = table({table, #{}, <<
		"|1 |2 |A\n"
		"|3 |4 |B\n"
		"|5 |6 |C">>, #{line => 1}}),
	ok.
-endif.

%% If the cols attribute is not specified, the number of
%% columns is the number of cells on the first line.
parse_table(Contents, #{<<"cols">> := Cols}) ->
	{parse_cells(Contents, []), num_cols(Cols)};
%% We get the first line, parse the cells in it then
%% count the number of columns in the table. Finally
%% we parse all the remaining cells.
parse_table(Contents, _) ->
	case binary:split(Contents, <<$\n>>) of
		%% We only have the one line. Who writes tables like this?
		[Line] ->
			Cells = parse_cells(Line, []),
			{Cells, length(Cells)};
		%% We have a useful table with more than one line. Good user!
		[Line, Rest] ->
			Cells0 = parse_cells(Line, []),
			Cells = parse_cells(Rest, lists:reverse(Cells0)),
			{Cells, length(Cells0)}
	end.

num_cols(Cols) ->
	%% @todo Handle column specifiers.
	Specs = binary:split(Cols, <<$,>>, [global]),
	length(Specs).

parse_cells(Contents, Acc) ->
	Cells = split_cells(Contents),%binary:split(Contents, [<<$|>>], [global]),
	do_parse_cells(Cells, Acc).
	%% Split on |
	%% Look at the end of each element see if there's a cell specifier
	%% Add it as an attribute to the cell for now and consolidate
	%% when processing rows.

split_cells(Contents) ->
	split_cells(Contents, <<>>, []).

split_cells(<<>>, Cell, Acc) ->
	lists:reverse([Cell|Acc]);
split_cells(<<$\\, $|, R/bits>>, Cell, Acc) ->
	split_cells(R, <<Cell/binary, $|>>, Acc);
split_cells(<<$|, R/bits>>, Cell, Acc) ->
	split_cells(R, <<>>, [Cell|Acc]);
split_cells(<<C, R/bits>>, Cell, Acc) ->
	split_cells(R, <<Cell/binary, C>>, Acc).

%% Malformed table (no pipe before cell). Process it like it is a single cell.
do_parse_cells([Contents], Acc) ->
	%% @todo Annotations.
	lists:reverse([{cell, #{specifiers => <<>>}, Contents, #{}}|Acc]);
%% Last cell. There are no further cell specifiers.
do_parse_cells([Specs, Contents0], Acc) ->
	Contents = asciideck_block_parser:trim(Contents0, both),
	%% @todo Annotations.
	Cell = {cell, #{specifiers => Specs}, Contents, #{}},
	lists:reverse([Cell|Acc]);
%% If there are cell specifiers we need to extract them from the cell
%% contents. Cell specifiers are everything from the last whitespace
%% until the end of the binary.
do_parse_cells([Specs, Contents0|Tail], Acc) ->
	NextSpecs = <<>>, %% @todo find_r(Contents0, <<>>),
	Len = byte_size(Contents0) - byte_size(NextSpecs),
	<<Contents1:Len/binary, _/bits>> = Contents0,
	Contents = asciideck_block_parser:trim(Contents1, both),
	%% @todo Annotations.
	Cell = {cell, #{specifiers => Specs}, Contents, #{}},
	do_parse_cells([NextSpecs|Tail], [Cell|Acc]).

%% @todo This is not correct. Not all remaining data is specifiers.
%% In addition, for columns at the end of the line this doesn't apply.
%% Find the remaining data after the last whitespace character.
%find_r(<<>>, Acc) ->
%	Acc;
%find_r(<<C, Rest/bits>>, _) when ?IS_WS(C) ->
%	find_r(Rest, Rest);
%find_r(<<_, Rest/bits>>, Acc) ->
%	find_r(Rest, Acc).

-ifdef(TEST).
parse_table_test() ->
	{[
		{cell, _, <<"1">>, _},
		{cell, _, <<"2">>, _},
		{cell, _, <<"A">>, _},
		{cell, _, <<"3">>, _},
		{cell, _, <<"4">>, _},
		{cell, _, <<"B">>, _},
		{cell, _, <<"5">>, _},
		{cell, _, <<"6">>, _},
		{cell, _, <<"C">>, _}
	], 3} = parse_table(<<
		"|1 |2 |A\n"
		"|3 |4 |B\n"
		"|5 |6 |C">>, #{}),
	ok.

parse_table_escape_pipe_test() ->
	{[
		{cell, _, <<"1">>, _},
		{cell, _, <<"2">>, _},
		{cell, _, <<"3 |4">>, _},
		{cell, _, <<"5">>, _}
	], 2} = parse_table(<<
		"|1 |2\n"
		"|3 \\|4 |5">>, #{}),
	ok.
-endif.

%% @todo We currently don't handle colspans and rowspans.
rows(Cells, NumCols) ->
	rows(Cells, [], NumCols, [], NumCols).

%% End of row.
rows(Tail, Acc, NumCols, RowAcc, CurCol) when CurCol =< 0 ->
	%% @todo Annotations.
	Row = {row, #{}, lists:reverse(RowAcc), #{}},
	rows(Tail, [Row|Acc], NumCols, [], NumCols);
%% Add a cell to the row.
rows([Cell|Tail], Acc, NumCols, RowAcc, CurCol) ->
	rows(Tail, Acc, NumCols, [Cell|RowAcc], CurCol - 1);
%% End of a properly formed table.
rows([], Acc, _, [], _) ->
	lists:reverse(Acc);
%% Malformed table. Even if we expect more columns,
%% if there are no more cells there's nothing we can do.
rows([], Acc, _, RowAcc, _) ->
	%% @todo Annotations.
	Row = {row, #{}, lists:reverse(RowAcc), #{}},
	lists:reverse([Row|Acc]).