summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Speicher <rspeicher@gmail.com>2015-04-02 20:46:43 -0400
committerRobert Speicher <rspeicher@gmail.com>2015-04-20 13:01:42 -0400
commit96c2b940480eb1bafd47e1f6d203ad11f8f646df (patch)
tree91d4a52b7e5558677f864b1eb1371a40bc681b33
parent76aade28e25d1f6e8924b35ed9bd365c8889987f (diff)
downloadgitlab-ce-96c2b940480eb1bafd47e1f6d203ad11f8f646df.tar.gz
Reference filters :sparkles:
Commit ranges, commits, external issues, issues, labels, merge requests, snippets, users.
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb16
-rw-r--r--lib/gitlab/markdown.rb324
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb127
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb108
-rw-r--r--lib/gitlab/markdown/cross_project_reference.rb26
-rw-r--r--lib/gitlab/markdown/external_issue_reference_filter.rb94
-rw-r--r--lib/gitlab/markdown/issue_reference_filter.rb110
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb100
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb107
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb98
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb117
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb52
-rw-r--r--spec/helpers/labels_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb105
-rw-r--r--spec/lib/gitlab/markdown/commit_reference_filter_spec.rb98
-rw-r--r--spec/lib/gitlab/markdown/cross_project_reference_spec.rb23
-rw-r--r--spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb101
-rw-r--r--spec/lib/gitlab/markdown/issue_reference_filter_spec.rb104
-rw-r--r--spec/lib/gitlab/markdown/label_reference_filter_spec.rb72
-rw-r--r--spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb93
-rw-r--r--spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb64
-rw-r--r--spec/lib/gitlab/markdown/user_reference_filter_spec.rb87
-rw-r--r--spec/support/reference_filter_spec_helper.rb37
24 files changed, 1728 insertions, 341 deletions
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ad4a7612724..7b034f22248 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -108,4 +108,6 @@ module IssuesHelper
xml.summary issue.title
end
end
+
+ module_function :url_for_issue, :title_for_issue
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 32ef2e7ca84..0259829a059 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -7,9 +7,13 @@ module LabelsHelper
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
- content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do
- label.name
- end
+ # Intentionally not using content_tag here so that this method can be called
+ # by LabelReferenceFilter
+ span = %(<span class="label color-label") +
+ %( style="background-color: #{label_color}; color: #{text_color}">) +
+ label.name + '</span>'
+
+ span.html_safe
end
def suggested_colors
@@ -42,13 +46,15 @@ module LabelsHelper
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
if (r + g + b) > 500
- "#333"
+ '#333333'
else
- "#FFF"
+ '#FFFFFF'
end
end
def project_labels_options(project)
options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
end
+
+ module_function :render_colored_label, :text_color_for_bg
end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 47c456d8dc7..93c279edc32 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -10,11 +10,11 @@ module Gitlab
# Supported reference formats are:
# * @foo for team members
# * #123 for issues
- # * #JIRA-123 for Jira issues
+ # * JIRA-123 for Jira issues
# * !123 for merge requests
# * $123 for snippets
- # * 123456 for commits
- # * 123456...7890123 for commit ranges (comparisons)
+ # * 1c002d for specific commit
+ # * 1c002d...35cfb2 for commit ranges (comparisons)
#
# It also parses Emoji codes to insert images. See
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
@@ -30,10 +30,6 @@ module Gitlab
# >> gfm(":trollface:")
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
module Markdown
- include IssuesHelper
-
- attr_reader :options, :html_options
-
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
@@ -65,42 +61,13 @@ module Gitlab
reference_only_path: true
)
- @options = options
- @html_options = html_options
-
- # TODO: add popups with additional information
-
- # Used markdown pipelines in GitLab:
- # GitlabEmojiFilter - performs emoji replacement.
- # SanitizationFilter - remove unsafe HTML tags and attributes
- #
- # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
- filters = [
- HTML::Pipeline::Gitlab::GitlabEmojiFilter,
- HTML::Pipeline::SanitizationFilter
- ]
-
- whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
- whitelist[:attributes][:all].push('class', 'id')
- whitelist[:elements].push('span')
-
- # Remove the rel attribute that the sanitize gem adds, and remove the
- # href attribute if it contains inline javascript
- fix_anchors = lambda do |env|
- name, node = env[:node_name], env[:node]
- if name == 'a'
- node.remove_attribute('rel')
- if node['href'] && node['href'].match('javascript:')
- node.remove_attribute('href')
- end
- end
- end
- whitelist[:transformers].push(fix_anchors)
-
markdown_context = {
- asset_root: Gitlab.config.gitlab.url,
- asset_host: Gitlab::Application.config.asset_host,
- whitelist: whitelist
+ asset_root: Gitlab.config.gitlab.url,
+ asset_host: Gitlab::Application.config.asset_host,
+ whitelist: sanitization_whitelist,
+ reference_class: html_options[:class],
+ only_path: options[:reference_only_path],
+ project: project
}
markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
@@ -114,21 +81,6 @@ module Gitlab
text = result[:output].to_html(save_with: save_options)
- # Extract pre blocks so they are not altered
- # from http://github.github.com/github-flavored-markdown/
- text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) }
- # Extract links with probably parsable hrefs
- text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) }
- # Extract images with probably parsable src
- text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) }
-
- text = parse(text, project)
-
- # Insert pre block extractions
- text.gsub!(/\{gfm-extraction-(\h{32})\}/) do
- insert_piece($1)
- end
-
if options[:parse_tasks]
text = parse_tasks(text)
end
@@ -138,242 +90,48 @@ module Gitlab
private
- def extract_piece(text)
- @extractions ||= {}
-
- md5 = Digest::MD5.hexdigest(text)
- @extractions[md5] = text
- "{gfm-extraction-#{md5}}"
- end
-
- def insert_piece(id)
- @extractions[id]
- end
-
- # Private: Parses text for references
- #
- # text - Text to parse
+ # Custom filters for html-pipeline:
#
- # Returns parsed text
- def parse(text, project = @project)
- parse_references(text, project) if project
-
- text
- end
-
- NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
- PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
-
- REFERENCE_PATTERN = %r{
- (?<prefix>\W)? # Prefix
- ( # Reference
- @(?<user>#{NAME_STR}) # User name
- |~(?<label>\d+) # Label ID
- |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
- |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
- |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
- |\$(?<snippet>\d+) # Snippet ID
- |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
- |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
- |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
- )
- (?<suffix>\W)? # Suffix
- }x.freeze
-
- TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze
-
- def parse_references(text, project = @project)
- # parse reference links
- text.gsub!(REFERENCE_PATTERN) do |match|
- type = TYPES.select{|t| !$~[t].nil?}.first
-
- actual_project = project
- project_prefix = nil
- project_path = $LAST_MATCH_INFO[:project]
- if project_path
- actual_project = ::Project.find_with_namespace(project_path)
- actual_project = nil unless can?(current_user, :read_project, actual_project)
- project_prefix = project_path
- end
-
- parse_result($LAST_MATCH_INFO, type,
- actual_project, project_prefix) || match
- end
- end
-
- # Called from #parse_references. Attempts to build a gitlab reference
- # link. Returns nil if +type+ is nil, if the match string is an HTML
- # entity, if the reference is invalid, or if the matched text includes an
- # invalid project path.
- def parse_result(match_info, type, project, project_prefix)
- prefix = match_info[:prefix]
- suffix = match_info[:suffix]
-
- return nil if html_entity?(prefix, suffix) || type.nil?
- return nil if project.nil? && !project_prefix.nil?
-
- identifier = match_info[type]
- ref_link = reference_link(type, identifier, project, project_prefix)
-
- if ref_link
- "#{prefix}#{ref_link}#{suffix}"
- else
- nil
- end
- end
-
- # Return true if the +prefix+ and +suffix+ indicate that the matched string
- # is an HTML entity like &amp;
- def html_entity?(prefix, suffix)
- prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
+ # See https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
+ def filters
+ [
+ Gitlab::Markdown::UserReferenceFilter,
+ Gitlab::Markdown::IssueReferenceFilter,
+ Gitlab::Markdown::ExternalIssueReferenceFilter,
+ Gitlab::Markdown::MergeRequestReferenceFilter,
+ Gitlab::Markdown::SnippetReferenceFilter,
+ Gitlab::Markdown::CommitRangeReferenceFilter,
+ Gitlab::Markdown::CommitReferenceFilter,
+ Gitlab::Markdown::LabelReferenceFilter,
+ HTML::Pipeline::Gitlab::GitlabEmojiFilter,
+ HTML::Pipeline::SanitizationFilter
+ ]
end
- # Private: Dispatches to a dedicated processing method based on reference
- #
- # reference - Object reference ("@1234", "!567", etc.)
- # identifier - Object identifier (Issue ID, SHA hash, etc.)
+ # Customize the SanitizationFilter whitelist
#
- # Returns string rendered by the processing method
- def reference_link(type, identifier, project = @project, prefix_text = nil)
- send("reference_#{type}", identifier, project, prefix_text)
- end
-
- def reference_user(identifier, project = @project, _ = nil)
- link_options = html_options.merge(
- class: "gfm gfm-project_member #{html_options[:class]}"
- )
+ # - Allow `class` and `id` attributes on all elements
+ # - Allow `span` elements
+ # - Remove `rel` attributes from `a` elements
+ # - Remove `a` nodes with `javascript:` in the `href` attribute
+ def sanitization_whitelist
+ whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
+ whitelist[:attributes][:all].push('class', 'id')
+ whitelist[:elements].push('span')
- if identifier == "all"
- link_to(
- "@all",
- namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]),
- link_options
- )
- elsif namespace = Namespace.find_by(path: identifier)
- url =
- if namespace.is_a?(Group)
- return nil unless can?(current_user, :read_group, namespace)
- group_url(identifier, only_path: options[:reference_only_path])
- else
- user_url(identifier, only_path: options[:reference_only_path])
+ fix_anchors = lambda do |env|
+ name, node = env[:node_name], env[:node]
+ if name == 'a'
+ node.remove_attribute('rel')
+ if node['href'] && node['href'].match('javascript:')
+ node.remove_attribute('href')
end
-
- link_to("@#{identifier}", url, link_options)
- end
- end
-
- def reference_label(identifier, project = @project, _ = nil)
- if label = project.labels.find_by(id: identifier)
- link_options = html_options.merge(
- class: "gfm gfm-label #{html_options[:class]}"
- )
- link_to(
- render_colored_label(label),
- namespace_project_issues_path(project.namespace, project, label_name: label.name),
- link_options
- )
- end
- end
-
- def reference_issue(identifier, project = @project, prefix_text = nil)
- if project.default_issues_tracker?
- if project.issue_exists? identifier
- url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
- title = title_for_issue(identifier, project)
- link_options = html_options.merge(
- title: "Issue: #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
-
- link_to("#{prefix_text}##{identifier}", url, link_options)
end
- else
- if project.external_issue_tracker.present?
- reference_external_issue(identifier, project,
- prefix_text)
- end
- end
- end
-
- def reference_merge_request(identifier, project = @project, prefix_text = nil)
- if merge_request = project.merge_requests.find_by(iid: identifier)
- link_options = html_options.merge(
- title: "Merge Request: #{merge_request.title}",
- class: "gfm gfm-merge_request #{html_options[:class]}"
- )
- url = namespace_project_merge_request_url(project.namespace, project,
- merge_request,
- only_path: options[:reference_only_path])
- link_to("#{prefix_text}!#{identifier}", url, link_options)
- end
- end
-
- def reference_snippet(identifier, project = @project, _ = nil)
- if snippet = project.snippets.find_by(id: identifier)
- link_options = html_options.merge(
- title: "Snippet: #{snippet.title}",
- class: "gfm gfm-snippet #{html_options[:class]}"
- )
- link_to(
- "$#{identifier}",
- namespace_project_snippet_url(project.namespace, project, snippet,
- only_path: options[:reference_only_path]),
- link_options
- )
end
- end
- def reference_commit(identifier, project = @project, prefix_text = nil)
- if project.valid_repo? && commit = project.repository.commit(identifier)
- link_options = html_options.merge(
- title: commit.link_title,
- class: "gfm gfm-commit #{html_options[:class]}"
- )
- prefix_text = "#{prefix_text}@" if prefix_text
- link_to(
- "#{prefix_text}#{identifier}",
- namespace_project_commit_url( project.namespace, project, commit,
- only_path: options[:reference_only_path]),
- link_options
- )
- end
- end
-
- def reference_commit_range(identifier, project = @project, prefix_text = nil)
- from_id, to_id = identifier.split(/\.{2,3}/, 2)
-
- inclusive = identifier !~ /\.{3}/
- from_id << "^" if inclusive
-
- if project.valid_repo? &&
- from = project.repository.commit(from_id) &&
- to = project.repository.commit(to_id)
-
- link_options = html_options.merge(
- title: "Commits #{from_id} through #{to_id}",
- class: "gfm gfm-commit_range #{html_options[:class]}"
- )
- prefix_text = "#{prefix_text}@" if prefix_text
-
- link_to(
- "#{prefix_text}#{identifier}",
- namespace_project_compare_url(project.namespace, project,
- from: from_id, to: to_id,
- only_path: options[:reference_only_path]),
- link_options
- )
- end
- end
-
- def reference_external_issue(identifier, project = @project, prefix_text = nil)
- url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
- title = project.external_issue_tracker.title
+ whitelist[:transformers].push(fix_anchors)
- link_options = html_options.merge(
- title: "Issue in #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
- link_to("#{prefix_text}##{identifier}", url, link_options)
+ whitelist
end
# Turn list items that start with "[ ]" into HTML checkbox inputs.
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..0d93bf4f280
--- /dev/null
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -0,0 +1,127 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit range references with links. References
+ # within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored when reference is
+ # cross-project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class CommitRangeReferenceFilter < HTML::Pipeline::Filter
+ include CrossProjectReference
+
+ # Public: Find commit range references in text
+ #
+ # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
+ # "<a href=...>#{commit_range}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit range, and an optional String
+ # of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_RANGE_PATTERN) do |match|
+ yield match, $~[:commit_range], $~[:project]
+ end
+ end
+
+ # Pattern used to extract commit range references from text
+ #
+ # The beginning and ending SHA1 sums can be between 6 and 40 hex
+ # characters, and the range selection can be double- or triple-dot.
+ #
+ # This pattern supports cross-project references.
+ COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>\h{6,40}\.{2,3}\h{6,40})/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(COMMIT_RANGE_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = commit_range_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Replace commit range references in text with links to compare the commit
+ # ranges.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit range references replaced with links. All
+ # links have `gfm` and `gfm-commit_range` class names attached for
+ # styling.
+ def commit_range_link_filter(text)
+ self.class.references_in(text) do |match, commit_range, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ from_id, to_id = split_commit_range(commit_range)
+
+ if valid_range?(project, from_id, to_id)
+ url = url_for_commit_range(project, from_id, to_id)
+
+ title = "Commits #{from_id} through #{to_id}"
+ klass = "gfm gfm-commit_range #{context[:reference_class]}".strip
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{commit_range}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def validate
+ needs :project
+ end
+
+ def split_commit_range(range)
+ from_id, to_id = range.split(/\.{2,3}/, 2)
+ from_id << "^" if range !~ /\.{3}/
+
+ [from_id, to_id]
+ end
+
+ def valid_range?(project, from_id, to_id)
+ project.valid_repo? &&
+ project.repository.commit(from_id) &&
+ project.repository.commit(to_id)
+ end
+
+ def url_for_commit_range(project, from_id, to_id)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_compare_url(project.namespace, project,
+ from: from_id, to: to_id,
+ only_path: context[:only_path])
+ end
+
+ def project
+ context[:project]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
new file mode 100644
index 00000000000..5697d7d97e7
--- /dev/null
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -0,0 +1,108 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit references with links. References within
+ # <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored when reference is
+ # cross-project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class CommitReferenceFilter < HTML::Pipeline::Filter
+ include CrossProjectReference
+
+ # Public: Find commit references in text
+ #
+ # CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
+ # "<a href=...>#{commit}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit identifier, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_PATTERN) do |match|
+ yield match, $~[:commit], $~[:project]
+ end
+ end
+
+ # Pattern used to extract commit references from text
+ #
+ # The SHA1 sum can be between 6 and 40 hex characters.
+ #
+ # This pattern supports cross-project references.
+ COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(COMMIT_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = commit_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace commit references in text with links to the commit specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit references replaced with links. All links
+ # have `gfm` and `gfm-commit` class names attached for styling.
+ def commit_link_filter(text)
+ self.class.references_in(text) do |match, commit_ref, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project.valid_repo? && commit = project.repository.commit(commit_ref)
+ url = url_for_commit(project, commit)
+
+ title = commit.link_title
+ klass = "gfm gfm-commit #{context[:reference_class]}".strip
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{commit_ref}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_commit(project, commit)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_commit_url(project.namespace, project, commit,
+ only_path: context[:only_path])
+ end
+
+ def project
+ context[:project]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
new file mode 100644
index 00000000000..114247d0a33
--- /dev/null
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Markdown
+ # Includes shared code for reference filters that support an optional
+ # cross-project reference.
+ module CrossProjectReference
+ NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR
+ PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
+
+ # Given a cross-project reference string, get the Project record
+ #
+ # If no valid reference is given, returns the `:project` value for the
+ # current context.
+ #
+ # ref - String reference.
+ #
+ # Returns a Project
+ def project_from_ref(ref)
+ if ref && other = Project.find_with_namespace(ref)
+ other
+ else
+ context[:project]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
new file mode 100644
index 00000000000..87f58b1ae79
--- /dev/null
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -0,0 +1,94 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces external issue tracker references with links.
+ # References within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class ExternalIssueReferenceFilter < HTML::Pipeline::Filter
+ # Public: Find `JIRA-123` issue references in text
+ #
+ # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the String issue reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue]
+ end
+ end
+
+ # Pattern used to extract `JIRA-123` issue references from text
+ ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next if project.default_issues_tracker?
+ next unless content.match(ISSUE_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = issue_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace `JIRA-123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `JIRA-123` references replaced with links. All
+ # links have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, issue|
+ url = url_for_issue(issue, project, only_path: context[:only_path])
+
+ title = "Issue in #{project.external_issue_tracker.title}"
+ klass = "gfm gfm-issue #{context[:reference_class]}".strip
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{issue}</a>)
+ end
+ end
+
+ # TODO (rspeicher): Duplicates IssueReferenceFilter
+ def project
+ context[:project]
+ end
+
+ # TODO (rspeicher): Duplicates IssueReferenceFilter
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
new file mode 100644
index 00000000000..ab0420da199
--- /dev/null
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -0,0 +1,110 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces issue references with links. References within
+ # <pre>, <code>, <a>, and <style> elements are ignored. References to issues
+ # that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored when reference is
+ # cross-project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class IssueReferenceFilter < HTML::Pipeline::Filter
+ include CrossProjectReference
+
+ # Public: Find `#123` issue references in text
+ #
+ # IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer issue ID, and an optional String of
+ # the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `#123` issue references from text
+ #
+ # This pattern supports cross-project references.
+ ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless project.default_issues_tracker?
+ next unless content.match(ISSUE_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = issue_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace `#123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `#123` references replaced with links. All links
+ # have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text)
+ self.class.references_in(text) do |match, issue, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project.issue_exists?(issue)
+ # FIXME: Ugly
+ url = url_for_issue(issue, project, only_path: context[:only_path])
+
+ title = "Issue: #{title_for_issue(issue, project)}"
+ klass = "gfm gfm-issue #{context[:reference_class]}".strip
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}##{issue}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+
+ def title_for_issue(*args)
+ IssuesHelper.title_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
new file mode 100644
index 00000000000..9753d0ac0f9
--- /dev/null
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -0,0 +1,100 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces label references with links. References within
+ # <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class LabelReferenceFilter < HTML::Pipeline::Filter
+ # Public: Find label references in text
+ #
+ # LabelReferenceFilter.references_in(text) do |match, label|
+ # "<a href=...>#{label}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the Integer label ID.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(LABEL_PATTERN) do |match|
+ yield match, $~[:label].to_i
+ end
+ end
+
+ # Pattern used to extract label references from text
+ #
+ # This pattern supports cross-project references.
+ LABEL_PATTERN = /~(?<label>\d+)/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(LABEL_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = label_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace label references in text with links to the label specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with label references replaced with links. All links
+ # have `gfm` and `gfm-label` class names attached for styling.
+ def label_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, id|
+ if label = project.labels.find_by(id: id)
+ url = url_for_label(project, label)
+
+ klass = "gfm gfm-label #{context[:reference_class]}".strip
+
+ %(<a href="#{url}" class="#{klass}">#{render_colored_label(label)}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_label(project, label)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_issues_path(project.namespace, project,
+ label_name: label.name,
+ only_path: context[:only_path])
+ end
+
+ def render_colored_label(label)
+ LabelsHelper.render_colored_label(label)
+ end
+
+ def project
+ context[:project]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..96a0c92942d
--- /dev/null
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -0,0 +1,107 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces merge request references with links. References
+ # within <pre>, <code>, <a>, and <style> elements are ignored. References to
+ # merge requests that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored when reference is
+ # cross-project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class MergeRequestReferenceFilter < HTML::Pipeline::Filter
+ include CrossProjectReference
+
+ # Public: Find `!123` merge request references in text
+ #
+ # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
+ # "<a href=...>##{merge_request}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer merge request ID, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(MERGE_REQUEST_PATTERN) do |match|
+ yield match, $~[:merge_request].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `!123` merge request references from text
+ #
+ # This pattern supports cross-project references.
+ MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(MERGE_REQUEST_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = merge_request_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace `!123` merge request references in text with links to the
+ # referenced merge request's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `!123` references replaced with links. All links
+ # have `gfm` and `gfm-merge_request` class names attached for styling.
+ def merge_request_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if merge_request = project.merge_requests.find_by(iid: id)
+ title = "Merge Request: #{merge_request.title}"
+ klass = "gfm gfm-merge_request #{context[:reference_class]}".strip
+
+ url = url_for_merge_request(merge_request, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}!#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ # TODO (rspeicher): Cleanup
+ def url_for_merge_request(mr, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_merge_request_url(project.namespace, project, mr,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
new file mode 100644
index 00000000000..0e34ab4bc1e
--- /dev/null
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -0,0 +1,98 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces snippet references with links. References within
+ # <pre>, <code>, <a>, and <style> elements are ignored. References to
+ # snippets that do not exist are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class SnippetReferenceFilter < HTML::Pipeline::Filter
+ # Public: Find `$123` snippet references in text
+ #
+ # SnippetReferenceFilter.references_in(text) do |match, snippet|
+ # "<a href=...>$#{snippet}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the Integer snippet ID.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(SNIPPET_PATTERN) do |match|
+ yield match, $~[:snippet].to_i
+ end
+ end
+
+ # Pattern used to extract `$123` snippet references from text
+ SNIPPET_PATTERN = /\$(?<snippet>\d+)/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(SNIPPET_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = snippet_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace `$123` snippet references in text with links to the referenced
+ # snippets's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `$123` references replaced with links. All links
+ # have `gfm` and `gfm-snippet` class names attached for styling.
+ def snippet_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, id|
+ if snippet = project.snippets.find_by(id: id)
+ title = "Snippet: #{snippet.title}"
+ klass = "gfm gfm-snippet #{context[:reference_class]}".strip
+
+ url = url_for_snippet(snippet, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">$#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ def url_for_snippet(snippet, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_snippet_url(project.namespace, project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
new file mode 100644
index 00000000000..eaf4094338b
--- /dev/null
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -0,0 +1,117 @@
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces user or group references with links. References
+ # within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # A special `@all` reference is also supported.
+ #
+ # Context options:
+ # :project (required) - Current project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class UserReferenceFilter < HTML::Pipeline::Filter
+ # Public: Find `@user` user references in text
+ #
+ # UserReferenceFilter.references_in(text) do |match, username|
+ # "<a href=...>@#{user}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String user name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(USER_PATTERN) do |match|
+ yield match, $~[:user]
+ end
+ end
+
+ # Pattern used to extract `@user` user references from text
+ USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next if project.nil?
+ next unless content.match(USER_PATTERN)
+ next if has_ancestor?(node, IGNORE_PARENTS)
+
+ html = user_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def validate
+ needs :project
+ end
+
+ # Replace `@user` user references in text with links to the referenced
+ # user's profile page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `@user` references replaced with links. All links
+ # have `gfm` and `gfm-project_member` class names attached for styling.
+ def user_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, user|
+ klass = "gfm gfm-project_member #{context[:reference_class]}".strip
+
+ if user == 'all'
+ url = link_to_all(project)
+
+ %(<a href="#{url}" class="#{klass}">@#{user}</a>)
+ elsif namespace = Namespace.find_by(path: user)
+ if namespace.is_a?(Group)
+ url = group_url(user, only_path: context[:only_path])
+ else
+ url = user_url(user, only_path: context[:only_path])
+ end
+
+ %(<a href="#{url}" class="#{klass}">@#{user}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ # TODO (rspeicher): Cleanup
+ def group_url(*args)
+ h = Rails.application.routes.url_helpers
+ h.group_url(*args)
+ end
+
+ # TODO (rspeicher): Cleanup
+ def user_url(*args)
+ h = Rails.application.routes.url_helpers
+ h.user_url(*args)
+ end
+
+ # TODO (rspeicher): Cleanup
+ def link_to_all(project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_url(project.namespace, project,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 944e743675c..1c88f49ff4f 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -348,56 +348,6 @@ describe GitlabMarkdownHelper do
end
end
- describe "referencing a Jira issue" do
- let(:actual) { "Reference to JIRA-#{issue.iid}" }
- let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" }
- let(:reference) { "JIRA-#{issue.iid}" }
-
- before do
- jira = @project.create_jira_service if @project.jira_service.nil?
- properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"}
- jira.update_attributes(properties: properties, active: true)
- end
-
- after do
- @project.jira_service.destroy! unless @project.jira_service.nil?
- end
-
- it "should link using a valid id" do
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- # Wrap the reference in parenthesis
- expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
-
- # Append some text to the end of the reference
- expect(gfm(actual.gsub(reference, "#{reference}, right?"))).
- to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Referenced #{reference} already."
- expected = /Referenced <a.+>[^\s]+<\/a> already/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- # Modify the reference string so it's still parsed, but is invalid
- invalid_reference = actual.gsub(/(\d+)$/, "r45")
- expect(gfm(invalid_reference)).to eq(invalid_reference)
- end
-
- it "should include a title attribute" do
- title = "Issue in JIRA tracker"
- expect(gfm(actual)).to match(/title="#{title}"/)
- end
-
- it "should include standard gfm classes" do
- expect(gfm(actual)).to match(/class="\s?gfm gfm-issue\s?"/)
- end
- end
-
describe "referencing a merge request" do
let(:object) { merge_request }
let(:reference) { "!#{merge_request.iid}" }
@@ -659,7 +609,7 @@ describe GitlabMarkdownHelper do
end
it "should leave ref-like href of 'manual' links untouched" do
- expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
+ expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\" class=\"gfm gfm-merge_request \">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
end
it "should leave ref-like src of images untouched" do
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 1e64a201942..0b7e3b1d11f 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
describe LabelsHelper do
- it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') }
- it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') }
+ it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') }
+ it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') }
end
diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
new file mode 100644
index 00000000000..08bdf05eb96
--- /dev/null
+++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CommitRangeReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:commit1) { project.repository.commit }
+ let(:commit2) { project.repository.commit("HEAD~2") }
+
+ it 'requires project context' do
+ expect { described_class.call('Commit Range 1c002d..d200c1', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "#{commit1.id}...#{commit2.id}" }
+ let(:reference2) { "#{commit1.id}..#{commit2.id}" }
+
+ it 'links to a valid two-dot reference' do
+ doc = filter("See #{reference2}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id)
+ end
+
+ it 'links to a valid three-dot reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs' do
+ exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(commit1.id.reverse)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("See #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path option' do
+ doc = filter("See #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
+ end
+ end
+
+ # TODO (rspeicher): Remove or re-enable
+ # context 'cross-project reference' do
+ # let(:namespace) { create(:namespace, name: 'cross-reference') }
+ # let(:project2) { create(:project, namespace: namespace) }
+ # let(:commit1) { project.repository.commit }
+ # let(:commit2) { project.repository.commit("HEAD~2") }
+ # let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
+
+ # it 'links to a valid reference' do
+ # doc = filter("See #{reference}")
+
+ # expect(doc.css('a').first.attr('href')).
+ # to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ # end
+
+ # it 'links with adjacent text' do
+ # doc = filter("Fixed (#{reference}.)")
+ # expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ # end
+
+ # it 'ignores invalid issue IDs on the referenced project' do
+ # exp = act = "Fixed #{project2.path_with_namespace}##{commit.id.reverse}"
+
+ # expect(filter(act).to_html).to eq exp
+ # end
+ # end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
new file mode 100644
index 00000000000..15851d3bd34
--- /dev/null
+++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CommitReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:commit) { project.repository.commit }
+
+ it 'requires project context' do
+ expect { described_class.call('Commit 1c002d', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { commit.id }
+
+ # Let's test a variety of commit SHA sizes just to be paranoid
+ [6, 8, 12, 18, 20, 32, 40].each do |size|
+ it "links to a valid reference of #{size} characters" do
+ doc = filter("See #{reference[0...size]}")
+
+ expect(doc.css('a').first.text).to eq reference[0...size]
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project.namespace, project, reference)
+ end
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs' do
+ exp = act = "See #{reference.reverse}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(reference.reverse)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq commit.link_title
+ end
+
+ it 'includes default classes' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("See #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("See #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:commit) { project.repository.commit }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path_with_namespace}##{commit.id.reverse}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
new file mode 100644
index 00000000000..2a3814c8499
--- /dev/null
+++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CrossProjectReference do
+ include CrossProjectReference
+
+ describe '#project_from_ref' do
+ let(:project) { double('project') }
+
+ it 'returns a project from a valid reference' do
+ expect(Project).to receive(:find_with_namespace).with('cross-reference/foo').and_return(project)
+
+ expect(project_from_ref('cross-reference/foo')).to eq project
+ end
+
+ it 'returns the project from context when reference is invalid' do
+ expect(self).to receive(:context).and_return({project: project})
+
+ expect(project_from_ref('invalid/reference')).to eq project
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..37c91195202
--- /dev/null
+++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe ExternalIssueReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:issue) { double('issue', iid: 123) }
+
+ context 'JIRA issue references' do
+ let(:reference) { "JIRA-#{issue.iid}" }
+
+ before do
+ jira = project.create_jira_service
+
+ props = {
+ 'title' => 'JIRA tracker',
+ 'project_url' => 'http://jira.example/issues/?jql=project=A',
+ 'issues_url' => 'http://jira.example/browse/:id',
+ 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
+ }
+
+ jira.update_attributes(properties: props, active: true)
+ end
+
+ after do
+ project.jira_service.destroy
+ end
+
+ it 'requires project context' do
+ expect { described_class.call('Issue JIRA-123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'ignores valid references when using default tracker' do
+ expect(project).to receive(:default_issues_tracker?).and_return(true)
+
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('href'))
+ .to eq helper.url_for_issue(reference, project)
+ end
+
+ it 'links to the external tracker' do
+ doc = filter("Issue #{reference}")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq "http://jira.example/browse/#{reference}"
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Issue (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Issue #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..75b3fccf325
--- /dev/null
+++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe IssueReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('Issue #123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ it 'ignores valid references when using non-default tracker' do
+ expect(project).to receive(:default_issues_tracker?).and_return(false)
+
+ exp = act = "Issue ##{issue.iid}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "##{issue.iid}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs' do
+ exp = act = "Fixed ##{issue.iid + 1}"
+
+ expect(project).to receive(:issue_exists?).with(issue.iid + 1)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Issue #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
new file mode 100644
index 00000000000..49243484c95
--- /dev/null
+++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+require 'html/pipeline'
+
+module Gitlab::Markdown
+ describe LabelReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:label) { create(:label, project: project) }
+ let(:reference) { "~#{label.id}" }
+
+ it 'requires project context' do
+ expect { described_class.call('Label ~123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Label #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label IDs' do
+ exp = act = "Label ~#{label.id + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes default classes' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Label #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Label #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true)
+ end
+
+ describe 'label span element' do
+ it 'includes default classes' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
+ end
+
+ it 'includes a style attribute' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
new file mode 100644
index 00000000000..17bb50b157b
--- /dev/null
+++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe MergeRequestReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:merge) { create(:merge_request, source_project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('MergeRequest !123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "!#{merge.iid}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_merge_request_url(project.namespace, project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs' do
+ exp = act = "Merge !#{merge.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Merge #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Merge #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Merge #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Merge #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
new file mode 100644
index 00000000000..d79a7e544a3
--- /dev/null
+++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe SnippetReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:reference) { "$#{snippet.id}" }
+
+ it 'requires project context' do
+ expect { described_class.call('Snippet $123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_snippet_url(project.namespace, project, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Snippet (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs' do
+ exp = act = "Snippet $#{snippet.id + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Snippet #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Snippet #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
new file mode 100644
index 00000000000..72f0746f0e2
--- /dev/null
+++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe UserReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ it 'requires project context' do
+ expect { described_class.call('Example @mention', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ it 'ignores invalid users' do
+ exp = act = 'Hey @somebody'
+ expect(filter(act).to_html).to eq(exp)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'mentioning a user' do
+ it 'links to a User' do
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ # TODO (rspeicher): This test might be overkill
+ it 'links to a User with a period' do
+ user = create(:user, name: 'alphA.Beta')
+
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ # TODO (rspeicher): This test might be overkill
+ it 'links to a User with an underscore' do
+ user = create(:user, name: 'ping_pong_king')
+
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ it 'links to a Group' do
+ group = create(:group)
+
+ doc = filter("Hey @#{group.name}")
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Mention me (@#{user.username}.)")
+ expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/)
+ end
+
+ it 'supports a special @all mention' do
+ doc = filter("Hey @all")
+ expect(doc.css('a').length).to eq 1
+ expect(doc.css('a').first.attr('href'))
+ .to eq urls.namespace_project_url(project.namespace, project)
+ end
+
+ it 'includes default classes' do
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Hey @#{user.username}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Hey @#{user.username}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.user_path(user)
+ end
+ end
+end
diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb
new file mode 100644
index 00000000000..e4138405602
--- /dev/null
+++ b/spec/support/reference_filter_spec_helper.rb
@@ -0,0 +1,37 @@
+# Common methods and setup for Gitlab::Markdown reference filter specs
+#
+# Must be included into specs manually
+module ReferenceFilterSpecHelper
+ extend ActiveSupport::Concern
+
+ included do
+ before { set_default_url_options }
+ end
+
+ # Allow *_url helpers to work
+ def set_default_url_options
+ Rails.application.routes.default_url_options = {
+ host: 'example.foo'
+ }
+ end
+
+ # Shortcut to Rails' auto-generated routes helpers, to avoid including the
+ # module
+ def urls
+ Rails.application.routes.url_helpers
+ end
+
+ # Perform `call` on the described class
+ #
+ # Automatically passes the current `project` value to the context if none is
+ # provided.
+ #
+ # html - String text to pass to the filter's `call` method.
+ # contexts - Hash context for the filter. (default: {project: project})
+ #
+ # Returns the String text returned by the filter's `call` method.
+ def filter(html, contexts = {})
+ contexts.reverse_merge!(project: project)
+ described_class.call(html, contexts)
+ end
+end