diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index b8199aa5e618fd604c61e4686972e8f977a241e9..859a62f740f5ea46dbeaadee21250dc361953708 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -17,410 +17,215 @@ require 'erb'
 #       -> Post-process HTML
 #         -> `gfm_with_options` helper
 #           -> HTML::Pipeline
-#             -> Sanitize
-#             -> RelativeLink
-#             -> Emoji
-#             -> Table of Contents
-#             -> Autolinks
-#               -> Rinku (http, https, ftp)
-#               -> Other schemes
-#             -> ExternalLink
-#             -> References
-#             -> TaskList
+#             -> SanitizationFilter
+#             -> Other filters, depending on pipeline
 #           -> `html_safe`
 #           -> Template
 #
 # See the MarkdownFeature class for setup details.
 
 describe 'GitLab Markdown', feature: true do
-  include ActionView::Helpers::TagHelper
-  include ActionView::Helpers::UrlHelper
   include Capybara::Node::Matchers
   include GitlabMarkdownHelper
+  include MarkdownMatchers
 
-  # `markdown` calls these two methods
-  def current_user
-    @feat.user
-  end
-
-  def user_color_scheme_class
-    :white
-  end
-
-  # Let's only parse this thing once
-  before(:all) do
-    @feat = MarkdownFeature.new
-
-    # `markdown` expects a `@project` variable
-    @project = @feat.project
-
-    @md = markdown(@feat.raw_markdown)
-    @doc = Nokogiri::HTML::DocumentFragment.parse(@md)
-  end
-
-  after(:all) do
-    @feat.teardown
+  # Sometimes it can be useful to see the parsed output of the Markdown document
+  # for debugging. Call this method to write the output to
+  # `tmp/capybara/<filename>.html`.
+  def write_markdown(filename = 'markdown_spec')
+    File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file|
+      file.puts @html
+    end
   end
 
-  # Given a header ID, goes to that element's parent (the header itself), then
-  # its next sibling element (the body).
-  def get_section(id)
-    @doc.at_css("##{id}").parent.next_element
+  def doc(html = @html)
+    Nokogiri::HTML::DocumentFragment.parse(html)
   end
 
-  # Sometimes it can be useful to see the parsed output of the Markdown document
-  # for debugging. Uncomment this block to write the output to
-  # tmp/capybara/markdown_spec.html.
-  #
-  # it 'writes to a file' do
-  #   File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file|
-  #     file.puts @md
-  #   end
-  # end
-
-  describe 'Markdown' do
-    describe 'No Intra Emphasis' do
+  # Shared behavior that all pipelines should exhibit
+  shared_examples 'all pipelines' do
+    describe 'Redcarpet extensions' do
       it 'does not parse emphasis inside of words' do
-        body = get_section('no-intra-emphasis')
-        expect(body.to_html).not_to match('foo<em>bar</em>baz')
+        expect(doc.to_html).not_to match('foo<em>bar</em>baz')
       end
-    end
 
-    describe 'Tables' do
       it 'parses table Markdown' do
-        body = get_section('tables')
-        expect(body).to have_selector('th:contains("Header")')
-        expect(body).to have_selector('th:contains("Row")')
-        expect(body).to have_selector('th:contains("Example")')
+        aggregate_failures do
+          expect(doc).to have_selector('th:contains("Header")')
+          expect(doc).to have_selector('th:contains("Row")')
+          expect(doc).to have_selector('th:contains("Example")')
+        end
       end
 
       it 'allows Markdown in tables' do
-        expect(@doc.at_css('td:contains("Baz")').children.to_html).
+        expect(doc.at_css('td:contains("Baz")').children.to_html).
           to eq '<strong>Baz</strong>'
       end
-    end
 
-    describe 'Fenced Code Blocks' do
       it 'parses fenced code blocks' do
