Class: Banzai::Filter::AbstractReferenceFilter
- Inherits:
-
ReferenceFilter
- Object
- HTML::Pipeline::Filter
- ReferenceFilter
- Banzai::Filter::AbstractReferenceFilter
- Includes:
- CrossProjectReference
- Defined in:
- lib/banzai/filter/abstract_reference_filter.rb
Overview
Issues, Merge Requests, Snippets, Commits and Commit Ranges share similar functionality in reference filtering.
Direct Known Subclasses
CommitRangeReferenceFilter, CommitReferenceFilter, DesignReferenceFilter, IssuableReferenceFilter, IterationReferenceFilter, LabelReferenceFilter, MilestoneReferenceFilter, SnippetReferenceFilter
Constant Summary collapse
- REFERENCE_PLACEHOLDER =
REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found reference (which we replace with placeholder during re-scaping). The random number helps ensure it's pretty close to unique. Since it's a transitory value (it never gets saved) we can initialize once, and it doesn't matter if it changes on a restart.
"_reference_#{SecureRandom.hex(16)}_"
- REFERENCE_PLACEHOLDER_PATTERN =
%r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
Class Method Summary collapse
- .identifier(match_data) ⇒ Object
- .object_class ⇒ Object
- .object_name ⇒ Object
- .object_sym ⇒ Object
-
.parse_symbol(symbol, match_data) ⇒ Object
Transform a symbol extracted from the text to a meaningful value In most cases these will be integers, so we call #to_i by default.
-
.references_in(text, pattern = object_class.reference_pattern) ⇒ Object
Public: Find references in text (like `!123` for merge requests).
- .symbol_from_match(match) ⇒ Object
Instance Method Summary collapse
- #call ⇒ Object
- #current_parent_path ⇒ Object
- #current_project_namespace_path ⇒ Object
- #data_attributes_for(text, parent, object, link_content: false, link_reference: false) ⇒ Object
-
#find_for_paths(paths) ⇒ Object
Returns projects for the given paths.
-
#find_object(parent_object, id) ⇒ Object
Implement in child class Example: project.merge_requests.find.
- #find_object_cached(parent_object, id) ⇒ Object
-
#find_object_from_link(parent_object, id) ⇒ Object
Override if the link reference pattern produces a different ID (global ID vs internal ID, for instance) to the regular reference pattern.
- #find_object_from_link_cached(parent_object, id) ⇒ Object
- #from_ref_cached(ref) ⇒ Object
- #identifier(match_data) ⇒ Object
- #object_class ⇒ Object
-
#object_link_filter(text, pattern, link_content: nil, link_reference: false) ⇒ Object
Replace references (like `!123` for merge requests) in text with links to the referenced object's details page.
- #object_link_text(object, matches) ⇒ Object
- #object_link_text_extras(object, matches) ⇒ Object
- #object_link_title(object, matches) ⇒ Object
- #object_sym ⇒ Object
-
#parent_per_reference ⇒ Object
Returns a Hash containing referenced projects grouped per their full path.
-
#record_identifier(record) ⇒ Object
We assume that most classes are identifying records by ID.
- #records_per_parent ⇒ Object
- #references_in(*args, &block) ⇒ Object
-
#references_per_parent ⇒ Object
Returns a Hash containing all object references (e.g. issue IDs) per the project they belong to.
- #relation_for_paths(paths) ⇒ Object
-
#url_for_object(object, parent_object) ⇒ Object
Implement in child class Example: project_merge_request_url.
- #url_for_object_cached(object, parent_object) ⇒ Object
- #wrap_link(link, object) ⇒ Object
Methods included from CrossProjectReference
Methods inherited from ReferenceFilter
call, #call_and_update_nodes, #data_attribute, #each_node, #element_node?, #group, #ignore_ancestor_query, #initialize, #nodes, #project, #reference_class, #replace_link_node_with_href, #replace_link_node_with_text, #replace_text_when_pattern_matches, #skip_project_check?, #text_node?, #unescape_link, #user, #validate, #yield_valid_link
Methods included from OutputSafety
Methods included from RequestStoreReferenceCache
#cached_call, #get_or_set_cache
Constructor Details
This class inherits a constructor from Banzai::Filter::ReferenceFilter
Class Method Details
.identifier(match_data) ⇒ Object
54 55 56 57 58 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 54 def self.identifier(match_data) symbol = symbol_from_match(match_data) parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol) end |
.object_class ⇒ Object
18 19 20 21 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 18 def self.object_class # Implement in child class # Example: MergeRequest end |
.object_name ⇒ Object
23 24 25 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 23 def self.object_name @object_name ||= object_class.name.underscore end |
.object_sym ⇒ Object
27 28 29 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 27 def self.object_sym @object_sym ||= object_name.to_sym end |
.parse_symbol(symbol, match_data) ⇒ Object
Transform a symbol extracted from the text to a meaningful value In most cases these will be integers, so we call #to_i by default
This method has the contract that if a string `ref` refers to a record `record`, then `parse_symbol(ref) == record_identifier(record)`.
74 75 76 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 74 def self.parse_symbol(symbol, match_data) symbol.to_i end |
.references_in(text, pattern = object_class.reference_pattern) ⇒ Object
Public: Find references in text (like `!123` for merge requests)
AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
object = find_object(project_ref, id)
"<a href=...>#{object.to_reference}</a>"
end
text - String text to search.
Yields the String match, the Integer referenced object ID, an optional String of the external project reference, and all of the matchdata.
Returns a String replaced with the return of the block.
44 45 46 47 48 49 50 51 52 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 44 def self.references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match| if ident = identifier($~) yield match, ident, $~[:project], $~[:namespace], $~ else match end end end |
.symbol_from_match(match) ⇒ Object
64 65 66 67 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 64 def self.symbol_from_match(match) key = object_sym match[key] if match.names.include?(key.to_s) end |
Instance Method Details
#call ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 138 def call return doc unless project || group || user ref_pattern = object_class.reference_pattern link_pattern = object_class.link_reference_pattern # Compile often used regexps only once outside of the loop ref_pattern_anchor = /\A#{ref_pattern}\z/ link_pattern_start = /\A#{link_pattern}/ link_pattern_anchor = /\A#{link_pattern}\z/ nodes.each_with_index do |node, index| if text_node?(node) && ref_pattern replace_text_when_pattern_matches(node, index, ref_pattern) do |content| object_link_filter(content, ref_pattern) end elsif element_node?(node) yield_valid_link(node) do |link, inner_html| if ref_pattern && link =~ ref_pattern_anchor replace_link_node_with_href(node, index, link) do object_link_filter(link, ref_pattern, link_content: inner_html) end next end next unless link_pattern if link == inner_html && inner_html =~ link_pattern_start replace_link_node_with_text(node, index) do object_link_filter(inner_html, link_pattern, link_reference: true) end next end if link =~ link_pattern_anchor replace_link_node_with_href(node, index, link) do object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true) end next end end end end doc end |
#current_parent_path ⇒ Object
374 375 376 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 374 def current_parent_path @current_parent_path ||= parent&.full_path end |
#current_project_namespace_path ⇒ Object
378 379 380 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 378 def current_project_namespace_path @current_project_namespace_path ||= project&.namespace&.full_path end |
#data_attributes_for(text, parent, object, link_content: false, link_reference: false) ⇒ Object
252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 252 def data_attributes_for(text, parent, object, link_content: false, link_reference: false) object_parent_type = parent.is_a?(Group) ? :group : :project { original: escape_html_entities(text), link: link_content, link_reference: link_reference, object_parent_type => parent.id, object_sym => object.id } end |
#find_for_paths(paths) ⇒ Object
Returns projects for the given paths.
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 347 def find_for_paths(paths) if Gitlab::SafeRequestStore.active? cache = refs_cache to_query = paths - cache.keys unless to_query.empty? records = relation_for_paths(to_query) found = [] records.each do |record| ref = record.full_path get_or_set_cache(cache, ref) { record } found << ref end not_found = to_query - found not_found.each do |ref| get_or_set_cache(cache, ref) { nil } end end cache.slice(*paths).values.compact else relation_for_paths(paths) end end |
#find_object(parent_object, id) ⇒ Object
Implement in child class Example: project.merge_requests.find
100 101 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 100 def find_object(parent_object, id) end |
#find_object_cached(parent_object, id) ⇒ Object
114 115 116 117 118 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 114 def find_object_cached(parent_object, id) cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do find_object(parent_object, id) end end |
#find_object_from_link(parent_object, id) ⇒ Object
Override if the link reference pattern produces a different ID (global ID vs internal ID, for instance) to the regular reference pattern.
105 106 107 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 105 def find_object_from_link(parent_object, id) find_object(parent_object, id) end |
#find_object_from_link_cached(parent_object, id) ⇒ Object
120 121 122 123 124 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 120 def find_object_from_link_cached(parent_object, id) cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do find_object_from_link(parent_object, id) end end |
#from_ref_cached(ref) ⇒ Object
126 127 128 129 130 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 126 def from_ref_cached(ref) cached_call("banzai_#{parent_type}_refs".to_sym, ref) do parent_from_ref(ref) end end |
#identifier(match_data) ⇒ Object
60 61 62 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 60 def identifier(match_data) self.class.identifier(match_data) end |
#object_class ⇒ Object
86 87 88 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 86 def object_class self.class.object_class end |
#object_link_filter(text, pattern, link_content: nil, link_reference: false) ⇒ Object
Replace references (like `!123` for merge requests) in text with links to the referenced object's details page.
text - String text to replace references in. pattern - Reference pattern to match against. link_content - Original content of the link being replaced. link_reference - True if this was using the link reference pattern,
false otherwise.
Returns a String with references replaced with links. All links have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 200 def object_link_filter(text, pattern, link_content: nil, link_reference: false) references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| parent_path = if parent_type == :group full_group_path(namespace_ref) else full_project_path(namespace_ref, project_ref) end parent = from_ref_cached(parent_path) if parent object = if link_reference find_object_from_link_cached(parent, id) else find_object_cached(parent, id) end end if object title = object_link_title(object, matches) klass = reference_class(object_sym) data_attributes = data_attributes_for(link_content || match, parent, object, link_content: !!link_content, link_reference: link_reference) data = data_attribute(data_attributes) url = if matches.names.include?("url") && matches[:url] matches[:url] else url_for_object_cached(object, parent) end content = link_content || object_link_text(object, matches) link = %(<a href="#{url}" #{data} title="#{escape_once(title)}" class="#{klass}">#{content}</a>) wrap_link(link, object) else match end end end |
#object_link_text(object, matches) ⇒ Object
282 283 284 285 286 287 288 289 290 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 282 def object_link_text(object, matches) parent = project || group || user text = object.reference_link_text(parent) extras = object_link_text_extras(object, matches) text += " (#{extras.join(", ")})" if extras.any? text end |
#object_link_text_extras(object, matches) ⇒ Object
264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 264 def object_link_text_extras(object, matches) extras = [] if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ extras << "comment #{Regexp.last_match(1)}" end extension = matches[:extension] if matches.names.include?("extension") extras << extension if extension extras end |
#object_link_title(object, matches) ⇒ Object
278 279 280 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 278 def object_link_title(object, matches) object.title end |
#object_sym ⇒ Object
90 91 92 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 90 def object_sym self.class.object_sym end |
#parent_per_reference ⇒ Object
Returns a Hash containing referenced projects grouped per their full path.
324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 324 def parent_per_reference @per_reference ||= {} @per_reference[parent_type] ||= begin refs = Set.new references_per_parent.each do |ref, _| refs << ref end find_for_paths(refs.to_a).index_by(&:full_path) end end |
#record_identifier(record) ⇒ Object
We assume that most classes are identifying records by ID.
This method has the contract that if a string `ref` refers to a record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
82 83 84 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 82 def record_identifier(record) record.id end |
#records_per_parent ⇒ Object
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 382 def records_per_parent @_records_per_project ||= {} @_records_per_project[object_class.to_s.underscore] ||= begin hash = Hash.new { |h, k| h[k] = {} } parent_per_reference.each do |path, parent| record_ids = references_per_parent[path] parent_records(parent, record_ids).each do |record| hash[parent][record_identifier(record)] = record end end hash end end |
#references_in(*args, &block) ⇒ Object
94 95 96 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 94 def references_in(*args, &block) self.class.references_in(*args, &block) end |
#references_per_parent ⇒ Object
Returns a Hash containing all object references (e.g. issue IDs) per the project they belong to.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 294 def references_per_parent @references_per ||= {} @references_per[parent_type] ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } regex = [ object_class.link_reference_pattern, object_class.reference_pattern ].compact.reduce { |a, b| Regexp.union(a, b) } nodes.each do |node| node.to_html.scan(regex) do path = if parent_type == :project full_project_path($~[:namespace], $~[:project]) else full_group_path($~[:group]) end if ident = identifier($~) refs[path] << ident end end end refs end end |
#relation_for_paths(paths) ⇒ Object
338 339 340 341 342 343 344 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 338 def relation_for_paths(paths) klass = parent_type.to_s.camelize.constantize result = klass.where_full_path_in(paths) return result if parent_type == :group result.includes(:namespace) if parent_type == :project end |
#url_for_object(object, parent_object) ⇒ Object
Implement in child class Example: project_merge_request_url
111 112 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 111 def url_for_object(object, parent_object) end |
#url_for_object_cached(object, parent_object) ⇒ Object
132 133 134 135 136 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 132 def url_for_object_cached(object, parent_object) cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do url_for_object(object, parent_object) end end |
#wrap_link(link, object) ⇒ Object
248 249 250 |
# File 'lib/banzai/filter/abstract_reference_filter.rb', line 248 def wrap_link(link, object) link end |