mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-18 06:57:59 +01:00
313 lines
9.1 KiB
Elixir
313 lines
9.1 KiB
Elixir
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 |> remove_linefeeds()),
|
|
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 packrat 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, nil, 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, _closing_token, []), do: {:ok, [], []}
|
|
defp well_formed_including_paragraphs(parser, closing_token, [{:double_newline, _nl} | r_tokens]) do
|
|
{:ok, tree, r2_tokens} = well_formed_including_paragraphs(parser, closing_token, r_tokens)
|
|
|
|
{:ok, [{:markup, "<br/><br/>"}, tree], r2_tokens}
|
|
end
|
|
|
|
defp well_formed_including_paragraphs(parser, closing_token, tokens) do
|
|
with {:markup, {:ok, tree, r_tokens}} <- {:markup, markup(parser, tokens)},
|
|
{:ok, next_tree, r2_tokens} <- well_formed_including_paragraphs(parser, closing_token, r_tokens)
|
|
do
|
|
{:ok, [tree, next_tree], r2_tokens}
|
|
else
|
|
_ ->
|
|
consume_nonclosing(parser, closing_token, tokens)
|
|
end
|
|
end
|
|
|
|
defp consume_nonclosing(_parser, closing_token, [{closing_token, _string} | _r_tokens] = tokens) do
|
|
{:ok, [], tokens}
|
|
end
|
|
defp consume_nonclosing(parser, closing_token, [{_next_token, string} | r_tokens]) do
|
|
{:ok, next_tree, r2_tokens} = well_formed_including_paragraphs(parser, closing_token, r_tokens)
|
|
|
|
{:ok, [{:text, escape_nl2br(string)}, next_tree], r2_tokens}
|
|
end
|
|
defp consume_nonclosing(_parser, _closing_token, []) do
|
|
{:ok, [], []}
|
|
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 |
|
|
# 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, &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
|
|
case well_formed_including_paragraphs(parser, :blockquote_close, r_tokens) do
|
|
{:ok, tree, [{:blockquote_close, _close} | r2_tokens]} ->
|
|
{:ok, [{:markup, ~s|<blockquote author="#{escape_html(author)}">|}, tree, {:markup, ~s|</blockquote>|}], r2_tokens}
|
|
|
|
{:ok, tree, r2_tokens} ->
|
|
{:ok, [{:text, escape_nl2br(~s|[bq="#{author}"]|)}, tree], r2_tokens}
|
|
end
|
|
end
|
|
|
|
defp blockquote(parser, [{:blockquote_open, open} | r_tokens]) do
|
|
case well_formed_including_paragraphs(parser, :blockquote_close, r_tokens) do
|
|
{:ok, tree, [{:blockquote_close, _close} | r2_tokens]} ->
|
|
{:ok, [{:markup, ~s|<blockquote>|}, tree, {:markup, ~s|</blockquote>|}], r2_tokens}
|
|
|
|
{:ok, tree, r2_tokens} ->
|
|
{:ok, [{:text, escape_nl2br(open)}, tree], r2_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
|
|
case well_formed_including_paragraphs(parser, :spoiler_close, r_tokens) do
|
|
{:ok, tree, [{:spoiler_close, _close} | r2_tokens]} ->
|
|
{:ok, [{:markup, ~s|<span class="spoiler">|}, tree, {:markup, ~s|</span>|}], r2_tokens}
|
|
|
|
{:ok, tree, r2_tokens} ->
|
|
{:ok, [{:text, escape_nl2br(open)}, tree], r2_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
|
|
case well_formed_including_paragraphs(parser, :link_end, r_tokens) do
|
|
{:ok, tree, [{:link_end, _end}, {:link_url, url} | r2_tokens]} ->
|
|
{:ok, [{:markup, ~s|<a href="#{escape_html(url)}">|}, tree, {:markup, ~s|</a>|}], r2_tokens}
|
|
|
|
{:ok, tree, r2_tokens} ->
|
|
{:ok, [{:text, escape_nl2br(start)}, tree], r2_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|<a href="#{escape_html(link_url)}"><span class="imgspoiler"><img src="#{escape_html(image_url)}" title="#{escape_html(title)}"/></span></a>|], 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|<span class="imgspoiler"><img src="#{escape_html(image_url)}" title="#{escape_html(title)}"/></span>|], 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|<a href="#{escape_html(link_url)}"><span class="imgspoiler"><img src="#{escape_html(image_url)}"/></span></a>|], r_tokens}
|
|
end
|
|
|
|
defp image(parser, [{:image_url, image_url} | r_tokens]) do
|
|
image_url = parser.image_transform.(image_url)
|
|
|
|
{:ok, [markup: ~s|<span class="imgspoiler"><img src="#{escape_html(image_url)}"/></span>|], 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, "<b>", "</b>")
|
|
|
|
#
|
|
# italic =
|
|
# i_open well_formed i_close |
|
|
# b_i_open well_formed b_i_close;
|
|
#
|
|
attribute_parser(:italic, :i_open, :i_close, "<i>", "</i>")
|
|
|
|
#
|
|
# strong =
|
|
# strong_open well_formed strong_close |
|
|
# b_strong_open well_formed b_strong_close;
|
|
#
|
|
attribute_parser(:strong, :strong_open, :strong_close, "<strong>", "</strong>")
|
|
|
|
#
|
|
# emphasis =
|
|
# em_open well_formed em_close |
|
|
# b_em_open well_formed b_em_close;
|
|
#
|
|
attribute_parser(:emphasis, :em_open, :em_close, "<em>", "</em>")
|
|
|
|
#
|
|
# code =
|
|
# code_open well_formed code_close |
|
|
# b_code_open well_formed b_code_close;
|
|
#
|
|
attribute_parser(:code, :code_open, :code_close, "<code>", "</code>")
|
|
|
|
#
|
|
# inserted =
|
|
# ins_open well_formed ins_close |
|
|
# b_ins_open well_formed b_ins_close;
|
|
#
|
|
attribute_parser(:inserted, :ins_open, :ins_close, "<ins>", "</ins>")
|
|
|
|
#
|
|
# superscript =
|
|
# sup_open well_formed sup_close |
|
|
# b_sup_open well_formed b_sup_close;
|
|
#
|
|
attribute_parser(:superscript, :sup_open, :sup_close, "<sup>", "</sup>")
|
|
|
|
#
|
|
# deleted =
|
|
# del_open well_formed del_close |
|
|
# b_del_open well_formed b_del_close;
|
|
#
|
|
attribute_parser(:deleted, :del_open, :del_close, "<del>", "</del>")
|
|
|
|
#
|
|
# subscript =
|
|
# sub_open well_formed sub_close |
|
|
# b_sub_open well_formed b_sub_close;
|
|
#
|
|
attribute_parser(:subscript, :sub_open, :sub_close, "<sub>", "</sub>")
|
|
|
|
|
|
#
|
|
# Terminals
|
|
#
|
|
|
|
defp literal(_parser, [{:literal, text} | r_tokens]),
|
|
do: {:ok, [markup: escape_nl2br(text)], r_tokens}
|
|
|
|
defp literal(_parser, _tokens),
|
|
do: {:error, "Expected a literal"}
|
|
|
|
|
|
defp newline(_parser, [{:newline, _nl} | r_tokens]),
|
|
do: {:ok, [markup: "<br/>"], r_tokens}
|
|
|
|
defp newline(_parser, _tokens),
|
|
do: {:error, "Expected a line break"}
|
|
|
|
|
|
defp text(_parser, [{:text, text} | r_tokens]),
|
|
do: {:ok, [text: escape_nl2br(text)], r_tokens}
|
|
|
|
defp text(_parser, _tokens),
|
|
do: {:error, "Expected text"}
|
|
end
|