-        expect(@doc).to have_selector('pre.code.highlight.white.c')
-        expect(@doc).to have_selector('pre.code.highlight.white.python')
+        aggregate_failures do
+          expect(doc).to have_selector('pre.code.highlight.white.c')
+          expect(doc).to have_selector('pre.code.highlight.white.python')
+        end
       end
-    end
 
-    describe 'Strikethrough' do
       it 'parses strikethroughs' do
-        expect(@doc).to have_selector(%{del:contains("and this text doesn't")})
+        expect(doc).to have_selector(%{del:contains("and this text doesn't")})
       end
-    end
 
-    describe 'Superscript' do
       it 'parses superscript' do
-        body = get_section('superscript')
-        expect(body.to_html).to match('1<sup>st</sup>')
-        expect(body.to_html).to match('2<sup>nd</sup>')
+        expect(doc).to have_selector('sup', count: 2)
       end
     end
-  end
 
-  describe 'HTML::Pipeline' do
     describe 'SanitizationFilter' do
-      it 'uses a permissive whitelist' do
-        expect(@doc).to have_selector('b:contains("b tag")')
-        expect(@doc).to have_selector('em:contains("em tag")')
-        expect(@doc).to have_selector('code:contains("code tag")')
-        expect(@doc).to have_selector('kbd:contains("s")')
-        expect(@doc).to have_selector('strike:contains(Emoji)')
-        expect(@doc).to have_selector('img[src*="smile.png"]')
-        expect(@doc).to have_selector('br')
-        expect(@doc).to have_selector('hr')
+      it 'permits b elements' do
+        expect(doc).to have_selector('b:contains("b tag")')
       end
 
-      it 'permits span elements' do
-        expect(@doc).to have_selector('span:contains("span tag")')
+      it 'permits em elements' do
+        expect(doc).to have_selector('em:contains("em tag")')
       end
 
-      it 'permits table alignment' do
-        expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
-        expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
-        expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
-
-        expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
-        expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
-        expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
+      it 'permits code elements' do
+        expect(doc).to have_selector('code:contains("code tag")')
       end
 
-      it 'removes `rel` attribute from links' do
-        body = get_section('sanitizationfilter')
-        expect(body).not_to have_selector('a[rel="bookmark"]')
+      it 'permits kbd elements' do
+        expect(doc).to have_selector('kbd:contains("s")')
       end
 
-      it "removes `href` from `a` elements if it's fishy" do
-        expect(@doc).not_to have_selector('a[href*="javascript"]')
+      it 'permits strike elements' do
+        expect(doc).to have_selector('strike:contains(Emoji)')
       end
-    end
 
-    describe 'Escaping' do
-      let(:table) { @doc.css('table').last.at_css('tbody') }
-
-      it 'escapes non-tag angle brackets' do
-        expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
+      it 'permits img elements' do
+        expect(doc).to have_selector('img[src*="smile.png"]')
       end
-    end
-
-    describe 'Edge Cases' do
-      it 'allows markup inside link elements' do
-        expect(@doc.at_css('a[href="#link-emphasis"]').to_html).
-          to eq %{<a href="#link-emphasis"><em>text</em></a>}
-
-        expect(@doc.at_css('a[href="#link-strong"]').to_html).
-          to eq %{<a href="#link-strong"><strong>text</strong></a>}
 
-        expect(@doc.at_css('a[href="#link-code"]').to_html).
-          to eq %{<a href="#link-code"><code>text</code></a>}
+      it 'permits br elements' do
+        expect(doc).to have_selector('br')
       end
-    end
 
-    describe 'EmojiFilter' do
-      it 'parses Emoji' do
-        expect(@doc).to have_selector('img.emoji', count: 10)
+      it 'permits hr elements' do
+        expect(doc).to have_selector('hr')
       end
-    end
 
-    describe 'TableOfContentsFilter' do
-      it 'creates anchors inside header elements' do
-        expect(@doc).to have_selector('h1 a#gitlab-markdown')
-        expect(@doc).to have_selector('h2 a#markdown')
-        expect(@doc).to have_selector('h3 a#autolinkfilter')
+      it 'permits span elements' do
+        expect(doc).to have_selector('span:contains("span tag")')
       end
