defmodule Textile.Parser do
import Textile.ParserHelpers
alias Textile.{
Lexer,
Parser,
TokenCoalescer
}
defstruct [
image_transform: nil
]
def parse(%Parser{} = parser, input) do
with {:ok, tokens, _1, _2, _3, _4} <- Lexer.lex(input),
tokens <- TokenCoalescer.coalesce_lex(tokens),
{:ok, tree, []} <- textile_top(parser, tokens),
tree <- TokenCoalescer.coalesce_parse(tree)
do
tree
else
err ->
err
end
end
#
# Backtracking LL parser for simplified Textile grammar
#
#
# textile = (well_formed_including_paragraphs | TOKEN)*;
#
defp textile_top(_parser, []), do: {:ok, [], []}
defp textile_top(parser, tokens) do
with {:ok, tree, r_tokens} <- well_formed_including_paragraphs(parser, tokens),
false <- tree == [],
{:ok, next_tree, r2_tokens} <- textile_top(parser, r_tokens)
do
{:ok, [tree, next_tree], r2_tokens}
else
_ ->
[{_token, string} | r_tokens] = tokens
{:ok, next_tree, r2_tokens} = textile_top(parser, r_tokens)
{:ok, [text: escape_nl2br(string)] ++ next_tree, r2_tokens}
end
end
#
# well_formed_including_paragraphs = (markup | double_newline)*;
#
defp well_formed_including_paragraphs(_parser, []), do: {:ok, [], []}
defp well_formed_including_paragraphs(parser, [{:double_newline, _nl} | r_tokens]) do
with {:ok, tree, r2_tokens} <- well_formed_including_paragraphs(parser, r_tokens) do
{:ok, [{:markup, "
"}, tree], r2_tokens}
else
_ ->
{:ok, [], r_tokens}
end
end
defp well_formed_including_paragraphs(parser, tokens) do
with {:ok, tree, r_tokens} <- markup(parser, tokens),
{:ok, next_tree, r2_tokens} <- well_formed_including_paragraphs(parser, r_tokens)
do
{:ok, [tree, next_tree], r2_tokens}
else
_ ->
{:ok, [], tokens}
end
end
#
# well_formed = (markup)*;
#
defp well_formed(parser, tokens) do
case markup(parser, tokens) do
{:ok, tree, r_tokens} ->
{:ok, next_tree, r2_tokens} = well_formed(parser, r_tokens)
{:ok, [tree, next_tree], r2_tokens}
_ ->
{:ok, [], tokens}
end
end
#
# markup =
# blockquote | spoiler | link | image | bold | italic | strong | emphasis |
# code | inserted | superscript | deleted | subscript | newline | literal |
# bracketed_literal | text;
#
defp markup(parser, tokens) do
markups = [
&blockquote/2, &spoiler/2, &link/2, &image/2, &bold/2, &italic/2, &strong/2,
&emphasis/2, &code/2, &inserted/2, &superscript/2, &deleted/2, &subscript/2,
&newline/2, &literal/2, &bracketed_literal/2, &text/2
]
value =
markups
|> Enum.find_value(fn func ->
case func.(parser, tokens) do
{:ok, tree, r_tokens} ->
{:ok, tree, r_tokens}
_ ->
nil
end
end)
value || {:error, "Expected markup"}
end
#
# blockquote =
# blockquote_open_cite well_formed_including_paragraphs blockquote_close |
# blockquote_open well_formed_including_paragraphs blockquote_close;
#
defp blockquote(parser, [{:blockquote_open_cite, author} | r_tokens]) do
with {:ok, tree, [{:blockquote_close, _close} | r2_tokens]} <- well_formed_including_paragraphs(parser, r_tokens) do
{:ok, [{:markup, ~s|
|}, tree, {:markup, ~s||}], r2_tokens} else _ -> {:ok, [text: escape_nl2br(~s|[bq="#{author}"]|)], r_tokens} end end defp blockquote(parser, [{:blockquote_open, open} | r_tokens]) do with {:ok, tree, [{:blockquote_close, _close} | r2_tokens]} <- well_formed_including_paragraphs(parser, r_tokens) do {:ok, [{:markup, ~s|
|}, tree, {:markup, ~s||}], r2_tokens} else _ -> {:ok, [text: escape_nl2br(open)], r_tokens} end end defp blockquote(_parser, _tokens), do: {:error, "Expected a blockquote tag with optional citation"} # # spoiler = # spoiler_open well_formed_including_paragraphs spoiler_close; # defp spoiler(parser, [{:spoiler_open, open} | r_tokens]) do with {:ok, tree, [{:spoiler_close, _close} | r2_tokens]} <- well_formed_including_paragraphs(parser, r_tokens) do {:ok, [{:markup, ~s||}, tree, {:markup, ~s||}], r2_tokens} else _ -> {:ok, [text: escape_nl2br(open)], r_tokens} end end defp spoiler(_parser, _tokens), do: {:error, "Expected a spoiler tag"} # # link = # link_start well_formed_including_paragraphs link_end link_url; # defp link(parser, [{:link_start, start} | r_tokens]) do with {:ok, tree, [{:link_end, _end}, {:link_url, url} | r2_tokens]} <- well_formed_including_paragraphs(parser, r_tokens) do {:ok, [{:markup, ~s||}, tree, {:markup, ~s||}], r2_tokens} else _ -> {:ok, [text: escape_nl2br(start)], r_tokens} end end defp link(_parser, _tokens), do: {:error, "Expected a link"} # # image = # image_url image_title? image_link_url?; # defp image(parser, [{:image_url, image_url}, {:image_title, title}, {:image_link_url, link_url} | r_tokens]) do image_url = parser.image_transform.(image_url) {:ok, [markup: ~s||], r_tokens} end defp image(parser, [{:image_url, image_url}, {:image_title, title} | r_tokens]) do image_url = parser.image_transform.(image_url) {:ok, [markup: ~s||], r_tokens} end defp image(parser, [{:image_url, image_url}, {:image_link_url, link_url} | r_tokens]) do image_url = parser.image_transform.(image_url) {:ok, [markup: ~s||], r_tokens} end defp image(parser, [{:image_url, image_url} | r_tokens]) do image_url = parser.image_transform.(image_url) {:ok, [markup: ~s||], r_tokens} end defp image(_parser, _tokens), do: {:error, "Expected an image tag"} # # bold = # b_open well_formed b_close | # b_b_open well_formed b_b_close; # attribute_parser(:bold, :b_open, :b_close, "", "") # # italic = # i_open well_formed i_close | # b_i_open well_formed b_i_close; # attribute_parser(:italic, :i_open, :i_close, "", "") # # strong = # strong_open well_formed strong_close | # b_strong_open well_formed b_strong_close; # attribute_parser(:strong, :strong_open, :strong_close, "", "") # # emphasis = # em_open well_formed em_close | # b_em_open well_formed b_em_close; # attribute_parser(:emphasis, :em_open, :em_close, "", "") # # code = # code_open well_formed code_close | # b_code_open well_formed b_code_close; # attribute_parser(:code, :code_open, :code_close, "
", "
")
#
# inserted =
# ins_open well_formed ins_close |
# b_ins_open well_formed b_ins_close;
#
attribute_parser(:inserted, :ins_open, :ins_close, "", "")
#
# superscript =
# sup_open well_formed sup_close |
# b_sup_open well_formed b_sup_close;
#
attribute_parser(:superscript, :sup_open, :sup_close, "", "")
#
# deleted =
# del_open well_formed del_close |
# b_del_open well_formed b_del_close;
#
attribute_parser(:deleted, :del_open, :del_close, "