1# frozen_string_literal: true 2 3module Banzai 4 module Filter 5 class CustomEmojiFilter < HTML::Pipeline::Filter 6 include Gitlab::Utils::StrongMemoize 7 8 IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set 9 10 def call 11 return doc unless context[:project] 12 return doc unless Feature.enabled?(:custom_emoji, context[:project]) 13 14 doc.xpath('descendant-or-self::text()').each do |node| 15 content = node.to_html 16 17 next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) 18 next unless content.include?(':') 19 next unless has_custom_emoji? 20 21 html = custom_emoji_name_element_filter(content) 22 23 node.replace(html) unless html == content 24 end 25 26 doc 27 end 28 29 def custom_emoji_pattern 30 @emoji_pattern ||= 31 /(?<=[^[:alnum:]:]|\n|^) 32 :(#{CustomEmoji::NAME_REGEXP}): 33 (?=[^[:alnum:]:]|$)/x 34 end 35 36 def custom_emoji_name_element_filter(text) 37 text.gsub(custom_emoji_pattern) do |match| 38 name = Regexp.last_match[1] 39 custom_emoji = all_custom_emoji[name] 40 41 if custom_emoji 42 Gitlab::Emoji.custom_emoji_tag(custom_emoji.name, custom_emoji.url) 43 else 44 match 45 end 46 end 47 end 48 49 private 50 51 def has_custom_emoji? 52 strong_memoize(:has_custom_emoji) do 53 namespace&.custom_emoji&.any? 54 end 55 end 56 57 def namespace 58 context[:project].namespace.root_ancestor 59 end 60 61 def custom_emoji_candidates 62 doc.to_html.scan(/:(#{CustomEmoji::NAME_REGEXP}):/).flatten 63 end 64 65 def all_custom_emoji 66 @all_custom_emoji ||= namespace.custom_emoji.by_name(custom_emoji_candidates).index_by(&:name) 67 end 68 end 69 end 70end 71