emoji_formatter.rb 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. # frozen_string_literal: true
  2. class EmojiFormatter
  3. include RoutingHelper
  4. DISALLOWED_BOUNDING_REGEX = /[[:alnum:]:]/
  5. attr_reader :html, :custom_emojis, :options
  6. # @param [ActiveSupport::SafeBuffer] html
  7. # @param [Array<CustomEmoji>] custom_emojis
  8. # @param [Hash] options
  9. # @option options [Boolean] :animate
  10. # @option options [String] :style
  11. # @option options [String] :raw_shortcode
  12. def initialize(html, custom_emojis, options = {})
  13. raise ArgumentError unless html.html_safe?
  14. @html = html
  15. @custom_emojis = custom_emojis
  16. @options = options
  17. end
  18. def to_s
  19. return html if custom_emojis.empty? || html.blank?
  20. tree = Nokogiri::HTML.fragment(html)
  21. tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
  22. i = -1
  23. inside_shortname = false
  24. shortname_start_index = -1
  25. last_index = 0
  26. text = node.content
  27. result = Nokogiri::XML::NodeSet.new(tree.document)
  28. while i + 1 < text.size
  29. i += 1
  30. if inside_shortname && text[i] == ':'
  31. inside_shortname = false
  32. shortcode = text[shortname_start_index + 1..i - 1]
  33. char_after = text[i + 1]
  34. next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
  35. result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
  36. result << Nokogiri::HTML.fragment(tag_for_emoji(shortcode, emoji))
  37. last_index = i + 1
  38. elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
  39. inside_shortname = true
  40. shortname_start_index = i
  41. end
  42. end
  43. result << Nokogiri::XML::Text.new(text[last_index..], tree.document)
  44. node.replace(result)
  45. end
  46. tree.to_html.html_safe # rubocop:disable Rails/OutputSafety
  47. end
  48. private
  49. def emoji_map
  50. @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))] }
  51. end
  52. def count_tag_nesting(tag)
  53. if tag[1] == '/'
  54. -1
  55. elsif tag[-2] == '/'
  56. 0
  57. else
  58. 1
  59. end
  60. end
  61. def tag_for_emoji(shortcode, emoji)
  62. return content_tag(:span, ":#{shortcode}:", translate: 'no') if raw_shortcode?
  63. original_url, static_url = emoji
  64. image_tag(
  65. animate? ? original_url : static_url,
  66. image_attributes.merge(alt: ":#{shortcode}:", title: ":#{shortcode}:", data: image_data_attributes(original_url, static_url))
  67. )
  68. end
  69. def image_attributes
  70. { rel: 'emoji', draggable: false, width: 16, height: 16, class: image_class_names, style: image_style }
  71. end
  72. def image_data_attributes(original_url, static_url)
  73. { original: original_url, static: static_url } unless animate?
  74. end
  75. def image_class_names
  76. animate? ? 'emojione' : 'emojione custom-emoji'
  77. end
  78. def image_style
  79. @options[:style]
  80. end
  81. def animate?
  82. @options[:animate] || @options.key?(:style)
  83. end
  84. def raw_shortcode?
  85. @options[:raw_shortcode]
  86. end
  87. end