From 976dfc5d92e3e23f356cb19f17ff51b22c75e634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 11 Jun 2018 12:38:07 +0200 Subject: Add scripts/asciidoc ad-hoc replacement and HTML output This allows me to build ninenines.eu using Asciideck and while the results are not perfect yet things are looking pretty, pretty good. Adding source-highlight support, showing images and fixing a few minor issues should bring me to the point where I can drop Asciidoc. --- src/asciideck.erl | 23 ++++- src/asciideck_block_parser.erl | 28 +++--- src/asciideck_line_reader.erl | 19 +---- src/asciideck_reader.erl | 33 ++++++++ src/asciideck_stdin_reader.erl | 74 ++++++++++++++++ src/asciideck_to_html.erl | 187 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 330 insertions(+), 34 deletions(-) create mode 100644 src/asciideck_reader.erl create mode 100644 src/asciideck_stdin_reader.erl create mode 100644 src/asciideck_to_html.erl (limited to 'src') diff --git a/src/asciideck.erl b/src/asciideck.erl index bd5792c..d962c1b 100644 --- a/src/asciideck.erl +++ b/src/asciideck.erl @@ -14,14 +14,25 @@ -module(asciideck). +-export([parse_stdin/0]). +-export([parse_stdin/1]). -export([parse_file/1]). -export([parse_file/2]). -export([parse/1]). -export([parse/2]). +-export([to_html/1]). +-export([to_html/2]). -export([to_manpage/1]). -export([to_manpage/2]). +parse_stdin() -> + parse_stdin(#{}). + +parse_stdin(St) -> + {ok, ReaderPid} = asciideck_stdin_reader:start_link(), + parse(ReaderPid, St). + parse_file(Filename) -> parse_file(Filename, #{}). @@ -32,7 +43,7 @@ parse_file(Filename, St) -> parse(Data) -> parse(Data, #{}). -parse(Data, _St) when is_binary(Data) -> +parse(Data, _St) -> Passes = [ asciideck_attributes_pass, asciideck_lists_pass, @@ -40,9 +51,13 @@ parse(Data, _St) when is_binary(Data) -> asciideck_inline_pass ], lists:foldl(fun(M, AST) -> M:run(AST) end, - asciideck_block_parser:parse(Data), Passes); -parse(Data, St) -> - parse(iolist_to_binary(Data), St). + asciideck_block_parser:parse(Data), Passes). + +to_html(AST) -> + asciideck_to_html:translate(AST, #{}). + +to_html(AST, Opts) -> + asciideck_to_html:translate(AST, Opts). to_manpage(AST) -> asciideck_to_manpage:translate(AST, #{}). diff --git a/src/asciideck_block_parser.erl b/src/asciideck_block_parser.erl index ad63fa6..e0faceb 100644 --- a/src/asciideck_block_parser.erl +++ b/src/asciideck_block_parser.erl @@ -51,11 +51,15 @@ define_NOT_test() -> ok. -endif. --spec parse(binary()) -> ast(). -parse(Data) -> +-spec parse(binary() | pid()) -> ast(). +parse(Data) when is_binary(Data) -> %% @todo Might want to start it supervised. %% @todo Might want to stop it also. {ok, ReaderPid} = asciideck_line_reader:start_link(Data), + parse(ReaderPid); +parse(Data) when is_list(Data) -> + parse(iolist_to_binary(Data)); +parse(ReaderPid) when is_pid(ReaderPid) -> blocks(#state{reader=ReaderPid}). blocks(St) -> @@ -1049,37 +1053,37 @@ para_test() -> %% Control functions. -oneof([], St) -> - throw({error, St}); %% @todo +oneof([], St=#state{reader=ReaderPid}) -> + throw({error, St, sys:get_state(ReaderPid)}); oneof([Parse|Tail], St=#state{reader=ReaderPid}) -> - Ln = asciideck_line_reader:get_position(ReaderPid), + Ln = asciideck_reader:get_position(ReaderPid), try Parse(St) catch _:_ -> - asciideck_line_reader:set_position(ReaderPid, Ln), + asciideck_reader:set_position(ReaderPid, Ln), oneof(Tail, St) end. skip(Parse, St=#state{reader=ReaderPid}) -> - Ln = asciideck_line_reader:get_position(ReaderPid), + Ln = asciideck_reader:get_position(ReaderPid), try _ = Parse(St), skip(Parse, St) catch _:_ -> - asciideck_line_reader:set_position(ReaderPid, Ln), + asciideck_reader:set_position(ReaderPid, Ln), ok end. %% Line functions. read_line(#state{reader=ReaderPid}) -> - asciideck_line_reader:read_line(ReaderPid). + asciideck_reader:read_line(ReaderPid). read_while(St=#state{reader=ReaderPid}, F, Acc) -> - Ln = asciideck_line_reader:get_position(ReaderPid), + Ln = asciideck_reader:get_position(ReaderPid), case F(read_line(St)) of done -> - asciideck_line_reader:set_position(ReaderPid, Ln), + asciideck_reader:set_position(ReaderPid, Ln), Acc; {more, Line} -> case Acc of @@ -1089,7 +1093,7 @@ read_while(St=#state{reader=ReaderPid}, F, Acc) -> end. ann(#state{reader=ReaderPid}) -> - #{line => asciideck_line_reader:get_position(ReaderPid)}. + #{line => asciideck_reader:get_position(ReaderPid)}. trim(Line) -> trim(Line, both). diff --git a/src/asciideck_line_reader.erl b/src/asciideck_line_reader.erl index 240c70b..c469692 100644 --- a/src/asciideck_line_reader.erl +++ b/src/asciideck_line_reader.erl @@ -15,11 +15,8 @@ -module(asciideck_line_reader). -behaviour(gen_server). -%% API. +%% The API is defined in asciideck_reader. -export([start_link/1]). --export([read_line/1]). --export([get_position/1]). --export([set_position/2]). %% gen_server. -export([init/1]). @@ -41,20 +38,6 @@ start_link(Data) -> gen_server:start_link(?MODULE, [Data], []). --spec read_line(pid()) -> binary() | eof. -read_line(Pid) -> - gen_server:call(Pid, read_line). - -%% @todo peek_line - --spec get_position(pid()) -> pos_integer(). -get_position(Pid) -> - gen_server:call(Pid, get_position). - --spec set_position(pid(), pos_integer()) -> ok. -set_position(Pid, Pos) -> - gen_server:cast(Pid, {set_position, Pos}). - %% gen_server. init([Data]) -> diff --git a/src/asciideck_reader.erl b/src/asciideck_reader.erl new file mode 100644 index 0000000..d098417 --- /dev/null +++ b/src/asciideck_reader.erl @@ -0,0 +1,33 @@ +%% Copyright (c) 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. + +-module(asciideck_reader). + +-export([read_line/1]). +-export([get_position/1]). +-export([set_position/2]). + +-spec read_line(pid()) -> binary() | eof. +read_line(Pid) -> + gen_server:call(Pid, read_line). + +%% @todo peek_line + +-spec get_position(pid()) -> pos_integer(). +get_position(Pid) -> + gen_server:call(Pid, get_position). + +-spec set_position(pid(), pos_integer()) -> ok. +set_position(Pid, Pos) -> + gen_server:cast(Pid, {set_position, Pos}). diff --git a/src/asciideck_stdin_reader.erl b/src/asciideck_stdin_reader.erl new file mode 100644 index 0000000..9ea9dc8 --- /dev/null +++ b/src/asciideck_stdin_reader.erl @@ -0,0 +1,74 @@ +%% Copyright (c) 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. + +-module(asciideck_stdin_reader). +-behaviour(gen_server). + +%% The API is defined in asciideck_reader. +-export([start_link/0]). + +%% gen_server. +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). + +-record(state, { + lines = [] :: [binary()], + pos = 1 :: non_neg_integer() +}). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + gen_server:start_link(?MODULE, [], []). + +%% gen_server. + +init([]) -> + {ok, #state{}}. + +handle_call(read_line, _From, State=#state{lines=Lines, pos=Pos}) + when length(Lines) >= Pos -> + {reply, lists:nth(Pos, lists:reverse(Lines)), State#state{pos=Pos + 1}}; +handle_call(read_line, _From, State=#state{lines=Lines, pos=Pos}) -> + case io:get_line('') of + eof -> + {reply, eof, State}; + Line0 -> + Line1 = string:strip(Line0, right, $\n), + Line = unicode:characters_to_binary(Line1), + {reply, Line, State#state{lines=[Line|Lines], pos=Pos + 1}} + end; +handle_call(get_position, _From, State=#state{pos=Pos}) -> + {reply, Pos, State}; +handle_call(_Request, _From, State) -> + {reply, ignored, State}. + +handle_cast({set_position, Pos}, State) -> + {noreply, State#state{pos=Pos}}; +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/asciideck_to_html.erl b/src/asciideck_to_html.erl new file mode 100644 index 0000000..c6fbbff --- /dev/null +++ b/src/asciideck_to_html.erl @@ -0,0 +1,187 @@ +%% Copyright (c) 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. + +%% @todo https://www.gnu.org/software/src-highlite/source-highlight.html + +-module(asciideck_to_html). + +-export([translate/2]). + +translate(AST, Opts) -> + Output0 = ast(AST), + Output1 = header_footer(Output0, Opts), + {CompressExt, Output} = case Opts of + #{compress := gzip} -> {".gz", zlib:gzip(Output1)}; + _ -> {"", Output1} + end, + case Opts of + #{outdir := Path, outfile := Filename} -> + file:write_file(binary_to_list(iolist_to_binary( + [Path, "/", Filename, ".html", CompressExt])), Output); + #{outdir := Path} -> + Filename = filename_from_ast(AST), + file:write_file(binary_to_list(iolist_to_binary( + [Path, "/", Filename, ".html", CompressExt])), Output); + _ -> + Output + end. + +header_footer(Body, _Opts) -> + [ + "\n" + "\n" + "\n" + "\n" + "TODO title\n" %% @todo + "\n" + "\n" + "\n", + Body, + "\n" + "\n" + ]. + +filename_from_ast([{section_title, #{level := 0}, Filename, _}|_]) -> + Filename. + +%% 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 := Level}, Title, _}) -> + LevelC = $1 + Level, + ["", inline(Title), "\n"]. + +%% Paragraphs. + +paragraph({paragraph, _, Text, _}) -> + ["

", inline(Text), "

\n"]. + +%% Listing blocks. + +listing_block({listing_block, Attrs, Listing, _}) -> + [ + "
", + case Attrs of + #{<<"title">> := Title} -> + ["
", inline(Title), "
\n"]; + _ -> + [] + end, + "
",
+		html_encode(Listing),
+		"
\n" + ]. + +%% Lists. + +list({list, #{type := bulleted}, Items, _}) -> + ["
    ", fold(Items, fun bulleted_list_item/1), "
\n"]; +list({list, #{type := labeled}, Items, _}) -> + ["
", fold(Items, fun labeled_list_item/1), "
\n"]. + +bulleted_list_item({list_item, _, [{paragraph, _, Text, _}|AST], _}) -> + [ + "
  • ", + inline(Text), "\n", + ast(AST), + "
  • \n" + ]. + +labeled_list_item({list_item, #{label := Label}, [{paragraph, _, Text, _}|AST], _}) -> + [ + "
    ", inline(Label), "
    \n", + "
    ", + inline(Text), "\n", + ast(AST), + "
    \n" + ]. + +%% Tables. + +table({table, _, [{row, _, Head, _}|Rows], _}) -> + [ + "\n" + "", table_head(Head), "" + "", table_body(Rows), "" + "
    \n" + ]. + +table_head(Cells) -> + [["", inline(Text), "\n"] + || {cell, _, Text, _} <- Cells]. + +table_body(Rows) -> + [["", table_body_cells(Cells), "\n"] + || {row, _, Cells, _} <- Rows]. + +table_body_cells(Cells) -> + [["", inline(Text), "\n"] + || {cell, _, Text, _} <- Cells]. + +%% Comment lines are printed in the generated file +%% but are not visible in viewers. + +comment_line({comment_line, _, Text, _}) -> + ["\n"]. + +%% Inline formatting. + +inline(Text) when is_binary(Text) -> + html_encode(Text); +inline({Link, #{target := Target}, Text, _}) + when Link =:= link; Link =:= xref -> + ["", html_encode(Text), ""]; +inline({emphasized, _, Text, _}) -> + ["", inline(Text), ""]; +inline({strong, _, Text, _}) -> + ["", inline(Text), ""]; +inline({inline_literal_passthrough, _, Text, _}) -> + ["", inline(Text), ""]; +inline(Text) when is_list(Text) -> + [inline(T) || T <- Text]. + +html_encode(Text) -> + < <<"&">>; + $< -> <<"<">>; + $> -> <<">">>; + _ -> <> + end || <> <= Text>>. -- cgit v1.2.3