summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/gitlab/markdown/emoji_filter.rb79
-rw-r--r--spec/lib/gitlab/markdown/emoji_filter_spec.rb97
2 files changed, 176 insertions, 0 deletions
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/emoji_filter.rb
new file mode 100644
index 00000000000..e239f766844
--- /dev/null
+++ b/lib/gitlab/markdown/emoji_filter.rb
@@ -0,0 +1,79 @@
+require 'gitlab_emoji'
+require 'html/pipeline/filter'
+require 'action_controller'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces :emoji: with images.
+ #
+ # Based on HTML::Pipeline::EmojiFilter
+ #
+ # Context options:
+ # :asset_root
+ # :asset_host
+ class EmojiFilter < HTML::Pipeline::Filter
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+ next unless content.include?(':')
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ html = emoji_image_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Replace :emoji: with corresponding images.
+ #
+ # text - String text to replace :emoji: in.
+ #
+ # Returns a String with :emoji: replaced with images.
+ def emoji_image_filter(text)
+ text.gsub(emoji_pattern) do |match|
+ name = $1
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+ end
+ end
+
+ private
+
+ def emoji_url(name)
+ emoji_path = "emoji/#{emoji_filename(name)}"
+ if context[:asset_host]
+ # Asset host is specified.
+ url_to_image(emoji_path)
+ elsif context[:asset_root]
+ # Gitlab url is specified
+ File.join(context[:asset_root], url_to_image(emoji_path))
+ else
+ # All other cases
+ url_to_image(emoji_path)
+ end
+ end
+
+ def url_to_image(image)
+ ActionController::Base.helpers.url_to_image(image)
+ end
+
+ # Build a regexp that matches all valid :emoji: names.
+ def self.emoji_pattern
+ @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ end
+
+ def emoji_pattern
+ self.class.emoji_pattern
+ end
+
+ def emoji_filename(name)
+ "#{Emoji.emoji_filename(name)}.png"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
new file mode 100644
index 00000000000..18d55c4818f
--- /dev/null
+++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe EmojiFilter do
+ def filter(html, contexts = {})
+ described_class.call(html, contexts)
+ end
+
+ before do
+ ActionController::Base.asset_host = 'https://foo.com'
+ end
+
+ it 'replaces supported emoji' do
+ doc = filter('<p>:heart:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
+ end
+
+ it 'ignores unsupported emoji' do
+ exp = act = '<p>:foo:</p>'
+ doc = filter(act)
+ expect(doc.to_html).to match Regexp.escape(exp)
+ end
+
+ it 'correctly encodes the URL' do
+ doc = filter('<p>:+1:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
+ end
+
+ it 'matches at the start of a string' do
+ doc = filter(':+1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches at the end of a string' do
+ doc = filter('This gets a :-1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter('+1 (:+1:)')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches multiple emoji in a row' do
+ doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
+ expect(doc.css('img').size).to eq 3
+ end
+
+ it 'has a title attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('title')).to eq ':-1:'
+ end
+
+ it 'has an alt attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('alt')).to eq ':-1:'
+ end
+
+ it 'has an align attribute' do
+ doc = filter(':8ball:')
+ expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+ end
+
+ it 'has an emoji class' do
+ doc = filter(':cat:')
+ expect(doc.css('img').first.attr('class')).to eq 'emoji'
+ end
+
+ it 'has height and width attributes' do
+ doc = filter(':dog:')
+ img = doc.css('img').first
+
+ expect(img.attr('width')).to eq '20'
+ expect(img.attr('height')).to eq '20'
+ end
+
+ it 'keeps whitespace intact' do
+ doc = filter('This deserves a :+1:, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ end
+
+ it 'uses a custom asset_root context' do
+ root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+ doc = filter(':smile:', asset_root: root)
+ expect(doc.css('img').first.attr('src')).to start_with(root)
+ end
+
+ it 'uses a custom asset_host context' do
+ ActionController::Base.asset_host = 'https://cdn.example.com'
+
+ doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
+ expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ end
+ end
+end