%% Copyright (c) 2017-2018, Loïc Hoguin %% %% 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, <>, Acc); split_cells(<<$|, R/bits>>, Cell, Acc) -> split_cells(R, <<>>, [Cell|Acc]); split_cells(<>, Cell, Acc) -> split_cells(R, <>, 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), <> = 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(<>, _) 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]).