# frozen_string_literal: true require 'singleton' class Formatter include Singleton include RoutingHelper include ActionView::Helpers::TextHelper DISALLOWED_BOUNDING_REGEX = /[[:alnum:]:]/.freeze def format(status, **options) if status.reblog? prepend_reblog = status.reblog.account.acct status = status.proper else prepend_reblog = false end raw_content = status.text if options[:inline_poll_options] && status.preloadable_poll raw_content = raw_content + "\n\n" + status.preloadable_poll.options.map { |title| "[ ] #{title}" }.join("\n") end return '' if raw_content.blank? unless status.local? html = reformat(raw_content) html = apply_inner_link(html) html = apply_reference_link(html, status) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] html = nyaize_html(html) if options[:nyaize] return html.html_safe # rubocop:disable Rails/OutputSafety end linkable_accounts = status.active_mentions.map(&:account) linkable_accounts << status.account html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = encode_and_link_urls(html, linkable_accounts) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] html = simple_format(html, {}, sanitize: false) html = quotify(html, status) if status.quote? && !options[:escape_quotify] html = add_compatible_reference_link(html, status) if status.references.exists? html = nyaize_html(html) if options[:nyaize] html = html.delete("\n") html.html_safe # rubocop:disable Rails/OutputSafety end def format_in_quote(status, **options) html = format(status) return '' if html.empty? doc = Nokogiri::HTML.parse(html, nil, 'utf-8') html = doc.css('body')[0].inner_html html.sub!(/^
(.+)<\/p>$/, '\1')
html = Sanitize.clean(html).delete("\n").truncate(150)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def reformat(html)
sanitize(html, Sanitize::Config::MASTODON_STRICT)
rescue ArgumentError
''
end
def plaintext(status)
return status.text if status.local?
text = status.text.gsub(/(
|
|<\/p>)+/) { |match| "#{match}\n" }
text = remove_reference_link(text)
strip_tags(text)
end
def simplified_format(account, **options)
html = account.local? ? linkify(account.note) : reformat(account.note)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def sanitize(html, config)
Sanitize.fragment(html, config)
end
def format_spoiler(status, **options)
html = encode(status.spoiler_text)
html = encode_custom_emojis(html, status.emojis, options[:autoplay])
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_poll_option(status, option, **options)
html = encode(option.title)
html = encode_custom_emojis(html, status.emojis, options[:autoplay])
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_display_name(account, **options)
html = encode(account.display_name.presence || account.username)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def format_field(account, str, **options)
html = account.local? ? encode_and_link_urls(str, me: true, with_domain: true) : reformat(str)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
def linkify(text)
html = encode_and_link_urls(text)
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
html.html_safe # rubocop:disable Rails/OutputSafety
end
private
def html_entities
@html_entities ||= HTMLEntities.new
end
def encode(html)
html_entities.encode(html)
end
def encode_and_link_urls(html, accounts = nil, options = {})
entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
if accounts.is_a?(Hash)
options = accounts
accounts = nil
end
rewrite(html.dup, entities) do |entity|
if entity[:url]
link_to_url(entity, options)
elsif entity[:hashtag]
link_to_hashtag(entity)
elsif entity[:screen_name]
link_to_mention(entity, accounts, options)
end
end
end
def count_tag_nesting(tag)
if tag[1] == '/' then -1
elsif tag[-2] == '/' then 0
else 1
end
end
# rubocop:disable Metrics/BlockNesting
def encode_custom_emojis(html, emojis, animate = false)
return html if emojis.empty?
emoji_map = emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
tree = Nokogiri::HTML.fragment(html)
tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
i = -1
inside_shortname = false
shortname_start_index = -1
last_index = 0
text = node.content
result = Nokogiri::XML::NodeSet.new(tree.document)
while i + 1 < text.size
i += 1
if inside_shortname && text[i] == ':'
inside_shortname = false
shortcode = text[shortname_start_index + 1..i - 1]
char_after = text[i + 1]
next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
original_url, static_url = emoji
result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
result << Nokogiri::HTML.fragment(
if animate
image_tag(original_url, draggable: false, class: 'emojione', alt: ":#{shortcode}:", title: ":#{shortcode}:")
else
image_tag(original_url, draggable: false, class: 'emojione custom-emoji', alt: ":#{shortcode}:", title: ":#{shortcode}:", data: { original: original_url, static: static_url })
end
)
last_index = i + 1
elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
inside_shortname = true
shortname_start_index = i
end
end
result << Nokogiri::XML::Text.new(text[last_index..-1], tree.document)
node.replace(result)
end
tree.to_html
end
# rubocop:enable Metrics/BlockNesting
def quotify(html, status)
url = ActivityPub::TagManager.instance.url_for(status.quote)
link = encode_and_link_urls(url)
html.sub(/(<[^>]+>)\z/, "
QT: #{link}\\1")
end
def add_compatible_reference_link(html, status)
url = references_short_account_status_url(status.account, status)
link = "#{I18n.t('status_references.link_text')}"
html.sub(/<\/p>\z/, " #{link}