aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-06-11 12:38:07 +0200
committerLoïc Hoguin <[email protected]>2018-06-11 12:38:07 +0200
commit976dfc5d92e3e23f356cb19f17ff51b22c75e634 (patch)
tree2589053dfc25bd6f38afdd9a06bdbbf63680464c
parent524777054be30c848c1883ffd15b245c29f73004 (diff)
downloadasciideck-976dfc5d92e3e23f356cb19f17ff51b22c75e634.tar.gz
asciideck-976dfc5d92e3e23f356cb19f17ff51b22c75e634.tar.bz2
asciideck-976dfc5d92e3e23f356cb19f17ff51b22c75e634.zip
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.
-rw-r--r--ebin/asciideck.app2
-rwxr-xr-xscripts/asciidoc95
-rw-r--r--src/asciideck.erl23
-rw-r--r--src/asciideck_block_parser.erl28
-rw-r--r--src/asciideck_line_reader.erl19
-rw-r--r--src/asciideck_reader.erl33
-rw-r--r--src/asciideck_stdin_reader.erl74
-rw-r--r--src/asciideck_to_html.erl187
8 files changed, 426 insertions, 35 deletions
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 <[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.
+
+-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 <[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.
+
+-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 <[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.
+
+%% @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) ->
+ [
+ "<!DOCTYPE html>\n"
+ "<html lang=\"en\">\n"
+ "<head>\n"
+ "<meta charset=\"utf-8\"/>\n"
+ "<title>TODO title</title>\n" %% @todo
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"https://ninenines.eu/css/99s.css?r=1\"/>\n"
+ "</head>\n"
+ "<body>\n",
+ Body,
+ "</body>\n"
+ "</html>\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,
+ ["<h", LevelC, ">", inline(Title), "</h", LevelC, ">\n"].
+
+%% Paragraphs.
+
+paragraph({paragraph, _, Text, _}) ->
+ ["<p>", inline(Text), "</p>\n"].
+
+%% Listing blocks.
+
+listing_block({listing_block, Attrs, Listing, _}) ->
+ [
+ "<div class=\"listingblock\">",
+ case Attrs of
+ #{<<"title">> := Title} ->
+ ["<div class=\"title\">", inline(Title), "</div>\n"];
+ _ ->
+ []
+ end,
+ "<div class=\"content\"><pre>",
+ html_encode(Listing),
+ "</pre></div></div>\n"
+ ].
+
+%% Lists.
+
+list({list, #{type := bulleted}, Items, _}) ->
+ ["<ul>", fold(Items, fun bulleted_list_item/1), "</ul>\n"];
+list({list, #{type := labeled}, Items, _}) ->
+ ["<dl>", fold(Items, fun labeled_list_item/1), "</dl>\n"].
+
+bulleted_list_item({list_item, _, [{paragraph, _, Text, _}|AST], _}) ->
+ [
+ "<li>",
+ inline(Text), "\n",
+ ast(AST),
+ "</li>\n"
+ ].
+
+labeled_list_item({list_item, #{label := Label}, [{paragraph, _, Text, _}|AST], _}) ->
+ [
+ "<dt>", inline(Label), "</dt>\n",
+ "<dd>",
+ inline(Text), "\n",
+ ast(AST),
+ "</dd>\n"
+ ].
+
+%% Tables.
+
+table({table, _, [{row, _, Head, _}|Rows], _}) ->
+ [
+ "<table rules=\"all\" width=\"100%\" frame=\"border\" cellspacing=\"0\" cellpadding=\"4\">\n"
+ "<thead><tr>", table_head(Head), "</tr></thead>"
+ "<tbody>", table_body(Rows), "</tbody>"
+ "</table>\n"
+ ].
+
+table_head(Cells) ->
+ [["<th>", inline(Text), "</th>\n"]
+ || {cell, _, Text, _} <- Cells].
+
+table_body(Rows) ->
+ [["<tr>", table_body_cells(Cells), "</tr>\n"]
+ || {row, _, Cells, _} <- Rows].
+
+table_body_cells(Cells) ->
+ [["<td>", inline(Text), "</td>\n"]
+ || {cell, _, Text, _} <- Cells].
+
+%% Comment lines are printed in the generated file
+%% but are not visible in viewers.
+
+comment_line({comment_line, _, Text, _}) ->
+ ["<!-- ", html_encode(Text), "-->\n"].
+
+%% Inline formatting.
+
+inline(Text) when is_binary(Text) ->
+ html_encode(Text);
+inline({Link, #{target := Target}, Text, _})
+ when Link =:= link; Link =:= xref ->
+ ["<a href=\"", html_encode(Target), "\">", html_encode(Text), "</a>"];
+inline({emphasized, _, Text, _}) ->
+ ["<em>", inline(Text), "</em>"];
+inline({strong, _, Text, _}) ->
+ ["<strong>", inline(Text), "</strong>"];
+inline({inline_literal_passthrough, _, Text, _}) ->
+ ["<code>", inline(Text), "</code>"];
+inline(Text) when is_list(Text) ->
+ [inline(T) || T <- Text].
+
+html_encode(Text) ->
+ <<case C of
+ $& -> <<"&amp;">>;
+ $< -> <<"&lt;">>;
+ $> -> <<"&gt;">>;
+ _ -> <<C>>
+ end || <<C>> <= Text>>.