@mention_filter.rb 4.89 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
require 'set'

module HTML
  class Pipeline
    # HTML filter that replaces @user mentions with links. Mentions within <pre>,
    # <code>, and <a> elements are ignored. Mentions that reference users that do
    # not exist are ignored.
    #
    # Context options:
    #   :base_url - Used to construct links to user profile pages for each
    #               mention.
    #   :info_url - Used to link to "more info" when someone mentions @mention
    #               or @mentioned.
14 15
    #   :username_pattern - Used to provide a custom regular expression to
    #                       identify usernames
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
    #
    class MentionFilter < Filter
      # Public: Find user @mentions in text.  See
      # MentionFilter#mention_link_filter.
      #
      #   MentionFilter.mentioned_logins_in(text) do |match, login, is_mentioned|
      #     "<a href=...>#{login}</a>"
      #   end
      #
      # text - String text to search.
      #
      # Yields the String match, the String login name, and a Boolean determining
      # if the match = "@mention[ed]".  The yield's return replaces the match in
      # the original text.
      #
      # Returns a String replaced with the return of the block.
32 33 34
      def self.mentioned_logins_in(text, username_pattern = UsernamePattern)
        text.gsub MentionPatterns[username_pattern] do |match|
          login = Regexp.last_match(1)
35 36 37 38
          yield match, login, MentionLogins.include?(login.downcase)
        end
      end

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
      # Hash that contains all of the mention patterns used by the pipeline
      MentionPatterns = Hash.new do |hash, key|
        hash[key] = /
          (?:^|\W)                    # beginning of string or non-word char
          @((?>#{key}))  # @username
          (?!\/)                      # without a trailing slash
          (?=
            \.+[ \t\W]|               # dots followed by space or non-word character
            \.+$|                     # dots at end of line
            [^0-9a-zA-Z_.]|           # non-word character except dot
            $                         # end of line
          )
        /ix
      end

      # Default pattern used to extract usernames from text. The value can be
      # overriden by providing the username_pattern variable in the context.
      UsernamePattern = /[a-z0-9][a-z0-9-]*/
57 58 59

      # List of username logins that, when mentioned, link to the blog post
      # about @mentions instead of triggering a real mention.
60
      MentionLogins = %w[
61 62 63 64
        mention
        mentions
        mentioned
        mentioning
65
      ].freeze
66 67

      # Don't look for mentions in text nodes that are children of these elements
68
      IGNORE_PARENTS = %w(pre code a style script).to_set
69 70 71 72

      def call
        result[:mentioned_usernames] ||= []

73
        doc.search('.//text()').each do |node|
74
          content = node.to_html
75
          next unless content.include?('@')
76
          next if has_ancestor?(node, IGNORE_PARENTS)
77
          html = mention_link_filter(content, base_url, info_url, username_pattern)
78 79 80 81 82 83 84 85 86 87 88 89
          next if html == content
          node.replace(html)
        end
        doc
      end

      # The URL to provide when someone @mentions a "mention" name, such as
      # @mention or @mentioned, that will give them more info on mentions.
      def info_url
        context[:info_url] || nil
      end

90 91 92 93
      def username_pattern
        context[:username_pattern] || UsernamePattern
      end

94 95 96 97 98 99 100
      # Replace user @mentions in text with links to the mentioned user's
      # profile page.
      #
      # text      - String text to replace @mention usernames in.
      # base_url  - The base URL used to construct user profile URLs.
      # info_url  - The "more info" URL used to link to more info on @mentions.
      #             If nil we don't link @mention or @mentioned.
101 102
      # username_pattern  - Regular expression used to identify usernames in
      #                     text
103 104 105
      #
      # Returns a string with @mentions replaced with links. All links have a
      # 'user-mention' class name attached for styling.
106 107
      def mention_link_filter(text, _base_url = '/', info_url = nil, username_pattern = UsernamePattern)
        self.class.mentioned_logins_in(text, username_pattern) do |match, login, is_mentioned|
108 109 110 111 112 113 114 115 116 117 118
          link =
            if is_mentioned
              link_to_mention_info(login, info_url)
            else
              link_to_mentioned_user(login)
            end

          link ? match.sub("@#{login}", link) : match
        end
      end

119
      def link_to_mention_info(text, info_url = nil)
120
        return "@#{text}" if info_url.nil?
121 122 123
        "<a href='#{info_url}' class='user-mention'>" \
          "@#{text}" \
          '</a>'
124 125 126 127
      end

      def link_to_mentioned_user(login)
        result[:mentioned_usernames] |= [login]
128 129 130 131 132 133 134

        url = base_url.dup
        url << '/' unless url =~ /[\/~]\z/

        "<a href='#{url << login}' class='user-mention'>" \
          "@#{login}" \
          '</a>'
135 136 137 138
      end
    end
  end
end