-    end
 
-    describe 'AutolinkFilter' do
-      let(:list) { get_section('autolinkfilter').next_element }
-
-      def item(index)
-        list.at_css("li:nth-child(#{index})")
+      it 'permits style attribute in th elements' do
+        aggregate_failures do
+          expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
+          expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
+          expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
+        end
       end
 
-      it 'autolinks http://' do
-        expect(item(1).children.first.name).to eq 'a'
-        expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/'
+      it 'permits style attribute in td elements' do
+        aggregate_failures do
+          expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
+          expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
+          expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
+        end
       end
 
-      it 'autolinks https://' do
-        expect(item(2).children.first.name).to eq 'a'
-        expect(item(2).children.first['href']).to eq 'https://google.com/'
+      it 'removes `rel` attribute from links' do
+        expect(doc).not_to have_selector('a[rel="bookmark"]')
       end
 
-      it 'autolinks ftp://' do
-        expect(item(3).children.first.name).to eq 'a'
-        expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/'
+      it "removes `href` from `a` elements if it's fishy" do
+        expect(doc).not_to have_selector('a[href*="javascript"]')
       end
+    end
 
-      it 'autolinks smb://' do
-        expect(item(4).children.first.name).to eq 'a'
-        expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz'
+    describe 'Escaping' do
+      it 'escapes non-tag angle brackets' do
+        table = doc.css('table').last.at_css('tbody')
+        expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
       end
+    end
 
-      it 'autolinks irc://' do
-        expect(item(5).children.first.name).to eq 'a'
-        expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git'
-      end
+    describe 'Edge Cases' do
+      it 'allows markup inside link elements' do
+        aggregate_failures do
+          expect(doc.at_css('a[href="#link-emphasis"]').to_html).
+            to eq %{<a href="#link-emphasis"><em>text</em></a>}
 
-      it 'autolinks short, invalid URLs' do
-        expect(item(6).children.first.name).to eq 'a'
-        expect(item(6).children.first['href']).to eq 'http://localhost:3000'
-      end
+          expect(doc.at_css('a[href="#link-strong"]').to_html).
+            to eq %{<a href="#link-strong"><strong>text</strong></a>}
 
-      %w(code a kbd).each do |elem|
-        it "ignores links inside '#{elem}' element" do
-          body = get_section('autolinkfilter')
-          expect(body).not_to have_selector("#{elem} a")
+          expect(doc.at_css('a[href="#link-code"]').to_html).
+            to eq %{<a href="#link-code"><code>text</code></a>}
         end
       end
     end
 
     describe 'ExternalLinkFilter' do
-      let(:links) { get_section('externallinkfilter').next_element }
-
       it 'adds nofollow to external link' do
-        expect(links.css('a').first.to_html).to match 'nofollow'
+        link = doc.at_css('a:contains("Google")')
+        expect(link.attr('rel')).to match 'nofollow'
       end
 
       it 'ignores internal link' do
-        expect(links.css('a').last.to_html).not_to match 'nofollow'
+        link = doc.at_css('a:contains("GitLab Root")')
+        expect(link.attr('rel')).not_to match 'nofollow'
       end
     end
+  end
 
