It moves bbcode to bbcode_pleroma as the former is owned by kaniini and transfering ownership wasn't done in a timely manner. Closes: https://git.pleroma.social/pleroma/pleroma/issues/1374 Closes: https://git.pleroma.social/pleroma/pleroma/issues/1375emr_stable
parent
80bc8c2cc9
commit
5f9fbd7d33
@ -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 |
@ -0,0 +1,79 @@ |
||||
# Pleroma: A lightweight social networking server |
||||
# Copyright © 2020 Pleroma Authors <https://pleroma.social/> |
||||
# SPDX-License-Identifier: AGPL-3.0-only |
||||
defmodule Pleroma.EarmarkRendererTest do |
||||
use ExUnit.Case |
||||
|
||||
test "Paragraph" do |
||||
code = ~s[Hello\n\nWorld!] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<p>Hello</p><p>World!</p>" |
||||
end |
||||
|
||||
test "raw HTML" do |
||||
code = ~s[<a href="http://example.org/">OwO</a><!-- what's this?-->] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<p>#{code}</p>" |
||||
end |
||||
|
||||
test "rulers" do |
||||
code = ~s[before\n\n-----\n\nafter] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<p>before</p><hr /><p>after</p>" |
||||
end |
||||
|
||||
test "headings" do |
||||
code = ~s[# h1\n## h2\n### h3\n] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<h1>h1</h1><h2>h2</h2><h3>h3</h3>] |
||||
end |
||||
|
||||
test "blockquote" do |
||||
code = ~s[> whoms't are you quoting?] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<blockquote><p>whoms’t are you quoting?</p></blockquote>" |
||||
end |
||||
|
||||
test "code" do |
||||
code = ~s[`mix`] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<p><code class="inline">mix</code></p>] |
||||
|
||||
code = ~s[``mix``] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<p><code class="inline">mix</code></p>] |
||||
|
||||
code = ~s[```\nputs "Hello World"\n```] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<pre><code class="">puts "Hello World"</code></pre>] |
||||
end |
||||
|
||||
test "lists" do |
||||
code = ~s[- one\n- two\n- three\n- four] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<ul><li>one</li><li>two</li><li>three</li><li>four</li></ul>" |
||||
|
||||
code = ~s[1. one\n2. two\n3. three\n4. four\n] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<ol><li>one</li><li>two</li><li>three</li><li>four</li></ol>" |
||||
end |
||||
|
||||
test "delegated renderers" do |
||||
code = ~s[a<br/>b] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == "<p>#{code}</p>" |
||||
|
||||
code = ~s[*aaaa~*] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<p><em>aaaa~</em></p>] |
||||
|
||||
code = ~s[**aaaa~**] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<p><strong>aaaa~</strong></p>] |
||||
|
||||
# strikethrought |
||||
code = ~s[<del>aaaa~</del>] |
||||
result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) |
||||
assert result == ~s[<p><del>aaaa~</del></p>] |
||||
end |
||||
end |
Loading…
Reference in new issue