diff options
author | Claire <claire.github-309c@sitedethib.com> | 2022-03-26 19:18:55 +0100 |
---|---|---|
committer | Claire <claire.github-309c@sitedethib.com> | 2022-03-26 19:18:55 +0100 |
commit | aaa9ec340b7291bace3a899cfcfef7524ecdfe72 (patch) | |
tree | c4070b5c32efc4b6474bfb0ca5814c5779a5b6f8 /app/lib/text_formatter.rb | |
parent | 2287eebae0c1d699436a8cf3218d7cfe990a3605 (diff) | |
parent | d7d049aab7578028492e73671769f0a350e34203 (diff) |
Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/lib/formatter.rb`: Upstream completely refactored the formatting code and removed that file, while glitch-soc had code for Markdown and HTML toots. Took upstream code, glitch-soc changes will be re-implemented on top of the refactored classes in a later commit. - `app/models/status.rb`: Upstream refactored status edit handling and moved code to `app/models/concerns/status_snapshot_concern.rb`. Applied glitch-soc's changes to that file. - `app/serializers/activitypub/note_serializer.rb`: Not really a conflict, just a line added too close to one modified by glitch-soc. Applied upstream changes while keeping the glitch-soc-modified one. - `app/services/update_status_service.rb`: Not really a conflict, upstream modified a line adjacent to one added by glitch-soc. Applied upstream changes while keeping the glitch-soc line. - `app/views/statuses/_simple_status.html.haml`: Upstream refactored formatting, glitch-soc changed the markup slightly. Applied upstream changes. - `spec/lib/formatter_spec.rb`: Upstream completely refactored the formatting code and removed that file, while glitch-soc had code for Markdown and HTML toots. Took upstream code, glitch-soc changes will be re-implemented on top of the refactored classes in a later commit.
Diffstat (limited to 'app/lib/text_formatter.rb')
-rw-r--r-- | app/lib/text_formatter.rb | 158 |
1 files changed, 158 insertions, 0 deletions
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<Account>] :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 + <a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a> + 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 + <a href="#{h(url)}" class="mention hashtag" rel="tag">#<span>#{h(hashtag)}</span></a> + 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 + <span class="h-card"><a href="#{h(url)}" class="u-url mention">@<span>#{h(display_username)}</span></a></span> + 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 |