Merge remote-tracking branch 'upstream/develop' into registration-workflow
[akkoma] / lib / pleroma / earmark_renderer.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4 #
5 # This file is derived from Earmark, under the following copyright:
6 # Copyright © 2014 Dave Thomas, The Pragmatic Programmers
7 # SPDX-License-Identifier: Apache-2.0
8 # Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex
9 defmodule Pleroma.EarmarkRenderer do
10 @moduledoc false
11
12 alias Earmark.Block
13 alias Earmark.Context
14 alias Earmark.HtmlRenderer
15 alias Earmark.Options
16
17 import Earmark.Inline, only: [convert: 3]
18 import Earmark.Helpers.HtmlHelpers
19 import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2]
20 import Earmark.Context, only: [append: 2, set_value: 2]
21 import Earmark.Options, only: [get_mapper: 1]
22
23 @doc false
24 def render(blocks, %Context{options: %Options{}} = context) do
25 messages = get_messages(context)
26
27 {contexts, html} =
28 get_mapper(context.options).(
29 blocks,
30 &render_block(&1, put_in(context.options.messages, []))
31 )
32 |> Enum.unzip()
33
34 all_messages =
35 contexts
36 |> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end)
37
38 {put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()}
39 end
40
41 #############
42 # Paragraph #
43 #############
44 defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do
45 lines = convert(lines, lnb, context)
46 add_attrs(lines, "<p>#{lines.value}</p>", attrs, [], lnb)
47 end
48
49 ########
50 # Html #
51 ########
52 defp render_block(%Block.Html{html: html}, context) do
53 {context, html}
54 end
55
56 defp render_block(%Block.HtmlComment{lines: lines}, context) do
57 {context, lines}
58 end
59
60 defp render_block(%Block.HtmlOneline{html: html}, context) do
61 {context, html}
62 end
63
64 #########
65 # Ruler #
66 #########
67 defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do
68 add_attrs(context, "<hr />", attrs, [], lnb)
69 end
70
71 ###########
72 # Heading #
73 ###########
74 defp render_block(
75 %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs},
76 context
77 ) do
78 converted = convert(content, lnb, context)
79 html = "<h#{level}>#{converted.value}</h#{level}>"
80 add_attrs(converted, html, attrs, [], lnb)
81 end
82
83 ##############
84 # Blockquote #
85 ##############
86
87 defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
88 {context1, body} = render(blocks, context)
89 html = "<blockquote>#{body}</blockquote>"
90 add_attrs(context1, html, attrs, [], lnb)
91 end
92
93 #########
94 # Table #
95 #########
96
97 defp render_block(
98 %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs},
99 context
100 ) do
101 {context1, html} = add_attrs(context, "<table>", attrs, [], lnb)
102 context2 = set_value(context1, html)
103
104 context3 =
105 if header do
106 append(add_trs(append(context2, "<thead>"), [header], "th", aligns, lnb), "</thead>")
107 else
108 # Maybe an error, needed append(context, html)
109 context2
110 end
111
112 context4 = append(add_trs(append(context3, "<tbody>"), rows, "td", aligns, lnb), "</tbody>")
113
114 {context4, [context4.value, "</table>"]}
115 end
116
117 ########
118 # Code #
119 ########
120
121 defp render_block(
122 %Block.Code{lnb: lnb, language: language, attrs: attrs} = block,
123 %Context{options: options} = context
124 ) do
125 class =
126 if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: ""
127
128 tag = ~s[<pre><code#{class}>]
129 lines = options.render_code.(block)
130 html = ~s[#{tag}#{lines}</code></pre>]
131 add_attrs(context, html, attrs, [], lnb)
132 end
133
134 #########
135 # Lists #
136 #########
137
138 defp render_block(
139 %Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start},
140 context
141 ) do
142 {context1, content} = render(items, context)
143 html = "<#{type}#{start}>#{content}</#{type}>"
144 add_attrs(context1, html, attrs, [], lnb)
145 end
146
147 # format a single paragraph list item, and remove the para tags
148 defp render_block(
149 %Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs},
150 context
151 )
152 when length(blocks) == 1 do
153 {context1, content} = render(blocks, context)
154 content = Regex.replace(~r{</?p>}, content, "")
155 html = "<li>#{content}</li>"
156 add_attrs(context1, html, attrs, [], lnb)
157 end
158
159 # format a spaced list item
160 defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
161 {context1, content} = render(blocks, context)
162 html = "<li>#{content}</li>"
163 add_attrs(context1, html, attrs, [], lnb)
164 end
165
166 ##################
167 # Footnote Block #
168 ##################
169
170 defp render_block(%Block.FnList{blocks: footnotes}, context) do
171 items =
172 Enum.map(footnotes, fn note ->
173 blocks = append_footnote_link(note)
174 %Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks}
175 end)
176
177 {context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context)
178 {context1, Enum.join([~s[<div class="footnotes">], "<hr />", html, "</div>"])}
179 end
180
181 #######################################
182 # Isolated IALs are rendered as paras #
183 #######################################
184
185 defp render_block(%Block.Ial{verbatim: verbatim}, context) do
186 {context, "<p>{:#{verbatim}}</p>"}
187 end
188
189 ####################
190 # IDDef is ignored #
191 ####################
192
193 defp render_block(%Block.IdDef{}, context), do: {context, ""}
194
195 #####################################
196 # And here are the inline renderers #
197 #####################################
198
199 defdelegate br, to: HtmlRenderer
200 defdelegate codespan(text), to: HtmlRenderer
201 defdelegate em(text), to: HtmlRenderer
202 defdelegate strong(text), to: HtmlRenderer
203 defdelegate strikethrough(text), to: HtmlRenderer
204
205 defdelegate link(url, text), to: HtmlRenderer
206 defdelegate link(url, text, title), to: HtmlRenderer
207
208 defdelegate image(path, alt, title), to: HtmlRenderer
209
210 defdelegate footnote_link(ref, backref, number), to: HtmlRenderer
211
212 # Table rows
213 defp add_trs(context, rows, tag, aligns, lnb) do
214 numbered_rows =
215 rows
216 |> Enum.zip(Stream.iterate(lnb, &(&1 + 1)))
217
218 numbered_rows
219 |> Enum.reduce(context, fn {row, lnb}, ctx ->
220 append(add_tds(append(ctx, "<tr>"), row, tag, aligns, lnb), "</tr>")
221 end)
222 end
223
224 defp add_tds(context, row, tag, aligns, lnb) do
225 Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb))
226 end
227
228 defp add_td_fn(row, tag, aligns, lnb) do
229 fn n, ctx ->
230 style =
231 case Enum.at(aligns, n - 1, :default) do
232 :default -> ""
233 align -> " style=\"text-align: #{align}\""
234 end
235
236 col = Enum.at(row, n - 1)
237 converted = convert(col, lnb, set_messages(ctx, []))
238 append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}</#{tag}>")
239 end
240 end
241
242 ###############################
243 # Append Footnote Return Link #
244 ###############################
245
246 defdelegate append_footnote_link(note), to: HtmlRenderer
247 defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer
248
249 defdelegate render_code(lines), to: HtmlRenderer
250
251 defp code_classes(language, prefix) do
252 ["" | String.split(prefix || "")]
253 |> Enum.map(fn pfx -> "#{pfx}#{language}" end)
254 |> Enum.join(" ")
255 end
256 end