Page MenuHomePhorge

No OneTemporary

Size
6 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/mfm-code-blocks.fix b/changelog.d/mfm-code-blocks.fix
new file mode 100644
index 000000000..d8eca3581
--- /dev/null
+++ b/changelog.d/mfm-code-blocks.fix
@@ -0,0 +1 @@
+Fix MFM being rendered inside <code> and <pre> elements; MFM syntax in quoted code is now shown verbatim, matching Misskey
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 26034d685..c160abf64 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -338,16 +338,73 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html")
end
+ # <pre> and <code> elements are shielded from MFM parsing, mirroring how
+ # Misskey renders MFM: markup like `$[x2 text]` inside quoted code is shown
+ # verbatim instead of being turned into an MFM span.
+ @mfm_code_element ~r/<pre(?:\s[^>]*)?>.*?<\/pre>|<code(?:\s[^>]*)?>.*?<\/code>/is
+
defp safe_mfm_to_html(html) do
- html
+ {open_marker, close_marker} = mfm_code_markers(html)
+ {masked, segments} = mask_mfm_code_elements(html, open_marker, close_marker)
+
+ masked
|> MfmParser.Parser.parse()
|> MfmParser.Encoder.to_html()
+ |> unmask_mfm_code_elements(segments, open_marker, close_marker)
rescue
_ -> html
catch
_, _ -> html
end
+ # Code elements are replaced by placeholders built from two private-use
+ # codepoints that do not occur in the input, so a placeholder can never
+ # collide with user-provided text.
+ defp mfm_code_markers(html) do
+ used =
+ html
++ |> String.codepoints()
+ |> MapSet.new()
+
+ 0xE000..0xF8FF
+ |> Enum.filter(fn codepoint -> not MapSet.member?(used, <<codepoint::utf8>>) end)
+ |> Enum.take(2)
+ |> case do
+ [open, close] -> {<<open::utf8>>, <<close::utf8>>}
+ _ -> {nil, nil}
+ end
+ end
+
+ defp mask_mfm_code_elements(html, nil, nil), do: {html, []}
+
+ defp mask_mfm_code_elements(html, open_marker, close_marker) do
+ {masked, segments} =
+ @mfm_code_element
+ |> Regex.split(html, include_captures: true)
+ |> Enum.with_index()
+ |> Enum.map_reduce([], fn
+ {part, index}, acc when rem(index, 2) == 1 ->
+ {open_marker <> Integer.to_string(length(acc)) <> close_marker, [part | acc]}
+
+ {part, _index}, acc ->
+ {part, acc}
+ end)
+
+ {IO.iodata_to_binary(masked), Enum.reverse(segments)}
+ end
+
+ defp unmask_mfm_code_elements(html, [], _open_marker, _close_marker), do: html
+
+ defp unmask_mfm_code_elements(html, segments, open_marker, close_marker) do
+ Enum.reduce(Enum.with_index(segments), html, fn {segment, index}, masked ->
+ String.replace(
+ masked,
+ open_marker <> Integer.to_string(index) <> close_marker,
+ segment
+ )
+ end)
+ end
+
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index bf9c70fb6..5bb9f8181 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -178,6 +178,33 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
refute content =~ "<script"
end
+ test "a Misskey MFM note does not render MFM inside code elements" do
+ user = insert(:user, ap_id: "https://misskey.example/users/code")
+
+ note = %{
+ "id" => "https://misskey.example/notes/6",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => "$[x2 big] `$[x2 small]`\n\n```\n$[spin.speed=1s block]\n```",
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content =~ ~s(<span class="mfm-x2">big</span>)
+ assert content =~ "<code class=\"inline\">$[x2 small]</code>"
+ assert content =~ "<pre><code>$[spin.speed=1s block]</code></pre>"
+ refute content =~ "mfm-spin"
+ end
+
test "a Misskey MFM note resolves only cached AP mention tags" do
remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
local_user = insert(:user, nickname: "local_user")
diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs
index d0cbc3111..589e8f4d5 100644
--- a/test/pleroma/web/common_api/utils_test.exs
+++ b/test/pleroma/web/common_api/utils_test.exs
@@ -161,6 +161,26 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert output ==
~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a class="u-url mention" data-user="#{user.id}" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a class="u-url mention" data-user="#{user.id}" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>)
end
+
+ test "does not render MFM inside code elements" do
+ text = "$[x2 big] `$[x2 small]`\n\n```\n$[spin.speed=1s block]\n```"
+
+ {output, [], []} = Utils.format_input(text, "text/x.misskeymarkdown")
+
+ assert output =~ ~s(<span class="mfm-x2">big</span>)
+ assert output =~ "<code class=\"inline\">$[x2 small]</code>"
+ assert output =~ "<pre><code>$[spin.speed=1s block]</code></pre>"
+ refute output =~ "mfm-spin"
+ end
+
+ test "MFM inside code elements does not break MFM outside of them" do
+ text = "```\n$[x2 unclosed\n```\n\n$[flip flipped]"
+
+ {output, [], []} = Utils.format_input(text, "text/x.misskeymarkdown")
+
+ assert output =~ ~s(<span class="mfm-flip">flipped</span>)
+ assert output =~ "$[x2 unclosed"
+ end
end
describe "format_input/3 with markdown" do

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 29, 10:59 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737563
Default Alt Text
(6 KB)

Event Timeline