X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fformatter.ex;h=1e4ede3f2c8fea4ba484a62ca2194208161cbef8;hb=34fc0dca2e879bcbb73acc80fdc72678411d0ebf;hp=b63f592fbc9218a84d72b3ea24dc82f0f8121a35;hpb=8902942128e3aee814c215d700e2eaee21b491e9;p=akkoma diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index b63f592fb..1e4ede3f2 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -1,32 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Formatter do + alias Pleroma.Emoji + alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.MediaProxy - alias Pleroma.HTML - alias Pleroma.Emoji - @tag_regex ~r/\#\w+/u - def parse_tags(text, data \\ %{}) do - Regex.scan(@tag_regex, text) - |> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end) - |> (fn map -> - if data["sensitive"] in [true, "True", "true", "1"], - do: [{"#nsfw", "nsfw"}] ++ map, - else: map - end).() + @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ + @link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + + @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 = + "@#{ + nickname_text + }" + + {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} + + _ -> + {buffer, acc} + end end - def parse_mentions(text) do - # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address - regex = - ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u + def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do + tag = String.downcase(tag) + url = "#{Pleroma.Web.base_url()}/tag/#{tag}" + link = "" - Regex.scan(regex, text) - |> List.flatten() - |> Enum.uniq() - |> Enum.map(fn "@" <> match = full_match -> - {full_match, User.get_cached_by_nickname(match)} - end) - |> Enum.filter(fn {_match, user} -> user end) + {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. + """ + @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 + 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 def emojify(text) do @@ -35,34 +62,40 @@ defmodule Pleroma.Formatter do def emojify(text, nil), do: text - def emojify(text, emoji) do + def emojify(text, emoji, strip \\ false) do Enum.reduce(emoji, text, fn {emoji, file}, text -> emoji = HTML.strip_tags(emoji) file = HTML.strip_tags(file) - String.replace( - text, - ":#{emoji}:", - "#{emoji}" - ) - |> HTML.filter_tags() + html = + if not strip do + "#{emoji}" + else + "" + end + + String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags() end) end + def demojify(text) do + emojify(text, Emoji.get_all(), true) + end + + def demojify(text, nil), do: text + def get_emoji(text) when is_binary(text) do Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end) end def get_emoji(_), do: [] - @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui - - @uri_schemes Application.get_env(:pleroma, :uri_schemes, []) - @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, []) + def html_escape({text, mentions, hashtags}, type) do + {html_escape(text, type), mentions, hashtags} + end - # TODO: make it use something other than @link_regex def html_escape(text, "text/html") do HTML.filter_tags(text) end @@ -76,107 +109,27 @@ defmodule Pleroma.Formatter do |> Enum.join("") end - @doc "changes scheme:... urls to html links" - def add_links({subs, text}) do - links = + def truncate(text, max_length \\ 200, omission \\ "...") do + # Remove trailing whitespace + text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") + + if String.length(text) < max_length do text - |> String.split([" ", "\t", "
"]) - |> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end) - |> Enum.filter(fn word -> Regex.match?(@link_regex, word) end) - |> Enum.map(fn url -> {Ecto.UUID.generate(), url} end) - |> Enum.sort_by(fn {_, url} -> -String.length(url) end) - - uuid_text = - links - |> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end) - - subs = - subs ++ - Enum.map(links, fn {uuid, url} -> - {uuid, "#{url}"} - end) - - {subs, uuid_text} + else + length_with_omission = max_length - String.length(omission) + String.slice(text, 0, length_with_omission) <> omission + end end - @doc "Adds the links to mentioned users" - def add_user_links({subs, text}, mentions) do - mentions = - mentions - |> Enum.sort_by(fn {name, _} -> -String.length(name) end) - |> Enum.map(fn {name, user} -> {name, user, Ecto.UUID.generate()} end) - - uuid_text = - mentions - |> Enum.reduce(text, fn {match, _user, uuid}, text -> - String.replace(text, match, uuid) - end) - - subs = - subs ++ - Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} -> - ap_id = - if is_binary(info.source_data["url"]) do - info.source_data["url"] - else - ap_id - end - - short_match = String.split(match, "@") |> tl() |> hd() - - {uuid, - "@#{short_match}"} - end) - - {subs, uuid_text} - end + 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 - @doc "Adds the hashtag links" - def add_hashtag_links({subs, text}, tags) do - tags = - tags - |> Enum.sort_by(fn {name, _} -> -String.length(name) end) - |> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end) - - uuid_text = - tags - |> Enum.reduce(text, fn {match, _short, uuid}, text -> - String.replace(text, match, uuid) - end) - - subs = - subs ++ - Enum.map(tags, fn {tag_text, tag, uuid} -> - url = - "" - - {uuid, url} - end) - - {subs, uuid_text} - end + defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname) + defp get_nickname_text(nickname, _), do: User.local_nickname(nickname) - def finalize({subs, text}) do - Enum.reduce(subs, text, fn {uuid, replacement}, result_text -> - String.replace(result_text, uuid, replacement) - end) + defp maybe_escape(str, %{mentions_escape: true}) do + String.replace(str, @markdown_characters_regex, "\\\\\\1") end - def truncate(text, opts \\ []) do - max_length = opts[:max_length] || 200 - omission = opts[:omission] || "..." - - cond do - not String.valid?(text) -> - text - String.length(text) < max_length -> - text - true -> - length_with_omission = max_length - String.length(omission) - - "#{String.slice(text, 0, length_with_omission)}#{omission}" - end - end + defp maybe_escape(str, _), do: str end