Commit 06655b7f authored by Peter Leitzen's avatar Peter Leitzen

Merge branch 'custom-emoji-banzai-filter' into 'master'

Add Custom emoji banzai filter

See merge request gitlab-org/gitlab!47122
parents 79fc7dd6 2d5b14e3
......@@ -17,6 +17,10 @@ class GlEmoji extends HTMLElement {
if (emojiInfo) {
if (name !== emojiInfo.name) {
if (emojiInfo.fallback && this.innerHTML) {
return; // When fallback emoji is used, but there is a <img> provided, use the <img> instead
}
({ name } = emojiInfo);
this.dataset.name = emojiInfo.name;
}
......
......@@ -181,6 +181,11 @@ export function searchEmoji(query, opts) {
} = opts || {};
const fallbackEmoji = emojiMap.grey_question;
if (fallbackEmoji) {
fallbackEmoji.fallback = true;
}
if (!query) {
if (fallback) {
return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji];
......
# frozen_string_literal: true
class CustomEmoji < ApplicationRecord
NAME_REGEXP = /[a-z0-9_-]+/.freeze
belongs_to :namespace, inverse_of: :custom_emoji
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
......@@ -17,7 +19,12 @@ class CustomEmoji < ApplicationRecord
uniqueness: { scope: [:namespace_id, :name] },
presence: true,
length: { maximum: 36 },
format: { with: /\A[a-z0-9][a-z0-9\-_]*[a-z0-9]\z/ }
format: { with: /\A#{NAME_REGEXP}\z/ }
scope :by_name, -> (names) { where(name: names) }
alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467
private
......
---
title: Add Banzai filter for CustomEmoji
merge_request: 47122
author: Rajendra Kadam
type: added
# frozen_string_literal: true
module Banzai
module Filter
class CustomEmojiFilter < HTML::Pipeline::Filter
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
return doc unless context[:project]
return doc unless Feature.enabled?(:custom_emoji, context[:project])
doc.search(".//text()").each do |node|
content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless content.include?(':')
next unless namespace && namespace.custom_emoji.any?
html = custom_emoji_name_element_filter(content)
node.replace(html) unless html == content
end
doc
end
def custom_emoji_pattern
@emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
:(#{CustomEmoji::NAME_REGEXP}):
(?=[^[:alnum:]:]|$)/x
end
def custom_emoji_name_element_filter(text)
text.gsub(custom_emoji_pattern) do |match|
name = Regexp.last_match[1]
custom_emoji = all_custom_emoji[name]
if custom_emoji
Gitlab::Emoji.custom_emoji_tag(custom_emoji.name, custom_emoji.url)
else
match
end
end
end
private
def namespace
context[:project].namespace.root_ancestor
end
def custom_emoji_candidates
doc.to_html.scan(/:(#{CustomEmoji::NAME_REGEXP}):/).flatten
end
def all_custom_emoji
@all_custom_emoji ||= namespace.custom_emoji.by_name(custom_emoji_candidates).index_by(&:name)
end
end
end
end
......@@ -34,6 +34,7 @@ module Banzai
Filter::FootnoteFilter,
*reference_filters,
Filter::EmojiFilter,
Filter::CustomEmojiFilter,
Filter::TaskListFilter,
Filter::InlineDiffFilter,
Filter::SetDirectionFilter
......
......@@ -63,6 +63,16 @@ module Gitlab
ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], options)
end
def custom_emoji_tag(name, image_source)
data = {
name: name
}
ActionController::Base.helpers.content_tag('gl-emoji', title: name, data: data) do
emoji_image_tag(name, image_source).html_safe
end
end
private
def emoji_unicode_versions_by_name
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::CustomEmojiFilter do
include FilterSpecHelper
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) }
let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') }
it 'replaces supported name custom emoji' do
doc = filter('<p>:tanuki:</p>', project: project)
expect(doc.css('gl-emoji').first.attributes['title'].value).to eq('tanuki')
expect(doc.css('gl-emoji img').size).to eq 1
end
it 'ignores non existent custom emoji' do
exp = act = '<p>:foo:</p>'
doc = filter(act)
expect(doc.to_html).to match Regexp.escape(exp)
end
it 'correctly uses the custom emoji URL' do
doc = filter('<p>:tanuki:</p>')
expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file)
end
it 'matches with adjacent text' do
doc = filter('tanuki (:tanuki:)')
expect(doc.css('img').size).to eq 1
end
it 'matches multiple same custom emoji' do
doc = filter(':tanuki: :tanuki:')
expect(doc.css('img').size).to eq 2
end
it 'matches multiple custom emoji' do
doc = filter(':tanuki: (:happy_tanuki:)')
expect(doc.css('img').size).to eq 2
end
it 'does not match enclosed colons' do
doc = filter('tanuki:tanuki:')
expect(doc.css('img').size).to be 0
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :tanuki:, big time.')
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'does not match emoji in a string' do
doc = filter("'2a00:tanuki:100::1'")
expect(doc.css('gl-emoji').size).to eq 0
end
it 'does not do N+1 query' do
create(:custom_emoji, name: 'party-parrot', group: group)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
filter('<p>:tanuki:</p>')
end
expect do
filter('<p>:tanuki: :party-parrot:</p>')
end.not_to exceed_all_query_limit(control_count.count)
end
end
......@@ -3,11 +3,14 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
before do
stub_commonmark_sourcepos_disabled
end
subject { described_class.to_html(exp, project: spy) }
subject { described_class.to_html(exp, project: project) }
context "allows `a` elements" do
let(:exp) { "<a>Link</a>" }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment