From 524777054be30c848c1883ffd15b245c29f73004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 8 Jun 2018 14:49:09 +0200 Subject: Rewrite the project The new code is much more readable and easier to extend. I took inspiration from Haskell's Parsec project which seems to only write the happy-path and applied the idea to Erlang's exceptions. When the parser tries to parse, say, a list, and crashes, it tries with a table next, and so on until something matches. Normal paragraphs always match so there can be no parsing failures. The parser now has a number of passes: first the block parser, then lists and tables passes to build a proper tree out of them and finally an inline pass to apply inline formatting. The resulting AST can then be modified at will and passed on to translator modules which output a different format. The man page translator was also rewritten and has been tested against both Cowboy and Gun. Numerous issues were fixed as a result of this rewrite. --- src/asciideck_to_manpage.erl | 236 +++++++++++++++++++++++++------------------ 1 file changed, 140 insertions(+), 96 deletions(-) (limited to 'src/asciideck_to_manpage.erl') diff --git a/src/asciideck_to_manpage.erl b/src/asciideck_to_manpage.erl index bdff90e..37e4e73 100644 --- a/src/asciideck_to_manpage.erl +++ b/src/asciideck_to_manpage.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2016, Loïc Hoguin +%% Copyright (c) 2016-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 @@ -19,7 +19,7 @@ -export([translate/2]). translate(AST, Opts) -> - {Man, Section, Output0} = translate_man(AST, Opts), + {Man, Section, Output0} = man(AST, Opts), {CompressExt, Output} = case Opts of #{compress := gzip} -> {".gz", zlib:gzip(Output0)}; _ -> {"", Output0} @@ -32,7 +32,9 @@ translate(AST, Opts) -> Output end. -translate_man([{title, #{level := 0}, Title0, _Ann}|AST], Opts) -> +%% Header of the man page file. + +man([{section_title, #{level := 0}, Title0, _Ann}|AST], Opts) -> ensure_name_section(AST), [Title, << Section:1/binary, _/bits >>] = binary:split(Title0, <<"(">>), Extra1 = maps:get(extra1, Opts, today()), @@ -42,10 +44,10 @@ translate_man([{title, #{level := 0}, Title0, _Ann}|AST], Opts) -> ".TH \"", Title, "\" \"", Section, "\" \"", Extra1, "\" \"", Extra2, "\" \"", Extra3, "\"\n" ".ta T 4n\n\\&\n", - man(AST, []) + ast(AST) ]}. -ensure_name_section([{title, #{level := 1}, Title, _}|_]) -> +ensure_name_section([{section_title, #{level := 1}, Title, _}|_]) -> case string:to_lower(string:strip(binary_to_list(Title))) of "name" -> ok; _ -> error(badarg) @@ -57,22 +59,56 @@ today() -> {{Y, M, D}, _} = calendar:universal_time(), io_lib:format("~b-~2.10.0b-~2.10.0b", [Y, M, D]). -man([], Acc) -> - lists:reverse(Acc); -man([{title, #{level := 1}, Title, _Ann}|Tail], Acc) -> - man(Tail, [[".SH ", string:to_upper(binary_to_list(Title)), "\n"]|Acc]); -man([{title, #{level := 2}, Title, _Ann}|Tail], Acc) -> - man(Tail, [[".SS ", Title, "\n"]|Acc]); -man([{p, _Attrs, Text, _Ann}|Tail], Acc) -> - man(Tail, [[".LP\n", man_format(Text), "\n.sp\n"]|Acc]); -man([{listing, Attrs, Listing, _Ann}|Tail], Acc0) -> - Acc1 = case Attrs of - #{title := Title} -> - [[".PP\n\\fB", Title, "\\fR\n"]|Acc0]; - _ -> - Acc0 - end, - Acc = [[ +%% Loop over all types of AST nodes. + +ast(AST) -> + fold(AST, fun ast_node/1). + +fold(AST, Fun) -> + lists:reverse(lists:foldl( + fun(Node, Acc) -> [Fun(Node)|Acc] end, + [], AST)). + +ast_node(Node={Type, _, _, _}) -> + try + case Type of + section_title -> section_title(Node); + paragraph -> paragraph(Node); + listing_block -> listing_block(Node); + list -> list(Node); + table -> table(Node); + comment_line -> comment_line(Node); + _ -> + io:format("Ignored AST node ~p~n", [Node]), + [] + end + catch _:_ -> + io:format("Ignored AST node ~p~n", [Node]), + [] + end. + +%% Section titles. + +section_title({section_title, #{level := 1}, Title, _}) -> + [".SH ", string:to_upper(binary_to_list(Title)), "\n"]; +section_title({section_title, #{level := 2}, Title, _}) -> + [".SS ", Title, "\n"]. + +%% Paragraphs. + +paragraph({paragraph, _, Text, _}) -> + [".LP\n", inline(Text), "\n.sp\n"]. + +%% Listing blocks. + +listing_block({listing_block, Attrs, Listing, _}) -> + [ + case Attrs of + #{<<"title">> := Title} -> + [".PP\n\\fB", Title, "\\fR\n"]; + _ -> + [] + end, ".if n \\{\\\n" ".RS 4\n" ".\\}\n" @@ -82,55 +118,18 @@ man([{listing, Attrs, Listing, _Ann}|Tail], Acc0) -> ".fi\n" ".if n \\{\\\n" ".RE\n" - ".\\}\n"]|Acc1], - man(Tail, Acc); -man([{ul, _Attrs, Items, _Ann}|Tail], Acc0) -> - Acc = man_ul(Items, Acc0), - man(Tail, Acc); -man([{ll, _Attrs, Items, _Ann}|Tail], Acc0) -> - Acc = man_ll(Items, Acc0), - man(Tail, Acc); -%% @todo Attributes. -%% Currently acts as if options="headers" was always set. -man([{table, _TAttrs, [{row, RowAttrs, Headers0, RowAnn}|Rows0], _TAnn}|Tail], Acc0) -> - Headers = [{cell, CAttrs, [{p, Attrs, [{strong, #{}, P, CAnn}], Ann}], CAnn} - || {cell, CAttrs, [{p, Attrs, P, Ann}], CAnn} <- Headers0], - Rows = [{row, RowAttrs, Headers, RowAnn}|Rows0], - Acc = [[ - ".TS\n" - "allbox tab(:);\n", - man_table_style(Rows, []), - man_table_contents(Rows), - ".TE\n" - ".sp 1\n"]|Acc0], - man(Tail, Acc); -%% Skip everything we don't understand. -man([_Ignore|Tail], Acc) -> - io:format("Ignore ~p~n", [_Ignore]), %% @todo lol io:format - man(Tail, Acc). - -man_ll([], Acc) -> - Acc; -man_ll([{li, #{label := Label}, Item, _LiAnn}|Tail], Acc0) -> - Acc = [[ - ".PP\n" - "\\fB", Label, "\\fR\n", - ".RS 4\n", - man_ll_item(Item), - ".RE\n"]|Acc0], - man_ll(Tail, Acc). - -man_ll_item([{ul, _Attrs, Items, _Ann}]) -> - [man_ul(Items, []), "\n"]; -man_ll_item([{p, _PAttrs, Text, _PAnn}]) -> - [man_format(Text), "\n"]; -man_ll_item([{p, _PAttrs, Text, _PAnn}|Tail]) -> - [man_format(Text), "\n\n", man_ll_item(Tail)]. - -man_ul([], Acc) -> - Acc; -man_ul([{li, _LiAttrs, [{p, _PAttrs, Text, _PAnn}], _LiAnn}|Tail], Acc0) -> - Acc = [[ + ".\\}\n" + ]. + +%% Lists. + +list({list, #{type := bulleted}, Items, _}) -> + fold(Items, fun bulleted_list_item/1); +list({list, #{type := labeled}, Items, _}) -> + fold(Items, fun labeled_list_item/1). + +bulleted_list_item({list_item, _, [{paragraph, _, Text, _}|AST], _}) -> + [ ".ie n \\{\\\n" ".RS 2\n" "\\h'-02'\\(bu\\h'+01'\\c\n" @@ -140,40 +139,85 @@ man_ul([{li, _LiAttrs, [{p, _PAttrs, Text, _PAnn}], _LiAnn}|Tail], Acc0) -> ".sp -1\n" ".IP \\(bu 2.3\n" ".\\}\n", - man_format(Text), "\n" - ".RE\n"]|Acc0], - man_ul(Tail, Acc). + inline(Text), "\n", + ast(AST), + ".RE\n" + ]. + +labeled_list_item({list_item, #{label := Label}, [{paragraph, _, Text, _}|AST], _}) -> + [ + ".PP\n" + "\\fB", inline(Label), "\\fR\n", + ".RS 4\n", + inline(Text), "\n", + ast(AST), + ".RE\n" + ]. + +%% Tables. + +table({table, _, Rows0, _}) -> + Rows = table_apply_options(Rows0), + [ + ".TS\n" + "allbox tab(:);\n", + table_style(Rows), ".\n", + table_contents(Rows), + ".TE\n" + ".sp 1\n" + ]. + +%% @todo Currently acts as if options="headers" was always set. +table_apply_options([{row, RAttrs, Headers0, RAnn}|Tail]) -> + Headers = [{cell, CAttrs, [{strong, #{}, CText, CAnn}], CAnn} + || {cell, CAttrs, CText, CAnn} <- Headers0], + [{row, RAttrs, Headers, RAnn}|Tail]. + +table_style(Rows) -> + [[table_style_cells(Cells), "\n"] + || {row, _, Cells, _} <- Rows]. + +table_style_cells(Cells) -> + ["lt " || {cell, _, _, _} <- Cells]. + +table_contents(Rows) -> + [[table_contents_cells(Cells), "\n"] + || {row, _, Cells, _} <- Rows]. + +table_contents_cells([FirstCell|Cells]) -> + [table_contents_cell(FirstCell), + [[":", table_contents_cell(Cell)] || Cell <- Cells]]. -man_table_style([], [_|Acc]) -> - lists:reverse([".\n"|Acc]); -man_table_style([{row, _, Cols, _}|Tail], Acc) -> - man_table_style(Tail, [$\n, man_table_style_cols(Cols, [])|Acc]). +table_contents_cell({cell, _, Text, _}) -> + ["T{\n", inline(Text), "\nT}"]. -man_table_style_cols([], [_|Acc]) -> - lists:reverse(Acc); -man_table_style_cols([{cell, _, _, _}|Tail], Acc) -> - man_table_style_cols(Tail, [$\s, "lt"|Acc]). +%% Comment lines are printed in the generated file +%% but are not visible in viewers. -man_table_contents(Rows) -> - [man_table_contents_cols(Cols, []) || {row, _, Cols, _} <- Rows]. +comment_line({comment_line, _, Text, _}) -> + ["\\# ", Text, "\n"]. -man_table_contents_cols([], [_|Acc]) -> - lists:reverse(["\n"|Acc]); -man_table_contents_cols([{cell, _CAttrs, [{p, _PAttrs, Text, _PAnn}], _CAnn}|Tail], Acc) -> - man_table_contents_cols(Tail, [$:, "\nT}", man_format(Text), "T{\n"|Acc]). +%% Inline formatting. -man_format(Text) when is_binary(Text) -> +inline(Text) when is_binary(Text) -> Text; -man_format({rel_link, #{target := Link}, Text, _}) -> +%% When the link is the text we only print it once. +inline({link, #{target := Link}, Link, _}) -> + Link; +inline({link, #{target := Link}, Text, _}) -> case re:run(Text, "^([-_:.a-zA-Z0-9]*)(\\([0-9]\\))$", [{capture, all, binary}]) of nomatch -> [Text, " (", Link, ")"]; {match, [_, ManPage, ManSection]} -> ["\\fB", ManPage, "\\fR", ManSection] end; -man_format({strong, _, Text, _}) -> - ["\\fB", man_format(Text), "\\fR"]; +inline({emphasized, _, Text, _}) -> + ["\\fI", inline(Text), "\\fR"]; +inline({strong, _, Text, _}) -> + ["\\fB", inline(Text), "\\fR"]; %% We are already using a monospace font. -%% @todo Maybe there's a readable formatting we could use to differentiate from normal text? -man_format({mono, _, Text, _}) -> - man_format(Text); -man_format(Text) when is_list(Text) -> - [man_format(T) || T <- Text]. +inline({inline_literal_passthrough, _, Text, _}) -> + inline(Text); +%% Xref links appear as plain text in manuals. +inline({xref, _, Text, _}) -> + inline(Text); +inline(Text) when is_list(Text) -> + [inline(T) || T <- Text]. -- cgit v1.2.3