Autolink package names in Gemfile
This commit is contained in:
parent
3f6d91c53c
commit
b1012d986c
|
|
@ -0,0 +1,18 @@
|
|||
module Gitlab
|
||||
module DependencyLinker
|
||||
LINKERS = [
|
||||
GemfileLinker,
|
||||
].freeze
|
||||
|
||||
def self.linker(blob_name)
|
||||
LINKERS.find { |linker| linker.support?(blob_name) }
|
||||
end
|
||||
|
||||
def self.link(blob_name, plain_text, highlighted_text)
|
||||
linker = linker(blob_name)
|
||||
return highlighted_text unless linker
|
||||
|
||||
linker.link(plain_text, highlighted_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
module Gitlab
|
||||
module DependencyLinker
|
||||
class BaseLinker
|
||||
def self.link(plain_text, highlighted_text)
|
||||
new(plain_text, highlighted_text).link
|
||||
end
|
||||
|
||||
attr_accessor :plain_text, :highlighted_text
|
||||
|
||||
def initialize(plain_text, highlighted_text)
|
||||
@plain_text = plain_text
|
||||
@highlighted_text = highlighted_text
|
||||
end
|
||||
|
||||
def link
|
||||
link_dependencies
|
||||
|
||||
highlighted_lines.join.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def package_url(name)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def link_dependencies
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def package_link(name, url = package_url(name))
|
||||
return name unless url
|
||||
|
||||
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="noopener noreferrer" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}
|
||||
end
|
||||
|
||||
# Links package names in a method call or assignment string argument.
|
||||
#
|
||||
# Example:
|
||||
# link_method_call("gem")
|
||||
# # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"`
|
||||
#
|
||||
# link_method_call("gem", "specific_package")
|
||||
# # Will link `specific_package` in `gem "specific_package"`
|
||||
#
|
||||
# link_method_call("github", /[^\/]+\/[^\/]+/)
|
||||
# # Will link `user/repo` in `github "user/repo"`, but not `github "package"`
|
||||
#
|
||||
# link_method_call(%w[add_dependency add_development_dependency])
|
||||
# # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"`
|
||||
#
|
||||
# link_method_call("name")
|
||||
# # Will link `package` in `self.name = "package"`
|
||||
def link_method_call(method_names, value = nil, &url_proc)
|
||||
value =
|
||||
case value
|
||||
when String
|
||||
Regexp.escape(value)
|
||||
when nil
|
||||
/[^'"]+/
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
method_names = Array(method_names).map { |name| Regexp.escape(name) }
|
||||
|
||||
regex = %r{
|
||||
#{Regexp.union(method_names)} # Method name
|
||||
\s* # Whitespace
|
||||
[(=]? # Opening brace or equals sign
|
||||
\s* # Whitespace
|
||||
['"](?<name>#{value})['"] # Package name in quotes
|
||||
}x
|
||||
|
||||
link_regex(regex, &url_proc)
|
||||
end
|
||||
|
||||
# Links package names based on regex.
|
||||
#
|
||||
# Example:
|
||||
# link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/)
|
||||
# # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
|
||||
def link_regex(regex)
|
||||
highlighted_lines.map!.with_index do |rich_line, i|
|
||||
marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe)
|
||||
|
||||
marker.mark(regex, group: :name) do |text, left:, right:|
|
||||
url = block_given? ? yield(text) : package_url(text)
|
||||
package_link(text, url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def plain_lines
|
||||
@plain_lines ||= plain_text.lines
|
||||
end
|
||||
|
||||
def highlighted_lines
|
||||
@highlighted_lines ||= highlighted_text.lines
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
module Gitlab
|
||||
module DependencyLinker
|
||||
class GemfileLinker < BaseLinker
|
||||
def self.support?(blob_name)
|
||||
blob_name == 'Gemfile' || blob_name == 'gems.rb'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_dependencies
|
||||
# Link `gem "package_name"` to https://rubygems.org/gems/package_name
|
||||
link_method_call("gem")
|
||||
|
||||
# Link `github: "user/repo"` to https://github.com/user/repo
|
||||
link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/) do |name|
|
||||
"https://github.com/#{name}"
|
||||
end
|
||||
|
||||
# Link `source "https://rubygems.org"` to https://rubygems.org
|
||||
link_method_call("source", %r{https?://[^'"]+}) { |url| url }
|
||||
end
|
||||
|
||||
def package_url(name)
|
||||
"https://rubygems.org/gems/#{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,6 +13,8 @@ module Gitlab
|
|||
highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
|
||||
end
|
||||
|
||||
attr_reader :blob_name
|
||||
|
||||
def initialize(blob_name, blob_content, repository: nil)
|
||||
@formatter = Rouge::Formatters::HTMLGitlab
|
||||
@repository = repository
|
||||
|
|
@ -21,16 +23,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def highlight(text, continue: true, plain: false)
|
||||
if plain
|
||||
hl_lexer = Rouge::Lexers::PlainText
|
||||
continue = false
|
||||
else
|
||||
hl_lexer = self.lexer
|
||||
end
|
||||
|
||||
@formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
|
||||
rescue
|
||||
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
|
||||
highlighted_text = highlight_text(text, continue: continue, plain: plain)
|
||||
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
|
||||
highlighted_text
|
||||
end
|
||||
|
||||
def lexer
|
||||
|
|
@ -50,5 +45,27 @@ module Gitlab
|
|||
|
||||
Rouge::Lexer.find_fancy(language_name)
|
||||
end
|
||||
|
||||
def highlight_text(text, continue: true, plain: false)
|
||||
if plain
|
||||
highlight_plain(text)
|
||||
else
|
||||
highlight_rich(text, continue: continue)
|
||||
end
|
||||
end
|
||||
|
||||
def highlight_plain(text)
|
||||
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
|
||||
end
|
||||
|
||||
def highlight_rich(text, continue: true)
|
||||
@formatter.format(lexer.lex(text, continue: continue), tag: lexer.tag).html_safe
|
||||
rescue
|
||||
highlight_plain(text)
|
||||
end
|
||||
|
||||
def link_dependencies(text, highlighted_text)
|
||||
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Gitlab::DependencyLinker::GemfileLinker, lib: true do
|
||||
describe '.support?' do
|
||||
it 'supports Gemfile' do
|
||||
expect(described_class.support?('Gemfile')).to be_truthy
|
||||
end
|
||||
|
||||
it 'supports gems.rb' do
|
||||
expect(described_class.support?('gems.rb')).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not support other files' do
|
||||
expect(described_class.support?('Gemfile.lock')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#link' do
|
||||
let(:file_name) { 'Gemfile' }
|
||||
|
||||
let(:file_content) do
|
||||
<<-CONTENT.strip_heredoc
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem "rails", '4.2.6', github: "rails/rails"
|
||||
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
|
||||
|
||||
# Responders respond_to and respond_with
|
||||
gem 'responders', '~> 2.0', :github => 'rails/responders'
|
||||
|
||||
# Specify a sprockets version due to increased performance
|
||||
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
|
||||
gem 'sprockets', '~> 3.6.0'
|
||||
|
||||
# Default values for AR models
|
||||
gem 'default_value_for', '~> 3.0.0'
|
||||
CONTENT
|
||||
end
|
||||
|
||||
subject { Gitlab::Highlight.highlight(file_name, file_content) }
|
||||
|
||||
def link(name, url)
|
||||
%{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>}
|
||||
end
|
||||
|
||||
it 'links sources' do
|
||||
expect(subject).to include(link('https://rubygems.org', 'https://rubygems.org'))
|
||||
end
|
||||
|
||||
it 'links dependencies' do
|
||||
expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
|
||||
expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
|
||||
expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
|
||||
expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
|
||||
expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
|
||||
end
|
||||
|
||||
it 'links GitHub repos' do
|
||||
expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
|
||||
expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Gitlab::DependencyLinker, lib: true do
|
||||
describe '.link' do
|
||||
it 'links using GemfileLinker' do
|
||||
blob_name = 'Gemfile'
|
||||
|
||||
expect(described_class::GemfileLinker).to receive(:link)
|
||||
|
||||
described_class.link(blob_name, nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -57,4 +57,15 @@ describe Gitlab::Highlight, lib: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#highlight' do
|
||||
subject { described_class.highlight(file_name, file_content, nowrap: false) }
|
||||
|
||||
it 'links dependencies via DependencyLinker' do
|
||||
expect(Gitlab::DependencyLinker).to receive(:link).
|
||||
with('file.name', 'Contents', anything).and_call_original
|
||||
|
||||
described_class.highlight('file.name', 'Contents')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue