summaryrefslogtreecommitdiff
path: root/lib/pleroma/earmark_renderer.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/earmark_renderer.ex')
-rw-r--r--lib/pleroma/earmark_renderer.ex256
1 files changed, 256 insertions, 0 deletions
diff --git a/lib/pleroma/earmark_renderer.ex b/lib/pleroma/earmark_renderer.ex
new file mode 100644
index 000000000..6211a3b4a
--- /dev/null
+++ b/lib/pleroma/earmark_renderer.ex
@@ -0,0 +1,256 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+#
+# This file is derived from Earmark, under the following copyright:
+# Copyright © 2014 Dave Thomas, The Pragmatic Programmers
+# SPDX-License-Identifier: Apache-2.0
+# Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex
+defmodule Pleroma.EarmarkRenderer do
+ @moduledoc false
+
+ alias Earmark.Block
+ alias Earmark.Context
+ alias Earmark.HtmlRenderer
+ alias Earmark.Options
+
+ import Earmark.Inline, only: [convert: 3]
+ import Earmark.Helpers.HtmlHelpers
+ import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2]
+ import Earmark.Context, only: [append: 2, set_value: 2]
+ import Earmark.Options, only: [get_mapper: 1]
+
+ @doc false
+ def render(blocks, %Context{options: %Options{}} = context) do
+ messages = get_messages(context)
+
+ {contexts, html} =
+ get_mapper(context.options).(
+ blocks,
+ &render_block(&1, put_in(context.options.messages, []))
+ )
+ |> Enum.unzip()
+
+ all_messages =
+ contexts
+ |> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end)
+
+ {put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()}
+ end
+
+ #############
+ # Paragraph #
+ #############
+ defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do
+ lines = convert(lines, lnb, context)
+ add_attrs(lines, "<p>#{lines.value}</p>", attrs, [], lnb)
+ end
+
+ ########
+ # Html #
+ ########
+ defp render_block(%Block.Html{html: html}, context) do
+ {context, html}
+ end
+
+ defp render_block(%Block.HtmlComment{lines: lines}, context) do
+ {context, lines}
+ end
+
+ defp render_block(%Block.HtmlOneline{html: html}, context) do
+ {context, html}
+ end
+
+ #########
+ # Ruler #
+ #########
+ defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do
+ add_attrs(context, "<hr />", attrs, [], lnb)
+ end
+
+ ###########
+ # Heading #
+ ###########
+ defp render_block(
+ %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs},
+ context
+ ) do
+ converted = convert(content, lnb, context)
+ html = "<h#{level}>#{converted.value}</h#{level}>"
+ add_attrs(converted, html, attrs, [], lnb)
+ end
+
+ ##############
+ # Blockquote #
+ ##############
+
+ defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
+ {context1, body} = render(blocks, context)
+ html = "<blockquote>#{body}</blockquote>"
+ add_attrs(context1, html, attrs, [], lnb)
+ end
+
+ #########
+ # Table #
+ #########
+
+ defp render_block(
+ %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs},
+ context
+ ) do
+ {context1, html} = add_attrs(context, "<table>", attrs, [], lnb)
+ context2 = set_value(context1, html)
+
+ context3 =
+ if header do
+ append(add_trs(append(context2, "<thead>"), [header], "th", aligns, lnb), "</thead>")
+ else
+ # Maybe an error, needed append(context, html)
+ context2
+ end
+
+ context4 = append(add_trs(append(context3, "<tbody>"), rows, "td", aligns, lnb), "</tbody>")
+
+ {context4, [context4.value, "</table>"]}
+ end
+
+ ########
+ # Code #
+ ########
+
+ defp render_block(
+ %Block.Code{lnb: lnb, language: language, attrs: attrs} = block,
+ %Context{options: options} = context
+ ) do
+ class =
+ if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: ""
+
+ tag = ~s[<pre><code#{class}>]
+ lines = options.render_code.(block)
+ html = ~s[#{tag}#{lines}</code></pre>]
+ add_attrs(context, html, attrs, [], lnb)
+ end
+
+ #########
+ # Lists #
+ #########
+
+ defp render_block(
+ %Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start},
+ context
+ ) do
+ {context1, content} = render(items, context)
+ html = "<#{type}#{start}>#{content}</#{type}>"
+ add_attrs(context1, html, attrs, [], lnb)
+ end
+
+ # format a single paragraph list item, and remove the para tags
+ defp render_block(
+ %Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs},
+ context
+ )
+ when length(blocks) == 1 do
+ {context1, content} = render(blocks, context)
+ content = Regex.replace(~r{</?p>}, content, "")
+ html = "<li>#{content}</li>"
+ add_attrs(context1, html, attrs, [], lnb)
+ end
+
+ # format a spaced list item
+ defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
+ {context1, content} = render(blocks, context)
+ html = "<li>#{content}</li>"
+ add_attrs(context1, html, attrs, [], lnb)
+ end
+
+ ##################
+ # Footnote Block #
+ ##################
+
+ defp render_block(%Block.FnList{blocks: footnotes}, context) do
+ items =
+ Enum.map(footnotes, fn note ->
+ blocks = append_footnote_link(note)
+ %Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks}
+ end)
+
+ {context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context)
+ {context1, Enum.join([~s[<div class="footnotes">], "<hr />", html, "</div>"])}
+ end
+
+ #######################################
+ # Isolated IALs are rendered as paras #
+ #######################################
+
+ defp render_block(%Block.Ial{verbatim: verbatim}, context) do
+ {context, "<p>{:#{verbatim}}</p>"}
+ end
+
+ ####################
+ # IDDef is ignored #
+ ####################
+
+ defp render_block(%Block.IdDef{}, context), do: {context, ""}
+
+ #####################################
+ # And here are the inline renderers #
+ #####################################
+
+ defdelegate br, to: HtmlRenderer
+ defdelegate codespan(text), to: HtmlRenderer
+ defdelegate em(text), to: HtmlRenderer
+ defdelegate strong(text), to: HtmlRenderer
+ defdelegate strikethrough(text), to: HtmlRenderer
+
+ defdelegate link(url, text), to: HtmlRenderer
+ defdelegate link(url, text, title), to: HtmlRenderer
+
+ defdelegate image(path, alt, title), to: HtmlRenderer
+
+ defdelegate footnote_link(ref, backref, number), to: HtmlRenderer
+
+ # Table rows
+ defp add_trs(context, rows, tag, aligns, lnb) do
+ numbered_rows =
+ rows
+ |> Enum.zip(Stream.iterate(lnb, &(&1 + 1)))
+
+ numbered_rows
+ |> Enum.reduce(context, fn {row, lnb}, ctx ->
+ append(add_tds(append(ctx, "<tr>"), row, tag, aligns, lnb), "</tr>")
+ end)
+ end
+
+ defp add_tds(context, row, tag, aligns, lnb) do
+ Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb))
+ end
+
+ defp add_td_fn(row, tag, aligns, lnb) do
+ fn n, ctx ->
+ style =
+ case Enum.at(aligns, n - 1, :default) do
+ :default -> ""
+ align -> " style=\"text-align: #{align}\""
+ end
+
+ col = Enum.at(row, n - 1)
+ converted = convert(col, lnb, set_messages(ctx, []))
+ append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}</#{tag}>")
+ end
+ end
+
+ ###############################
+ # Append Footnote Return Link #
+ ###############################
+
+ defdelegate append_footnote_link(note), to: HtmlRenderer
+ defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer
+
+ defdelegate render_code(lines), to: HtmlRenderer
+
+ defp code_classes(language, prefix) do
+ ["" | String.split(prefix || "")]
+ |> Enum.map(fn pfx -> "#{pfx}#{language}" end)
+ |> Enum.join(" ")
+ end
+end