From 45177e78b2dd06d60c89093d0d45651631623766 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Feb 2017 16:23:48 +0200 Subject: Add GFM support to nested groups Signed-off-by: Dmitriy Zaporozhets --- app/models/concerns/mentionable.rb | 3 +- app/models/group.rb | 9 +++- app/services/projects/participants_service.rb | 2 +- lib/banzai/filter/abstract_reference_filter.rb | 2 +- lib/banzai/filter/group_reference_filter.rb | 54 ++++++++++++++++++++ lib/banzai/filter/user_reference_filter.rb | 56 ++++++++------------ lib/banzai/pipeline/gfm_pipeline.rb | 1 + lib/banzai/pipeline/single_line_pipeline.rb | 1 + lib/banzai/reference_parser/group_parser.rb | 7 +++ lib/gitlab/reference_extractor.rb | 2 +- lib/gitlab/regex.rb | 1 + .../banzai/filter/group_reference_filter_spec.rb | 59 ++++++++++++++++++++++ .../banzai/filter/user_reference_filter_spec.rb | 25 +-------- 13 files changed, 159 insertions(+), 63 deletions(-) create mode 100644 lib/banzai/filter/group_reference_filter.rb create mode 100644 lib/banzai/reference_parser/group_parser.rb create mode 100644 spec/lib/banzai/filter/group_reference_filter_spec.rb diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index ef2c1e5d414..25efde47353 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -62,7 +62,8 @@ module Mentionable end def mentioned_users(current_user = nil) - all_references(current_user).users + refs = all_references(current_user) + refs.users + refs.groups end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. diff --git a/app/models/group.rb b/app/models/group.rb index a5b92283daa..31878dfd1b0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -62,7 +62,12 @@ class Group < Namespace end def reference_pattern - User.reference_pattern + x = Gitlab::Regex::FULL_PATH_REGEX_STR + + %r{ + #{Regexp.escape(reference_prefix)} + (?#{x}) + }x end def visible_to_user(user) @@ -81,7 +86,7 @@ class Group < Namespace end def to_reference(_from_project = nil, full: nil) - "#{self.class.reference_prefix}#{name}" + "#{self.class.reference_prefix}#{full_path}" end def web_url diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 96c363c8d1a..e6193fcacee 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -36,7 +36,7 @@ module Projects def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url } + { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url } end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index a3d495a5da0..955d857c679 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -285,7 +285,7 @@ module Banzai end def current_project_namespace_path - @current_project_namespace_path ||= project.namespace.path + @current_project_namespace_path ||= project.namespace.full_path end private diff --git a/lib/banzai/filter/group_reference_filter.rb b/lib/banzai/filter/group_reference_filter.rb new file mode 100644 index 00000000000..f9a3886f3f0 --- /dev/null +++ b/lib/banzai/filter/group_reference_filter.rb @@ -0,0 +1,54 @@ +module Banzai + module Filter + # HTML filter that replaces user or group references with links. + # + # A special `@all` reference is also supported. + class GroupReferenceFilter < UserReferenceFilter + self.reference_type = :group + + def self.reference_pattern + Group.reference_pattern + end + + # Replace `@group` group references in text with links to the referenced + # group's landing page. + # + # text - String text to replace references in. + # link_content - Original content of the link being replaced. + # + # Returns a String with `@group` references replaced with links. All links + # have `gfm` and `gfm-project_member` class names attached for styling. + def user_link_filter(text, link_content: nil) + self.class.references_in(text) do |match, username| + if group = groups[username] + link_to_group(group, link_content: link_content) || match + else + match + end + end + end + + # Returns a Hash containing all Group objects for the username + # references in the current document. + # + # The keys of this Hash are the group paths, the values the + # corresponding Group objects. + def groups + @groups ||= + Group.where_full_path_in(usernames).each_with_object({}) do |row, hash| + hash[row.full_path] = row + end + end + + private + + def link_to_group(group, link_content: nil) + url = urls.group_url(group, only_path: context[:only_path]) + data = data_attribute(group: group.id) + content = link_content || Group.reference_prefix + group.full_path + + link_tag(url, data, content, group.name) + end + end + end +end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 1aa9355b256..d589288eecc 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -6,6 +6,10 @@ module Banzai class UserReferenceFilter < ReferenceFilter self.reference_type = :user + def self.reference_pattern + User.reference_pattern + end + # Public: Find `@user` user references in text # # UserReferenceFilter.references_in(text) do |match, username| @@ -18,15 +22,15 @@ module Banzai # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(User.reference_pattern) do |match| - yield match, $~[:user] + text.gsub(reference_pattern) do |match| + yield match, $~[reference_type] end end def call return doc if project.nil? && !skip_project_check? - ref_pattern = User.reference_pattern + ref_pattern = self.class.reference_pattern ref_pattern_start = /\A#{ref_pattern}\z/ nodes.each do |node| @@ -60,23 +64,23 @@ module Banzai self.class.references_in(text) do |match, username| if username == 'all' && !skip_project_check? link_to_all(link_content: link_content) - elsif namespace = namespaces[username] - link_to_namespace(namespace, link_content: link_content) || match + elsif user = users[username] + link_to_user(user, link_content: link_content) || match else match end end end - # Returns a Hash containing all Namespace objects for the username + # Returns a Hash containing all User objects for the username # references in the current document. # - # The keys of this Hash are the namespace paths, the values the - # corresponding Namespace objects. - def namespaces - @namespaces ||= - Namespace.where(path: usernames).each_with_object({}) do |row, hash| - hash[row.path] = row + # The keys of this Hash are the user paths, the values the + # corresponding User objects. + def users + @users ||= + User.where(username: usernames).each_with_object({}) do |row, hash| + hash[row.username] = row end end @@ -85,8 +89,8 @@ module Banzai refs = Set.new nodes.each do |node| - node.to_html.scan(User.reference_pattern) do - refs << $~[:user] + node.to_html.scan(self.class.reference_pattern) do + refs << $~[self.class.reference_type] end end @@ -120,28 +124,12 @@ module Banzai end end - def link_to_namespace(namespace, link_content: nil) - if namespace.is_a?(Group) - link_to_group(namespace.path, namespace, link_content: link_content) - else - link_to_user(namespace.path, namespace, link_content: link_content) - end - end - - def link_to_group(group, namespace, link_content: nil) - url = urls.group_url(group, only_path: context[:only_path]) - data = data_attribute(group: namespace.id) - content = link_content || Group.reference_prefix + group - - link_tag(url, data, content, namespace.name) - end - - def link_to_user(user, namespace, link_content: nil) + def link_to_user(user, link_content: nil) url = urls.user_url(user, only_path: context[:only_path]) - data = data_attribute(user: namespace.owner_id) - content = link_content || User.reference_prefix + user + data = data_attribute(user: user.id) + content = link_content || User.reference_prefix + user.username - link_tag(url, data, content, namespace.owner_name) + link_tag(url, data, content, user.name) end def link_tag(url, data, link_content, title) diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index b25d6f18d59..091c831fa89 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -23,6 +23,7 @@ module Banzai Filter::ExternalLinkFilter, Filter::UserReferenceFilter, + Filter::GroupReferenceFilter, Filter::IssueReferenceFilter, Filter::ExternalIssueReferenceFilter, Filter::MergeRequestReferenceFilter, diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 1929099931b..5880b8f9246 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -11,6 +11,7 @@ module Banzai Filter::ExternalLinkFilter, Filter::UserReferenceFilter, + Filter::GroupReferenceFilter, Filter::IssueReferenceFilter, Filter::ExternalIssueReferenceFilter, Filter::MergeRequestReferenceFilter, diff --git a/lib/banzai/reference_parser/group_parser.rb b/lib/banzai/reference_parser/group_parser.rb new file mode 100644 index 00000000000..df74cebebba --- /dev/null +++ b/lib/banzai/reference_parser/group_parser.rb @@ -0,0 +1,7 @@ +module Banzai + module ReferenceParser + class GroupParser < UserParser + self.reference_type = :group + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 11c0b01f0dc..d764d60438d 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,7 +1,7 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor - REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range) + REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range group) attr_accessor :project, :current_user, :author def initialize(project, current_user = nil) diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index a3fa7c1331a..e8406b46468 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -9,6 +9,7 @@ module Gitlab # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze + FULL_PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.\/]*'.freeze NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?#{group.to_reference}

") + filter = described_class.new(document, project: project) + + expect(filter.groups).to eq({ group.path => group }) + end + end +end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 3e1ac9fb2b2..ac0ca7e2c64 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -92,26 +92,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end - context 'mentioning a group' do - it_behaves_like 'a reference containing an element node' - - let(:group) { create(:group) } - let(:reference) { group.to_reference } - - it 'links to the Group' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) - end - - it 'includes a data-group attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-group') - expect(link.attr('data-group')).to eq group.id.to_s - end - end - it 'links with adjacent text' do doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) @@ -176,13 +156,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end end - describe '#namespaces' do + describe '#users' do it 'returns a Hash containing all Namespaces' do document = Nokogiri::HTML.fragment("

#{user.to_reference}

") filter = described_class.new(document, project: project) - ns = user.namespace - expect(filter.namespaces).to eq({ ns.path => ns }) + expect(filter.users).to eq({ user.username => user }) end end -- cgit v1.2.3