formatter.ex 4.91 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
defmodule Pleroma.Formatter do
Haelwenn's avatar
Haelwenn committed
6
7
8
  alias Pleroma.Emoji
  alias Pleroma.HTML
  alias Pleroma.User
href's avatar
href committed
9
  alias Pleroma.Web.MediaProxy
lain's avatar
lain committed
10

lain's avatar
lain committed
11
  @safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/
Maksim's avatar
Maksim committed
12
  @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
minibikini's avatar
minibikini committed
13
  @link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
14
  # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
Maksim's avatar
Maksim committed
15

minibikini's avatar
minibikini committed
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  @auto_linker_config hashtag: true,
                      hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
                      mention: true,
                      mention_handler: &Pleroma.Formatter.mention_handler/4

  def mention_handler("@" <> nickname, buffer, opts, acc) do
    case User.get_cached_by_nickname(nickname) do
      %User{id: id} = user ->
        ap_id = get_ap_id(user)
        nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts)

        link =
          "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
            nickname_text
          }</span></a></span>"

        {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}

      _ ->
        {buffer, acc}
    end
lain's avatar
lain committed
37
  end
38

minibikini's avatar
minibikini committed
39
40
41
42
43
44
45
46
47
48
  def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
    tag = String.downcase(tag)
    url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
    link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"

    {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
  end

  @doc """
  Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
lain's avatar
lain committed
49
50

  If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
minibikini's avatar
minibikini committed
51
52
53
54
55
56
  """
  @spec linkify(String.t(), keyword()) ::
          {String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
  def linkify(text, options \\ []) do
    options = options ++ @auto_linker_config

lain's avatar
lain committed
57
58
59
60
61
62
63
64
65
66
67
68
69
70
    if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
      %{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
      acc = %{mentions: MapSet.new(), tags: MapSet.new()}

      {text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
      {text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)

      {text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
    else
      acc = %{mentions: MapSet.new(), tags: MapSet.new()}
      {text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)

      {text, MapSet.to_list(mentions), MapSet.to_list(tags)}
    end
71
  end
lain's avatar
lain committed
72

href's avatar
href committed
73
74
75
  def emojify(text) do
    emojify(text, Emoji.get_all())
  end
lain's avatar
lain committed
76

lain's avatar
lain committed
77
  def emojify(text, nil), do: text
lain's avatar
lain committed
78

79
  def emojify(text, emoji, strip \\ false) do
lain's avatar
lain committed
80
    Enum.reduce(emoji, text, fn {emoji, file}, text ->
81
82
      emoji = HTML.strip_tags(emoji)
      file = HTML.strip_tags(file)
lain's avatar
lain committed
83

minibikini's avatar
minibikini committed
84
      html =
85
86
87
88
89
90
91
        if not strip do
          "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
            MediaProxy.url(file)
          }' />"
        else
          ""
        end
minibikini's avatar
minibikini committed
92
93

      String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
lain's avatar
lain committed
94
95
    end)
  end
lain's avatar
lain committed
96

97
98
99
100
101
102
  def demojify(text) do
    emojify(text, Emoji.get_all(), true)
  end

  def demojify(text, nil), do: text

103
  def get_emoji(text) when is_binary(text) do
href's avatar
href committed
104
    Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
lain's avatar
lain committed
105
  end
eal's avatar
eal committed
106

107
108
  def get_emoji(_), do: []

minibikini's avatar
minibikini committed
109
110
111
112
  def html_escape({text, mentions, hashtags}, type) do
    {html_escape(text, type), mentions, hashtags}
  end

113
  def html_escape(text, "text/html") do
114
    HTML.filter_tags(text)
115
116
117
  end

  def html_escape(text, "text/plain") do
lain's avatar
lain committed
118
119
120
121
122
123
124
125
    Regex.split(@link_regex, text, include_captures: true)
    |> Enum.map_every(2, fn chunk ->
      {:safe, part} = Phoenix.HTML.html_escape(chunk)
      part
    end)
    |> Enum.join("")
  end

126
  def truncate(text, max_length \\ 200, omission \\ "...") do
127
128
129
    # Remove trailing whitespace
    text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")

130
131
132
133
134
    if String.length(text) < max_length do
      text
    else
      length_with_omission = max_length - String.length(omission)
      String.slice(text, 0, length_with_omission) <> omission
135
136
    end
  end
minibikini's avatar
minibikini committed
137
138
139
140
141
142
143
144
145
146
147
148

  defp get_ap_id(%User{info: %{source_data: %{"url" => url}}}) when is_binary(url), do: url
  defp get_ap_id(%User{ap_id: ap_id}), do: ap_id

  defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
  defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)

  defp maybe_escape(str, %{mentions_escape: true}) do
    String.replace(str, @markdown_characters_regex, "\\\\\\1")
  end

  defp maybe_escape(str, _), do: str
lain's avatar
lain committed
149
end