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. --- ebin/asciideck.app | 2 +- scripts/asciidoc | 95 +++++++++++++++++++++ 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 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 426 insertions(+), 35 deletions(-) create mode 100755 scripts/asciidoc create mode 100644 src/asciideck_reader.erl create mode 100644 src/asciideck_stdin_reader.erl create mode 100644 src/asciideck_to_html.erl diff --git a/ebin/asciideck.app b/ebin/asciideck.app index 8f631cd..7c85660 100644 --- a/ebin/asciideck.app +++ b/ebin/asciideck.app @@ -1,7 +1,7 @@ {application, 'asciideck', [ {description, "Asciidoc for Erlang."}, {vsn, "0.2.0"}, - {modules, ['asciideck','asciideck_attributes_parser','asciideck_attributes_pass','asciideck_block_parser','asciideck_inline_pass','asciideck_line_reader','asciideck_lists_pass','asciideck_tables_pass','asciideck_to_manpage']}, + {modules, ['asciideck','asciideck_attributes_parser','asciideck_attributes_pass','asciideck_block_parser','asciideck_inline_pass','asciideck_line_reader','asciideck_lists_pass','asciideck_reader','asciideck_stdin_reader','asciideck_tables_pass','asciideck_to_html','asciideck_to_manpage']}, {registered, []}, {applications, [kernel,stdlib]}, {env, []} diff --git a/scripts/asciidoc b/scripts/asciidoc new file mode 100755 index 0000000..9e0d196 --- /dev/null +++ b/scripts/asciidoc @@ -0,0 +1,95 @@ +#!/usr/bin/env sh + +set -e +#set -x + +TEMP=$(getopt -o 'a:b:d:hso:nv' -l 'attribute:,backend:,doctype:,help,no-header-footer,out-file:,section-numbers,safe,theme:,verbose,version' -n asciidoc -- "$@") + +if [ $? -ne 0 ]; then + exit 1 +fi + +eval set -- "$TEMP" +unset TEMP + +NO_HEADER_FOOTER=0 +OUT_DIR= +OUT_FILE= +SAFE=0 +VERBOSE=0 + +while true; do + case "$1" in + '-a'|'--attribute') + echo 'The option -a|--attribute is currently ignored.' >&2 + shift 2 ;; + '-b'|'--backend') + echo 'The option -b|--backend is currently ignored.' >&2 + shift 2 ;; + '-d'|'--doctype') + echo 'The option -d|--doctype is currently ignored.' >&2 + shift 2 ;; + '-h'|'--help') + echo 'TODO' + exit 0 ;; + '-s'|'--no-header-footer') + NO_HEADER_FOOTER=1 + shift ;; + '-o'|'--out-file') + OUT_DIR=`dirname $2` + OUT_FILE=`basename ${2%.*}` + shift 2 ;; + '-n'|'--section-numbers') + echo 'The option -n|--section-numbers is currently ignored.' >&2 + shift ;; + '--safe') + SAFE=1 + shift ;; + '--theme') + echo 'The option --theme is currently ignored.' >&2 + shift ;; + '-v'|'--verbose') + VERBOSE=1 + shift ;; + '--version') + echo 'Asciideck compatibility script' + exit 0 ;; + '--') + shift + break ;; + *) + echo 'Unexpected error:' $1 >&2 + exit 1 ;; + esac +done + +IN_FILE= + +case "$1" in + '') + echo 'No file name was provided. Use - for standard input.' >&2 + exit 1 ;; + '-') + PARSE_CALL="asciideck:parse_stdin()" ;; + *) + IN_FILE=$1 + PARSE_CALL="asciideck:parse_file(\"$IN_FILE\")" ;; +esac + +if [ $IN_FILE -a -z $OUT_FILE ]; then + OUT_DIR=`dirname $IN_FILE` + OUT_FILE=`basename ${IN_FILE%.*}` +fi + +if [ $OUT_FILE ]; then + TRANSLATE_OPTS="#{outdir => \"$OUT_DIR\", outfile => \"$OUT_FILE\"}" +else + TRANSLATE_OPTS="#{}" +fi + +<&0 erl +A0 -boot no_dot_erlang -noshell -pz `dirname $0`/../ebin -eval " \ + case asciideck:to_html($PARSE_CALL, $TRANSLATE_OPTS) of \ + ok -> ok; \ + Output -> io:format(\"~s~n\", [Output]) \ + end, \ + halt()" 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