diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 98c6d9d5d2ece0660c58b92194537cda308a1d74..5004e02ea0b396269f8f165e3b377e6255cd06ad 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -20,7 +20,7 @@ module GitlabMarkdownHelper
                    end
 
     user = current_user if defined?(current_user)
-    gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user)
+    gfm_body = Gitlab::Markdown.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
 
     fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
     if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -46,23 +46,35 @@ module GitlabMarkdownHelper
   end
 
   def markdown(text, context = {})
-    process_markdown(text, context)
-  end
+    return "" unless text.present?
+
+    context[:project] ||= @project
 
-  # TODO (rspeicher): Remove all usages of this helper and just call `markdown`
-  # with a custom pipeline depending on the content being rendered
-  def gfm(text, options = {})
-    process_markdown(text, options, :gfm)
+    html = Gitlab::Markdown.render(text, context)
+
+    context.merge!(
+      current_user:   (current_user if defined?(current_user)),
+
+      # RelativeLinkFilter
+      requested_path: @path,
+      project_wiki:   @project_wiki,
+      ref:            @ref
+    )
+
+    Gitlab::Markdown.post_process(html, context)
   end
 
   def asciidoc(text)
-    Gitlab::Asciidoc.render(text, {
-      commit: @commit,
-      project: @project,
-      project_wiki: @project_wiki,
+    Gitlab::Asciidoc.render(text,
+      project:      @project,
+      current_user: (current_user if defined?(current_user)),
+
+      # RelativeLinkFilter
+      project_wiki:   @project_wiki,
       requested_path: @path,
-      ref: @ref
-    })
+      ref:            @ref,
+      commit:         @commit
+    )
   end
 
   # Return the first line of +text+, up to +max_chars+, after parsing the line
@@ -178,26 +190,4 @@ module GitlabMarkdownHelper
       ''
     end
   end
-
-  def process_markdown(text, options, method = :markdown)
-    return "" unless text.present?
-
-    options.reverse_merge!(
-      path:         @path,
-      pipeline:     :default,
-      project:      @project,
-      project_wiki: @project_wiki,
-      ref:          @ref
-    )
-
-    user = current_user if defined?(current_user)
-
-    html = if method == :gfm
-             Gitlab::Markdown.gfm(text, options)
-           else
-             Gitlab::Markdown.render(text, options)
-           end
-
-    Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
-  end
 end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index fa88a408fa33c3745346be912876b48673603ac4..0ba7b584d91191983c3b2a6f4bdbf5ef71586f02 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -7,7 +7,7 @@ class Commit
   include Referable
   include StaticModel
 
-  attr_mentionable :safe_message
+  attr_mentionable :safe_message, pipeline: :single_line
   participant :author, :committer, :notes
 
   attr_accessor :project
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index badeadfa4180cfb896f162cd4abc235b284624ea..f56fd3e02d472a06d839c18f6d63d189b01e896c 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -50,7 +50,8 @@ module Issuable
              allow_nil: true,
              prefix: true
 
-    attr_mentionable :title, :description
+    attr_mentionable :title, pipeline: :single_line
+    attr_mentionable :description, cache: true
     participant :author, :assignee, :notes_with_associations
     strip_attributes :title
   end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 634a8d0f2747d3f327aed4eca3e167fbe8040480..d2ea9ab731332638c09dfa176d86612e479aad7e 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -10,8 +10,9 @@ module Mentionable
 
   module ClassMethods
     # Indicate which attributes of the Mentionable to search for GFM references.
-    def attr_mentionable(*attrs)
-      mentionable_attrs.concat(attrs.map(&:to_s))
+    def attr_mentionable(attr, options = {})
+      attr = attr.to_s
+      mentionable_attrs << [attr, options]
     end
 
     # Accessor for attributes marked mentionable.
@@ -37,19 +38,24 @@ module Mentionable
     "#{friendly_name} #{to_reference(from_project)}"
   end
 
-  # Construct a String that contains possible GFM references.
-  def mentionable_text
-    self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n")
-  end
-
   # The GFM reference to this Mentionable, which shouldn't be included in its #references.
   def local_reference
     self
   end
 
-  def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
+  def all_references(current_user = self.author, text = nil, load_lazy_references: true)
     ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references)
-    ext.analyze(text)
+    
+    if text
+      ext.analyze(text)
+    else
+      self.class.mentionable_attrs.each do |attr, options|
+        text = send(attr)
+        options[:cache_key] = [self, attr] if options.delete(:cache)
+        ext.analyze(text, options)
+      end
+    end
+
     ext
   end
 
@@ -58,9 +64,7 @@ module Mentionable
   end
 
   # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
-  def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
-    return [] if text.blank?
-
+  def referenced_mentionables(current_user = self.author, text = nil, load_lazy_references: true)
     refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
     refs = (refs.issues + refs.merge_requests + refs.commits)
 
@@ -70,8 +74,8 @@ module Mentionable
     refs.reject { |ref| ref == local_reference }
   end
 
-  # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
-  def create_cross_references!(author = self.author, without = [], text = self.mentionable_text)
+  # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
+  def create_cross_references!(author = self.author, without = [], text = nil)
     refs = referenced_mentionables(author, text)
 
     # We're using this method instead of Array diffing because that requires
@@ -111,7 +115,7 @@ module Mentionable
   def detect_mentionable_changes
     source = (changes.present? ? changes : previous_changes).dup
 
-    mentionable = self.class.mentionable_attrs
+    mentionable = self.class.mentionable_attrs.map { |attr, options| attr }
 
     # Only include changed fields that are mentionable
     source.select { |key, val| mentionable.include?(key) }
diff --git a/app/models/note.rb b/app/models/note.rb
index 98c29ddc4cd1d44f1c57ba7acdd769491e49e8a1..de9392adbf44e3e5dd5eb28b984f32939a23f7e1 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -29,7 +29,7 @@ class Note < ActiveRecord::Base
 
   default_value_for :system, false
 
-  attr_mentionable :note
+  attr_mentionable :note, cache: true, pipeline: :note
   participant :author
 
   belongs_to :project
diff --git a/app/models/project.rb b/app/models/project.rb
index cb965ce1b9e327ea8b0771c67a3e349ad1775a38..e78868af1cc5ce594da2e85d238ff7200c7ea936 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -662,6 +662,7 @@ class Project < ActiveRecord::Base
         gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
         send_move_instructions(old_path_with_namespace)
         reset_events_cache
+        @repository = nil
       rescue
         # Returning false does not rollback after_* transaction but gives
         # us information about failing some of tasks
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 3536bbeaf4b9452d98a05f995234fed512b04021..44b7efe523277e32a59cef41fb0631b75068e2fe 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -12,7 +12,7 @@
 
   .gray-content-block.middle-block
     %h2.issue-title
-      = gfm escape_once(@milestone.title)
+      = markdown escape_once(@milestone.title), pipeline: :single_line
 
 - if @milestone.complete? && @milestone.active?
   .alert.alert-success.prepend-top-default
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index ad63841ccf3fee2d9f7ec3870a7a3637935b2aff..4ba8b84fd923cd308852f4e084544f2bceb85dc4 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -2,4 +2,4 @@
   .commit-row-title
     = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
     &middot;
-    = gfm event_commit_title(commit[:message]), project: project
+    = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 3c1d8815013a5883b7fef63940dcb729628502c2..350e216fcc61f6e2a61c12fb197a9fb924534090 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -18,7 +18,7 @@
 
   .gray-content-block.middle-block
     %h2.issue-title
-      = gfm escape_once(@milestone.title)
+      = markdown escape_once(@milestone.title), pipeline: :single_line
 
 - if @milestone.complete? && @milestone.active?
   .alert.alert-success.prepend-top-default
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index bb37e4a7049c9aa91e17c02bf792b5218ed5afe3..132cdc35c941d8031c2bc16f66fe934d6c6e56cd 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -52,10 +52,10 @@
 
 .commit-box.gray-content-block.middle-block
   %h3.commit-title
-    = gfm escape_once(@commit.title)
+    = markdown escape_once(@commit.title), pipeline: :single_line
   - if @commit.description.present?
     %pre.commit-description
-      = preserve(gfm(escape_once(@commit.description)))
+      = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
 
 :javascript
   $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 2e489a0a4d5b6646b105e7948b75349a6279a411..0d64486164e7c3b9df6a6643c4de84e9f11f18f5 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -32,7 +32,7 @@
     - if commit.description?
       .commit-row-description.js-toggle-content
         %pre
-          = preserve(gfm(escape_once(commit.description)))
+          = preserve(markdown(escape_once(commit.description), pipeline: :single_line))
 
     .commit-row-info
       = commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 268b9b815ee89b8197abec3a523df41c7c6f6619..7ffa731719607462872c9645a337b563708de995 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -17,7 +17,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
         xml.name commit.author_name
         xml.email commit.author_email
       end
-      xml.summary gfm(commit.description)
+      xml.summary markdown(commit.description, pipeline: :single_line)
     end
   end
 end
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b28ada909b7f2876c787897608ffb21eec434f42..d865d2991352a92e1928ff6caec5a53d778429a0 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -24,10 +24,10 @@
               .col-sm-10
                 = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
 
-            - if @project.repository.exists? && @project.repository.branch_names.any?
+            - unless @project.empty_repo?
               .form-group
                 = f.label :default_branch, "Default Branch", class: 'control-label'
-                .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'})
+                .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
 
 
           = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
index 917d5181689d99a75627e1cc79b264f533e9e97a..3c491c1a8b8a6a40077e8346ad6311f1a404b4ba 100644
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -1,3 +1,3 @@
 .issue-closed-by-widget
   = icon('check')
-  This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests))} is accepted.
+  This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted.
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e2de17cee6d9852a8b0dc350aded83b70bdad910..a78d20cc07e44393e8a8829860bcb09abb0243f8 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -38,13 +38,13 @@
 
     .gray-content-block.middle-block
       %h2.issue-title
-        = gfm escape_once(@issue.title)
+        = markdown escape_once(@issue.title), pipeline: :single_line
       %div
         - if @issue.description.present?
           .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
             .wiki
               = preserve do
-                = markdown(@issue.description)
+                = markdown(@issue.description, cache_key: [@issue, "description"])
             %textarea.hidden.js-task-list-field
               = @issue.description
   - if @closed_by_merge_requests.present?
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index b4f62a758900e92452fa66bba86dce737218a938..9bfe202589ec0a7d31fe18918ef2f3481baf244b 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,12 +1,12 @@
 .gray-content-block.middle-block
   %h2.issue-title
-    = gfm escape_once(@merge_request.title)
+    = markdown escape_once(@merge_request.title), pipeline: :single_line
 
   %div
     - if @merge_request.description.present?
       .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
         .wiki
           = preserve do
-            = markdown(@merge_request.description)
+            = markdown(@merge_request.description, cache_key: [@merge_request, "description"])
         %textarea.hidden.js-task-list-field
           = @merge_request.description
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index e0013fb769ae562365288872a6061cc2ff82a3f8..55dbae598d3c88a8ef917c26a8813b00b3d574bb 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -26,4 +26,4 @@
         %i.fa.fa-check
         Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)}
         = succeed '.' do
-          != gfm(issues_sentence(@closes_issues))
+          != markdown issues_sentence(@closes_issues), pipeline: :gfm
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index c3bda794c65b20d2cc6351f7c56307e78e216a93..7ecee4403373311e5304895fb717c7afbb887c7b 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -32,7 +32,7 @@
 
   .gray-content-block.middle-block
     %h2.issue-title
-      = gfm escape_once(@milestone.title)
+      = markdown escape_once(@milestone.title), pipeline: :single_line
     %div
       - if @milestone.description.present?
         .description
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index dd0abc8c74681047c18d08761d7f027f0c60852e..922535e5c4a67fbc22a94684d3c24ef45bdee9e6 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -38,7 +38,7 @@
       .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
         .note-text
           = preserve do
-            = markdown(note.note, {no_header_anchors: true})
+            = markdown(note.note, pipeline: :note, cache_key: [note, "note"])
         - if note_editable?(note)
           = render 'projects/notes/edit_form', note: note
 
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index f3526ad0747a8dbe31117f5e0dd7308efc9c3db1..6ca919f7f80292ad448a95b606e63a783a88fbc8 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
       = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
         %code= commit.short_id
       = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
-      = gfm escape_once(truncate(commit.title, length: 40))
+      = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line
   %td
     %span.pull-right.cgray
       = time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index eb0fd21c2d46d963742be7b429499e611dbd0981..669e6119fb65a54a8d29b52acf6d8b1881b5105d 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -22,4 +22,4 @@
 
   .gray-content-block.middle-block
     %h2.issue-title
-      = gfm escape_once(@snippet.title)
+      = markdown escape_once(@snippet.title), pipeline: :single_line
diff --git a/config/environments/test.rb b/config/environments/test.rb
index f96ac6f97530378c206d2cebcc0842f29c797113..d6842affa6c19ba06694766f7720e6590e50adcc 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -7,6 +7,8 @@ Rails.application.configure do
   # and recreated between test runs. Don't rely on the data there!
   config.cache_classes = false
 
+  config.cache_store = :null_store
+
   # Configure static asset server for tests with Cache-Control for performance
   config.serve_static_files = true
   config.static_cache_control = "public, max-age=3600"
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index d5208b8c93efc46bba8ff038697ed469d8a1256e..0fc725842ba16fd738338faa4443b257362061ee 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -10,7 +10,9 @@ rescue
   Settings.gitlab['session_expire_delay'] ||= 10080
 end
 
