@@ -1,256 +0,0 @@ | |||
# 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 |
@@ -138,6 +138,14 @@ defmodule Pleroma.Formatter do | |||
|> Enum.join("") | |||
end | |||
def minify({text, mentions, hashtags}, type) do | |||
{minify(text, type), mentions, hashtags} | |||
end | |||
def minify(text, "text/html") do | |||
String.replace(text, "\n", "") | |||
end | |||
def truncate(text, max_length \\ 200, omission \\ "...") do | |||
# Remove trailing whitespace | |||
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") | |||
@@ -5,7 +5,6 @@ | |||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do | |||
use Ecto.Schema | |||
alias Pleroma.EarmarkRenderer | |||
alias Pleroma.EctoType.ActivityPub.ObjectValidators | |||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator | |||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes | |||
@@ -96,7 +95,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do | |||
when is_binary(content) do | |||
content = | |||
content | |||
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) | |||
|> Earmark.as_html!() | |||
|> Pleroma.HTML.filter_tags() | |||
Map.put(data, "content", content) | |||
@@ -294,8 +294,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
def format_input(text, "text/markdown", options) do | |||
text | |||
|> Formatter.mentions_escape(options) | |||
|> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer}) | |||
|> Earmark.as_html!() | |||
|> Formatter.linkify(options) | |||
|> Formatter.minify("text/html") | |||
|> Formatter.html_escape("text/html") | |||
end | |||
@@ -144,7 +144,7 @@ defmodule Pleroma.Mixfile do | |||
{:ex_aws, "~> 2.1.6"}, | |||
{:ex_aws_s3, "~> 2.0"}, | |||
{:sweet_xml, "~> 0.6.6"}, | |||
{:earmark, "1.4.3"}, | |||
{:earmark, "1.4.10"}, | |||
{:bbcode_pleroma, "~> 0.2.0"}, | |||
{:crypt, | |||
git: "https://github.com/msantos/crypt.git", | |||
@@ -27,7 +27,7 @@ | |||
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, | |||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, | |||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, | |||
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, | |||
"earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, | |||
"earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, | |||
"ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"}, | |||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, | |||
@@ -307,4 +307,11 @@ defmodule Pleroma.FormatterTest do | |||
assert Formatter.html_escape(text, "text/plain") == expected | |||
end | |||
test "it minifies html" do | |||
text = "<p>\nhello</p>\n<p>\nworld</p>\n" | |||
expected = "<p>hello</p><p>world</p>" | |||
assert Formatter.minify(text, "text/html") == expected | |||
end | |||
end |
@@ -168,6 +168,81 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do | |||
end | |||
end | |||
describe "format_input/3 with markdown" do | |||
test "Paragraph" do | |||
code = ~s[Hello\n\nWorld!] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
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, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == "<p>#{code}</p>" | |||
end | |||
test "rulers" do | |||
code = ~s[before\n\n-----\n\nafter] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == "<p>before</p><hr /><p>after</p>" | |||
end | |||
test "headings" do | |||
code = ~s[# h1\n## h2\n### h3\n] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<h1>h1</h1><h2>h2</h2><h3>h3</h3>] | |||
end | |||
test "blockquote" do | |||
code = ~s[> whoms't are you quoting?] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == "<blockquote><p>whoms’t are you quoting?</p></blockquote>" | |||
end | |||
test "code" do | |||
code = ~s[`mix`] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<p><code class="inline">mix</code></p>] | |||
code = ~s[``mix``] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<p><code class="inline">mix</code></p>] | |||
code = ~s[```\nputs "Hello World"\n```] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
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, [], []} = Utils.format_input(code, "text/markdown") | |||
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, [], []} = Utils.format_input(code, "text/markdown") | |||
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, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == "<p>#{code}</p>" | |||
code = ~s[*aaaa~*] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<p><em>aaaa~</em></p>] | |||
code = ~s[**aaaa~**] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<p><strong>aaaa~</strong></p>] | |||
# strikethrought | |||
code = ~s[<del>aaaa~</del>] | |||
{result, [], []} = Utils.format_input(code, "text/markdown") | |||
assert result == ~s[<p><del>aaaa~</del></p>] | |||
end | |||
end | |||
describe "context_to_conversation_id" do | |||
test "creates a mapping object" do | |||
conversation_id = Utils.context_to_conversation_id("random context") | |||