diff --git a/Gemfile b/Gemfile
index 11407ca8e7..3c42885ef8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -88,7 +88,7 @@ gem "six"
gem "seed-fu"
# Markup pipeline for GitLab
-gem 'html-pipeline-gitlab', '~> 0.1'
+gem 'html-pipeline', '~> 1.11.0'
# Markdown to HTML
gem "github-markup"
diff --git a/Gemfile.lock b/Gemfile.lock
index a134fbf6bf..8f66476ad6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -278,12 +278,6 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
- html-pipeline-gitlab (0.2.0)
- actionpack (~> 4)
- gitlab_emoji (~> 0.1)
- html-pipeline (~> 1.11.0)
- mime-types
- sanitize (~> 2.1)
http_parser.rb (0.5.3)
httparty (0.13.3)
json (~> 1.8)
@@ -717,7 +711,7 @@ DEPENDENCIES
guard-spinach
haml-rails
hipchat (~> 1.5.0)
- html-pipeline-gitlab (~> 0.1)
+ html-pipeline (~> 1.11.0)
httparty
jasmine (= 2.0.2)
jquery-atwho-rails (~> 0.3.3)
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index f230201543..37b250d353 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -1,5 +1,4 @@
require 'html/pipeline'
-require 'html/pipeline/gitlab'
module Gitlab
# Custom parser for GitLab-flavored Markdown
@@ -61,19 +60,24 @@ module Gitlab
reference_only_path: true
)
- markdown_context = {
+ pipeline = HTML::Pipeline.new(filters)
+
+ context = {
+ # SanitizationFilter
+ whitelist: sanitization_whitelist,
+
+ # EmojiFilter
asset_root: Gitlab.config.gitlab.url,
asset_host: Gitlab::Application.config.asset_host,
- whitelist: sanitization_whitelist,
- reference_class: html_options[:class],
- only_path: options[:reference_only_path],
+
+ # ReferenceFilter
current_user: current_user,
- project: project
+ only_path: options[:reference_only_path],
+ project: project,
+ reference_class: html_options[:class]
}
- markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
-
- result = markdown_pipeline.call(text, markdown_context)
+ result = pipeline.call(text, context)
save_options = 0
if options[:xhtml]
@@ -91,7 +95,7 @@ module Gitlab
private
- # Custom filters for html-pipeline:
+ # Filters used in our pipeline
#
# SanitizationFilter should come first so that all generated reference HTML
# goes through untouched.
@@ -101,6 +105,8 @@ module Gitlab
[
HTML::Pipeline::SanitizationFilter,
+ Gitlab::Markdown::EmojiFilter,
+
Gitlab::Markdown::UserReferenceFilter,
Gitlab::Markdown::IssueReferenceFilter,
Gitlab::Markdown::ExternalIssueReferenceFilter,
@@ -109,8 +115,6 @@ module Gitlab
Gitlab::Markdown::CommitRangeReferenceFilter,
Gitlab::Markdown::CommitReferenceFilter,
Gitlab::Markdown::LabelReferenceFilter,
-
- HTML::Pipeline::Gitlab::GitlabEmojiFilter
]
end
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/emoji_filter.rb
new file mode 100644
index 0000000000..e239f76684
--- /dev/null
+++ b/lib/gitlab/markdown/emoji_filter.rb
@@ -0,0 +1,79 @@
+require 'gitlab_emoji'
+require 'html/pipeline/filter'
+require 'action_controller'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces :emoji: with images.
+ #
+ # Based on HTML::Pipeline::EmojiFilter
+ #
+ # Context options:
+ # :asset_root
+ # :asset_host
+ class EmojiFilter < HTML::Pipeline::Filter
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+ next unless content.include?(':')
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ html = emoji_image_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Replace :emoji: with corresponding images.
+ #
+ # text - String text to replace :emoji: in.
+ #
+ # Returns a String with :emoji: replaced with images.
+ def emoji_image_filter(text)
+ text.gsub(emoji_pattern) do |match|
+ name = $1
+ ""
+ end
+ end
+
+ private
+
+ def emoji_url(name)
+ emoji_path = "emoji/#{emoji_filename(name)}"
+ if context[:asset_host]
+ # Asset host is specified.
+ url_to_image(emoji_path)
+ elsif context[:asset_root]
+ # Gitlab url is specified
+ File.join(context[:asset_root], url_to_image(emoji_path))
+ else
+ # All other cases
+ url_to_image(emoji_path)
+ end
+ end
+
+ def url_to_image(image)
+ ActionController::Base.helpers.url_to_image(image)
+ end
+
+ # Build a regexp that matches all valid :emoji: names.
+ def self.emoji_pattern
+ @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ end
+
+ def emoji_pattern
+ self.class.emoji_pattern
+ end
+
+ def emoji_filename(name)
+ "#{Emoji.emoji_filename(name)}.png"
+ end
+ end
+ end
+end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 315e91d4f3..64f130e4ae 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -2,43 +2,27 @@ require 'spec_helper'
describe GitlabMarkdownHelper do
include ApplicationHelper
- include IssuesHelper
-
- # TODO: Properly test this
- def can?(*)
- true
- end
let!(:project) { create(:project) }
- let(:empty_project) { create(:empty_project) }
let(:user) { create(:user, username: 'gfm') }
let(:commit) { project.repository.commit }
- let(:earlier_commit){ project.repository.commit("HEAD~2") }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
- let(:member) { project.project_members.where(user_id: user).first }
# Helper expects a current_user method.
let(:current_user) { user }
- def url_helper(image_name)
- File.join(root_url, 'assets', image_name)
- end
-
before do
# Helper expects a @project instance variable
@project = project
- @ref = 'markdown'
- @repository = project.repository
- @request.host = Gitlab.config.gitlab.host
end
describe "#gfm" do
it "should forward HTML options to links" do
expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')).
- to have_selector('a.gfm.foo')
+ to have_selector('a.gfm.foo')
end
describe "referencing multiple objects" do
@@ -60,53 +44,6 @@ describe GitlabMarkdownHelper do
end
end
- # TODO (rspeicher): These tests belong in the emoji filter spec
- describe "emoji" do
- it "matches at the start of a string" do
- expect(gfm(":+1:")).to match(/
big time/)
- end
-
- it "ignores invalid emoji" do
- expect(gfm(":invalid-emoji:")).not_to match(/
")
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
# Leading commit link
- expect(groups[0]).to match(/href="#{commit_path}"/)
- expect(groups[0]).to match(/This should finally fix $/)
+ expect(doc.css('a')[0].attr('href')).to eq commit_path
+ expect(doc.css('a')[0].text).to eq 'This should finally fix '
# First issue link
- expect(groups[1]).
- to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[0])}"/)
- expect(groups[1]).to match(/##{issues[0].iid}$/)
+ expect(doc.css('a')[1].attr('href')).
+ to eq namespace_project_issue_path(project.namespace, project, issues[0])
+ expect(doc.css('a')[1].text).to eq "##{issues[0].iid}"
# Internal commit link
- expect(groups[2]).to match(/href="#{commit_path}"/)
- expect(groups[2]).to match(/ and /)
+ expect(doc.css('a')[2].attr('href')).to eq commit_path
+ expect(doc.css('a')[2].text).to eq ' and '
# Second issue link
- expect(groups[3]).
- to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[1])}"/)
- expect(groups[3]).to match(/##{issues[1].iid}$/)
+ expect(doc.css('a')[3].attr('href')).
+ to eq namespace_project_issue_path(project.namespace, project, issues[1])
+ expect(doc.css('a')[3].text).to eq "##{issues[1].iid}"
# Trailing commit link
- expect(groups[4]).to match(/href="#{commit_path}"/)
- expect(groups[4]).to match(/ for real$/)
+ expect(doc.css('a')[4].attr('href')).to eq commit_path
+ expect(doc.css('a')[4].text).to eq ' for real'
end
- it "should forward HTML options" do
+ it 'should forward HTML options' do
actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
- expect(actual).to have_selector 'a.gfm.gfm-commit.foo'
+ doc = Nokogiri::HTML.parse(actual)
+
+ expect(doc.css('a')).to satisfy do |v|
+ # 'foo' gets added to all links
+ v.all? { |a| a.attr('class').match(/foo$/) }
+ end
end
it "escapes HTML passed in as the body" do
@@ -267,17 +209,6 @@ describe GitlabMarkdownHelper do
describe "#markdown" do
# TODO (rspeicher) - This block tests multiple different contexts. Break this up!
- # REFERENCES (PART TWO: THE REVENGE) ---------------------------------------
-
- it "should handle references in headers" do
- actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}"
-
- expect(markdown(actual, no_header_anchors: true)).
- to match(%r{
some code from $#{snippet.id}\nhere too\n\n"
- expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")).
+ expect(markdown("\n some code from $#{snippet.id}\n here too\n")).
to eq(target_html)
- expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")).
+ expect(markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")).
to eq(target_html)
end
it "should leave inline code untouched" do
- expect(markdown("\nDon't use `$#{snippet.id}` here.\n")).to eq(
- "Don't use $#{snippet.id} here.
Don't use $#{snippet.id} here.
screen shot:
:heart:
') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + end + + it 'ignores unsupported emoji' do + exp = act = ':foo:
' + doc = filter(act) + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly encodes the URL' do + doc = filter(':+1:
') + 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