gitlab-org--gitlab-foss/lib/gitlab/quick_actions/extractor.rb
gfyoung f93f8f569d Enable even more frozen string for lib/gitlab
Enables frozen string for the following:

* lib/gitlab/patch/**/*.rb
* lib/gitlab/popen/**/*.rb
* lib/gitlab/profiler/**/*.rb
* lib/gitlab/project_authorizations/**/*.rb
* lib/gitlab/prometheus/**/*.rb
* lib/gitlab/query_limiting/**/*.rb
* lib/gitlab/quick_actions/**/*.rb
* lib/gitlab/redis/**/*.rb
* lib/gitlab/request_profiler/**/*.rb
* lib/gitlab/search/**/*.rb
* lib/gitlab/sherlock/**/*.rb
* lib/gitlab/sidekiq_middleware/**/*.rb
* lib/gitlab/slash_commands/**/*.rb
* lib/gitlab/sql/**/*.rb
* lib/gitlab/template/**/*.rb
* lib/gitlab/testing/**/*.rb
* lib/gitlab/utils/**/*.rb
* lib/gitlab/webpack/**/*.rb

Partially addresses gitlab-org/gitlab-ce#47424.
2018-11-19 18:24:22 -08:00

151 lines
4 KiB
Ruby

# frozen_string_literal: true
module Gitlab
module QuickActions
# This class takes an array of commands that should be extracted from a
# given text.
#
# ```
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
# ```
class Extractor
attr_reader :command_definitions
def initialize(command_definitions)
@command_definitions = command_definitions
end
# Extracts commands from content and return an array of commands.
# The array looks like the following:
# [
# ['command1'],
# ['command3', 'arg1 arg2'],
# ]
# The command and the arguments are stripped.
# The original command text is removed from the given `content`.
#
# Usage:
# ```
# extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
# msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
def extract_commands(content, only: nil)
return [content, []] unless content
content = content.dup
commands = []
content.delete!("\r")
content.gsub!(commands_regex(only: only)) do
if $~[:cmd]
commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
''
else
$~[0]
end
end
content, commands = perform_substitutions(content, commands)
[content.strip, commands]
end
private
# Builds a regular expression to match known commands.
# First match group captures the command name and
# second match group captures its arguments.
#
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
def commands_regex(only:)
names = command_names(limit_to_commands: only).map(&:to_s)
@commands_regex ||= %r{
(?<code>
# Code blocks:
# ```
# Anything, including `/cmd arg` which are ignored by this filter
# ```
^```
.+?
\n```$
)
|
(?<html>
# HTML block:
# <tag>
# Anything, including `/cmd arg` which are ignored by this filter
# </tag>
^<[^>]+?>\n
.+?
\n<\/[^>]+?>$
)
|
(?<html>
# Quote block:
# >>>
# Anything, including `/cmd arg` which are ignored by this filter
# >>>
^>>>
.+?
\n>>>$
)
|
(?:
# Command not in a blockquote, blockcode, or HTML tag:
# /close
^\/
(?<cmd>#{Regexp.new(Regexp.union(names).source, Regexp::IGNORECASE)})
(?:
[ ]
(?<arg>[^\n]*)
)?
(?:\n|$)
)
}mix
end
def perform_substitutions(content, commands)
return unless content
substitution_definitions = self.command_definitions.select do |definition|
definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
end
substitution_definitions.each do |substitution|
match_data = substitution.match(content.downcase)
if match_data
command = [substitution.name.to_s]
command << match_data[1] unless match_data[1].empty?
commands << command
end
content = substitution.perform_substitution(self, content)
end
[content, commands]
end
def command_names(limit_to_commands:)
command_definitions.flat_map do |command|
next if command.noop?
if limit_to_commands && (command.all_names & limit_to_commands).empty?
next
end
command.all_names
end.compact
end
end
end
end