From cefa526c6d3a45df2d0fcb7643ced828e2e87dea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 26 Mar 2022 02:53:34 +0100 Subject: Refactor formatter (#17828) * Refactor formatter * Move custom emoji pre-rendering logic to view helpers * Move more methods out of Formatter * Fix code style issues * Remove Formatter * Add inline poll options to RSS feeds * Remove unused helper method * Fix code style issues * Various fixes and improvements * Fix test --- app/lib/text_formatter.rb | 158 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 app/lib/text_formatter.rb (limited to 'app/lib/text_formatter.rb') diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb new file mode 100644 index 000000000..48e2fc233 --- /dev/null +++ b/app/lib/text_formatter.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +class TextFormatter + include ActionView::Helpers::TextHelper + include ERB::Util + include RoutingHelper + + URL_PREFIX_REGEX = /\A(https?:\/\/(www\.)?|xmpp:)/.freeze + + DEFAULT_REL = %w(nofollow noopener noreferrer).freeze + + DEFAULT_OPTIONS = { + multiline: true, + }.freeze + + attr_reader :text, :options + + # @param [String] text + # @param [Hash] options + # @option options [Boolean] :multiline + # @option options [Boolean] :with_domains + # @option options [Boolean] :with_rel_me + # @option options [Array] :preloaded_accounts + def initialize(text, options = {}) + @text = text + @options = DEFAULT_OPTIONS.merge(options) + end + + def entities + @entities ||= Extractor.extract_entities_with_indices(text, extract_url_without_protocol: false) + end + + def to_s + return ''.html_safe if text.blank? + + html = rewrite do |entity| + if entity[:url] + link_to_url(entity) + elsif entity[:hashtag] + link_to_hashtag(entity) + elsif entity[:screen_name] + link_to_mention(entity) + end + end + + html = simple_format(html, {}, sanitize: false).delete("\n") if multiline? + + html.html_safe # rubocop:disable Rails/OutputSafety + end + + private + + def rewrite + entities.sort_by! do |entity| + entity[:indices].first + end + + result = ''.dup + + last_index = entities.reduce(0) do |index, entity| + indices = entity[:indices] + result << h(text[index...indices.first]) + result << yield(entity) + indices.last + end + + result << h(text[last_index..-1]) + + result + end + + def link_to_url(entity) + url = Addressable::URI.parse(entity[:url]).to_s + rel = with_rel_me? ? (DEFAULT_REL + %w(me)) : DEFAULT_REL + + prefix = url.match(URL_PREFIX_REGEX).to_s + display_url = url[prefix.length, 30] + suffix = url[prefix.length + 30..-1] + cutoff = url[prefix.length..-1].length > 30 + + <<~HTML.squish + #{h(display_url)} + HTML + rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError + h(entity[:url]) + end + + def link_to_hashtag(entity) + hashtag = entity[:hashtag] + url = tag_url(hashtag) + + <<~HTML.squish + + HTML + end + + def link_to_mention(entity) + username, domain = entity[:screen_name].split('@') + domain = nil if local_domain?(domain) + account = nil + + if preloaded_accounts? + same_username_hits = 0 + + preloaded_accounts.each do |other_account| + same_username = other_account.username.casecmp(username).zero? + same_domain = other_account.domain.nil? ? domain.nil? : other_account.domain.casecmp(domain)&.zero? + + if same_username && !same_domain + same_username_hits += 1 + elsif same_username && same_domain + account = other_account + end + end + else + account = entity_cache.mention(username, domain) + end + + return "@#{h(entity[:screen_name])}" if account.nil? + + url = ActivityPub::TagManager.instance.url_for(account) + display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username + + <<~HTML.squish + @#{h(display_username)} + HTML + end + + def entity_cache + @entity_cache ||= EntityCache.instance + end + + def tag_manager + @tag_manager ||= TagManager.instance + end + + delegate :local_domain?, to: :tag_manager + + def multiline? + options[:multiline] + end + + def with_domains? + options[:with_domains] + end + + def with_rel_me? + options[:with_rel_me] + end + + def preloaded_accounts + options[:preloaded_accounts] + end + + def preloaded_accounts? + preloaded_accounts.present? + end +end -- cgit