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