diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index b8d2673c1a67f7739adb81c35368ec1660d0a638..15f7da5d934aadb75663b6978a7d63788a46d442 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -26,7 +26,7 @@ module Banzai # in the generated link. # # Rubular: http://rubular.com/r/cxjPyZc7Sb - LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?])} # Text matching LINK_PATTERN inside these elements will not be linked IGNORE_PARENTS = %w(a code kbd pre script style).to_set @@ -54,15 +54,13 @@ module Banzai # # `@doc` will be re-parsed with the HTML String from Rinku. def rinku_parse - # Convert the options from a Hash to a String that Rinku expects - options = tag_options(link_options) - # NOTE: We don't parse email links because it will erroneously match # external Commit and CommitRange references. # # The final argument tells Rinku to link short URLs that don't include a # period (e.g., http://localhost:3000/) - rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1) + mode = context[:autolink_emails] ? :all : :urls + rinku = Rinku.auto_link(html, mode, tag_options(link_options), IGNORE_PARENTS.to_a, 1) return if rinku == html @@ -111,9 +109,9 @@ module Banzai # order to be output literally rather than escaped. match.gsub!(/((?:&[\w#]+;)+)\z/, '') dropped = ($1 || '').html_safe + match = ERB::Util.html_escape_once(match) - options = link_options.merge(href: match) - content_tag(:a, match, options) + dropped + %{#{match}#{dropped}}.html_safe end def autolink_filter(text) diff --git a/lib/banzai/pipeline/autolink_pipeline.rb b/lib/banzai/pipeline/autolink_pipeline.rb new file mode 100644 index 0000000000000000000000000000000000000000..53f2da5c7b5861e318cc7bbbd3110b8c8e29c001 --- /dev/null +++ b/lib/banzai/pipeline/autolink_pipeline.rb @@ -0,0 +1,12 @@ +module Banzai + module Pipeline + class AutolinkPipeline < BasePipeline + def self.filters + @filters ||= FilterArray[ + Filter::AutolinkFilter, + Filter::ExternalLinkFilter + ] + end + end + end +end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 83bc230df3e1d4df12e50f4792e12953cb70d082..e25d0f060200fd58099129a0cd39bb8976f37380 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -25,7 +25,7 @@ module Gitlab def highlight(text, continue: true, plain: false) highlighted_text = highlight_text(text, continue: continue, plain: plain) highlighted_text = link_dependencies(text, highlighted_text) if blob_name - highlighted_text + autolink_strings(text, highlighted_text) end def lexer @@ -67,5 +67,43 @@ module Gitlab def link_dependencies(text, highlighted_text) Gitlab::DependencyLinker.link(blob_name, text, highlighted_text) end + + def autolink_strings(text, highlighted_text) + raw_lines = text.lines + + # TODO: Don't run pre-processing pipeline, because this may break the highlighting + linked_text = Banzai.render( + ERB::Util.html_escape(text), + pipeline: :autolink, + autolink_emails: true + ).html_safe + + linked_lines = linked_text.lines + + highlighted_lines = highlighted_text.lines + + highlighted_lines.map!.with_index do |rich_line, i| + matches = [] + linked_lines[i].scan(/(?]+>)(?[^<]+)(?<\/a>)/) { matches << Regexp.last_match } + next rich_line if matches.empty? + + raw_line = raw_lines[i] + marked_line = rich_line.html_safe + + matches.each do |match| + marker = StringRegexMarker.new(raw_line, marked_line) + + regex = /#{Regexp.escape(match[:content])}/ + + marked_line = marker.mark(regex) do |text, left:, right:| + "#{match[:start]}#{text}#{match[:end]}" + end + end + + marked_line + end + + highlighted_lines.join.html_safe + end end end diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index a6d2ea11fcc697c2e619e55692f74fdbc0e04563..7fdcd1148aa2d9a387242c724e3fcf45064bec27 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -130,6 +130,15 @@ describe Banzai::Filter::AutolinkFilter, lib: true do doc = filter("See #{link}...") expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}\"") + expect(doc.at_css('a').text).to eq link + + doc = filter("See #{link}'") + 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 diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 95da344802dc2820a16442d98f2c0001e550ecb8..e24241e70598ebfc4803f12dc69385fd7f1e0029 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Diff::InlineDiffMarker, lib: true do end end - context "when the text text is not html safe" do + context "when the rich text is not html safe" do let(:raw) { "abc 'def'" } let(:inline_diffs) { [2..5] } let(:subject) { described_class.new(raw).mark(inline_diffs) } diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e57b3053871b211826e046463801d816e505fa93..4a106fc675c08f8a7bf62107047caee744e13409 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Highlight, lib: true do describe '.highlight_lines' do let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') + described_class.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end it 'highlights all the lines properly' do @@ -59,7 +59,7 @@ describe Gitlab::Highlight, lib: true do end describe '#highlight' do - subject { described_class.highlight(file_name, file_content, nowrap: false) } + subject { described_class.highlight(file_name, file_content) } it 'links dependencies via DependencyLinker' do expect(Gitlab::DependencyLinker).to receive(:link). @@ -67,5 +67,66 @@ describe Gitlab::Highlight, lib: true do described_class.highlight('file.name', 'Contents') end + + context "plain text file" do + let(:file_name) { "example.txt" } + let(:file_content) do + <<-CONTENT.strip_heredoc + URL: http://www.google.com + Email: hello@example.com + CONTENT + end + + it "links URLs" do + expect(subject).to include(%{http://www.google.com}) + end + + it "links emails" do + expect(subject).to include(%{hello@example.com}) + end + end + + context "file with highlighting" do + let(:file_name) { "example.rb" } + let(:file_content) do + <<-CONTENT.strip_heredoc + # URL in comment: http://www.google.com + # Email in comment: hello@example.com + + "URL in string: http://www.google.com" + "Email in string: hello@example.com" + + # + # http://www.google.com + CONTENT + end + + context "in a comment" do + it "links URLs" do + expect(subject).to include(%{URL in comment: http://www.google.com}) + end + + it "links emails" do + expect(subject).to include(%{Email in comment: hello@example.com}) + end + end + + context "in a string" do + it "links URLs" do + expect(subject).to include(%{URL in string: http://www.google.com}) + end + + it "links emails" do + expect(subject).to include(%{Email in string: hello@example.com}) + end + end + + context 'in HTML/XML tags' do + it "links URLs" do + expect(subject).to include(%{<http://www.google.com>}) + expect(subject).to include(%{<url>http://www.google.com</url>}) + end + end + end end end