-    describe 'ReferenceFilter' do
-      it 'handles references in headers' do
-        header = @doc.at_css('#reference-filters-eg-1').parent
-
-        expect(header.css('a').size).to eq 2
-      end
-
-      it "handles references in Markdown" do
-        body = get_section('reference-filters-eg-1')
-        expect(body).to have_selector('em a.gfm-merge_request', count: 1)
-      end
-
-      it 'parses user references' do
-        body = get_section('userreferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-project_member', count: 3)
-      end
-
-      it 'parses issue references' do
-        body = get_section('issuereferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-issue', count: 2)
-      end
-
-      it 'parses merge request references' do
-        body = get_section('mergerequestreferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2)
-      end
+  context 'default pipeline' do
+    before(:all) do
+      @feat = MarkdownFeature.new
 
-      it 'parses snippet references' do
-        body = get_section('snippetreferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-snippet', count: 2)
-      end
+      # `gfm_with_options` depends on a `@project` variable
+      @project = @feat.project
 
-      it 'parses commit range references' do
-        body = get_section('commitrangereferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2)
-      end
+      @html = markdown(@feat.raw_markdown)
+    end
 
-      it 'parses commit references' do
-        body = get_section('commitreferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-commit', count: 2)
-      end
+    it_behaves_like 'all pipelines'
 
-      it 'parses label references' do
-        body = get_section('labelreferencefilter')
-        expect(body).to have_selector('a.gfm.gfm-label', count: 3)
-      end
+    it 'includes RelativeLinkFilter' do
+      expect(doc).to parse_relative_links
     end
 
-    describe 'Task Lists' do
-      it 'generates task lists' do
-        body = get_section('task-lists')
-        expect(body).to have_selector('ul.task-list', count: 2)
-        expect(body).to have_selector('li.task-list-item', count: 7)
-        expect(body).to have_selector('input[checked]', count: 3)
-      end
+    it 'includes EmojiFilter' do
+      expect(doc).to parse_emoji
     end
-  end
-end
-
-# This is a helper class used by the GitLab Markdown feature spec
-#
-# Because the feature spec only cares about the output of the Markdown, and the
-# test setup and teardown and parsing is fairly expensive, we only want to do it
-# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
-# block, so we fake it by encapsulating all the shared setup in this class.
-#
-# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
-# reference to the factory-created objects.
-class MarkdownFeature
-  include FactoryGirl::Syntax::Methods
-
-  def initialize
-    DatabaseCleaner.start
-  end
-
-  def teardown
-    DatabaseCleaner.clean
-  end
-
-  def user
-    @user ||= create(:user)
-  end
 
-  def group
-    unless @group
-      @group = create(:group)
-      @group.add_user(user, Gitlab::Access::DEVELOPER)
+    it 'includes TableOfContentsFilter' do
+      expect(doc).to create_header_links
     end
 
-    @group
-  end
-
-  # Direct references ----------------------------------------------------------
-
-  def project
-    @project ||= create(:project)
-  end
-
-  def issue
-    @issue ||= create(:issue, project: project)
-  end
-
-  def merge_request
-    @merge_request ||= create(:merge_request, :simple, source_project: project)
-  end
-
-  def snippet
-    @snippet ||= create(:project_snippet, project: project)
-  end
-
-  def commit
-    @commit ||= project.commit
-  end
-
-  def commit_range
-    unless @commit_range
-      commit2 = project.commit('HEAD~3')
-      @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
+    it 'includes AutolinkFilter' do
+      expect(doc).to create_autolinks
     end
 
-    @commit_range
-  end
-
-  def simple_label
-    @simple_label ||= create(:label, name: 'gfm', project: project)
-  end
-
-  def label
-    @label ||= create(:label, name: 'awaiting feedback', project: project)
-  end
-
-  # Cross-references -----------------------------------------------------------
-
-  def xproject
-    unless @xproject
-      namespace = create(:namespace, name: 'cross-reference')
-      @xproject = create(:project, namespace: namespace)
-      @xproject.team << [user, :developer]
+    it 'includes all reference filters' do
+      aggregate_failures do
+        expect(doc).to reference_users
+        expect(doc).to reference_issues
+        expect(doc).to reference_merge_requests
+        expect(doc).to reference_snippets
+        expect(doc).to reference_commit_ranges
+        expect(doc).to reference_commits
+        expect(doc).to reference_labels
+      end
     end
 
-    @xproject
-  end
-
-  def xissue
-    @xissue ||= create(:issue, project: xproject)
-  end
-
-  def xmerge_request
-    @xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
-  end
-
-  def xsnippet
-    @xsnippet ||= create(:project_snippet, project: xproject)
-  end
-
-  def xcommit
-    @xcommit ||= xproject.commit
-  end
-
-  def xcommit_range
-    unless @xcommit_range
-      xcommit2 = xproject.commit('HEAD~2')
-      @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
+    it 'includes TaskListFilter' do
+      expect(doc).to parse_task_lists
     end
+  end
 
-    @xcommit_range
+  # `markdown` calls these two methods
+  def current_user
+    @feat.user
   end
 
-  def raw_markdown
-    fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
-    ERB.new(File.read(fixture)).result(binding)
+  def user_color_scheme_class
+    :white
   end
 end
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 02ab46c905a3723548a23003e66871a443a9ecc6..41d12afa9ce8f41561c7f960f9e87f567bec0a06 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try!
 - [**text**](#link-strong)
 - [`text`](#link-code)
 
+### RelativeLinkFilter
+
+Linking to a file relative to this project's repository should work.
+
+[Relative Link](doc/README.md)
+![Relative Image](app/assets/images/touch-icon-ipad.png)
+
 ### EmojiFilter
 
 Because life would be :zzz: without Emoji, right? :rocket:
@@ -123,9 +130,9 @@ These are all plain text that should get turned into links:
 
 But it shouldn't autolink text inside certain tags:
 
-- <code>http://about.gitlab.com/</code>
-- <a>http://about.gitlab.com/</a>
-- <kbd>http://about.gitlab.com/</kbd>
+- <code>http://code.gitlab.com/</code>
+- <a>http://a.gitlab.com/</a>
+- <kbd>http://kbd.gitlab.com/</kbd>
 
 ### ExternalLinkFilter
 
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a868aed73b89868b7930827db9825451828251e
--- /dev/null
+++ b/spec/support/markdown_feature.rb
@@ -0,0 +1,106 @@
+# This is a helper class used by the GitLab Markdown feature spec
+#
+# Because the feature spec only cares about the output of the Markdown, and the
+# test setup and teardown and parsing is fairly expensive, we only want to do it
+# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
+# block, so we fake it by encapsulating all the shared setup in this class.
+#
+# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
+# reference to the factory-created objects.
+class MarkdownFeature
+  include FactoryGirl::Syntax::Methods
+
+  def user
+    @user ||= create(:user)
+  end
+
+  def group
+    unless @group
+      @group = create(:group)
+      @group.add_user(user, Gitlab::Access::DEVELOPER)
+    end
+
+    @group
+  end
+
+  # Direct references ----------------------------------------------------------
+
+  def project
+    @project ||= create(:project)
+  end
+
+  def issue
+    @issue ||= create(:issue, project: project)
+  end
+
+  def merge_request
+    @merge_request ||= create(:merge_request, :simple, source_project: project)
+  end
+
+  def snippet
+    @snippet ||= create(:project_snippet, project: project)
+  end
+
+  def commit
+    @commit ||= project.commit
+  end
+
+  def commit_range
+    unless @commit_range
+      commit2 = project.commit('HEAD~3')
+      @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
+    end
+
+    @commit_range
+  end
+
+  def simple_label
+    @simple_label ||= create(:label, name: 'gfm', project: project)
+  end
+
+  def label
+    @label ||= create(:label, name: 'awaiting feedback', project: project)
+  end
+
+  # Cross-references -----------------------------------------------------------
+
+  def xproject
+    unless @xproject
+      namespace = create(:namespace, name: 'cross-reference')
+      @xproject = create(:project, namespace: namespace)
+      @xproject.team << [user, :developer]
+    end
+
+    @xproject
+  end
+
+  def xissue
+    @xissue ||= create(:issue, project: xproject)
+  end
+
+  def xmerge_request
+    @xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
+  end
+
+  def xsnippet
+    @xsnippet ||= create(:project_snippet, project: xproject)
+  end
+
+  def xcommit
+    @xcommit ||= xproject.commit
+  end
+
+  def xcommit_range
+    unless @xcommit_range
+      xcommit2 = xproject.commit('HEAD~2')
+      @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
+    end
+
+    @xcommit_range
+  end
+
+  def raw_markdown
+    fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
+    ERB.new(File.read(fixture)).result(binding)
+  end
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9df226c3af888ea8b89e5efad81cf6500cfe713d
--- /dev/null
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -0,0 +1,156 @@
+# MarkdownMatchers
+#
+# Custom matchers for our custom HTML::Pipeline filters. These are used to test
+# that specific filters are or are not used by our defined pipelines.
+#
+# Must be included manually.
+module MarkdownMatchers
+  extend RSpec::Matchers::DSL
+  include Capybara::Node::Matchers
+
+  # RelativeLinkFilter
+  matcher :parse_relative_links do
+    set_default_markdown_messages
+
+    match do |actual|
+      link  = actual.at_css('a:contains("Relative Link")')
+      image = actual.at_css('img[alt="Relative Image"]')
+
+      expect(link['href']).to end_with('master/doc/README.md')
+      expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
+    end
+  end
+
+  # EmojiFilter
+  matcher :parse_emoji do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('img.emoji', count: 10)
+    end
+  end
+
+  # TableOfContentsFilter
+  matcher :create_header_links do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('h1 a#gitlab-markdown')
+      expect(actual).to have_selector('h2 a#markdown')
+      expect(actual).to have_selector('h3 a#autolinkfilter')
+    end
+  end
+
+  # AutolinkFilter
+  matcher :create_autolinks do
+    def have_autolink(link)
+      have_link(link, href: link)
+    end
+
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_autolink('http://about.gitlab.com/')
+      expect(actual).to have_autolink('https://google.com/')
+      expect(actual).to have_autolink('ftp://ftp.us.debian.org/debian/')
+      expect(actual).to have_autolink('smb://foo/bar/baz')
+      expect(actual).to have_autolink('irc://irc.freenode.net/git')
+      expect(actual).to have_autolink('http://localhost:3000')
+
+      %w(code a kbd).each do |elem|
+        expect(body).not_to have_selector("#{elem} a")
+      end
+    end
+  end
+
+  # UserReferenceFilter
+  matcher :reference_users do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3)
+    end
+  end
+
+  # IssueReferenceFilter
+  matcher :reference_issues do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-issue', count: 3)
+    end
+  end
+
+  # MergeRequestReferenceFilter
+  matcher :reference_merge_requests do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3)
+      expect(actual).to have_selector('em a.gfm-merge_request')
+    end
+  end
+
+  # SnippetReferenceFilter
+  matcher :reference_snippets do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2)
+    end
+  end
+
+  # CommitRangeReferenceFilter
+  matcher :reference_commit_ranges do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2)
+    end
+  end
+
+  # CommitReferenceFilter
+  matcher :reference_commits do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-commit', count: 2)
+    end
+  end
+
+  # LabelReferenceFilter
+  matcher :reference_labels do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('a.gfm.gfm-label', count: 3)
+    end
+  end
+
+  # TaskListFilter
+  matcher :parse_task_lists do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('ul.task-list', count: 2)
+      expect(actual).to have_selector('li.task-list-item', count: 7)
+      expect(actual).to have_selector('input[checked]', count: 3)
+    end
+  end
+end
+
+# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
+# setting the failure messages for these matchers
+module RSpec::Matchers::DSL::Macros
+  def set_default_markdown_messages
+    failure_message do
+      # expected to parse emoji, but didn't
+      "expected to #{description}, but didn't"
+    end
+
+    failure_message_when_negated do
+      # expected not to parse task lists, but did
+      "expected not to #{description}, but did"
+    end
+  end
+end