diff options
Diffstat (limited to 'src/asciideck_inline_pass.erl')
-rw-r--r-- | src/asciideck_inline_pass.erl | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/src/asciideck_inline_pass.erl b/src/asciideck_inline_pass.erl new file mode 100644 index 0000000..3ed79b1 --- /dev/null +++ b/src/asciideck_inline_pass.erl @@ -0,0 +1,308 @@ +%% Copyright (c) 2017-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. + +%% This pass walks over the tree and parses inline elements. +-module(asciideck_inline_pass). + +-export([run/1]). + +-import(asciideck_block_parser, [trim/1, while/2]). + +-type inline_ast() :: list(). %% @todo +-export_type([inline_ast/0]). + +run([]) -> + []; +run([Data|Tail]) when is_binary(Data) -> + [inline(Data)|run(Tail)]; +%% We do not do any inline formatting for verbatim blocks, +%% for example listing blocks. +%% +%% @todo subs is a list of values. +run([Item={_, #{<<"subs">> := <<"verbatim">>}, _, _}|Tail]) -> + [Item|run(Tail)]; +%% Labeled lists' labels can also have inline formatting. +run([{Type, Attrs=#{label := Label}, Items, Ann}|Tail]) when is_list(Items) -> + [{Type, Attrs#{label => inline(Label)}, run(Items), Ann}|run(Tail)]; +run([{Type, Attrs, Items, Ann}|Tail]) when is_list(Items) -> + [{Type, Attrs, run(Items), Ann}|run(Tail)]; +run([{Type, Attrs, Data, Ann}|Tail]) -> + [{Type, Attrs, inline(Data), Ann}|run(Tail)]. + +%% We reduce inline content with a single text element +%% with no formatting to a simple binary. +inline(<<>>) -> + <<>>; +inline(Data) -> + case inline(Data, <<>>, []) of + [] -> <<>>; + [Text] when is_binary(Text) -> Text; + AST -> AST + end. + +-spec inline(binary(), binary(), inline_ast()) -> inline_ast(). +inline(<<>>, <<>>, Acc) -> + lists:reverse(Acc); +inline(<<>>, BinAcc, Acc) -> + lists:reverse([BinAcc|Acc]); +inline(Data, BinAcc, Acc) -> + oneof(Data, BinAcc, Acc, [ + %% Links. + fun xref/2, + fun link/2, + fun http_link/2, + fun https_link/2, + %% Quoted text. + fun emphasized_single_quote/2, + fun emphasized_underline/2, + fun strong/2, + %% Passthrough macros. + fun inline_literal_passthrough/2 + ]). + +%% The inline pass replaces \r\n and \n with a simple space +%% when it occurs within normal text. +oneof(<<$\r, $\n, Rest/bits>>, BinAcc, Acc, []) -> + inline(Rest, <<BinAcc/binary, $\s>>, Acc); +oneof(<<$\n, Rest/bits>>, BinAcc, Acc, []) -> + inline(Rest, <<BinAcc/binary, $\s>>, Acc); +oneof(<<C, Rest/bits>>, BinAcc, Acc, []) -> + inline(Rest, <<BinAcc/binary, C>>, Acc); +oneof(Data, BinAcc, Acc, [Parse|Tail]) -> + Prev = case BinAcc of + <<>> -> undefined; + _ -> binary:last(BinAcc) + end, + try Parse(Data, Prev) of + {ok, Inline, Rest} when BinAcc =:= <<>> -> + inline(Rest, BinAcc, [Inline|Acc]); + {ok, Inline, Rest} -> + inline(Rest, <<>>, [Inline, BinAcc|Acc]); + {skip, Text, Rest} -> + oneof(Rest, <<BinAcc/binary, Text/binary>>, Acc, Tail) + catch _:_ -> + oneof(Data, BinAcc, Acc, Tail) + end. + +-ifdef(TEST). +text_test() -> + <<>> = inline(<<>>), + <<"Hello, Robert">> = inline(<<"Hello, Robert">>), + ok. +-endif. + +-define(IS_BOUNDARY(C), C =:= undefined; C =:= $\s; C =:= $\t; C =:= $\r; C =:= $\n; C =:= $(). + +%% Asciidoc User Guide 21.2.1 +%% +%% We currently do not implement the <<...>> form. +xref(<<"xref:", IDAndCaption/bits>>, Prev) when ?IS_BOUNDARY(Prev) -> + %% ID must not contain whitespace characters. + {ID, <<"[", Caption0/bits>>} = while(fun(C) -> + (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t) + end, IDAndCaption), + %% It is followed by a caption. + {Caption1, <<"]", Rest/bits>>} = while(fun(C) -> + C =/= $] + end, Caption0), + Caption = trim(Caption1), + {ok, {xref, #{ + id => ID + }, Caption, inline}, Rest}. + +-ifdef(TEST). +xref_test() -> + [{xref, #{ + id := <<"tiger_image">> + }, <<"face of a tiger">>, _}] = inline(<<"xref:tiger_image[face of a tiger]">>), + ok. +-endif. + +%% Asciidoc User Guide 21.1.3 +link(<<"link:", TargetAndCaption/bits>>, Prev) when ?IS_BOUNDARY(Prev) -> + %% Target must not contain whitespace characters. + {Target, <<"[", Caption0/bits>>} = while(fun(C) -> + (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t) + andalso (C =/= $\r) andalso (C =/= $\n) + end, TargetAndCaption), + %% It is followed by a caption. + {Caption1, <<"]", Rest/bits>>} = while(fun(C) -> + C =/= $] + end, Caption0), + Caption = trim(Caption1), + {ok, {link, #{ + target => Target + }, Caption, inline}, Rest}. + +-ifdef(TEST). +link_test() -> + [{link, #{ + target := <<"downloads/foo.zip">> + }, <<"download foo.zip">>, _}] = inline(<<"link:downloads/foo.zip[download foo.zip]">>), + [{link, #{ + target := <<"chapter1.asciidoc#fragment">> + }, <<"Chapter 1.">>, _}] = inline(<<"link:chapter1.asciidoc#fragment[Chapter 1.]">>), + [ + {link, #{target := <<"first.zip">>}, <<"first">>, _}, + <<", ">>, + {link, #{target := <<"second.zip">>}, <<"second">>, _} + ] = inline(<<"link:first.zip[first],\nlink:second.zip[second]">>), + ok. +-endif. + +%% Asciidoc User Guide 21.1.3 +http_link(<<"http:", Rest/bits>>, Prev) when ?IS_BOUNDARY(Prev) -> + direct_link(Rest, <<"http:">>). + +direct_link(Data, Prefix) -> + %% Target must not contain whitespace characters. + {Target0, Rest0} = while(fun(C) -> + (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t) + andalso (C =/= $\r) andalso (C =/= $\n) + end, Data), + Target = <<Prefix/binary, Target0/binary>>, + %% It is optionally followed by a caption. Otherwise + %% the link itself is the caption. + case Rest0 of + <<"[", Caption0/bits>> -> + {Caption1, <<"]", Rest/bits>>} = while(fun(C) -> + C =/= $] + end, Caption0), + Caption = trim(Caption1), + {ok, {link, #{ + target => Target + }, Caption, inline}, Rest}; + _ -> + {ok, {link, #{ + target => Target + }, Target, inline}, Rest0} + end. + +-ifdef(TEST). +http_link_test() -> + [ + <<"If you have ">>, + {link, #{ + target := <<"http://example.org/hello#fragment">> + }, <<"http://example.org/hello#fragment">>, _}, + <<" then:">> + ] = inline(<<"If you have http://example.org/hello#fragment then:">>), + [ + <<"If you have ">>, + {link, #{ + target := <<"http://example.org/hello#fragment">> + }, <<"http://example.org/hello#fragment">>, _}, + <<" then:">> + ] = inline(<<"If you have http://example.org/hello#fragment\nthen:">>), + [ + <<"Oh, ">>, + {link, #{ + target := <<"http://example.org/hello#fragment">> + }, <<"hello there">>, _}, + <<", young lad.">> + ] = inline(<<"Oh, http://example.org/hello#fragment[hello there], young lad.">>), + ok. +-endif. + +%% Asciidoc User Guide 21.1.3 +https_link(<<"https:", Rest/bits>>, Prev) when ?IS_BOUNDARY(Prev) -> + direct_link(Rest, <<"https:">>). + +-ifdef(TEST). +https_link_test() -> + [ + <<"If you have ">>, + {link, #{ + target := <<"https://example.org/hello#fragment">> + }, <<"https://example.org/hello#fragment">>, _}, + <<" then:">> + ] = inline(<<"If you have https://example.org/hello#fragment then:">>), + [ + <<"If you have ">>, + {link, #{ + target := <<"https://example.org/hello#fragment">> + }, <<"https://example.org/hello#fragment">>, _}, + <<" then:">> + ] = inline(<<"If you have https://example.org/hello#fragment\nthen:">>), + [ + <<"Oh, ">>, + {link, #{ + target := <<"https://example.org/hello#fragment">> + }, <<"hello there">>, _}, + <<", young lad.">> + ] = inline(<<"Oh, https://example.org/hello#fragment[hello there], young lad.">>), + ok. +-endif. + +%% Asciidoc User Guide 10.1 +%% @todo <<"\\**" +%% @todo <<"\\*" +%% @todo <<"**" +emphasized_single_quote(Data, Prev) -> + quoted_text(Data, Prev, emphasized, $', $'). +emphasized_underline(Data, Prev) -> + quoted_text(Data, Prev, emphasized, $_, $_). +strong(Data, Prev) -> + quoted_text(Data, Prev, strong, $*, $*). + +quoted_text(<<Left, Rest0/bits>>, Prev, Type, Left, Right) when ?IS_BOUNDARY(Prev) -> + {Content, <<Right, Rest/bits>>} = while(fun(C) -> C =/= Right end, Rest0), + {ok, {Type, #{ + left => Left, + right => Right + }, inline(Content), inline}, Rest}. + +-ifdef(TEST). +emphasized_test() -> + [ + <<"Word phrases ">>, + {emphasized, #{left := $', right := $'}, + <<"enclosed in single quote characters">>, _}, + <<" (acute accents) or ">>, + {emphasized, #{left := $_, right := $_}, + <<"underline characters">>, _}, + <<" are emphasized.">> + ] = inline(<< + "Word phrases 'enclosed in single quote characters' (acute accents) " + "or _underline characters_ are emphasized." + >>), + ok. + +strong_test() -> + [ + <<"Word phrases ">>, + {strong, #{left := $*, right := $*}, + <<"enclosed in asterisk characters">>, _}, + <<" are rendered in a strong font (usually bold).">> + ] = inline(<< + "Word phrases *enclosed in asterisk characters* " + "are rendered in a strong font (usually bold)." + >>), + ok. +-endif. + +%% Asciidoc User Guide 21.4 +inline_literal_passthrough(<<"`", Rest0/bits>>, Prev) when ?IS_BOUNDARY(Prev) -> + {Content, <<"`", Rest/bits>>} = while(fun(C) -> C =/= $` end, Rest0), + {ok, {inline_literal_passthrough, #{}, Content, inline}, Rest}. + +-ifdef(TEST). +inline_literal_passthrough_test() -> + [ + <<"Word phrases ">>, + {inline_literal_passthrough, #{}, <<"enclosed in backtick characters">>, _}, + <<" (grave accents)...">> + ] = inline(<<"Word phrases `enclosed in backtick characters` (grave accents)...">>), + ok. +-endif. |