-unless Rails.env.test?
+if Rails.env.test?
+  Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
+else
   Gitlab::Application.config.session_store(
     :redis_store, # Using the cookie_store would enable session replay attacks.
     servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index bf33e5b1b1e4526051dce64ad584c67bf2e43203..330d3342dd17f42db2b9dde389f2b32342e9ca5f 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,14 +1,10 @@
 require 'asciidoctor'
-require 'html/pipeline'
 
 module Gitlab
   # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
   # the resulting HTML through HTML pipeline filters.
   module Asciidoc
 
-    # Provide autoload paths for filters to prevent a circular dependency error
-    autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
-
     DEFAULT_ADOC_ATTRS = [
       'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
       'env-gitlab', 'source-highlighter=html-pipeline'
@@ -24,13 +20,11 @@ module Gitlab
     #                 :requested_path
     #                 :ref
     # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter
-    # html_opts     - a Hash of options for HTML output:
-    #                 :xhtml - output XHTML instead of HTML
     #
-    def self.render(input, context, asciidoc_opts = {}, html_opts = {})
-      asciidoc_opts = asciidoc_opts.reverse_merge(
+    def self.render(input, context, asciidoc_opts = {})
+      asciidoc_opts.reverse_merge!(
         safe: :secure,
-        backend: html_opts[:xhtml] ? :xhtml5 : :html5,
+        backend: :html5,
         attributes: []
       )
       asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
@@ -38,23 +32,10 @@ module Gitlab
       html = ::Asciidoctor.convert(input, asciidoc_opts)
 
       if context[:project]
-        result = HTML::Pipeline.new(filters).call(html, context)
-
-        save_opts = html_opts[:xhtml] ?
-          Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0
-
-        html = result[:output].to_html(save_with: save_opts)
+        html = Gitlab::Markdown.render(html, context.merge(pipeline: :asciidoc))
       end
 
       html.html_safe
     end
-
-    private
-
-    def self.filters
-      [
-        Gitlab::Markdown::RelativeLinkFilter
-      ]
-    end
   end
 end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 886a09f52af7d0b1fb36751a0377f044bc3a02af..f4e2cefca51745875b745d3c46d069e7078ccf5c 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -19,24 +19,21 @@ module Gitlab
     # context  - Hash of context options passed to our HTML Pipeline
     #
     # Returns an HTML-safe String
-    def self.render(markdown, context = {})
-      html = renderer.render(markdown)
-      html = gfm(html, context)
-
-      html.html_safe
+    def self.render(text, context = {})
+      cache_key = context.delete(:cache_key)
+      cache_key = full_cache_key(cache_key, context[:pipeline])
+
+      if cache_key
+        Rails.cache.fetch(cache_key) do
+          cacheless_render(text, context)
+        end
+      else
+        cacheless_render(text, context)
+      end
     end
 
-    # Convert a Markdown String into HTML without going through the HTML
-    # Pipeline.
-    #
-    # Note that because the pipeline is skipped, SanitizationFilter is as well.
-    # Do not output the result of this method to the user.
-    #
-    # markdown - Markdown String
-    #
-    # Returns a String
-    def self.render_without_gfm(markdown)
-      renderer.render(markdown)
+    def self.render_result(text, context = {})
+      Pipeline[context[:pipeline]].call(text, context)
     end
 
     # Perform post-processing on an HTML String
@@ -46,156 +43,73 @@ module Gitlab
     # permission to make (`RedactorFilter`).
     #
     # html     - String to process
-    # options  - Hash of options to customize output
+    # context  - Hash of options to customize output
     #            :pipeline  - Symbol pipeline type
     #            :project   - Project
     #            :user      - User object
     #
     # Returns an HTML-safe String
-    def self.post_process(html, options)
-      context = {
-        project:      options[:project],
-        current_user: options[:user]
-      }
-      doc = post_processor.to_document(html, context)
+    def self.post_process(html, context)
+      context = Pipeline[context[:pipeline]].transform_context(context)
 
-      if options[:pipeline] == :atom
-        doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
+      pipeline = Pipeline[:post_process]
+      if context[:xhtml]
+        pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
       else
-        doc.to_html
+        pipeline.to_html(html, context)
       end.html_safe
     end
 
-    # Provide autoload paths for filters to prevent a circular dependency error
-    autoload :AutolinkFilter,               'gitlab/markdown/autolink_filter'
-    autoload :CommitRangeReferenceFilter,   'gitlab/markdown/commit_range_reference_filter'
-    autoload :CommitReferenceFilter,        'gitlab/markdown/commit_reference_filter'
-    autoload :EmojiFilter,                  'gitlab/markdown/emoji_filter'
-    autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter'
-    autoload :ExternalLinkFilter,           'gitlab/markdown/external_link_filter'
-    autoload :IssueReferenceFilter,         'gitlab/markdown/issue_reference_filter'
-    autoload :LabelReferenceFilter,         'gitlab/markdown/label_reference_filter'
-    autoload :MergeRequestReferenceFilter,  'gitlab/markdown/merge_request_reference_filter'
-    autoload :RedactorFilter,               'gitlab/markdown/redactor_filter'
-    autoload :RelativeLinkFilter,           'gitlab/markdown/relative_link_filter'
-    autoload :SanitizationFilter,           'gitlab/markdown/sanitization_filter'
-    autoload :SnippetReferenceFilter,       'gitlab/markdown/snippet_reference_filter'
-    autoload :SyntaxHighlightFilter,        'gitlab/markdown/syntax_highlight_filter'
-    autoload :TableOfContentsFilter,        'gitlab/markdown/table_of_contents_filter'
-    autoload :TaskListFilter,               'gitlab/markdown/task_list_filter'
-    autoload :UserReferenceFilter,          'gitlab/markdown/user_reference_filter'
-    autoload :UploadLinkFilter,             'gitlab/markdown/upload_link_filter'
-
-    # Public: Parse the provided HTML with GitLab-Flavored Markdown
-    #
-    # html    - HTML String
-    # options - A Hash of options used to customize output (default: {})
-    #           :no_header_anchors - Disable header anchors in TableOfContentsFilter
-    #           :path              - Current path String
-    #           :pipeline          - Symbol pipeline type
-    #           :project           - Current Project object
-    #           :project_wiki      - Current ProjectWiki object
-    #           :ref               - Current ref String
-    #
-    # Returns an HTML-safe String
-    def self.gfm(html, options = {})
-      return '' unless html.present?
-
-      @pipeline ||= HTML::Pipeline.new(filters)
-
-      context = {
-        # SanitizationFilter
-        pipeline: options[:pipeline],
-
-        # EmojiFilter
-        asset_host: Gitlab::Application.config.asset_host,
-        asset_root: Gitlab.config.gitlab.base_url,
-
-        # ReferenceFilter
-        only_path: only_path_pipeline?(options[:pipeline]),
-        project:   options[:project],
-
-        # RelativeLinkFilter
-        project_wiki:   options[:project_wiki],
-        ref:            options[:ref],
-        requested_path: options[:path],
-
-        # TableOfContentsFilter
-        no_header_anchors: options[:no_header_anchors]
-      }
-
-      @pipeline.to_html(html, context).html_safe
-    end
-
     private
 
-    # Check if a pipeline enables the `only_path` context option
-    #
-    # Returns Boolean
-    def self.only_path_pipeline?(pipeline)
-      case pipeline
-      when :atom, :email
-        false
-      else
-        true
-      end
-    end
-
-    def self.redcarpet_options
-      # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
-      @redcarpet_options ||= {
-        fenced_code_blocks:  true,
-        footnotes:           true,
-        lax_spacing:         true,
-        no_intra_emphasis:   true,
-        space_after_headers: true,
-        strikethrough:       true,
-        superscript:         true,
-        tables:              true
-      }.freeze
-    end
+    def self.cacheless_render(text, context = {})
+      result = render_result(text, context)
 
-    def self.renderer
-      @markdown ||= begin
-        renderer = Redcarpet::Render::HTML.new
-        Redcarpet::Markdown.new(renderer, redcarpet_options)
+      output = result[:output]
+      if output.respond_to?(:to_html)
+        output.to_html
+      else
+        output.to_s
       end
     end
 
-    def self.post_processor
-      @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter])
+    def self.full_cache_key(cache_key, pipeline_name)
+      return unless cache_key
+      ["markdown", *cache_key, pipeline_name || :full]
     end
 
-    # Filters used in our pipeline
-    #
-    # SanitizationFilter should come first so that all generated reference HTML
-    # goes through untouched.
-    #
-    # See https://github.com/jch/html-pipeline#filters for more filters.
-    def self.filters
-      [
-        Gitlab::Markdown::SyntaxHighlightFilter,
-        Gitlab::Markdown::SanitizationFilter,
-
-        Gitlab::Markdown::UploadLinkFilter,
-        Gitlab::Markdown::EmojiFilter,
-        Gitlab::Markdown::TableOfContentsFilter,
-        Gitlab::Markdown::AutolinkFilter,
-        Gitlab::Markdown::ExternalLinkFilter,
-
-        Gitlab::Markdown::UserReferenceFilter,
-        Gitlab::Markdown::IssueReferenceFilter,
-        Gitlab::Markdown::ExternalIssueReferenceFilter,
-        Gitlab::Markdown::MergeRequestReferenceFilter,
-        Gitlab::Markdown::SnippetReferenceFilter,
-        Gitlab::Markdown::CommitRangeReferenceFilter,
-        Gitlab::Markdown::CommitReferenceFilter,
-        Gitlab::Markdown::LabelReferenceFilter,
-
-        Gitlab::Markdown::RelativeLinkFilter,
-
-        Gitlab::Markdown::TaskListFilter
-      ]
-    end
+    # Provide autoload paths for filters to prevent a circular dependency error
+    autoload :AutolinkFilter,               'gitlab/markdown/filter/autolink_filter'
+    autoload :CommitRangeReferenceFilter,   'gitlab/markdown/filter/commit_range_reference_filter'
+    autoload :CommitReferenceFilter,        'gitlab/markdown/filter/commit_reference_filter'
+    autoload :EmojiFilter,                  'gitlab/markdown/filter/emoji_filter'
+    autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/filter/external_issue_reference_filter'
+    autoload :ExternalLinkFilter,           'gitlab/markdown/filter/external_link_filter'
+    autoload :IssueReferenceFilter,         'gitlab/markdown/filter/issue_reference_filter'
+    autoload :LabelReferenceFilter,         'gitlab/markdown/filter/label_reference_filter'
+    autoload :MarkdownFilter,               'gitlab/markdown/filter/markdown_filter'
+    autoload :MergeRequestReferenceFilter,  'gitlab/markdown/filter/merge_request_reference_filter'
+    autoload :RedactorFilter,               'gitlab/markdown/filter/redactor_filter'
+    autoload :ReferenceGathererFilter,      'gitlab/markdown/filter/reference_gatherer_filter'
+    autoload :RelativeLinkFilter,           'gitlab/markdown/filter/relative_link_filter'
+    autoload :SanitizationFilter,           'gitlab/markdown/filter/sanitization_filter'
+    autoload :SnippetReferenceFilter,       'gitlab/markdown/filter/snippet_reference_filter'
+    autoload :SyntaxHighlightFilter,        'gitlab/markdown/filter/syntax_highlight_filter'
+    autoload :TableOfContentsFilter,        'gitlab/markdown/filter/table_of_contents_filter'
+    autoload :TaskListFilter,               'gitlab/markdown/filter/task_list_filter'
+    autoload :UserReferenceFilter,          'gitlab/markdown/filter/user_reference_filter'
+    autoload :UploadLinkFilter,             'gitlab/markdown/filter/upload_link_filter'
+
+    autoload :AsciidocPipeline,             'gitlab/markdown/pipeline/asciidoc_pipeline'
+    autoload :AtomPipeline,                 'gitlab/markdown/pipeline/atom_pipeline'
+    autoload :DescriptionPipeline,          'gitlab/markdown/pipeline/description_pipeline'
+    autoload :EmailPipeline,                'gitlab/markdown/pipeline/email_pipeline'
+    autoload :FullPipeline,                 'gitlab/markdown/pipeline/full_pipeline'
+    autoload :GfmPipeline,                  'gitlab/markdown/pipeline/gfm_pipeline'
+    autoload :NotePipeline,                 'gitlab/markdown/pipeline/note_pipeline'
+    autoload :PlainMarkdownPipeline,        'gitlab/markdown/pipeline/plain_markdown_pipeline'
+    autoload :PostProcessPipeline,          'gitlab/markdown/pipeline/post_process_pipeline'
+    autoload :ReferenceExtractionPipeline,  'gitlab/markdown/pipeline/reference_extraction_pipeline'
+    autoload :SingleLinePipeline,           'gitlab/markdown/pipeline/single_line_pipeline'
   end
 end
diff --git a/lib/gitlab/markdown/combined_pipeline.rb b/lib/gitlab/markdown/combined_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6b08a5e9f728cf182de467e812b3f558b5375000
--- /dev/null
+++ b/lib/gitlab/markdown/combined_pipeline.rb
@@ -0,0 +1,27 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    module CombinedPipeline
+      def self.new(*pipelines)
+        Class.new(Pipeline) do
+          const_set :PIPELINES, pipelines
+
+          def self.pipelines
+            self::PIPELINES
+          end
+
+          def self.filters
+            pipelines.flat_map(&:filters)
+          end
+
+          def self.transform_context(context)
+            pipelines.reduce(context) do |context, pipeline|
+              pipeline.transform_context(context)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/filter/autolink_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/autolink_filter.rb
rename to lib/gitlab/markdown/filter/autolink_filter.rb
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/filter/commit_range_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/commit_range_reference_filter.rb
rename to lib/gitlab/markdown/filter/commit_range_reference_filter.rb
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/filter/commit_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/commit_reference_filter.rb
rename to lib/gitlab/markdown/filter/commit_reference_filter.rb
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/filter/emoji_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/emoji_filter.rb
rename to lib/gitlab/markdown/filter/emoji_filter.rb
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/filter/external_issue_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/external_issue_reference_filter.rb
rename to lib/gitlab/markdown/filter/external_issue_reference_filter.rb
diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/filter/external_link_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/external_link_filter.rb
rename to lib/gitlab/markdown/filter/external_link_filter.rb
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/filter/issue_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/issue_reference_filter.rb
rename to lib/gitlab/markdown/filter/issue_reference_filter.rb
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/filter/label_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/label_reference_filter.rb
rename to lib/gitlab/markdown/filter/label_reference_filter.rb
diff --git a/lib/gitlab/markdown/filter/markdown_filter.rb b/lib/gitlab/markdown/filter/markdown_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..921e2a0794e62c75945084628e43ba98371c7fe4
--- /dev/null
+++ b/lib/gitlab/markdown/filter/markdown_filter.rb
@@ -0,0 +1,39 @@
+module Gitlab
+  module Markdown
+    class MarkdownFilter < HTML::Pipeline::TextFilter
+      def initialize(text, context = nil, result = nil)
+        super text, context, result
+        @text = @text.gsub "\r", ''
+      end
+
+      def call
+        html = self.class.renderer.render(@text)
+        html.rstrip!
+        html
+      end
+  
+      private      
+
+      def self.redcarpet_options
+        # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
+        @redcarpet_options ||= {
+          fenced_code_blocks:  true,
+          footnotes:           true,
+          lax_spacing:         true,
+          no_intra_emphasis:   true,
+          space_after_headers: true,
+          strikethrough:       true,
+          superscript:         true,
+          tables:              true
+        }.freeze
+      end
+
+      def self.renderer
+        @renderer ||= begin
+          renderer = Redcarpet::Render::HTML.new
+          Redcarpet::Markdown.new(renderer, redcarpet_options)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/filter/merge_request_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/merge_request_reference_filter.rb
rename to lib/gitlab/markdown/filter/merge_request_reference_filter.rb
diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/filter/redactor_filter.rb
similarity index 93%
rename from lib/gitlab/markdown/redactor_filter.rb
rename to lib/gitlab/markdown/filter/redactor_filter.rb
index bea714a01e75d4d18140ef1db16fd86de3cb5646..33ef7ce18b51a86daf12db49f2a69ed30ace8404 100644
--- a/lib/gitlab/markdown/redactor_filter.rb
+++ b/lib/gitlab/markdown/filter/redactor_filter.rb
@@ -27,7 +27,7 @@ module Gitlab
       def user_can_reference?(node)
         if node.has_attribute?('data-reference-filter')
           reference_type = node.attr('data-reference-filter')
-          reference_filter = reference_type.constantize
+          reference_filter = Gitlab::Markdown.const_get(reference_type)
 
           reference_filter.user_can_reference?(current_user, node, context)
         else
diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb
similarity index 95%
rename from lib/gitlab/markdown/reference_gatherer_filter.rb
rename to lib/gitlab/markdown/filter/reference_gatherer_filter.rb
index 00f983675e6b90ba15b344e4ec56bd4ffc61c606..62f241b4967df49a10bfe2be40931cb8b58f4efd 100644
--- a/lib/gitlab/markdown/reference_gatherer_filter.rb
+++ b/lib/gitlab/markdown/filter/reference_gatherer_filter.rb
@@ -31,7 +31,7 @@ module Gitlab
         return unless node.has_attribute?('data-reference-filter')
 
         reference_type = node.attr('data-reference-filter')
-        reference_filter = reference_type.constantize
+        reference_filter = Gitlab::Markdown.const_get(reference_type)
 
         return if context[:reference_filter] && reference_filter != context[:reference_filter]
 
diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/filter/relative_link_filter.rb
similarity index 96%
rename from lib/gitlab/markdown/relative_link_filter.rb
rename to lib/gitlab/markdown/filter/relative_link_filter.rb
index 692c51fd324ad8783ed9204159c3e259aa278023..81f60120fcd786dd1a675b557dc512fb9cb64817 100644
--- a/lib/gitlab/markdown/relative_link_filter.rb
+++ b/lib/gitlab/markdown/filter/relative_link_filter.rb
@@ -16,10 +16,7 @@ module Gitlab
       def call
         return doc unless linkable_files?
 
-        doc.search('a').each do |el|
-          klass = el.attr('class')
-          next if klass && klass.include?('gfm')
-          
+        doc.search('a:not(.gfm)').each do |el|
           process_link_attr el.attribute('href')
         end
 
diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/filter/sanitization_filter.rb
similarity index 96%
rename from lib/gitlab/markdown/sanitization_filter.rb
rename to lib/gitlab/markdown/filter/sanitization_filter.rb
index ffb9dc33b641d7109dae0456272d60c3e853346c..cf153f30622b03f3cd5d70f1cc65b799fdac41cd 100644
--- a/lib/gitlab/markdown/sanitization_filter.rb
+++ b/lib/gitlab/markdown/filter/sanitization_filter.rb
@@ -11,7 +11,7 @@ module Gitlab
       def whitelist
         # Descriptions are more heavily sanitized, allowing only a few elements.
         # See http://git.io/vkuAN
-        if pipeline == :description
+        if context[:inline_sanitization]
           whitelist = LIMITED
           whitelist[:elements] -= %w(pre code img ol ul li)
         else
@@ -25,10 +25,6 @@ module Gitlab
 
       private
 
-      def pipeline
-        context[:pipeline] || :default
-      end
-
       def customized?(transformers)
         transformers.last.source_location[0] == __FILE__
       end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/filter/snippet_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/snippet_reference_filter.rb
rename to lib/gitlab/markdown/filter/snippet_reference_filter.rb
diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/gitlab/markdown/filter/syntax_highlight_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/syntax_highlight_filter.rb
rename to lib/gitlab/markdown/filter/syntax_highlight_filter.rb
diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/gitlab/markdown/filter/table_of_contents_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/table_of_contents_filter.rb
rename to lib/gitlab/markdown/filter/table_of_contents_filter.rb
diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/gitlab/markdown/filter/task_list_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/task_list_filter.rb
rename to lib/gitlab/markdown/filter/task_list_filter.rb
diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/filter/upload_link_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/upload_link_filter.rb
rename to lib/gitlab/markdown/filter/upload_link_filter.rb
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/filter/user_reference_filter.rb
similarity index 100%
rename from lib/gitlab/markdown/user_reference_filter.rb
rename to lib/gitlab/markdown/filter/user_reference_filter.rb
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d683756f95ad7b2425d26d39d603f6b4ec479af3
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -0,0 +1,34 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class Pipeline
+      def self.[](name)
+        name ||= :full
+        Markdown.const_get("#{name.to_s.camelize}Pipeline")
+      end
+
+      def self.filters
+        []
+      end
+
+      def self.transform_context(context)
+        context
+      end
+
+      def self.html_pipeline
+        @html_pipeline ||= HTML::Pipeline.new(filters)
+      end
+
+      class << self
+        %i(call to_document to_html).each do |meth|
+          define_method(meth) do |text, context|
+            context = transform_context(context)
+
+            html_pipeline.send(meth, text, context)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb b/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6829b4acb9596ceab1b078a09e05b8f397cc1225
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/asciidoc_pipeline.rb
@@ -0,0 +1,13 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class AsciidocPipeline < Pipeline
+      def self.filters
+        [
+          Gitlab::Markdown::RelativeLinkFilter
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/atom_pipeline.rb b/lib/gitlab/markdown/pipeline/atom_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e151f8f5e5aec8e4e9afe4aa470a2f9eb97b7603
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/atom_pipeline.rb
@@ -0,0 +1,14 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class AtomPipeline < FullPipeline
+      def self.transform_context(context)
+        super(context).merge( 
+          only_path: false, 
+          xhtml: true 
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/description_pipeline.rb b/lib/gitlab/markdown/pipeline/description_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..76f6948af8f3f9a8745080ab471faf1ff2070654
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/description_pipeline.rb
@@ -0,0 +1,14 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class DescriptionPipeline < FullPipeline
+      def self.transform_context(context)
+        super(context).merge( 
+          # SanitizationFilter
+          inline_sanitization: true
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/email_pipeline.rb b/lib/gitlab/markdown/pipeline/email_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b88cb790270cafb3a7b0df646e05aeb8d6ac4528
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/email_pipeline.rb
@@ -0,0 +1,13 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class EmailPipeline < FullPipeline
+      def self.transform_context(context)
+        super(context).merge( 
+          only_path: false
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/full_pipeline.rb b/lib/gitlab/markdown/pipeline/full_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b3b7a3c27c016115c2b75383ea99894d368b3dfa
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/full_pipeline.rb
@@ -0,0 +1,9 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
+
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/gfm_pipeline.rb b/lib/gitlab/markdown/pipeline/gfm_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca90bd75d7748e9db6457192e73dd366f6131501
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/gfm_pipeline.rb
@@ -0,0 +1,41 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class GfmPipeline < Pipeline
+      def self.filters
+        @filters ||= [
+          Gitlab::Markdown::SyntaxHighlightFilter,
+          Gitlab::Markdown::SanitizationFilter,
+
+          Gitlab::Markdown::UploadLinkFilter,
+          Gitlab::Markdown::EmojiFilter,
+          Gitlab::Markdown::TableOfContentsFilter,
+          Gitlab::Markdown::AutolinkFilter,
+          Gitlab::Markdown::ExternalLinkFilter,
+
+          Gitlab::Markdown::UserReferenceFilter,
+          Gitlab::Markdown::IssueReferenceFilter,
+          Gitlab::Markdown::ExternalIssueReferenceFilter,
+          Gitlab::Markdown::MergeRequestReferenceFilter,
+          Gitlab::Markdown::SnippetReferenceFilter,
+          Gitlab::Markdown::CommitRangeReferenceFilter,
+          Gitlab::Markdown::CommitReferenceFilter,
+          Gitlab::Markdown::LabelReferenceFilter,
+
+          Gitlab::Markdown::TaskListFilter
+        ]
+      end
+
+      def self.transform_context(context)
+        context.merge(
+          only_path: true,
+
+          # EmojiFilter
+          asset_host: Gitlab::Application.config.asset_host,
+          asset_root: Gitlab.config.gitlab.base_url
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/note_pipeline.rb b/lib/gitlab/markdown/pipeline/note_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8bf5f42d8e18700abd0bf83a64238a0d5e4aaa6
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/note_pipeline.rb
@@ -0,0 +1,14 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class NotePipeline < FullPipeline
+      def self.transform_context(context)
+        super(context).merge( 
+          # TableOfContentsFilter
+          no_header_anchors: true
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb b/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0abb93f8a033de44f2516c914cdeadd8cc6fae43
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/plain_markdown_pipeline.rb
@@ -0,0 +1,13 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class PlainMarkdownPipeline < Pipeline
+      def self.filters
+        [
+          Gitlab::Markdown::MarkdownFilter
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/post_process_pipeline.rb b/lib/gitlab/markdown/pipeline/post_process_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60cc32f490e4a66696b32201e6114313ddb52b4f
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/post_process_pipeline.rb
@@ -0,0 +1,20 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class PostProcessPipeline < Pipeline
+      def self.filters
+        [
+          Gitlab::Markdown::RelativeLinkFilter, 
+          Gitlab::Markdown::RedactorFilter
+        ]
+      end
+
+      def self.transform_context(context)
+        context.merge(
+          post_process: true
+        )
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb b/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a89ab462bac3f83c0f7de1b89a4f89b62729fd80
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/reference_extraction_pipeline.rb
@@ -0,0 +1,13 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class ReferenceExtractionPipeline < Pipeline
+      def self.filters
+        [
+          Gitlab::Markdown::ReferenceGathererFilter
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/pipeline/single_line_pipeline.rb b/lib/gitlab/markdown/pipeline/single_line_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2f24927b8798f68a87ba97b7adff3bc06ac72438
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline/single_line_pipeline.rb
@@ -0,0 +1,9 @@
+require 'gitlab/markdown'
+
+module Gitlab
+  module Markdown
+    class SingleLinePipeline < GfmPipeline
+      
+    end
+  end
+end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb
index b6d93e05ec7987ac198679902451b394c319ff86..3b83b8bd8f880da24d42eb58152edbf51cf05ff7 100644
--- a/lib/gitlab/markdown/reference_filter.rb
+++ b/lib/gitlab/markdown/reference_filter.rb
@@ -29,6 +29,10 @@ module Gitlab
         end
       end
 
+      def self.[](name)
+        Markdown.const_get("#{name.to_s.camelize}ReferenceFilter")
+      end
+
       def self.user_can_reference?(user, node, context)
         if node.has_attribute?('data-project')
           project_id = node.attr('data-project').to_i
@@ -53,14 +57,14 @@ module Gitlab
       # Examples:
       #
       #   data_attribute(project: 1, issue: 2)
-      #   # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+      #   # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
       #
       #   data_attribute(project: 3, merge_request: 4)
-      #   # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+      #   # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
       #
       # Returns a String
       def data_attribute(attributes = {})
-        attributes[:reference_filter] = self.class.name
+        attributes[:reference_filter] = self.class.name.demodulize
         attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
       end
 
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 3c3478a12719e9a66c639918a41fe2b2740a6223..e83167fa7d79f13010a0b9404ef612c9b5b975fa 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -9,30 +9,23 @@ module Gitlab
       @project = project
       @current_user = current_user
       @load_lazy_references = load_lazy_references
+
+      @texts = []
+      @references = {}
     end
 
-    def analyze(text)
-      references.clear
-      @text = Gitlab::Markdown.render_without_gfm(text)
+    def analyze(text, options = {})
+      @texts << Gitlab::Markdown.render(text, options.merge(project: project))
     end
 
     %i(user label issue merge_request snippet commit commit_range).each do |type|
       define_method("#{type}s") do
-        references[type]
+        @references[type] ||= pipeline_result(type)
       end
     end
 
     private
 
-    def references
-      @references ||= Hash.new do |references, type|
-        type = type.to_sym
-        next references[type] if references.has_key?(type)
-
-        references[type] = pipeline_result(type)
-      end
-    end
-
     # Instantiate and call HTML::Pipeline with a single reference filter type,
     # returning the result
     #
@@ -40,36 +33,24 @@ module Gitlab
     #
     # Returns the results Array for the requested filter type
     def pipeline_result(filter_type)
-      return [] if @text.blank?
-
-      klass  = "#{filter_type.to_s.camelize}ReferenceFilter"
-      filter = Gitlab::Markdown.const_get(klass)
+      filter = Gitlab::Markdown::ReferenceFilter[filter_type]
 
       context = {
-        project: project,
-        current_user: current_user,
+        pipeline: :reference_extraction,
 
-        # We don't actually care about the links generated
-        only_path: true,
-        ignore_blockquotes: true,
+        project:      project,
+        current_user: current_user,
 
         # ReferenceGathererFilter
         load_lazy_references: false,
         reference_filter:     filter
       }
 
-      # We need to autolink first to finds links to referables, and to prevent
-      # numeric anchors to be parsed as issue references.
-      filters = [
-        Gitlab::Markdown::AutolinkFilter,
-        filter,
-        Gitlab::Markdown::ReferenceGathererFilter
-      ]
-
-      pipeline = HTML::Pipeline.new(filters, context)
-      result = pipeline.call(@text)
-
-      values = result[:references][filter_type].uniq
+      values = @texts.flat_map do |html|
+        text_context = context.dup
+        result = Gitlab::Markdown.render_result(html, text_context)
+        result[:references][filter_type]
+      end.uniq
 
       if @load_lazy_references
         values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 7d985fb6f1e820cb7e75d788fe2dafc91f589a43..3a860899e1856479b14fe9d2be2d15c374cef902 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -50,9 +50,9 @@ module Gitlab
         filtered_html = '<b>ASCII</b>'
 
         allow(Asciidoctor).to receive(:convert).and_return(html)
-        expect_any_instance_of(HTML::Pipeline).to receive(:call)
-          .with(html, context)
-          .and_return(output: Nokogiri::HTML.fragment(filtered_html))
+        expect(Gitlab::Markdown).to receive(:render)
+          .with(html, context.merge(pipeline: :asciidoc))
+          .and_return(filtered_html)
 
         expect( render('foo', context) ).to eql filtered_html
       end
diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
deleted file mode 100644
index 7cfec554a457d8a1e17cbb8c21be5c8853607e9d..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe AutolinkFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:link) { 'http://about.gitlab.com/' }
-
-    it 'does nothing when :autolink is false' do
-      exp = act = link
-      expect(filter(act, autolink: false).to_html).to eq exp
-    end
-
-    it 'does nothing with non-link text' do
-      exp = act = 'This text contains no links to autolink'
-      expect(filter(act).to_html).to eq exp
-    end
-
-    context 'Rinku schemes' do
-      it 'autolinks http' do
-        doc = filter("See #{link}")
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'autolinks https' do
-        link = 'https://google.com/'
-        doc = filter("See #{link}")
-
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'autolinks ftp' do
-        link = 'ftp://ftp.us.debian.org/debian/'
-        doc = filter("See #{link}")
-
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'autolinks short URLs' do
-        link = 'http://localhost:3000/'
-        doc = filter("See #{link}")
-
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'accepts link_attr options' do
-        doc = filter("See #{link}", link_attr: { class: 'custom' })
-
-        expect(doc.at_css('a')['class']).to eq 'custom'
-      end
-
-      described_class::IGNORE_PARENTS.each do |elem|
-        it "ignores valid links contained inside '#{elem}' element" do
-          exp = act = "<#{elem}>See #{link}</#{elem}>"
-          expect(filter(act).to_html).to eq exp
-        end
-      end
-    end
-
-    context 'other schemes' do
-      let(:link) { 'foo://bar.baz/' }
-
-      it 'autolinks smb' do
-        link = 'smb:///Volumes/shared/foo.pdf'
-        doc = filter("See #{link}")
-
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'autolinks irc' do
-        link = 'irc://irc.freenode.net/git'
-        doc = filter("See #{link}")
-
-        expect(doc.at_css('a').text).to eq link
-        expect(doc.at_css('a')['href']).to eq link
-      end
-
-      it 'does not include trailing punctuation' do
-        doc = filter("See #{link}.")
-        expect(doc.at_css('a').text).to eq link
-
-        doc = filter("See #{link}, ok?")
-        expect(doc.at_css('a').text).to eq link
-
-        doc = filter("See #{link}...")
-        expect(doc.at_css('a').text).to eq link
-      end
-
-      it 'does not include trailing HTML entities' do
-        doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
-
-        expect(doc.at_css('a')['href']).to eq link
-        expect(doc.text).to eq "See <<<#{link}>>>"
-      end
-
-      it 'accepts link_attr options' do
-        doc = filter("See #{link}", link_attr: { class: 'custom' })
-        expect(doc.at_css('a')['class']).to eq 'custom'
-      end
-
-      described_class::IGNORE_PARENTS.each do |elem|
-        it "ignores valid links contained inside '#{elem}' element" do
-          exp = act = "<#{elem}>See #{link}</#{elem}>"
-          expect(filter(act).to_html).to eq exp
-        end
-      end
-    end
-  end
-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
deleted file mode 100644
index 6aecba5a676713a9fe6495d9410e726f6c68163c..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
+++ /dev/null
@@ -1,184 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe CommitRangeReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project) { create(:project, :public) }
-    let(:commit1) { project.commit("HEAD~2") }
-    let(:commit2) { project.commit }
-
-    let(:range)  { CommitRange.new("#{commit1.id}...#{commit2.id}", project) }
-    let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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 #{range.to_reference}</#{elem}>"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'internal reference' do
-      let(:reference)  { range.to_reference }
-      let(:reference2) { range2.to_reference }
-
-      it 'links to a valid two-dot reference' do
-        doc = reference_filter("See #{reference2}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
-      end
-
-      it 'links to a valid three-dot reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
-      end
-
-      it 'links to a valid short ID' do
-        reference = "#{commit1.short_id}...#{commit2.id}"
-        reference2 = "#{commit1.id}...#{commit2.short_id}"
-
-        exp = commit1.short_id + '...' + commit2.short_id
-
-        expect(reference_filter("See #{reference}").css('a').first.text).to eq exp
-        expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("See (#{reference}.)")
-
-        exp = Regexp.escape(range.reference_link_text)
-        expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit 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(project.repository).to receive(:commit).with(commit2.id)
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'includes a title attribute' do
-        doc = reference_filter("See #{reference}")
-        expect(doc.css('a').first.attr('title')).to eq range.reference_title
-      end
-
-      it 'includes default classes' do
-        doc = reference_filter("See #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-commit-range attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-commit-range')
-        expect(link.attr('data-commit-range')).to eq range.to_s
-      end
-
-      it 'supports an :only_path option' do
-        doc = reference_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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit_range]).not_to be_empty
-      end
-    end
-
-    context 'cross-project reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:reference) { range.to_reference(project) }
-
-      before do
-        range.project = project2
-      end
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Fixed (#{reference}.)")
-
-        exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}")
-        expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit IDs on the referenced project' do
-        exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
-        expect(reference_filter(act).to_html).to eq exp
-
-        exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit_range]).not_to be_empty
-      end
-    end
-
-    context 'cross-project URL reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:range)  { CommitRange.new("#{commit1.id}...master", project) }
-      let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
-
-      before do
-        range.project = project2
-      end
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq reference
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Fixed (#{reference}.)")
-
-        exp = Regexp.escape(range.reference_link_text(project))
-        expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit IDs on the referenced project' do
-        exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
-        expect(reference_filter(act).to_html).to eq exp
-
-        exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit_range]).not_to be_empty
-      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
deleted file mode 100644
index 9ac76b3228b6a7660591954f605e21ce82ab5e01..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
+++ /dev/null
@@ -1,165 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe CommitReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project) { create(:project, :public) }
-    let(:commit)  { project.commit }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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(reference_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 = reference_filter("See #{reference[0...size]}")
-
-          expect(doc.css('a').first.text).to eq commit.short_id
-          expect(doc.css('a').first.attr('href')).
-            to eq urls.namespace_project_commit_url(project.namespace, project, reference)
-        end
-      end
-
-      it 'always uses the short ID as the link text' do
-        doc = reference_filter("See #{commit.id}")
-        expect(doc.text).to eq "See #{commit.short_id}"
-
-        doc = reference_filter("See #{commit.id[0...6]}")
-        expect(doc.text).to eq "See #{commit.short_id}"
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("See (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit IDs' do
-        invalid = invalidate_reference(reference)
-        exp = act = "See #{invalid}"
-
-        expect(project).to receive(:valid_repo?).and_return(true)
-        expect(project.repository).to receive(:commit).with(invalid)
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'includes a title attribute' do
-        doc = reference_filter("See #{reference}")
-        expect(doc.css('a').first.attr('title')).to eq commit.link_title
-      end
-
-      it 'escapes the title attribute' do
-        allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
-
-        doc = reference_filter("See #{reference}")
-        expect(doc.text).to eq "See #{commit.short_id}"
-      end
-
-      it 'includes default classes' do
-        doc = reference_filter("See #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-commit attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-commit')
-        expect(link.attr('data-commit')).to eq commit.id
-      end
-
-      it 'supports an :only_path context' do
-        doc = reference_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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit]).not_to be_empty
-      end
-    end
-
-    context 'cross-project reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:commit)    { project2.commit }
-      let(:reference) { commit.to_reference(project) }
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Fixed (#{reference}.)")
-
-        exp = Regexp.escape(project2.to_reference)
-        expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit IDs on the referenced project' do
-        exp = act = "Committed #{invalidate_reference(reference)}"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit]).not_to be_empty
-      end
-    end
-
-    context 'cross-project URL reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:commit)    { project2.commit }
-      let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Fixed (#{reference}.)")
-
-        expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid commit IDs on the referenced project' do
-        act = "Committed #{invalidate_reference(reference)}"
-        expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("See #{reference}")
-        expect(result[:references][:commit]).not_to be_empty
-      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
index e777489778053865f870d96e1456e95f4e6e2102..f594fe4ccf638552211a05451652e01f0d1aa7c9 100644
--- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
+++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
@@ -1,35 +1,33 @@
 require 'spec_helper'
 
-module Gitlab::Markdown
-  describe CrossProjectReference, lib: true do
-    include described_class
+describe Gitlab::Markdown::CrossProjectReference, lib: true do
+  include described_class
 
-    describe '#project_from_ref' do
-      context 'when no project was referenced' do
-        it 'returns the project from context' do
-          project = double
+  describe '#project_from_ref' do
+    context 'when no project was referenced' do
+      it 'returns the project from context' do
+        project = double
 
-          allow(self).to receive(:context).and_return({ project: project })
+        allow(self).to receive(:context).and_return({ project: project })
 
-          expect(project_from_ref(nil)).to eq project
-        end
+        expect(project_from_ref(nil)).to eq project
       end
+    end
 
-      context 'when referenced project does not exist' do
-        it 'returns nil' do
-          expect(project_from_ref('invalid/reference')).to be_nil
-        end
+    context 'when referenced project does not exist' do
+      it 'returns nil' do
+        expect(project_from_ref('invalid/reference')).to be_nil
       end
+    end
 
-      context 'when referenced project exists' do
-        it 'returns the referenced project' do
-          project2 = double('referenced project')
+    context 'when referenced project exists' do
+      it 'returns the referenced project' do
+        project2 = double('referenced project')
 
-          expect(Project).to receive(:find_with_namespace).
-            with('cross/reference').and_return(project2)
+        expect(Project).to receive(:find_with_namespace).
+          with('cross/reference').and_return(project2)
 
-          expect(project_from_ref('cross/reference')).to eq project2
-        end
+        expect(project_from_ref('cross/reference')).to eq project2
       end
     end
   end
diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
deleted file mode 100644
index fb98aac334013423ac8ab4376289e9c8bdb7685a..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe EmojiFilter, lib: true do
-    include FilterSpecHelper
-
-    before do
-      ActionController::Base.asset_host = 'https://foo.com'
-    end
-
-    it 'replaces supported emoji' do
-      doc = filter('<p>:heart:</p>')
-      expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
-    end
-
-    it 'ignores unsupported emoji' do
-      exp = act = '<p>:foo:</p>'
-      doc = filter(act)
-      expect(doc.to_html).to match Regexp.escape(exp)
-    end
-
-    it 'correctly encodes the URL' do
-      doc = filter('<p>:+1:</p>')
-      expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
-    end
-
-    it 'matches at the start of a string' do
-      doc = filter(':+1:')
-      expect(doc.css('img').size).to eq 1
-    end
-
-    it 'matches at the end of a string' do
-      doc = filter('This gets a :-1:')
-      expect(doc.css('img').size).to eq 1
-    end
-
-    it 'matches with adjacent text' do
-      doc = filter('+1 (:+1:)')
-      expect(doc.css('img').size).to eq 1
-    end
-
-    it 'matches multiple emoji in a row' do
-      doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
-      expect(doc.css('img').size).to eq 3
-    end
-
-    it 'has a title attribute' do
-      doc = filter(':-1:')
-      expect(doc.css('img').first.attr('title')).to eq ':-1:'
-    end
-
-    it 'has an alt attribute' do
-      doc = filter(':-1:')
-      expect(doc.css('img').first.attr('alt')).to eq ':-1:'
-    end
-
-    it 'has an align attribute' do
-      doc = filter(':8ball:')
-      expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
-    end
-
-    it 'has an emoji class' do
-      doc = filter(':cat:')
-      expect(doc.css('img').first.attr('class')).to eq 'emoji'
-    end
-
-    it 'has height and width attributes' do
-      doc = filter(':dog:')
-      img = doc.css('img').first
-
-      expect(img.attr('width')).to eq '20'
-      expect(img.attr('height')).to eq '20'
-    end
-
-    it 'keeps whitespace intact' do
-      doc = filter('This deserves a :+1:, big time.')
-
-      expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
-    end
-
-    it 'uses a custom asset_root context' do
-      root = Gitlab.config.gitlab.url + 'gitlab/root'
-
-      doc = filter(':smile:', asset_root: root)
-      expect(doc.css('img').first.attr('src')).to start_with(root)
-    end
-
-    it 'uses a custom asset_host context' do
-      ActionController::Base.asset_host = 'https://cdn.example.com'
-
-      doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
-      expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
-    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
deleted file mode 100644
index cd92007e0478260887ecd64676750d319c4eda8c..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe ExternalIssueReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    def helper
-      IssuesHelper
-    end
-
-    let(:project) { create(:jira_project) }
-
-    context 'JIRA issue references' do
-      let(:issue)     { ExternalIssue.new('JIRA-123', project) }
-      let(:reference) { issue.to_reference }
-
-      it 'requires project context' do
-        expect { described_class.call('') }.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 #{reference}</#{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
-
-      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 'escapes the title attribute' do
-        allow(project.external_issue_tracker).to receive(:title).
-          and_return(%{"></a>whatever<a title="})
-
-        doc = filter("Issue #{reference}")
-        expect(doc.text).to eq "Issue #{reference}"
-      end
-
-      it 'includes default classes' do
-        doc = filter("Issue #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
-      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/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb
deleted file mode 100644
index 493c19a287d945ab040a4d56fa6f8c26d5397834..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe ExternalLinkFilter, lib: true do
-    include FilterSpecHelper
-
-    it 'ignores elements without an href attribute' do
-      exp = act = %q(<a id="ignored">Ignore Me</a>)
-      expect(filter(act).to_html).to eq exp
-    end
-
-    it 'ignores non-HTTP(S) links' do
-      exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>)
-      expect(filter(act).to_html).to eq exp
-    end
-
-    it 'skips internal links' do
-      internal = Gitlab.config.gitlab.url
-      exp = act = %Q(<a href="#{internal}/sign_in">Login</a>)
-      expect(filter(act).to_html).to eq exp
-    end
-
-    it 'adds rel="nofollow" to external links' do
-      act = %q(<a href="https://google.com/">Google</a>)
-      doc = filter(act)
-
-      expect(doc.at_css('a')).to have_attribute('rel')
-      expect(doc.at_css('a')['rel']).to eq 'nofollow'
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a0844aee55915b4332e8009e34351ba4c32e9974
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/autolink_filter_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::AutolinkFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:link) { 'http://about.gitlab.com/' }
+
+  it 'does nothing when :autolink is false' do
+    exp = act = link
+    expect(filter(act, autolink: false).to_html).to eq exp
+  end
+
+  it 'does nothing with non-link text' do
+    exp = act = 'This text contains no links to autolink'
+    expect(filter(act).to_html).to eq exp
+  end
+
+  context 'Rinku schemes' do
+    it 'autolinks http' do
+      doc = filter("See #{link}")
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'autolinks https' do
+      link = 'https://google.com/'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'autolinks ftp' do
+      link = 'ftp://ftp.us.debian.org/debian/'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'autolinks short URLs' do
+      link = 'http://localhost:3000/'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'accepts link_attr options' do
+      doc = filter("See #{link}", link_attr: { class: 'custom' })
+
+      expect(doc.at_css('a')['class']).to eq 'custom'
+    end
+
+    described_class::IGNORE_PARENTS.each do |elem|
+      it "ignores valid links contained inside '#{elem}' element" do
+        exp = act = "<#{elem}>See #{link}</#{elem}>"
+        expect(filter(act).to_html).to eq exp
+      end
+    end
+  end
+
+  context 'other schemes' do
+    let(:link) { 'foo://bar.baz/' }
+
+    it 'autolinks smb' do
+      link = 'smb:///Volumes/shared/foo.pdf'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'autolinks irc' do
+      link = 'irc://irc.freenode.net/git'
+      doc = filter("See #{link}")
+
+      expect(doc.at_css('a').text).to eq link
+      expect(doc.at_css('a')['href']).to eq link
+    end
+
+    it 'does not include trailing punctuation' do
+      doc = filter("See #{link}.")
+      expect(doc.at_css('a').text).to eq link
+
+      doc = filter("See #{link}, ok?")
+      expect(doc.at_css('a').text).to eq link
+
+      doc = filter("See #{link}...")
+      expect(doc.at_css('a').text).to eq link
+    end
+
+    it 'does not include trailing HTML entities' do
+      doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
+
+      expect(doc.at_css('a')['href']).to eq link
+      expect(doc.text).to eq "See <<<#{link}>>>"
+    end
+
+    it 'accepts link_attr options' do
+      doc = filter("See #{link}", link_attr: { class: 'custom' })
+      expect(doc.at_css('a')['class']).to eq 'custom'
+    end
+
+    described_class::IGNORE_PARENTS.each do |elem|
+      it "ignores valid links contained inside '#{elem}' element" do
+        exp = act = "<#{elem}>See #{link}</#{elem}>"
+        expect(filter(act).to_html).to eq exp
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..570c976762859fe8051dea363cb889ae03e4765f
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/commit_range_reference_filter_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::CommitRangeReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project) { create(:project, :public) }
+  let(:commit1) { project.commit("HEAD~2") }
+  let(:commit2) { project.commit }
+
+  let(:range)  { CommitRange.new("#{commit1.id}...#{commit2.id}", project) }
+  let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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 #{range.to_reference}</#{elem}>"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'internal reference' do
+    let(:reference)  { range.to_reference }
+    let(:reference2) { range2.to_reference }
+
+    it 'links to a valid two-dot reference' do
+      doc = reference_filter("See #{reference2}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
+    end
+
+    it 'links to a valid three-dot reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
+    end
+
+    it 'links to a valid short ID' do
+      reference = "#{commit1.short_id}...#{commit2.id}"
+      reference2 = "#{commit1.id}...#{commit2.short_id}"
+
+      exp = commit1.short_id + '...' + commit2.short_id
+
+      expect(reference_filter("See #{reference}").css('a').first.text).to eq exp
+      expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("See (#{reference}.)")
+
+      exp = Regexp.escape(range.reference_link_text)
+      expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit 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(project.repository).to receive(:commit).with(commit2.id)
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'includes a title attribute' do
+      doc = reference_filter("See #{reference}")
+      expect(doc.css('a').first.attr('title')).to eq range.reference_title
+    end
+
+    it 'includes default classes' do
+      doc = reference_filter("See #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-commit-range attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-commit-range')
+      expect(link.attr('data-commit-range')).to eq range.to_s
+    end
+
+    it 'supports an :only_path option' do
+      doc = reference_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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit_range]).not_to be_empty
+    end
+  end
+
+  context 'cross-project reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:reference) { range.to_reference(project) }
+
+    before do
+      range.project = project2
+    end
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Fixed (#{reference}.)")
+
+      exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}")
+      expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit IDs on the referenced project' do
+      exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+      expect(reference_filter(act).to_html).to eq exp
+
+      exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit_range]).not_to be_empty
+    end
+  end
+
+  context 'cross-project URL reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:range)  { CommitRange.new("#{commit1.id}...master", project) }
+    let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
+
+    before do
+      range.project = project2
+    end
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq reference
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Fixed (#{reference}.)")
+
+      exp = Regexp.escape(range.reference_link_text(project))
+      expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit IDs on the referenced project' do
+      exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+      expect(reference_filter(act).to_html).to eq exp
+
+      exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit_range]).not_to be_empty
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..76e7957bbb9e6c0cdc01148bb4d65c6a4508a4a9
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/commit_reference_filter_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::CommitReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project) { create(:project, :public) }
+  let(:commit)  { project.commit }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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(reference_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 = reference_filter("See #{reference[0...size]}")
+
+        expect(doc.css('a').first.text).to eq commit.short_id
+        expect(doc.css('a').first.attr('href')).
+          to eq urls.namespace_project_commit_url(project.namespace, project, reference)
+      end
+    end
+
+    it 'always uses the short ID as the link text' do
+      doc = reference_filter("See #{commit.id}")
+      expect(doc.text).to eq "See #{commit.short_id}"
+
+      doc = reference_filter("See #{commit.id[0...6]}")
+      expect(doc.text).to eq "See #{commit.short_id}"
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("See (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit IDs' do
+      invalid = invalidate_reference(reference)
+      exp = act = "See #{invalid}"
+
+      expect(project).to receive(:valid_repo?).and_return(true)
+      expect(project.repository).to receive(:commit).with(invalid)
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'includes a title attribute' do
+      doc = reference_filter("See #{reference}")
+      expect(doc.css('a').first.attr('title')).to eq commit.link_title
+    end
+
+    it 'escapes the title attribute' do
+      allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
+
+      doc = reference_filter("See #{reference}")
+      expect(doc.text).to eq "See #{commit.short_id}"
+    end
+
+    it 'includes default classes' do
+      doc = reference_filter("See #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-commit attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-commit')
+      expect(link.attr('data-commit')).to eq commit.id
+    end
+
+    it 'supports an :only_path context' do
+      doc = reference_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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit]).not_to be_empty
+    end
+  end
+
+  context 'cross-project reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:commit)    { project2.commit }
+    let(:reference) { commit.to_reference(project) }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Fixed (#{reference}.)")
+
+      exp = Regexp.escape(project2.to_reference)
+      expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit IDs on the referenced project' do
+      exp = act = "Committed #{invalidate_reference(reference)}"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit]).not_to be_empty
+    end
+  end
+
+  context 'cross-project URL reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:commit)    { project2.commit }
+    let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Fixed (#{reference}.)")
+
+      expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid commit IDs on the referenced project' do
+      act = "Committed #{invalidate_reference(reference)}"
+      expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("See #{reference}")
+      expect(result[:references][:commit]).not_to be_empty
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea9b81862cfb04fbb178399f8d853a1de32ca834
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/emoji_filter_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::EmojiFilter, lib: true do
+  include FilterSpecHelper
+
+  before do
+    @original_asset_host = ActionController::Base.asset_host
+    ActionController::Base.asset_host = 'https://foo.com'
+  end
+
+  after do
+    ActionController::Base.asset_host = @original_asset_host
+  end
+
+  it 'replaces supported emoji' do
+    doc = filter('<p>:heart:</p>')
+    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
+  end
+
+  it 'ignores unsupported emoji' do
+    exp = act = '<p>:foo:</p>'
+    doc = filter(act)
+    expect(doc.to_html).to match Regexp.escape(exp)
+  end
+
+  it 'correctly encodes the URL' do
+    doc = filter('<p>:+1:</p>')
+    expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
+  end
+
+  it 'matches at the start of a string' do
+    doc = filter(':+1:')
+    expect(doc.css('img').size).to eq 1
+  end
+
+  it 'matches at the end of a string' do
+    doc = filter('This gets a :-1:')
+    expect(doc.css('img').size).to eq 1
+  end
+
+  it 'matches with adjacent text' do
+    doc = filter('+1 (:+1:)')
+    expect(doc.css('img').size).to eq 1
+  end
+
+  it 'matches multiple emoji in a row' do
+    doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
+    expect(doc.css('img').size).to eq 3
+  end
+
+  it 'has a title attribute' do
+    doc = filter(':-1:')
+    expect(doc.css('img').first.attr('title')).to eq ':-1:'
+  end
+
+  it 'has an alt attribute' do
+    doc = filter(':-1:')
+    expect(doc.css('img').first.attr('alt')).to eq ':-1:'
+  end
+
+  it 'has an align attribute' do
+    doc = filter(':8ball:')
+    expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+  end
+
+  it 'has an emoji class' do
+    doc = filter(':cat:')
+    expect(doc.css('img').first.attr('class')).to eq 'emoji'
+  end
+
+  it 'has height and width attributes' do
+    doc = filter(':dog:')
+    img = doc.css('img').first
+
+    expect(img.attr('width')).to eq '20'
+    expect(img.attr('height')).to eq '20'
+  end
+
+  it 'keeps whitespace intact' do
+    doc = filter('This deserves a :+1:, big time.')
+
+    expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+  end
+
+  it 'uses a custom asset_root context' do
+    root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+    doc = filter(':smile:', asset_root: root)
+    expect(doc.css('img').first.attr('src')).to start_with(root)
+  end
+
+  it 'uses a custom asset_host context' do
+    ActionController::Base.asset_host = 'https://cdn.example.com'
+
+    doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
+    expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d84201026489bf819e6c122c81fd5d596cba02f2
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/external_issue_reference_filter_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::ExternalIssueReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  def helper
+    IssuesHelper
+  end
+
+  let(:project) { create(:jira_project) }
+
+  context 'JIRA issue references' do
+    let(:issue)     { ExternalIssue.new('JIRA-123', project) }
+    let(:reference) { issue.to_reference }
+
+    it 'requires project context' do
+      expect { described_class.call('') }.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 #{reference}</#{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
+
+    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 'escapes the title attribute' do
+      allow(project.external_issue_tracker).to receive(:title).
+        and_return(%{"></a>whatever<a title="})
+
+      doc = filter("Issue #{reference}")
+      expect(doc.text).to eq "Issue #{reference}"
+    end
+
+    it 'includes default classes' do
+      doc = filter("Issue #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+    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
diff --git a/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e559f5741cc43a16c16cba6a9789d9dd96db3063
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/external_link_filter_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::ExternalLinkFilter, lib: true do
+  include FilterSpecHelper
+
+  it 'ignores elements without an href attribute' do
+    exp = act = %q(<a id="ignored">Ignore Me</a>)
+    expect(filter(act).to_html).to eq exp
+  end
+
+  it 'ignores non-HTTP(S) links' do
+    exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>)
+    expect(filter(act).to_html).to eq exp
+  end
+
+  it 'skips internal links' do
+    internal = Gitlab.config.gitlab.url
+    exp = act = %Q(<a href="#{internal}/sign_in">Login</a>)
+    expect(filter(act).to_html).to eq exp
+  end
+
+  it 'adds rel="nofollow" to external links' do
+    act = %q(<a href="https://google.com/">Google</a>)
+    doc = filter(act)
+
+    expect(doc.at_css('a')).to have_attribute('rel')
+    expect(doc.at_css('a')['rel']).to eq 'nofollow'
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1aa5d44568e321228f1fd71f318d80746b824c52
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/issue_reference_filter_spec.rb
@@ -0,0 +1,209 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::IssueReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  def helper
+    IssuesHelper
+  end
+
+  let(:project) { create(:empty_project, :public) }
+  let(:issue)   { create(:issue, project: project) }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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 #{issue.to_reference}</#{elem}>"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'internal reference' do
+    let(:reference) { issue.to_reference }
+
+    it 'ignores valid references when using non-default tracker' do
+      expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
+
+      exp = act = "Issue #{reference}"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'links to a valid reference' do
+      doc = reference_filter("Fixed #{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 = reference_filter("Fixed (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid issue IDs' do
+      invalid = invalidate_reference(reference)
+      exp = act = "Fixed #{invalid}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'includes a title attribute' do
+      doc = reference_filter("Issue #{reference}")
+      expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+    end
+
+    it 'escapes the title attribute' do
+      issue.update_attribute(:title, %{"></a>whatever<a title="})
+
+      doc = reference_filter("Issue #{reference}")
+      expect(doc.text).to eq "Issue #{reference}"
+    end
+
+    it 'includes default classes' do
+      doc = reference_filter("Issue #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("Issue #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-issue attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-issue')
+      expect(link.attr('data-issue')).to eq issue.id.to_s
+    end
+
+    it 'supports an :only_path context' do
+      doc = reference_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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Fixed #{reference}")
+      expect(result[:references][:issue]).to eq [issue]
+    end
+  end
+
+  context 'cross-project reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:issue)     { create(:issue, project: project2) }
+    let(:reference) { issue.to_reference(project) }
+
+    it 'ignores valid references when cross-reference project uses external tracker' do
+      expect_any_instance_of(Project).to receive(:get_issue).
+        with(issue.iid).and_return(nil)
+
+      exp = act = "Issue #{reference}"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_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 #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Fixed #{reference}")
+      expect(result[:references][:issue]).to eq [issue]
+    end
+  end
+
+  context 'cross-project URL reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:issue)     { create(:issue, project: project2) }
+    let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq reference
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Fixed (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Fixed #{reference}")
+      expect(result[:references][:issue]).to eq [issue]
+    end
+  end
+
+  context 'cross-project reference in link href' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:issue)     { create(:issue, project: project2) }
+    let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Fixed (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Fixed #{reference}")
+      expect(result[:references][:issue]).to eq [issue]
+    end
+  end
+
+  context 'cross-project URL in link href' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:issue)     { create(:issue, project: project2) }
+    let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Fixed (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Fixed #{reference}")
+      expect(result[:references][:issue]).to eq [issue]
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4fcbb329fe475f491a2a44b1e3f54f2473287bc0
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/label_reference_filter_spec.rb
@@ -0,0 +1,179 @@
+require 'spec_helper'
+require 'html/pipeline'
+
+describe Gitlab::Markdown::LabelReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project)   { create(:empty_project, :public) }
+  let(:label)     { create(:label, project: project) }
+  let(:reference) { label.to_reference }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  it 'includes default classes' do
+    doc = reference_filter("Label #{reference}")
+    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+  end
+
+  it 'includes a data-project attribute' do
+    doc = reference_filter("Label #{reference}")
+    link = doc.css('a').first
+
+    expect(link).to have_attribute('data-project')
+    expect(link.attr('data-project')).to eq project.id.to_s
+  end
+
+  it 'includes a data-label attribute' do
+    doc = reference_filter("See #{reference}")
+    link = doc.css('a').first
+
+    expect(link).to have_attribute('data-label')
+    expect(link.attr('data-label')).to eq label.id.to_s
+  end
+
+  it 'supports an :only_path context' do
+    doc = reference_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_path(project.namespace, project, label_name: label.name)
+  end
+
+  it 'adds to the results hash' do
+    result = reference_pipeline_result("Label #{reference}")
+    expect(result[:references][:label]).to eq [label]
+  end
+
+  describe 'label span element' do
+    it 'includes default classes' do
+      doc = reference_filter("Label #{reference}")
+      expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
+    end
+
+    it 'includes a style attribute' do
+      doc = reference_filter("Label #{reference}")
+      expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
+    end
+  end
+
+  context 'Integer-based references' do
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_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 #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'String-based single-word references' do
+    let(:label)     { create(:label, name: 'gfm', project: project) }
+    let(:reference) { "#{Label.reference_prefix}#{label.name}" }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).to eq urls.
+        namespace_project_issues_url(project.namespace, project, label_name: label.name)
+      expect(doc.text).to eq 'See gfm'
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Label (#{reference}.)")
+      expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+    end
+
+    it 'ignores invalid label names' do
+      exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'String-based multi-word references in quotes' do
+    let(:label)     { create(:label, name: 'gfm references', project: project) }
+    let(:reference) { label.to_reference(:name) }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).to eq urls.
+        namespace_project_issues_url(project.namespace, project, label_name: label.name)
+      expect(doc.text).to eq 'See gfm references'
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Label (#{reference}.)")
+      expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+    end
+
+    it 'ignores invalid label names' do
+      exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  describe 'edge cases' do
+    it 'gracefully handles non-references matching the pattern' do
+      exp = act = '(format nil "~0f" 3.0) ; 3.0'
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  describe 'referencing a label in a link href' do
+    let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Label (#{reference}.)")
+      expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\)))
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("Label #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-label attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-label')
+      expect(link.attr('data-label')).to eq label.id.to_s
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Label #{reference}")
+      expect(result[:references][:label]).to eq [label]
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..589550e15c4514c3df5fadd671e9d5fdc48b37c0
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/merge_request_reference_filter_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::MergeRequestReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project) { create(:project, :public) }
+  let(:merge)   { create(:merge_request, source_project: project) }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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.to_reference}</#{elem}>"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'internal reference' do
+    let(:reference) { merge.to_reference }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Merge (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid merge IDs' do
+      exp = act = "Merge #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'includes a title attribute' do
+      doc = reference_filter("Merge #{reference}")
+      expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
+    end
+
+    it 'escapes the title attribute' do
+      merge.update_attribute(:title, %{"></a>whatever<a title="})
+
+      doc = reference_filter("Merge #{reference}")
+      expect(doc.text).to eq "Merge #{reference}"
+    end
+
+    it 'includes default classes' do
+      doc = reference_filter("Merge #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("Merge #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-merge-request attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-merge-request')
+      expect(link.attr('data-merge-request')).to eq merge.id.to_s
+    end
+
+    it 'supports an :only_path context' do
+      doc = reference_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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Merge #{reference}")
+      expect(result[:references][:merge_request]).to eq [merge]
+    end
+  end
+
+  context 'cross-project reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:merge)     { create(:merge_request, source_project: project2) }
+    let(:reference) { merge.to_reference(project) }
+
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_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 #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Merge #{reference}")
+      expect(result[:references][:merge_request]).to eq [merge]
+    end
+  end
+
+  context 'cross-project URL reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:project, :public, namespace: namespace) }
+    let(:merge)     { create(:merge_request, source_project: project2, target_project: project2) }
+    let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq reference
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Merge (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Merge #{reference}")
+      expect(result[:references][:merge_request]).to eq [merge]
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e6ee9f0d61884d84f8f7d5831cfbc05e8217485
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/redactor_filter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::RedactorFilter, lib: true do
+  include ActionView::Helpers::UrlHelper
+  include FilterSpecHelper
+
+  it 'ignores non-GFM links' do
+    html = %(See <a href="https://google.com/">Google</a>)
+    doc = filter(html, current_user: double)
+
+    expect(doc.css('a').length).to eq 1
+  end
+
+  def reference_link(data)
+    link_to('text', '', class: 'gfm', data: data)
+  end
+
+  context 'with data-project' do
+    it 'removes unpermitted Project references' do
+      user = create(:user)
+      project = create(:empty_project)
+
+      link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+      doc = filter(link, current_user: user)
+
+      expect(doc.css('a').length).to eq 0
+    end
+
+    it 'allows permitted Project references' do
+      user = create(:user)
+      project = create(:empty_project)
+      project.team << [user, :master]
+
+      link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+      doc = filter(link, current_user: user)
+
+      expect(doc.css('a').length).to eq 1
+    end
+
+    it 'handles invalid Project references' do
+      link = reference_link(project: 12345, reference_filter: 'ReferenceFilter')
+
+      expect { filter(link) }.not_to raise_error
+    end
+  end
+
+  context "for user references" do
+
+    context 'with data-group' do
+      it 'removes unpermitted Group references' do
+        user = create(:user)
+        group = create(:group)
+
+        link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+        doc = filter(link, current_user: user)
+
+        expect(doc.css('a').length).to eq 0
+      end
+
+      it 'allows permitted Group references' do
+        user = create(:user)
+        group = create(:group)
+        group.add_developer(user)
+
+        link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+        doc = filter(link, current_user: user)
+
+        expect(doc.css('a').length).to eq 1
+      end
+
+      it 'handles invalid Group references' do
+        link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
+
+        expect { filter(link) }.not_to raise_error
+      end
+    end
+
+    context 'with data-user' do
+      it 'allows any User reference' do
+        user = create(:user)
+
+        link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
+        doc = filter(link)
+
+        expect(doc.css('a').length).to eq 1
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..abfb5ad5e49fb1d17644ab2cec91f5485b8b773a
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/reference_gatherer_filter_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::ReferenceGathererFilter, lib: true do
+  include ActionView::Helpers::UrlHelper
+  include FilterSpecHelper
+
+  def reference_link(data)
+    link_to('text', '', class: 'gfm', data: data)
+  end
+
+  context "for issue references" do
+
+    context 'with data-project' do
+      it 'removes unpermitted Project references' do
+        user = create(:user)
+        project = create(:empty_project)
+        issue = create(:issue, project: project)
+
+        link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+        result = pipeline_result(link, current_user: user)
+
+        expect(result[:references][:issue]).to be_empty
+      end
+
+      it 'allows permitted Project references' do
+        user = create(:user)
+        project = create(:empty_project)
+        issue = create(:issue, project: project)
+        project.team << [user, :master]
+
+        link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+        result = pipeline_result(link, current_user: user)
+
+        expect(result[:references][:issue]).to eq([issue])
+      end
+
+      it 'handles invalid Project references' do
+        link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter')
+
+        expect { pipeline_result(link) }.not_to raise_error
+      end
+    end
+  end
+
+  context "for user references" do
+
+    context 'with data-group' do
+      it 'removes unpermitted Group references' do
+        user = create(:user)
+        group = create(:group)
+
+        link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+        result = pipeline_result(link, current_user: user)
+
+        expect(result[:references][:user]).to be_empty
+      end
+
+      it 'allows permitted Group references' do
+        user = create(:user)
+        group = create(:group)
+        group.add_developer(user)
+
+        link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+        result = pipeline_result(link, current_user: user)
+
+        expect(result[:references][:user]).to eq([user])
+      end
+
+      it 'handles invalid Group references' do
+        link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
+
+        expect { pipeline_result(link) }.not_to raise_error
+      end
+    end
+
+    context 'with data-user' do
+      it 'allows any User reference' do
+        user = create(:user)
+
+        link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
+        result = pipeline_result(link)
+
+        expect(result[:references][:user]).to eq([user])
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0f53e2a533e335690817298fa8bb0d727d01c8a
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/relative_link_filter_spec.rb
@@ -0,0 +1,147 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Gitlab::Markdown::RelativeLinkFilter, lib: true do
+  def filter(doc, contexts = {})
+    contexts.reverse_merge!({
+      commit:         project.commit,
+      project:        project,
+      project_wiki:   project_wiki,
+      ref:            ref,
+      requested_path: requested_path
+    })
+
+    described_class.call(doc, contexts)
+  end
+
+  def image(path)
+    %(<img src="#{path}" />)
+  end
+
+  def link(path)
+    %(<a href="#{path}">#{path}</a>)
+  end
+
+  let(:project)        { create(:project) }
+  let(:project_path)   { project.path_with_namespace }
+  let(:ref)            { 'markdown' }
+  let(:project_wiki)   { nil }
+  let(:requested_path) { '/' }
+
+  shared_examples :preserve_unchanged do
+    it 'does not modify any relative URL in anchor' do
+      doc = filter(link('README.md'))
+      expect(doc.at_css('a')['href']).to eq 'README.md'
+    end
+
+    it 'does not modify any relative URL in image' do
+      doc = filter(image('files/images/logo-black.png'))
+      expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
+    end
+  end
+
+  shared_examples :relative_to_requested do
+    it 'rebuilds URL relative to the requested path' do
+      doc = filter(link('users.md'))
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
+    end
+  end
+
+  context 'with a project_wiki' do
+    let(:project_wiki) { double('ProjectWiki') }
+    include_examples :preserve_unchanged
+  end
+
+  context 'without a repository' do
+    let(:project) { create(:empty_project) }
+    include_examples :preserve_unchanged
+  end
+
+  context 'with an empty repository' do
+    let(:project) { create(:project_empty_repo) }
+    include_examples :preserve_unchanged
+  end
+
+  it 'does not raise an exception on invalid URIs' do
+    act = link("://foo")
+    expect { filter(act) }.not_to raise_error
+  end
+
+  context 'with a valid repository' do
+    it 'rebuilds relative URL for a file in the repo' do
+      doc = filter(link('doc/api/README.md'))
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+    end
+
+    it 'rebuilds relative URL for a file in the repo up one directory' do
+      relative_link = link('../api/README.md')
+      doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
+
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+    end
+
+    it 'rebuilds relative URL for a file in the repo up multiple directories' do
+      relative_link = link('../../../api/README.md')
+      doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md')
+
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+    end
+
+    it 'rebuilds relative URL for a file in the repo with an anchor' do
+      doc = filter(link('README.md#section'))
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/blob/#{ref}/README.md#section"
+    end
+
+    it 'rebuilds relative URL for a directory in the repo' do
+      doc = filter(link('doc/api/'))
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/tree/#{ref}/doc/api"
+    end
+
+    it 'rebuilds relative URL for an image in the repo' do
+      doc = filter(link('files/images/logo-black.png'))
+      expect(doc.at_css('a')['href']).
+        to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
+    end
+
+    it 'does not modify relative URL with an anchor only' do
+      doc = filter(link('#section-1'))
+      expect(doc.at_css('a')['href']).to eq '#section-1'
+    end
+
+    it 'does not modify absolute URL' do
+      doc = filter(link('http://example.com'))
+      expect(doc.at_css('a')['href']).to eq 'http://example.com'
+    end
+
+    it 'supports Unicode filenames' do
+      path = 'files/images/한글.png'
+      escaped = Addressable::URI.escape(path)
+
+      # Stub these methods so the file doesn't actually need to be in the repo
+      allow_any_instance_of(described_class).
+        to receive(:file_exists?).and_return(true)
+      allow_any_instance_of(described_class).
+        to receive(:image?).with(path).and_return(true)
+
+      doc = filter(image(escaped))
+      expect(doc.at_css('img')['src']).to match '/raw/'
+    end
+
+    context 'when requested path is a file in the repo' do
+      let(:requested_path) { 'doc/api/README.md' }
+      include_examples :relative_to_requested
+    end
+
+    context 'when requested path is a directory in the repo' do
+      let(:requested_path) { 'doc/api' }
+      include_examples :relative_to_requested
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a5e5ee0e08a7b0c1541c92c06b3731591c623a71
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/sanitization_filter_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::SanitizationFilter, lib: true do
+  include FilterSpecHelper
+
+  describe 'default whitelist' do
+    it 'sanitizes tags that are not whitelisted' do
+      act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
+      exp = 'no inputs and no blinks'
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'sanitizes tag attributes' do
+      act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
+      exp = %q{<a href="http://example.com/bar.html">Text</a>}
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'sanitizes javascript in attributes' do
+      act = %q(<a href="javascript:alert('foo')">Text</a>)
+      exp = '<a>Text</a>'
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'allows whitelisted HTML tags from the user' do
+      exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'sanitizes `class` attribute on any element' do
+      act = %q{<strong class="foo">Strong</strong>}
+      expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
+    end
+
+    it 'sanitizes `id` attribute on any element' do
+      act = %q{<em id="foo">Emphasis</em>}
+      expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
+    end
+  end
+
+  describe 'custom whitelist' do
+    it 'customizes the whitelist only once' do
+      instance = described_class.new('Foo')
+      3.times { instance.whitelist }
+
+      expect(instance.whitelist[:transformers].size).to eq 5
+    end
+
+    it 'allows syntax highlighting' do
+      exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>}
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'sanitizes `class` attribute from non-highlight spans' do
+      act = %q{<span class="k">def</span>}
+      expect(filter(act).to_html).to eq %q{<span>def</span>}
+    end
+
+    it 'allows `style` attribute on table elements' do
+      html = <<-HTML.strip_heredoc
+      <table>
+        <tr><th style="text-align: center">Head</th></tr>
+        <tr><td style="text-align: right">Body</th></tr>
+      </table>
+      HTML
+
+      doc = filter(html)
+
+      expect(doc.at_css('th')['style']).to eq 'text-align: center'
+      expect(doc.at_css('td')['style']).to eq 'text-align: right'
+    end
+
+    it 'allows `span` elements' do
+      exp = act = %q{<span>Hello</span>}
+      expect(filter(act).to_html).to eq exp
+    end
+
+    it 'removes `rel` attribute from `a` elements' do
+      act = %q{<a href="#" rel="nofollow">Link</a>}
+      exp = %q{<a href="#">Link</a>}
+
+      expect(filter(act).to_html).to eq exp
+    end
+
+    # Adapted from the Sanitize test suite: http://git.io/vczrM
+    protocols = {
+      'protocol-based JS injection: simple, no spaces' => {
+        input:  '<a href="javascript:alert(\'XSS\');">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: simple, spaces before' => {
+        input:  '<a href="javascript    :alert(\'XSS\');">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: simple, spaces after' => {
+        input:  '<a href="javascript:    alert(\'XSS\');">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: simple, spaces before and after' => {
+        input:  '<a href="javascript    :   alert(\'XSS\');">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: preceding colon' => {
+        input:  '<a href=":javascript:alert(\'XSS\');">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: UTF-8 encoding' => {
+        input:  '<a href="javascript&#58;">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: long UTF-8 encoding' => {
+        input:  '<a href="javascript&#0058;">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: long UTF-8 encoding without semicolons' => {
+        input:  '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: hex encoding' => {
+        input:  '<a href="javascript&#x3A;">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: long hex encoding' => {
+        input:  '<a href="javascript&#x003A;">foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: hex encoding without semicolons' => {
+        input:  '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+        output: '<a>foo</a>'
+      },
+
+      'protocol-based JS injection: null char' => {
+        input:  "<a href=java\0script:alert(\"XSS\")>foo</a>",
+        output: '<a href="java"></a>'
+      },
+
+      'protocol-based JS injection: spaces and entities' => {
+        input:  '<a href=" &#14;  javascript:alert(\'XSS\');">foo</a>',
+        output: '<a href="">foo</a>'
+      },
+    }
+
+    protocols.each do |name, data|
+      it "handles #{name}" do
+        doc = filter(data[:input])
+
+        expect(doc.to_html).to eq data[:output]
+      end
+    end
+
+    it 'allows non-standard anchor schemes' do
+      exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
+      act = filter(exp)
+
+      expect(act.to_html).to eq exp
+    end
+
+    it 'allows relative links' do
+      exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
+      act = filter(exp)
+
+      expect(act.to_html).to eq exp
+    end
+  end
+
+  context 'when inline_sanitization is true' do
+    it 'uses a stricter whitelist' do
+      doc = filter('<h1>Description</h1>', inline_sanitization: true)
+      expect(doc.to_html.strip).to eq 'Description'
+    end
+
+    %w(pre code img ol ul li).each do |elem|
+      it "removes '#{elem}' elements" do
+        act = "<#{elem}>Description</#{elem}>"
+        expect(filter(act, inline_sanitization: true).to_html.strip).
+          to eq 'Description'
+      end
+    end
+
+    %w(b i strong em a ins del sup sub p).each do |elem|
+      it "still allows '#{elem}' elements" do
+        exp = act = "<#{elem}>Description</#{elem}>"
+        expect(filter(act, inline_sanitization: true).to_html).to eq exp
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..51526b58597909e5601a269f7447e8ff83e12b75
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/snippet_reference_filter_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::SnippetReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project)   { create(:empty_project, :public) }
+  let(:snippet)   { create(:project_snippet, project: project) }
+  let(:reference) { snippet.to_reference }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.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(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'internal reference' do
+    it 'links to a valid reference' do
+      doc = reference_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 = reference_filter("Snippet (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid snippet IDs' do
+      exp = act = "Snippet #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'includes a title attribute' do
+      doc = reference_filter("Snippet #{reference}")
+      expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+    end
+
+    it 'escapes the title attribute' do
+      snippet.update_attribute(:title, %{"></a>whatever<a title="})
+
+      doc = reference_filter("Snippet #{reference}")
+      expect(doc.text).to eq "Snippet #{reference}"
+    end
+
+    it 'includes default classes' do
+      doc = reference_filter("Snippet #{reference}")
+      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+    end
+
+    it 'includes a data-project attribute' do
+      doc = reference_filter("Snippet #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-project')
+      expect(link.attr('data-project')).to eq project.id.to_s
+    end
+
+    it 'includes a data-snippet attribute' do
+      doc = reference_filter("See #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-snippet')
+      expect(link.attr('data-snippet')).to eq snippet.id.to_s
+    end
+
+    it 'supports an :only_path context' do
+      doc = reference_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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Snippet #{reference}")
+      expect(result[:references][:snippet]).to eq [snippet]
+    end
+  end
+
+  context 'cross-project reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:snippet)   { create(:project_snippet, project: project2) }
+    let(:reference) { snippet.to_reference(project) }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("See (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid snippet IDs on the referenced project' do
+      exp = act = "See #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to eq exp
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Snippet #{reference}")
+      expect(result[:references][:snippet]).to eq [snippet]
+    end
+  end
+
+  context 'cross-project URL reference' do
+    let(:namespace) { create(:namespace, name: 'cross-reference') }
+    let(:project2)  { create(:empty_project, :public, namespace: namespace) }
+    let(:snippet)   { create(:project_snippet, project: project2) }
+    let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href')).
+        to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("See (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
+    end
+
+    it 'ignores invalid snippet IDs on the referenced project' do
+      act = "See #{invalidate_reference(reference)}"
+
+      expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Snippet #{reference}")
+      expect(result[:references][:snippet]).to eq [snippet]
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b76048f3e3a28b5c4dd116bbdbef5c1e3d51a65
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/syntax_highlight_filter_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::SyntaxHighlightFilter, lib: true do
+  include FilterSpecHelper
+
+  it 'highlights valid code blocks' do
+    result = filter('<pre><code>def fun end</code>')
+    expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
+  end
+
+  it 'passes through invalid code blocks' do
+    allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError)
+
+    result = filter('<pre><code>This is a test</code></pre>')
+    expect(result.to_html).to eq('<pre>This is a test</pre>')
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c8c79c41847314102a36190b2d36d5a5029b6d4d
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/table_of_contents_filter_spec.rb
@@ -0,0 +1,97 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Gitlab::Markdown::TableOfContentsFilter, lib: true do
+  include FilterSpecHelper
+
+  def header(level, text)
+    "<h#{level}>#{text}</h#{level}>\n"
+  end
+
+  it 'does nothing when :no_header_anchors is truthy' do
+    exp = act = header(1, 'Header')
+    expect(filter(act, no_header_anchors: 1).to_html).to eq exp
+  end
+
+  it 'does nothing with empty headers' do
+    exp = act = header(1, nil)
+    expect(filter(act).to_html).to eq exp
+  end
+
+  1.upto(6) do |i|
+    it "processes h#{i} elements" do
+      html = header(i, "Header #{i}")
+      doc = filter(html)
+
+      expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
+    end
+  end
+
+  describe 'anchor tag' do
+    it 'has an `anchor` class' do
+      doc = filter(header(1, 'Header'))
+      expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
+    end
+
+    it 'links to the id' do
+      doc = filter(header(1, 'Header'))
+      expect(doc.css('h1 a').first.attr('href')).to eq '#header'
+    end
+
+    describe 'generated IDs' do
+      it 'translates spaces to dashes' do
+        doc = filter(header(1, 'This header has spaces in it'))
+        expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
+      end
+
+      it 'squeezes multiple spaces and dashes' do
+        doc = filter(header(1, 'This---header     is poorly-formatted'))
+        expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
+      end
+
+      it 'removes punctuation' do
+        doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
+        expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
+      end
+
+      it 'appends a unique number to duplicates' do
+        doc = filter(header(1, 'One') + header(2, 'One'))
+
+        expect(doc.css('h1 a').first.attr('id')).to eq 'one'
+        expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
+      end
+
+      it 'supports Unicode' do
+        doc = filter(header(1, '한글'))
+        expect(doc.css('h1 a').first.attr('id')).to eq '한글'
+        expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
+      end
+    end
+  end
+
+  describe 'result' do
+    def result(html)
+      HTML::Pipeline.new([described_class]).call(html)
+    end
+
+    let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
+    let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
+
+    it 'is contained within a `ul` element' do
+      expect(doc.children.first.name).to eq 'ul'
+      expect(doc.children.first.attr('class')).to eq 'section-nav'
+    end
+
+    it 'contains an `li` element for each header' do
+      expect(doc.css('li').length).to eq 2
+
+      links = doc.css('li a')
+
+      expect(links.first.attr('href')).to eq '#header-1'
+      expect(links.first.text).to eq 'Header 1'
+      expect(links.last.attr('href')).to eq '#header-2'
+      expect(links.last.text).to eq 'Header 2'
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b1714ef8821dd5998489ee8b6a862e7fc00d8c3
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/task_list_filter_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::TaskListFilter, lib: true do
+  include FilterSpecHelper
+
+  it 'does not apply `task-list` class to non-task lists' do
+    exp = act = %(<ul><li>Item</li></ul>)
+    expect(filter(act).to_html).to eq exp
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38a007b5beeaeae18aa4a91fa345c7131815e033
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/upload_link_filter_spec.rb
@@ -0,0 +1,73 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Gitlab::Markdown::UploadLinkFilter, lib: true do
+  def filter(doc, contexts = {})
+    contexts.reverse_merge!({
+      project: project
+    })
+
+    described_class.call(doc, contexts)
+  end
+
+  def image(path)
+    %(<img src="#{path}" />)
+  end
+
+  def link(path)
+    %(<a href="#{path}">#{path}</a>)
+  end
+
+  let(:project) { create(:project) }
+
+  shared_examples :preserve_unchanged do
+    it 'does not modify any relative URL in anchor' do
+      doc = filter(link('README.md'))
+      expect(doc.at_css('a')['href']).to eq 'README.md'
+    end
+
+    it 'does not modify any relative URL in image' do
+      doc = filter(image('files/images/logo-black.png'))
+      expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
+    end
+  end
+
+  it 'does not raise an exception on invalid URIs' do
+    act = link("://foo")
+    expect { filter(act) }.not_to raise_error
+  end
+
+  context 'with a valid repository' do
+    it 'rebuilds relative URL for a link' do
+      doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+      expect(doc.at_css('a')['href']).
+        to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+    end
+
+    it 'rebuilds relative URL for an image' do
+      doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+      expect(doc.at_css('a')['href']).
+        to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+    end
+
+    it 'does not modify absolute URL' do
+      doc = filter(link('http://example.com'))
+      expect(doc.at_css('a')['href']).to eq 'http://example.com'
+    end
+
+    it 'supports Unicode filenames' do
+      path = '/uploads/한글.png'
+      escaped = Addressable::URI.escape(path)
+
+      # Stub these methods so the file doesn't actually need to be in the repo
+      allow_any_instance_of(described_class).
+        to receive(:file_exists?).and_return(true)
+      allow_any_instance_of(described_class).
+        to receive(:image?).with(path).and_return(true)
+
+      doc = filter(image(escaped))
+      expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png"
+    end
+  end
+end
diff --git a/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..277037cf68aa1f765f1b94de4554393b0ad2fdf2
--- /dev/null
+++ b/spec/lib/gitlab/markdown/filter/user_reference_filter_spec.rb
@@ -0,0 +1,147 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::UserReferenceFilter, lib: true do
+  include FilterSpecHelper
+
+  let(:project)   { create(:empty_project, :public) }
+  let(:user)      { create(:user) }
+  let(:reference) { user.to_reference }
+
+  it 'requires project context' do
+    expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+  end
+
+  it 'ignores invalid users' do
+    exp = act = "Hey #{invalidate_reference(reference)}"
+    expect(reference_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 #{reference}</#{elem}>"
+      expect(reference_filter(act).to_html).to eq exp
+    end
+  end
+
+  context 'mentioning @all' do
+    let(:reference) { User.reference_prefix + 'all' }
+
+    before do
+      project.team << [project.creator, :developer]
+    end
+
+    it 'supports a special @all mention' do
+      doc = reference_filter("Hey #{reference}")
+      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 'adds to the results hash' do
+      result = reference_pipeline_result("Hey #{reference}")
+      expect(result[:references][:user]).to eq [project.creator]
+    end
+  end
+
+  context 'mentioning a user' do
+    it 'links to a User' do
+      doc = reference_filter("Hey #{reference}")
+      expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+    end
+
+    it 'links to a User with a period' do
+      user = create(:user, name: 'alphA.Beta')
+
+      doc = reference_filter("Hey #{user.to_reference}")
+      expect(doc.css('a').length).to eq 1
+    end
+
+    it 'links to a User with an underscore' do
+      user = create(:user, name: 'ping_pong_king')
+
+      doc = reference_filter("Hey #{user.to_reference}")
+      expect(doc.css('a').length).to eq 1
+    end
+
+    it 'includes a data-user attribute' do
+      doc = reference_filter("Hey #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-user')
+      expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Hey #{reference}")
+      expect(result[:references][:user]).to eq [user]
+    end
+  end
+
+  context 'mentioning a group' do
+    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
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Hey #{reference}")
+      expect(result[:references][:user]).to eq group.users
+    end
+  end
+
+  it 'links with adjacent text' do
+    doc = reference_filter("Mention me (#{reference}.)")
+    expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+  end
+
+  it 'includes default classes' do
+    doc = reference_filter("Hey #{reference}")
+    expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+  end
+
+  it 'supports an :only_path context' do
+    doc = reference_filter("Hey #{reference}", 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
+
+  context 'referencing a user in a link href' do
+    let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} }
+
+    it 'links to a User' do
+      doc = reference_filter("Hey #{reference}")
+      expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+    end
+
+    it 'links with adjacent text' do
+      doc = reference_filter("Mention me (#{reference}.)")
+      expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
+    end
+
+    it 'includes a data-user attribute' do
+      doc = reference_filter("Hey #{reference}")
+      link = doc.css('a').first
+
+      expect(link).to have_attribute('data-user')
+      expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
+    end
+
+    it 'adds to the results hash' do
+      result = reference_pipeline_result("Hey #{reference}")
+      expect(result[:references][:user]).to eq [user]
+    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
deleted file mode 100644
index dee0826336bc8e61e87ae425d2e326f8ce0ce047..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
+++ /dev/null
@@ -1,211 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe IssueReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    def helper
-      IssuesHelper
-    end
-
-    let(:project) { create(:empty_project, :public) }
-    let(:issue)   { create(:issue, project: project) }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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 #{issue.to_reference}</#{elem}>"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'internal reference' do
-      let(:reference) { issue.to_reference }
-
-      it 'ignores valid references when using non-default tracker' do
-        expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
-
-        exp = act = "Issue #{reference}"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'links to a valid reference' do
-        doc = reference_filter("Fixed #{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 = reference_filter("Fixed (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid issue IDs' do
-        invalid = invalidate_reference(reference)
-        exp = act = "Fixed #{invalid}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'includes a title attribute' do
-        doc = reference_filter("Issue #{reference}")
-        expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
-      end
-
-      it 'escapes the title attribute' do
-        issue.update_attribute(:title, %{"></a>whatever<a title="})
-
-        doc = reference_filter("Issue #{reference}")
-        expect(doc.text).to eq "Issue #{reference}"
-      end
-
-      it 'includes default classes' do
-        doc = reference_filter("Issue #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("Issue #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-issue attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-issue')
-        expect(link.attr('data-issue')).to eq issue.id.to_s
-      end
-
-      it 'supports an :only_path context' do
-        doc = reference_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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Fixed #{reference}")
-        expect(result[:references][:issue]).to eq [issue]
-      end
-    end
-
-    context 'cross-project reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:issue)     { create(:issue, project: project2) }
-      let(:reference) { issue.to_reference(project) }
-
-      it 'ignores valid references when cross-reference project uses external tracker' do
-        expect_any_instance_of(Project).to receive(:get_issue).
-          with(issue.iid).and_return(nil)
-
-        exp = act = "Issue #{reference}"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_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 #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Fixed #{reference}")
-        expect(result[:references][:issue]).to eq [issue]
-      end
-    end
-
-    context 'cross-project URL reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:issue)     { create(:issue, project: project2) }
-      let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq reference
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Fixed (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Fixed #{reference}")
-        expect(result[:references][:issue]).to eq [issue]
-      end
-    end
-
-    context 'cross-project reference in link href' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:issue)     { create(:issue, project: project2) }
-      let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Fixed (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Fixed #{reference}")
-        expect(result[:references][:issue]).to eq [issue]
-      end
-    end
-
-    context 'cross-project URL in link href' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:issue)     { create(:issue, project: project2) }
-      let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Fixed (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Fixed #{reference}")
-        expect(result[:references][:issue]).to eq [issue]
-      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
deleted file mode 100644
index 799dbe6f2db8abaae19af44ba3bf4060cc8d5d92..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-require 'spec_helper'
-require 'html/pipeline'
-
-module Gitlab::Markdown
-  describe LabelReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project)   { create(:empty_project, :public) }
-    let(:label)     { create(:label, project: project) }
-    let(:reference) { label.to_reference }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    it 'includes default classes' do
-      doc = reference_filter("Label #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
-    end
-
-    it 'includes a data-project attribute' do
-      doc = reference_filter("Label #{reference}")
-      link = doc.css('a').first
-
-      expect(link).to have_attribute('data-project')
-      expect(link.attr('data-project')).to eq project.id.to_s
-    end
-
-    it 'includes a data-label attribute' do
-      doc = reference_filter("See #{reference}")
-      link = doc.css('a').first
-
-      expect(link).to have_attribute('data-label')
-      expect(link.attr('data-label')).to eq label.id.to_s
-    end
-
-    it 'supports an :only_path context' do
-      doc = reference_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_path(project.namespace, project, label_name: label.name)
-    end
-
-    it 'adds to the results hash' do
-      result = reference_pipeline_result("Label #{reference}")
-      expect(result[:references][:label]).to eq [label]
-    end
-
-    describe 'label span element' do
-      it 'includes default classes' do
-        doc = reference_filter("Label #{reference}")
-        expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
-      end
-
-      it 'includes a style attribute' do
-        doc = reference_filter("Label #{reference}")
-        expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
-      end
-    end
-
-    context 'Integer-based references' do
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_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 #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'String-based single-word references' do
-      let(:label)     { create(:label, name: 'gfm', project: project) }
-      let(:reference) { "#{Label.reference_prefix}#{label.name}" }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).to eq urls.
-          namespace_project_issues_url(project.namespace, project, label_name: label.name)
-        expect(doc.text).to eq 'See gfm'
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Label (#{reference}.)")
-        expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
-      end
-
-      it 'ignores invalid label names' do
-        exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'String-based multi-word references in quotes' do
-      let(:label)     { create(:label, name: 'gfm references', project: project) }
-      let(:reference) { label.to_reference(:name) }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).to eq urls.
-          namespace_project_issues_url(project.namespace, project, label_name: label.name)
-        expect(doc.text).to eq 'See gfm references'
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Label (#{reference}.)")
-        expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
-      end
-
-      it 'ignores invalid label names' do
-        exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    describe 'edge cases' do
-      it 'gracefully handles non-references matching the pattern' do
-        exp = act = '(format nil "~0f" 3.0) ; 3.0'
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    describe 'referencing a label in a link href' do
-      let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} }
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Label (#{reference}.)")
-        expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\)))
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("Label #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-label attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-label')
-        expect(link.attr('data-label')).to eq label.id.to_s
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Label #{reference}")
-        expect(result[:references][:label]).to eq [label]
-      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
deleted file mode 100644
index ed70d5c4fa2412e7c5d6007e24356ad53b4ac6a4..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe MergeRequestReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project) { create(:project, :public) }
-    let(:merge)   { create(:merge_request, source_project: project) }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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.to_reference}</#{elem}>"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'internal reference' do
-      let(:reference) { merge.to_reference }
-
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Merge (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid merge IDs' do
-        exp = act = "Merge #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'includes a title attribute' do
-        doc = reference_filter("Merge #{reference}")
-        expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
-      end
-
-      it 'escapes the title attribute' do
-        merge.update_attribute(:title, %{"></a>whatever<a title="})
-
-        doc = reference_filter("Merge #{reference}")
-        expect(doc.text).to eq "Merge #{reference}"
-      end
-
-      it 'includes default classes' do
-        doc = reference_filter("Merge #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("Merge #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-merge-request attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-merge-request')
-        expect(link.attr('data-merge-request')).to eq merge.id.to_s
-      end
-
-      it 'supports an :only_path context' do
-        doc = reference_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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Merge #{reference}")
-        expect(result[:references][:merge_request]).to eq [merge]
-      end
-    end
-
-    context 'cross-project reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:merge)     { create(:merge_request, source_project: project2, target_project: project2) }
-      let(:reference) { merge.to_reference(project) }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_merge_request_url(project2.namespace,
-                                                        project2, merge)
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_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 #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Merge #{reference}")
-        expect(result[:references][:merge_request]).to eq [merge]
-      end
-    end
-
-    context 'cross-project URL reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:project, :public, namespace: namespace) }
-      let(:merge)     { create(:merge_request, source_project: project2, target_project: project2) }
-      let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq reference
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Merge (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Merge #{reference}")
-        expect(result[:references][:merge_request]).to eq [merge]
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb
deleted file mode 100644
index 27a3d8ec4efda169f4512feb4350aa2f35410e8f..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe RedactorFilter, lib: true do
-    include ActionView::Helpers::UrlHelper
-    include FilterSpecHelper
-
-    it 'ignores non-GFM links' do
-      html = %(See <a href="https://google.com/">Google</a>)
-      doc = filter(html, current_user: double)
-
-      expect(doc.css('a').length).to eq 1
-    end
-
-    def reference_link(data)
-      link_to('text', '', class: 'gfm', data: data)
-    end
-
-    context 'with data-project' do
-      it 'removes unpermitted Project references' do
-        user = create(:user)
-        project = create(:empty_project)
-
-        link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
-        doc = filter(link, current_user: user)
-
-        expect(doc.css('a').length).to eq 0
-      end
-
-      it 'allows permitted Project references' do
-        user = create(:user)
-        project = create(:empty_project)
-        project.team << [user, :master]
-
-        link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
-        doc = filter(link, current_user: user)
-
-        expect(doc.css('a').length).to eq 1
-      end
-
-      it 'handles invalid Project references' do
-        link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
-
-        expect { filter(link) }.not_to raise_error
-      end
-    end
-
-    context "for user references" do
-
-      context 'with data-group' do
-        it 'removes unpermitted Group references' do
-          user = create(:user)
-          group = create(:group)
-
-          link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          doc = filter(link, current_user: user)
-
-          expect(doc.css('a').length).to eq 0
-        end
-
-        it 'allows permitted Group references' do
-          user = create(:user)
-          group = create(:group)
-          group.add_developer(user)
-
-          link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          doc = filter(link, current_user: user)
-
-          expect(doc.css('a').length).to eq 1
-        end
-
-        it 'handles invalid Group references' do
-          link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-
-          expect { filter(link) }.not_to raise_error
-        end
-      end
-
-      context 'with data-user' do
-        it 'allows any User reference' do
-          user = create(:user)
-
-          link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          doc = filter(link)
-
-          expect(doc.css('a').length).to eq 1
-        end
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
deleted file mode 100644
index 375dd689472cd65436e7278480005b2fe746bd37..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe ReferenceGathererFilter, lib: true do
-    include ActionView::Helpers::UrlHelper
-    include FilterSpecHelper
-
-    def reference_link(data)
-      link_to('text', '', class: 'gfm', data: data)
-    end
-
-    context "for issue references" do
-
-      context 'with data-project' do
-        it 'removes unpermitted Project references' do
-          user = create(:user)
-          project = create(:empty_project)
-          issue = create(:issue, project: project)
-
-          link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
-          result = pipeline_result(link, current_user: user)
-
-          expect(result[:references][:issue]).to be_empty
-        end
-
-        it 'allows permitted Project references' do
-          user = create(:user)
-          project = create(:empty_project)
-          issue = create(:issue, project: project)
-          project.team << [user, :master]
-
-          link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
-          result = pipeline_result(link, current_user: user)
-
-          expect(result[:references][:issue]).to eq([issue])
-        end
-
-        it 'handles invalid Project references' do
-          link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
-
-          expect { pipeline_result(link) }.not_to raise_error
-        end
-      end
-    end
-
-    context "for user references" do
-
-      context 'with data-group' do
-        it 'removes unpermitted Group references' do
-          user = create(:user)
-          group = create(:group)
-
-          link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          result = pipeline_result(link, current_user: user)
-
-          expect(result[:references][:user]).to be_empty
-        end
-
-        it 'allows permitted Group references' do
-          user = create(:user)
-          group = create(:group)
-          group.add_developer(user)
-
-          link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          result = pipeline_result(link, current_user: user)
-
-          expect(result[:references][:user]).to eq([user])
-        end
-
-        it 'handles invalid Group references' do
-          link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-
-          expect { pipeline_result(link) }.not_to raise_error
-        end
-      end
-
-      context 'with data-user' do
-        it 'allows any User reference' do
-          user = create(:user)
-
-          link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
-          result = pipeline_result(link)
-
-          expect(result[:references][:user]).to eq([user])
-        end
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
deleted file mode 100644
index 7f193004e329c7d405582b2b74eb6203cddd892e..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe RelativeLinkFilter, lib: true do
-    def filter(doc, contexts = {})
-      contexts.reverse_merge!({
-        commit:         project.commit,
-        project:        project,
-        project_wiki:   project_wiki,
-        ref:            ref,
-        requested_path: requested_path
-      })
-
-      described_class.call(doc, contexts)
-    end
-
-    def image(path)
-      %(<img src="#{path}" />)
-    end
-
-    def link(path)
-      %(<a href="#{path}">#{path}</a>)
-    end
-
-    let(:project)        { create(:project) }
-    let(:project_path)   { project.path_with_namespace }
-    let(:ref)            { 'markdown' }
-    let(:project_wiki)   { nil }
-    let(:requested_path) { '/' }
-
-    shared_examples :preserve_unchanged do
-      it 'does not modify any relative URL in anchor' do
-        doc = filter(link('README.md'))
-        expect(doc.at_css('a')['href']).to eq 'README.md'
-      end
-
-      it 'does not modify any relative URL in image' do
-        doc = filter(image('files/images/logo-black.png'))
-        expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
-      end
-    end
-
-    shared_examples :relative_to_requested do
-      it 'rebuilds URL relative to the requested path' do
-        doc = filter(link('users.md'))
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
-      end
-    end
-
-    context 'with a project_wiki' do
-      let(:project_wiki) { double('ProjectWiki') }
-      include_examples :preserve_unchanged
-    end
-
-    context 'without a repository' do
-      let(:project) { create(:empty_project) }
-      include_examples :preserve_unchanged
-    end
-
-    context 'with an empty repository' do
-      let(:project) { create(:project_empty_repo) }
-      include_examples :preserve_unchanged
-    end
-
-    it 'does not raise an exception on invalid URIs' do
-      act = link("://foo")
-      expect { filter(act) }.not_to raise_error
-    end
-
-    context 'with a valid repository' do
-      it 'rebuilds relative URL for a file in the repo' do
-        doc = filter(link('doc/api/README.md'))
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
-      end
-
-      it 'rebuilds relative URL for a file in the repo up one directory' do
-        relative_link = link('../api/README.md')
-        doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
-
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
-      end
-
-      it 'rebuilds relative URL for a file in the repo up multiple directories' do
-        relative_link = link('../../../api/README.md')
-        doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md')
-
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
-      end
-
-      it 'rebuilds relative URL for a file in the repo with an anchor' do
-        doc = filter(link('README.md#section'))
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/blob/#{ref}/README.md#section"
-      end
-
-      it 'rebuilds relative URL for a directory in the repo' do
-        doc = filter(link('doc/api/'))
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/tree/#{ref}/doc/api"
-      end
-
-      it 'rebuilds relative URL for an image in the repo' do
-        doc = filter(link('files/images/logo-black.png'))
-        expect(doc.at_css('a')['href']).
-          to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
-      end
-
-      it 'does not modify relative URL with an anchor only' do
-        doc = filter(link('#section-1'))
-        expect(doc.at_css('a')['href']).to eq '#section-1'
-      end
-
-      it 'does not modify absolute URL' do
-        doc = filter(link('http://example.com'))
-        expect(doc.at_css('a')['href']).to eq 'http://example.com'
-      end
-
-      it 'supports Unicode filenames' do
-        path = 'files/images/한글.png'
-        escaped = Addressable::URI.escape(path)
-
-        # Stub these methods so the file doesn't actually need to be in the repo
-        allow_any_instance_of(described_class).
-          to receive(:file_exists?).and_return(true)
-        allow_any_instance_of(described_class).
-          to receive(:image?).with(path).and_return(true)
-
-        doc = filter(image(escaped))
-        expect(doc.at_css('img')['src']).to match '/raw/'
-      end
-
-      context 'when requested path is a file in the repo' do
-        let(:requested_path) { 'doc/api/README.md' }
-        include_examples :relative_to_requested
-      end
-
-      context 'when requested path is a directory in the repo' do
-        let(:requested_path) { 'doc/api' }
-        include_examples :relative_to_requested
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
deleted file mode 100644
index 522481b45a5fbce19a69c8feeefd1cb796a9498a..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe SanitizationFilter, lib: true do
-    include FilterSpecHelper
-
-    describe 'default whitelist' do
-      it 'sanitizes tags that are not whitelisted' do
-        act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
-        exp = 'no inputs and no blinks'
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'sanitizes tag attributes' do
-        act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
-        exp = %q{<a href="http://example.com/bar.html">Text</a>}
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'sanitizes javascript in attributes' do
-        act = %q(<a href="javascript:alert('foo')">Text</a>)
-        exp = '<a>Text</a>'
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'allows whitelisted HTML tags from the user' do
-        exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'sanitizes `class` attribute on any element' do
-        act = %q{<strong class="foo">Strong</strong>}
-        expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
-      end
-
-      it 'sanitizes `id` attribute on any element' do
-        act = %q{<em id="foo">Emphasis</em>}
-        expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
-      end
-    end
-
-    describe 'custom whitelist' do
-      it 'customizes the whitelist only once' do
-        instance = described_class.new('Foo')
-        3.times { instance.whitelist }
-
-        expect(instance.whitelist[:transformers].size).to eq 5
-      end
-
-      it 'allows syntax highlighting' do
-        exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>}
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'sanitizes `class` attribute from non-highlight spans' do
-        act = %q{<span class="k">def</span>}
-        expect(filter(act).to_html).to eq %q{<span>def</span>}
-      end
-
-      it 'allows `style` attribute on table elements' do
-        html = <<-HTML.strip_heredoc
-        <table>
-          <tr><th style="text-align: center">Head</th></tr>
-          <tr><td style="text-align: right">Body</th></tr>
-        </table>
-        HTML
-
-        doc = filter(html)
-
-        expect(doc.at_css('th')['style']).to eq 'text-align: center'
-        expect(doc.at_css('td')['style']).to eq 'text-align: right'
-      end
-
-      it 'allows `span` elements' do
-        exp = act = %q{<span>Hello</span>}
-        expect(filter(act).to_html).to eq exp
-      end
-
-      it 'removes `rel` attribute from `a` elements' do
-        act = %q{<a href="#" rel="nofollow">Link</a>}
-        exp = %q{<a href="#">Link</a>}
-
-        expect(filter(act).to_html).to eq exp
-      end
-
-      # Adapted from the Sanitize test suite: http://git.io/vczrM
-      protocols = {
-        'protocol-based JS injection: simple, no spaces' => {
-          input:  '<a href="javascript:alert(\'XSS\');">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: simple, spaces before' => {
-          input:  '<a href="javascript    :alert(\'XSS\');">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: simple, spaces after' => {
-          input:  '<a href="javascript:    alert(\'XSS\');">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: simple, spaces before and after' => {
-          input:  '<a href="javascript    :   alert(\'XSS\');">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: preceding colon' => {
-          input:  '<a href=":javascript:alert(\'XSS\');">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: UTF-8 encoding' => {
-          input:  '<a href="javascript&#58;">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: long UTF-8 encoding' => {
-          input:  '<a href="javascript&#0058;">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: long UTF-8 encoding without semicolons' => {
-          input:  '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: hex encoding' => {
-          input:  '<a href="javascript&#x3A;">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: long hex encoding' => {
-          input:  '<a href="javascript&#x003A;">foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: hex encoding without semicolons' => {
-          input:  '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
-          output: '<a>foo</a>'
-        },
-
-        'protocol-based JS injection: null char' => {
-          input:  "<a href=java\0script:alert(\"XSS\")>foo</a>",
-          output: '<a href="java"></a>'
-        },
-
-        'protocol-based JS injection: spaces and entities' => {
-          input:  '<a href=" &#14;  javascript:alert(\'XSS\');">foo</a>',
-          output: '<a href="">foo</a>'
-        },
-      }
-
-      protocols.each do |name, data|
-        it "handles #{name}" do
-          doc = filter(data[:input])
-
-          expect(doc.to_html).to eq data[:output]
-        end
-      end
-
-      it 'allows non-standard anchor schemes' do
-        exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
-        act = filter(exp)
-
-        expect(act.to_html).to eq exp
-      end
-
-      it 'allows relative links' do
-        exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
-        act = filter(exp)
-
-        expect(act.to_html).to eq exp
-      end
-    end
-
-    context 'when pipeline is :description' do
-      it 'uses a stricter whitelist' do
-        doc = filter('<h1>Description</h1>', pipeline: :description)
-        expect(doc.to_html.strip).to eq 'Description'
-      end
-
-      %w(pre code img ol ul li).each do |elem|
-        it "removes '#{elem}' elements" do
-          act = "<#{elem}>Description</#{elem}>"
-          expect(filter(act, pipeline: :description).to_html.strip).
-            to eq 'Description'
-        end
-      end
-
-      %w(b i strong em a ins del sup sub p).each do |elem|
-        it "still allows '#{elem}' elements" do
-          exp = act = "<#{elem}>Description</#{elem}>"
-          expect(filter(act, pipeline: :description).to_html).to eq exp
-        end
-      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
deleted file mode 100644
index 279244c5ac24cf8b5d9e5ef7a0b670b2c8e38628..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe SnippetReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project)   { create(:empty_project, :public) }
-    let(:snippet)   { create(:project_snippet, project: project) }
-    let(:reference) { snippet.to_reference }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.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(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'internal reference' do
-      it 'links to a valid reference' do
-        doc = reference_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 = reference_filter("Snippet (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid snippet IDs' do
-        exp = act = "Snippet #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'includes a title attribute' do
-        doc = reference_filter("Snippet #{reference}")
-        expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
-      end
-
-      it 'escapes the title attribute' do
-        snippet.update_attribute(:title, %{"></a>whatever<a title="})
-
-        doc = reference_filter("Snippet #{reference}")
-        expect(doc.text).to eq "Snippet #{reference}"
-      end
-
-      it 'includes default classes' do
-        doc = reference_filter("Snippet #{reference}")
-        expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
-      end
-
-      it 'includes a data-project attribute' do
-        doc = reference_filter("Snippet #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-project')
-        expect(link.attr('data-project')).to eq project.id.to_s
-      end
-
-      it 'includes a data-snippet attribute' do
-        doc = reference_filter("See #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-snippet')
-        expect(link.attr('data-snippet')).to eq snippet.id.to_s
-      end
-
-      it 'supports an :only_path context' do
-        doc = reference_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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Snippet #{reference}")
-        expect(result[:references][:snippet]).to eq [snippet]
-      end
-    end
-
-    context 'cross-project reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:snippet)   { create(:project_snippet, project: project2) }
-      let(:reference) { snippet.to_reference(project) }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("See (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid snippet IDs on the referenced project' do
-        exp = act = "See #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to eq exp
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Snippet #{reference}")
-        expect(result[:references][:snippet]).to eq [snippet]
-      end
-    end
-
-    context 'cross-project URL reference' do
-      let(:namespace) { create(:namespace, name: 'cross-reference') }
-      let(:project2)  { create(:empty_project, :public, namespace: namespace) }
-      let(:snippet)   { create(:project_snippet, project: project2) }
-      let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) }
-
-      it 'links to a valid reference' do
-        doc = reference_filter("See #{reference}")
-
-        expect(doc.css('a').first.attr('href')).
-          to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("See (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
-      end
-
-      it 'ignores invalid snippet IDs on the referenced project' do
-        act = "See #{invalidate_reference(reference)}"
-
-        expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Snippet #{reference}")
-        expect(result[:references][:snippet]).to eq [snippet]
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
deleted file mode 100644
index f9be5ad2a6eac17480de0904845e01820cdc1f9e..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe SyntaxHighlightFilter, lib: true do
-    include FilterSpecHelper
-
-    it 'highlights valid code blocks' do
-      result = filter('<pre><code>def fun end</code>')
-      expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
-    end
-
-    it 'passes through invalid code blocks' do
-      allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError)
-
-      result = filter('<pre><code>This is a test</code></pre>')
-      expect(result.to_html).to eq('<pre>This is a test</pre>')
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
deleted file mode 100644
index 8c3c6e50b75846df4f0c182b54aca893b5db5452..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe TableOfContentsFilter, lib: true do
-    include FilterSpecHelper
-
-    def header(level, text)
-      "<h#{level}>#{text}</h#{level}>\n"
-    end
-
-    it 'does nothing when :no_header_anchors is truthy' do
-      exp = act = header(1, 'Header')
-      expect(filter(act, no_header_anchors: 1).to_html).to eq exp
-    end
-
-    it 'does nothing with empty headers' do
-      exp = act = header(1, nil)
-      expect(filter(act).to_html).to eq exp
-    end
-
-    1.upto(6) do |i|
-      it "processes h#{i} elements" do
-        html = header(i, "Header #{i}")
-        doc = filter(html)
-
-        expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
-      end
-    end
-
-    describe 'anchor tag' do
-      it 'has an `anchor` class' do
-        doc = filter(header(1, 'Header'))
-        expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
-      end
-
-      it 'links to the id' do
-        doc = filter(header(1, 'Header'))
-        expect(doc.css('h1 a').first.attr('href')).to eq '#header'
-      end
-
-      describe 'generated IDs' do
-        it 'translates spaces to dashes' do
-          doc = filter(header(1, 'This header has spaces in it'))
-          expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
-        end
-
-        it 'squeezes multiple spaces and dashes' do
-          doc = filter(header(1, 'This---header     is poorly-formatted'))
-          expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
-        end
-
-        it 'removes punctuation' do
-          doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
-          expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
-        end
-
-        it 'appends a unique number to duplicates' do
-          doc = filter(header(1, 'One') + header(2, 'One'))
-
-          expect(doc.css('h1 a').first.attr('id')).to eq 'one'
-          expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
-        end
-
-        it 'supports Unicode' do
-          doc = filter(header(1, '한글'))
-          expect(doc.css('h1 a').first.attr('id')).to eq '한글'
-          expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
-        end
-      end
-    end
-
-    describe 'result' do
-      def result(html)
-        HTML::Pipeline.new([described_class]).call(html)
-      end
-
-      let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
-      let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
-
-      it 'is contained within a `ul` element' do
-        expect(doc.children.first.name).to eq 'ul'
-        expect(doc.children.first.attr('class')).to eq 'section-nav'
-      end
-
-      it 'contains an `li` element for each header' do
-        expect(doc.css('li').length).to eq 2
-
-        links = doc.css('li a')
-
-        expect(links.first.attr('href')).to eq '#header-1'
-        expect(links.first.text).to eq 'Header 1'
-        expect(links.last.attr('href')).to eq '#header-2'
-        expect(links.last.text).to eq 'Header 2'
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb
deleted file mode 100644
index cbcfd92956f1ad4fa4020c7bc25a668082d6816f..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe TaskListFilter, lib: true do
-    include FilterSpecHelper
-
-    it 'does not apply `task-list` class to non-task lists' do
-      exp = act = %(<ul><li>Item</li></ul>)
-      expect(filter(act).to_html).to eq exp
-    end
-  end
-end
diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
deleted file mode 100644
index 9087f823d7f6a018ef832d2be78602d097bcd3e0..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe UploadLinkFilter, lib: true do
-    def filter(doc, contexts = {})
-      contexts.reverse_merge!({
-        project: project
-      })
-
-      described_class.call(doc, contexts)
-    end
-
-    def image(path)
-      %(<img src="#{path}" />)
-    end
-
-    def link(path)
-      %(<a href="#{path}">#{path}</a>)
-    end
-
-    let(:project) { create(:project) }
-
-    shared_examples :preserve_unchanged do
-      it 'does not modify any relative URL in anchor' do
-        doc = filter(link('README.md'))
-        expect(doc.at_css('a')['href']).to eq 'README.md'
-      end
-
-      it 'does not modify any relative URL in image' do
-        doc = filter(image('files/images/logo-black.png'))
-        expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
-      end
-    end
-
-    it 'does not raise an exception on invalid URIs' do
-      act = link("://foo")
-      expect { filter(act) }.not_to raise_error
-    end
-
-    context 'with a valid repository' do
-      it 'rebuilds relative URL for a link' do
-        doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
-        expect(doc.at_css('a')['href']).
-          to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
-      end
-
-      it 'rebuilds relative URL for an image' do
-        doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
-        expect(doc.at_css('a')['href']).
-          to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
-      end
-
-      it 'does not modify absolute URL' do
-        doc = filter(link('http://example.com'))
-        expect(doc.at_css('a')['href']).to eq 'http://example.com'
-      end
-
-      it 'supports Unicode filenames' do
-        path = '/uploads/한글.png'
-        escaped = Addressable::URI.escape(path)
-
-        # Stub these methods so the file doesn't actually need to be in the repo
-        allow_any_instance_of(described_class).
-          to receive(:file_exists?).and_return(true)
-        allow_any_instance_of(described_class).
-          to receive(:image?).with(path).and_return(true)
-
-        doc = filter(image(escaped))
-        expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png"
-      end
-    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
deleted file mode 100644
index 3dca68edfa86142472cad65bf85a4955bb36e0c9..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
-  describe UserReferenceFilter, lib: true do
-    include FilterSpecHelper
-
-    let(:project)   { create(:empty_project, :public) }
-    let(:user)      { create(:user) }
-    let(:reference) { user.to_reference }
-
-    it 'requires project context' do
-      expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
-    end
-
-    it 'ignores invalid users' do
-      exp = act = "Hey #{invalidate_reference(reference)}"
-      expect(reference_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 #{reference}</#{elem}>"
-        expect(reference_filter(act).to_html).to eq exp
-      end
-    end
-
-    context 'mentioning @all' do
-      let(:reference) { User.reference_prefix + 'all' }
-
-      before do
-        project.team << [project.creator, :developer]
-      end
-
-      it 'supports a special @all mention' do
-        doc = reference_filter("Hey #{reference}")
-        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 'adds to the results hash' do
-        result = reference_pipeline_result("Hey #{reference}")
-        expect(result[:references][:user]).to eq [project.creator]
-      end
-    end
-
-    context 'mentioning a user' do
-      it 'links to a User' do
-        doc = reference_filter("Hey #{reference}")
-        expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
-      end
-
-      it 'links to a User with a period' do
-        user = create(:user, name: 'alphA.Beta')
-
-        doc = reference_filter("Hey #{user.to_reference}")
-        expect(doc.css('a').length).to eq 1
-      end
-
-      it 'links to a User with an underscore' do
-        user = create(:user, name: 'ping_pong_king')
-
-        doc = reference_filter("Hey #{user.to_reference}")
-        expect(doc.css('a').length).to eq 1
-      end
-
-      it 'includes a data-user attribute' do
-        doc = reference_filter("Hey #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-user')
-        expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Hey #{reference}")
-        expect(result[:references][:user]).to eq [user]
-      end
-    end
-
-    context 'mentioning a group' do
-      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
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Hey #{reference}")
-        expect(result[:references][:user]).to eq group.users
-      end
-    end
-
-    it 'links with adjacent text' do
-      doc = reference_filter("Mention me (#{reference}.)")
-      expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
-    end
-
-    it 'includes default classes' do
-      doc = reference_filter("Hey #{reference}")
-      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
-    end
-
-    it 'supports an :only_path context' do
-      doc = reference_filter("Hey #{reference}", 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
-
-    context 'referencing a user in a link href' do
-      let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} }
-
-      it 'links to a User' do
-        doc = reference_filter("Hey #{reference}")
-        expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
-      end
-
-      it 'links with adjacent text' do
-        doc = reference_filter("Mention me (#{reference}.)")
-        expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
-      end
-
-      it 'includes a data-user attribute' do
-        doc = reference_filter("Hey #{reference}")
-        link = doc.css('a').first
-
-        expect(link).to have_attribute('data-user')
-        expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
-      end
-
-      it 'adds to the results hash' do
-        result = reference_pipeline_result("Hey #{reference}")
-        expect(result[:references][:user]).to eq [user]
-      end
-    end
-  end
-end