emoji_formatter.rb 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. # frozen_string_literal: true
  2. class EmojiFormatter
  3. include RoutingHelper
  4. DISALLOWED_BOUNDING_REGEX = /[[:alnum:]:]/.freeze
  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. def initialize(html, custom_emojis, options = {})
  12. raise ArgumentError unless html.html_safe?
  13. @html = html
  14. @custom_emojis = custom_emojis
  15. @options = options
  16. end
  17. def to_s
  18. return html if custom_emojis.empty? || html.blank?
  19. i = -1
  20. tag_open_index = nil
  21. inside_shortname = false
  22. shortname_start_index = -1
  23. invisible_depth = 0
  24. last_index = 0
  25. result = ''.dup
  26. while i + 1 < html.size
  27. i += 1
  28. if invisible_depth.zero? && inside_shortname && html[i] == ':'
  29. inside_shortname = false
  30. shortcode = html[shortname_start_index + 1..i - 1]
  31. char_after = html[i + 1]
  32. next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
  33. result << html[last_index..shortname_start_index - 1] if shortname_start_index.positive?
  34. result << image_for_emoji(shortcode, emoji)
  35. last_index = i + 1
  36. elsif tag_open_index && html[i] == '>'
  37. tag = html[tag_open_index..i]
  38. tag_open_index = nil
  39. if invisible_depth.positive?
  40. invisible_depth += count_tag_nesting(tag)
  41. elsif tag == '<span class="invisible">'
  42. invisible_depth = 1
  43. end
  44. elsif html[i] == '<'
  45. tag_open_index = i
  46. inside_shortname = false
  47. elsif !tag_open_index && html[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(html[i - 1]))
  48. inside_shortname = true
  49. shortname_start_index = i
  50. end
  51. end
  52. result << html[last_index..-1]
  53. result.html_safe # rubocop:disable Rails/OutputSafety
  54. end
  55. private
  56. def emoji_map
  57. @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))] }
  58. end
  59. def count_tag_nesting(tag)
  60. if tag[1] == '/'
  61. -1
  62. elsif tag[-2] == '/'
  63. 0
  64. else
  65. 1
  66. end
  67. end
  68. def image_for_emoji(shortcode, emoji)
  69. original_url, static_url = emoji
  70. image_tag(
  71. animate? ? original_url : static_url,
  72. image_attributes.merge(alt: ":#{shortcode}:", title: ":#{shortcode}:", data: image_data_attributes(original_url, static_url))
  73. )
  74. end
  75. def image_attributes
  76. { rel: 'emoji', draggable: false, width: 16, height: 16, class: image_class_names, style: image_style }
  77. end
  78. def image_data_attributes(original_url, static_url)
  79. { original: original_url, static: static_url } unless animate?
  80. end
  81. def image_class_names
  82. animate? ? 'emojione' : 'emojione custom-emoji'
  83. end
  84. def image_style
  85. @options[:style]
  86. end
  87. def animate?
  88. @options[:animate] || @options.key?(:style)
  89. end
  90. end