2022-03-26 01:53:34 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class EmojiFormatter
|
|
|
|
include RoutingHelper
|
|
|
|
|
2023-04-26 23:46:18 +00:00
|
|
|
DISALLOWED_BOUNDING_REGEX = /[[:alnum:]:]/
|
2022-03-26 01:53:34 +00:00
|
|
|
|
|
|
|
attr_reader :html, :custom_emojis, :options
|
|
|
|
|
|
|
|
# @param [ActiveSupport::SafeBuffer] html
|
|
|
|
# @param [Array<CustomEmoji>] custom_emojis
|
|
|
|
# @param [Hash] options
|
|
|
|
# @option options [Boolean] :animate
|
2022-05-09 05:43:08 +00:00
|
|
|
# @option options [String] :style
|
2023-05-31 22:10:21 +00:00
|
|
|
# @option options [String] :raw_shortcode
|
2022-03-26 01:53:34 +00:00
|
|
|
def initialize(html, custom_emojis, options = {})
|
|
|
|
raise ArgumentError unless html.html_safe?
|
|
|
|
|
|
|
|
@html = html
|
|
|
|
@custom_emojis = custom_emojis
|
|
|
|
@options = options
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
return html if custom_emojis.empty? || html.blank?
|
|
|
|
|
2022-11-14 19:26:21 +00:00
|
|
|
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])
|
|
|
|
|
|
|
|
result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
|
2023-05-31 22:10:21 +00:00
|
|
|
result << Nokogiri::HTML.fragment(tag_for_emoji(shortcode, emoji))
|
2022-11-14 19:26:21 +00:00
|
|
|
|
|
|
|
last_index = i + 1
|
|
|
|
elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
|
|
|
|
inside_shortname = true
|
|
|
|
shortname_start_index = i
|
2022-03-26 01:53:34 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-07-12 08:03:06 +00:00
|
|
|
result << Nokogiri::XML::Text.new(text[last_index..], tree.document)
|
2022-11-14 19:26:21 +00:00
|
|
|
node.replace(result)
|
|
|
|
end
|
2022-03-26 01:53:34 +00:00
|
|
|
|
2022-11-14 19:26:21 +00:00
|
|
|
tree.to_html.html_safe # rubocop:disable Rails/OutputSafety
|
2022-03-26 01:53:34 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def emoji_map
|
|
|
|
@emoji_map ||= custom_emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
|
|
|
|
end
|
|
|
|
|
2023-05-31 22:10:21 +00:00
|
|
|
def tag_for_emoji(shortcode, emoji)
|
|
|
|
return content_tag(:span, ":#{shortcode}:", translate: 'no') if raw_shortcode?
|
|
|
|
|
2022-03-26 01:53:34 +00:00
|
|
|
original_url, static_url = emoji
|
|
|
|
|
2022-05-09 05:43:08 +00:00
|
|
|
image_tag(
|
|
|
|
animate? ? original_url : static_url,
|
|
|
|
image_attributes.merge(alt: ":#{shortcode}:", title: ":#{shortcode}:", data: image_data_attributes(original_url, static_url))
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def image_attributes
|
|
|
|
{ rel: 'emoji', draggable: false, width: 16, height: 16, class: image_class_names, style: image_style }
|
|
|
|
end
|
|
|
|
|
|
|
|
def image_data_attributes(original_url, static_url)
|
|
|
|
{ original: original_url, static: static_url } unless animate?
|
|
|
|
end
|
|
|
|
|
|
|
|
def image_class_names
|
|
|
|
animate? ? 'emojione' : 'emojione custom-emoji'
|
|
|
|
end
|
|
|
|
|
|
|
|
def image_style
|
|
|
|
@options[:style]
|
2022-03-26 01:53:34 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def animate?
|
2022-05-09 05:43:08 +00:00
|
|
|
@options[:animate] || @options.key?(:style)
|
2022-03-26 01:53:34 +00:00
|
|
|
end
|
2023-05-31 22:10:21 +00:00
|
|
|
|
|
|
|
def raw_shortcode?
|
|
|
|
@options[:raw_shortcode]
|
|
|
|
end
|
2022-03-26 01:53:34 +00:00
|
|
|
end
|