1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
# frozen_string_literal: true
module Banzai
module Filter
# HTML filter that appends extra information to issuable links.
# Runs as a post-process filter as issuable might change while
# Markdown is in the cache.
#
# This filter supports cross-project references.
class IssuableReferenceExpansionFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize
NUMBER_OF_SUMMARY_ASSIGNEES = 2
VISIBLE_STATES = %w(closed merged).freeze
EXTENDED_FORMAT_XPATH = Gitlab::Utils::Nokogiri.css_to_xpath('a[data-reference-format="+s"]')
def call
return doc unless context[:issuable_reference_expansion_enabled]
options = { extended_preload: doc.xpath(EXTENDED_FORMAT_XPATH).present? }
extractor_context = RenderContext.new(project, current_user, options: options)
extractor = Banzai::IssuableExtractor.new(extractor_context)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
next if !can_read_cross_project? && cross_referenced?(issuable)
next unless should_expand?(node, issuable)
case node.attr('data-reference-format')
when '+'
expand_reference_with_title_and_state(node, issuable)
when '+s'
expand_reference_with_title_and_state(node, issuable)
expand_reference_with_summary(node, issuable)
else
expand_reference_with_state(node, issuable)
end
end
doc
end
private
# Example: Issue Title (#123 - closed)
def expand_reference_with_title_and_state(node, issuable)
node.content = "#{expand_emoji(issuable.title).truncate(50)} (#{node.content}"
node.content += " - #{issuable_state_text(issuable)}" if VISIBLE_STATES.include?(issuable.state)
node.content += ')'
end
# rubocop:disable Style/AsciiComments
# Example: Issue Title (#123 - closed) assignee name 1, assignee name 2+ • v15.9 • On track
def expand_reference_with_summary(node, issuable)
summary = []
summary << assignees_text(issuable) if issuable.supports_assignee?
summary << milestone_text(issuable.milestone) if issuable.supports_milestone?
summary << health_status_text(issuable.health_status) if issuable.supports_health_status?
node.content = [node.content, *summary].compact_blank.join(' • ')
end
# rubocop:enable Style/AsciiComments
# Example: #123 (closed)
def expand_reference_with_state(node, issuable)
node.content += " (#{issuable_state_text(issuable)})"
end
def assignees_text(issuable)
assignee_names = issuable.assignees.first(NUMBER_OF_SUMMARY_ASSIGNEES + 1).map(&:sanitize_name)
return _('Unassigned') if assignee_names.empty?
"#{assignee_names.first(NUMBER_OF_SUMMARY_ASSIGNEES).to_sentence(two_words_connector: ', ')}" \
"#{assignee_names.size > NUMBER_OF_SUMMARY_ASSIGNEES ? '+' : ''}"
end
def milestone_text(milestone)
milestone&.title
end
def health_status_text(health_status)
health_status&.humanize
end
def issuable_state_text(issuable)
moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state
end
def moved_issue?(issuable)
issuable.is_a?(Issue) && issuable.moved?
end
def should_expand?(node, issuable)
# We add this extra check to avoid unescaping HTML and generating reference link text for every reference
return unless node.attr('data-reference-format').present? || VISIBLE_STATES.include?(issuable.state)
CGI.unescapeHTML(node.inner_html) == issuable.reference_link_text(project || group)
end
def cross_referenced?(issuable)
return true if issuable.project != project
return true if issuable.respond_to?(:group) && issuable.group != group
false
end
def can_read_cross_project?
strong_memoize(:can_read_cross_project) do
Ability.allowed?(current_user, :read_cross_project)
end
end
def current_user
context[:current_user]
end
def project
context[:project]
end
def group
context[:group]
end
def expand_emoji(string)
string.gsub(/(?<!\w):(\w+):(?!\w)/) do |match|
emoji_codepoint = TanukiEmoji.find_by_alpha_code(::Regexp.last_match(1))&.codepoints
!emoji_codepoint.nil? ? emoji_codepoint : match
end
end
end
end
end
|