diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 09951fe3d3e38bb649087a2f1c67790142606cf0..6e3829d994fe8d5e9410d0fd5bd821222512c3e2 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -185,6 +185,11 @@ $dark-il: #de935f; color: $dark-highlight-color !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $dark-na; + } + .hll { background-color: $dark-hll-bg; } .c { color: $dark-c; } /* Comment */ .err { color: $dark-err; } /* Error */ diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index b6a6d298adf592b2af502ceb063c9b2e699f5fd3..68eb0c7720f3737101760a8c7c1684584bb762d5 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -185,6 +185,11 @@ $monokai-gi: #a6e22e; color: $black !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $monokai-k; + } + .hll { background-color: $monokai-hll; } .c { color: $monokai-c; } /* Comment */ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 4f7a50dcb4fdf3888c772c8594ec2f69e4d1b922..2cc968c32f2b3ef9e1f02e6bc55d006aac795a77 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -188,6 +188,11 @@ $solarized-dark-il: #2aa198; background-color: $solarized-dark-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $solarized-dark-kd; + } + /* Solarized Dark For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 6463fe96c1b8ddb0b8a99aacb17b12aaec1b4c6f..b61b85a2cd13b60d3254c34e30ca5e96097b9306 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -196,6 +196,11 @@ $solarized-light-il: #2aa198; background-color: $solarized-light-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $solarized-light-kd; + } + /* Solarized Light For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index ab2018bfbca795df7b7182b51e453bdc3ef3cb15..1daa10aef24912a98fc23666a4ca39f4bb921688 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -203,6 +203,11 @@ $white-gc-bg: #eaf2f5; background-color: $white-highlight !important; } + // Links to URLs, emails, or dependencies + .line a { + color: $white-nb; + } + .hll { background-color: $white-hll-bg; } .c { color: $white-c; font-style: italic; } .err { color: $white-err; background-color: $white-err-bg; } 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+)(?<!,|\.)} + 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 + %{<a href="#{match}" #{tag_options(link_options)}>#{match}</a>#{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/dependency_linker.rb b/lib/gitlab/dependency_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..57fc3c6e0e5df9f54b9e3847fb664e5d06a8253e --- /dev/null +++ b/lib/gitlab/dependency_linker.rb @@ -0,0 +1,29 @@ +module Gitlab + module DependencyLinker + LINKERS = [ + GemfileLinker, + GemspecLinker, + PackageJsonLinker, + ComposerJsonLinker, + PodfileLinker, + PodspecLinker, + PodspecJsonLinker, + CartfileLinker, + GodepsJsonLinker, + RequirementsTxtLinker + ] + + 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 + + private + + def self.linker(blob_name) + LINKERS.find { |linker| linker.support?(blob_name) } + end + end +end diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..d5449eafc40a4cae0f7ff8f58bf42b151d372220 --- /dev/null +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -0,0 +1,144 @@ +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 license_url(name) + "http://spdx.org/licenses/#{name}.html" if name =~ /[A-Za-z0-9.-]+/ + end + + def package_link(name, url = package_url(name)) + return name unless url + + %{<a href="#{ERB::Util.html_escape_once(url)}">#{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) } + link_regex(/#{Regexp.union(method_names)}\s*[(=]?\s*['"](?<name>#{value})['"]/, &url_proc) + end + + # Links package names in a JSON key or values. + # + # Example: + # link_json("name") + # # Will link `package` in `"name": "package"` + # + # link_json("name", "specific_package") + # # Will link `specific_package` in `"name": "specific_package"` + # + # link_json("name", /[^\/]+\/[^\/]+/) + # # Will link `user/repo` in `"name": "user/repo"`, but not `"name": "package"` + # + # link_json("specific_package", "1.0.1", package: :key) + # # Will link `specific_package` in `"specific_package": "1.0.1"` + def link_json(key, value = nil, package: :value, &url_proc) + key = + case key + when String + Regexp.escape(key) + when nil + '[^"]+' + else + key + end + + value = + case value + when String + Regexp.escape(value) + when nil + '[^"]+' + else + value + end + + if package == :value + value = "(?<name>#{value})" + else + key = "(?<name>#{key})" + end + + link_regex(/"#{key}":\s*"#{value}"/, &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) + + marked_line = marker.mark(regex, group: :name) do |text, left:, right:| + url = block_given? ? yield(text) : package_url(text) + package_link(text, url) + end + + marked_line + end + end + + def plain_lines + @plain_lines ||= plain_text.lines + end + + def highlighted_lines + @highlighted_lines ||= highlighted_text.lines + end + end + end +end diff --git a/lib/gitlab/dependency_linker/cartfile_linker.rb b/lib/gitlab/dependency_linker/cartfile_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..9f7d10b59740f7e3e629915d2da13a29cb00218a --- /dev/null +++ b/lib/gitlab/dependency_linker/cartfile_linker.rb @@ -0,0 +1,19 @@ +module Gitlab + module DependencyLinker + class CartfileLinker < BaseLinker + def self.support?(blob_name) + blob_name.start_with?('Cartfile') + end + + private + + def link_dependencies + link_method_call("github", /[^\/"]+\/[^\/"]+/) + end + + def package_url(name) + "https://github.com/#{name}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..4c8a80a29cab89b15c7432819bf8395176ca4aee --- /dev/null +++ b/lib/gitlab/dependency_linker/composer_json_linker.rb @@ -0,0 +1,23 @@ +module Gitlab + module DependencyLinker + class ComposerJsonLinker < PackageJsonLinker + def self.support?(blob_name) + blob_name == 'composer.json' + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("require") + link_dependencies_at_key("require-dev") + end + + def package_url(name) + "https://packagist.org/packages/#{name}" if name =~ /\A[^\/]+\/[^\/]+\z/ + end + end + end +end diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..1e0d08f22914ee1256f67b1ea2b0a4b9d484792c --- /dev/null +++ b/lib/gitlab/dependency_linker/gemfile_linker.rb @@ -0,0 +1,25 @@ +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 + end + + def package_url(name) + "https://rubygems.org/gems/#{name}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..cf8acc5b004f715eb920d6a6ff7469b74593ef60 --- /dev/null +++ b/lib/gitlab/dependency_linker/gemspec_linker.rb @@ -0,0 +1,16 @@ +module Gitlab + module DependencyLinker + class GemspecLinker < GemfileLinker + def self.support?(blob_name) + blob_name.end_with?('.gemspec') + end + + private + + def link_dependencies + link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) + link_method_call("license", &method(:license_url)) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..227f7439fa0c36ed5c4d98478f1287a25dacb5f7 --- /dev/null +++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb @@ -0,0 +1,23 @@ +module Gitlab + module DependencyLinker + class GodepsJsonLinker < BaseLinker + def self.support?(blob_name) + blob_name == 'Godeps.json' + end + + private + + def link_dependencies + link_json("ImportPath") + end + + def package_url(name) + if name =~ /\A(?<repo>git(lab|hub)\.com\/[^\/]+\/[^\/]+)\/(?<path>.+)\z/ + "http://#{$~[:repo]}/tree/master/#{$~[:path]}" + else + "http://#{name}" + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..4225cfe4126dd35c8b47a6244e70f6f870fe3e9b --- /dev/null +++ b/lib/gitlab/dependency_linker/json_linker.rb @@ -0,0 +1,17 @@ +module Gitlab + module DependencyLinker + class JsonLinker < BaseLinker + def link + return highlighted_text unless json + + super + end + + private + + def json + @json ||= JSON.parse(plain_text) rescue nil + end + end + end +end diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c5f3b97d88e65c85227a8dc0b05fb2fb4f49495 --- /dev/null +++ b/lib/gitlab/dependency_linker/package_json_linker.rb @@ -0,0 +1,35 @@ +module Gitlab + module DependencyLinker + class PackageJsonLinker < JsonLinker + def self.support?(blob_name) + blob_name == 'package.json' + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("dependencies") + link_dependencies_at_key("devDependencies") + end + + def link_dependencies_at_key(key) + dependencies = json[key] + return unless dependencies + + dependencies.each do |name, version| + link_json(name, version, package: :key) + link_json(name, /[^\/"]+\/[^\/"]+/) do |name| + "https://github.com/#{name}" + end + end + end + + def package_url(name) + "https://npmjs.com/package/#{name}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..6716d5bdfd4f05280f93a18d8dc2369b9e85b48d --- /dev/null +++ b/lib/gitlab/dependency_linker/podfile_linker.rb @@ -0,0 +1,20 @@ +module Gitlab + module DependencyLinker + class PodfileLinker < BaseLinker + def self.support?(blob_name) + blob_name == 'Podfile' + end + + private + + def link_dependencies + link_method_call("pod") + end + + def package_url(name) + package = name.split("/", 2).first + "https://cocoapods.org/pods/#{package}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podspec_json_linker.rb b/lib/gitlab/dependency_linker/podspec_json_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..825c548c1712ccad11a4ecdb4a1bef537d0df65c --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_json_linker.rb @@ -0,0 +1,39 @@ +module Gitlab + module DependencyLinker + class PodspecJsonLinker < JsonLinker + def self.support?(blob_name) + blob_name.end_with?('.podspec.json') + end + + private + + def link_dependencies + link_json("name", json["name"]) + link_json("license", &method(:license_url)) + + link_dependencies_at_key("dependencies") + + subspecs = json["subspecs"] + if subspecs + subspecs.each do |subspec| + link_dependencies_at_key("dependencies", subspec) + end + end + end + + def link_dependencies_at_key(key, root = json) + dependencies = root[key] + return unless dependencies + + dependencies.each do |name, _| + link_regex(/"(?<name>#{Regexp.escape(name)})":\s*\[/) + end + end + + def package_url(name) + package = name.split("/", 2).first + "https://cocoapods.org/pods/#{package}" + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd4b17e39bbb9cb8b18b8d9e0d4a0382a3b5cfd0 --- /dev/null +++ b/lib/gitlab/dependency_linker/podspec_linker.rb @@ -0,0 +1,33 @@ +module Gitlab + module DependencyLinker + class PodspecLinker < PodfileLinker + def self.support?(blob_name) + blob_name.end_with?('.podspec') + end + + private + + def link_dependencies + link_method_call(%w[name dependency]) + + license_regex = %r{ + license + \s* + = + \s* + (?: + # spec.license = 'MIT' + ['"](?<name>[^'"]+)['"] + | + # spec.license = { :type => 'MIT' } + \{\s*:type\s*=>\s*['"](?<name>[^'"]+)['"] + | + # spec.license = { type: 'MIT' } + \{\s*type:\s*['"](?<name>[^'"]+)['"] + ) + }x + link_regex(license_regex, &method(:license_url)) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb new file mode 100644 index 0000000000000000000000000000000000000000..92fcb8177ea6fc7a2c4d6333b12629962fa37c0a --- /dev/null +++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb @@ -0,0 +1,19 @@ +module Gitlab + module DependencyLinker + class RequirementsTxtLinker < BaseLinker + def self.support?(blob_name) + blob_name.end_with?('requirements.txt') + end + + private + + def link_dependencies + link_regex(/(?<name>^(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) + end + + def package_url(name) + "https://pypi.python.org/pypi/#{name}" + end + end + end +end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 736933b1c4b0a6cdbc33cd3a04005f0d0dec9087..96f5ca31e5c019223c3862b62902e8b832063c8a 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -1,38 +1,18 @@ module Gitlab module Diff - class InlineDiffMarker + class InlineDiffMarker < Gitlab::StringRangeMarker MARKDOWN_SYMBOLS = { addition: "+", deletion: "-" }.freeze - attr_accessor :raw_line, :rich_line - - def initialize(raw_line, rich_line = raw_line) - @raw_line = raw_line - @rich_line = ERB::Util.html_escape(rich_line) - end - def mark(line_inline_diffs, mode: nil, markdown: false) - return rich_line unless line_inline_diffs - - marker_ranges = [] - line_inline_diffs.each do |inline_diff_range| - # Map the inline-diff range based on the raw line to character positions in the rich line - inline_diff_positions = position_mapping[inline_diff_range].flatten - # Turn the array of character positions into ranges - marker_ranges.concat(collapse_ranges(inline_diff_positions)) - end - - offset = 0 - - # Mark each range - marker_ranges.each_with_index do |range, index| + super(line_inline_diffs) do |text, left:, right:| before_content = if markdown "{#{MARKDOWN_SYMBOLS[mode]}" else - "<span class='#{html_class_names(marker_ranges, mode, index)}'>" + "<span class='#{html_class_names(left, right, mode)}'>" end after_content = if markdown @@ -40,98 +20,20 @@ module Gitlab else "</span>" end - offset = insert_around_range(rich_line, range, before_content, after_content, offset) - end - rich_line.html_safe + "#{before_content}#{text}#{after_content}" + end end private - def html_class_names(marker_ranges, mode, index) + def html_class_names(left, right, mode) class_names = ["idiff"] - class_names << "left" if index == 0 - class_names << "right" if index == marker_ranges.length - 1 + class_names << "left" if left + class_names << "right" if right class_names << mode if mode class_names.join(" ") end - - # Mapping of character positions in the raw line, to the rich (highlighted) line - def position_mapping - @position_mapping ||= begin - mapping = [] - rich_pos = 0 - (0..raw_line.length).each do |raw_pos| - rich_char = rich_line[rich_pos] - - # The raw and rich lines are the same except for HTML tags, - # so skip over any `<...>` segment - while rich_char == '<' - until rich_char == '>' - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - rich_pos += 1 - rich_char = rich_line[rich_pos] - end - - # multi-char HTML entities in the rich line correspond to a single character in the raw line - if rich_char == '&' - multichar_mapping = [rich_pos] - until rich_char == ';' - rich_pos += 1 - multichar_mapping << rich_pos - rich_char = rich_line[rich_pos] - end - - mapping[raw_pos] = multichar_mapping - else - mapping[raw_pos] = rich_pos - end - - rich_pos += 1 - end - - mapping - end - end - - # Takes an array of integers, and returns an array of ranges covering the same integers - def collapse_ranges(positions) - return [] if positions.empty? - ranges = [] - - start = prev = positions[0] - range = start..prev - positions[1..-1].each do |pos| - if pos == prev + 1 - range = start..pos - prev = pos - else - ranges << range - start = prev = pos - range = start..prev - end - end - ranges << range - - ranges - end - - # Inserts tags around the characters identified by the given range - def insert_around_range(text, range, before, after, offset = 0) - # Just to be sure - return offset if offset + range.end + 1 > text.length - - text.insert(offset + range.begin, before) - offset += before.length - - text.insert(offset + range.end + 1, after) - offset += after.length - - offset - end end end end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index d787d5db4a0bd80013097d1dbe08d5d9eeedebfc..0d5c7a925409b2cf4c924b3dbb815008302031bd 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,8 +1,8 @@ module Gitlab class Highlight - def self.highlight(blob_name, blob_content, repository: nil, plain: false) + def self.highlight(blob_name, blob_content, repository: nil, plain: false, link_deps: true, autolink: true) new(blob_name, blob_content, repository: repository). - highlight(blob_content, continue: false, plain: plain) + highlight(blob_content, continue: false, plain: plain, link_deps: link_deps, autolink: autolink) end def self.highlight_lines(repository, ref, file_name) @@ -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 @@ -20,17 +22,11 @@ module Gitlab @blob_content = blob_content 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 + def highlight(text, continue: true, plain: false, link_deps: true, autolink: true) + highlighted_text = highlight_text(text, continue: continue, plain: plain) + highlighted_text = link_dependencies(text, highlighted_text) if blob_name && link_deps + highlighted_text = autolink_strings(text, highlighted_text) if autolink + highlighted_text end def lexer @@ -50,5 +46,65 @@ 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 + + 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(/(?<start><a[^>]+>)(?<content>[^<]+)(?<end><\/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/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb new file mode 100644 index 0000000000000000000000000000000000000000..94fba0a221a181346c96048914b4f578dab1bf48 --- /dev/null +++ b/lib/gitlab/string_range_marker.rb @@ -0,0 +1,102 @@ +module Gitlab + class StringRangeMarker + attr_accessor :raw_line, :rich_line + + def initialize(raw_line, rich_line = raw_line) + @raw_line = raw_line + @rich_line = ERB::Util.html_escape(rich_line) + end + + def mark(marker_ranges) + return rich_line unless marker_ranges + + rich_marker_ranges = [] + marker_ranges.each do |range| + # Map the inline-diff range based on the raw line to character positions in the rich line + rich_positions = position_mapping[range].flatten + # Turn the array of character positions into ranges + rich_marker_ranges.concat(collapse_ranges(rich_positions)) + end + + offset = 0 + # Mark each range + rich_marker_ranges.each_with_index do |range, i| + offset_range = (range.begin + offset)..(range.end + offset) + original_text = rich_line[offset_range] + + text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1) + + rich_line[offset_range] = text + + offset += text.length - original_text.length + end + + rich_line.html_safe + end + + private + + # Mapping of character positions in the raw line, to the rich (highlighted) line + def position_mapping + @position_mapping ||= begin + mapping = [] + rich_pos = 0 + (0..raw_line.length).each do |raw_pos| + rich_char = rich_line[rich_pos] + + # The raw and rich lines are the same except for HTML tags, + # so skip over any `<...>` segment + while rich_char == '<' + until rich_char == '>' + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + rich_pos += 1 + rich_char = rich_line[rich_pos] + end + + # multi-char HTML entities in the rich line correspond to a single character in the raw line + if rich_char == '&' + multichar_mapping = [rich_pos] + until rich_char == ';' + rich_pos += 1 + multichar_mapping << rich_pos + rich_char = rich_line[rich_pos] + end + + mapping[raw_pos] = multichar_mapping + else + mapping[raw_pos] = rich_pos + end + + rich_pos += 1 + end + + mapping + end + end + + # Takes an array of integers, and returns an array of ranges covering the same integers + def collapse_ranges(positions) + return [] if positions.empty? + ranges = [] + + start = prev = positions[0] + range = start..prev + positions[1..-1].each do |pos| + if pos == prev + 1 + range = start..pos + prev = pos + else + ranges << range + start = prev = pos + range = start..prev + end + end + ranges << range + + ranges + end + end +end diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb new file mode 100644 index 0000000000000000000000000000000000000000..7ebf1c0428ca2786435c73e98b0489a56b7f71e3 --- /dev/null +++ b/lib/gitlab/string_regex_marker.rb @@ -0,0 +1,13 @@ +module Gitlab + class StringRegexMarker < StringRangeMarker + def mark(regex, group: 0, &block) + regex_match = raw_line.match(regex) + return rich_line unless regex_match + + begin_index, end_index = regex_match.offset(group) + name_range = begin_index..(end_index - 1) + + super([name_range], &block) + 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/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d562f81fab83d8502e36e566ad88ba05d5ade441 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::CartfileLinker, lib: true do + describe '.support?' do + it 'supports Cartfile' do + expect(described_class.support?('Cartfile')).to be_truthy + end + + it 'supports Cartfile.private' do + expect(described_class.support?('Cartfile.private')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('test.Cartfile')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Cartfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # Require version 2.3.1 or later + github "ReactiveCocoa/ReactiveCocoa" >= 2.3.1 + + # Require version 1.x + github "Mantle/Mantle" ~> 1.0 # (1.0 or later, but less than 2.0) + + # Require exactly version 0.4.1 + github "jspahrsummers/libextobjc" == 0.4.1 + + # Use the latest version + github "jspahrsummers/xcconfigs" + + # Use the branch + github "jspahrsummers/xcconfigs" "branch" + + # Use a project from GitHub Enterprise + github "https://enterprise.local/ghe/desktop/git-error-translations" + + # Use a project from any arbitrary server, on the "development" branch + git "https://enterprise.local/desktop/git-error-translations2.git" "development" + + # Use a local project + git "file:///directory/to/project" "branch" + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("ReactiveCocoa/ReactiveCocoa", "https://github.com/ReactiveCocoa/ReactiveCocoa")) + expect(subject).to include(link("Mantle/Mantle", "https://github.com/Mantle/Mantle")) + expect(subject).to include(link("jspahrsummers/libextobjc", "https://github.com/jspahrsummers/libextobjc")) + expect(subject).to include(link("jspahrsummers/xcconfigs", "https://github.com/jspahrsummers/xcconfigs")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..92bdf0fb37092d64f6f06315cf22bad7b84d5dfe --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::ComposerJsonLinker, lib: true do + describe '.support?' do + it 'supports composer.json' do + expect(described_class.support?('composer.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('composer.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "composer.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "laravel/laravel", + "description": "The Laravel Framework.", + "keywords": ["framework", "laravel"], + "license": "MIT", + "type": "project", + "require": { + "php": ">=5.5.9", + "laravel/framework": "5.2.*" + }, + "require-dev": { + "fzaninotto/faker": "~1.4", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~4.0", + "symfony/css-selector": "2.8.*|3.0.*", + "symfony/dom-crawler": "2.8.*|3.0.*" + } + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the module name" do + expect(subject).to include(link("laravel/laravel", "https://packagist.org/packages/laravel/laravel")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("laravel/framework", "https://packagist.org/packages/laravel/framework")) + expect(subject).to include(link("fzaninotto/faker", "https://packagist.org/packages/fzaninotto/faker")) + expect(subject).to include(link("mockery/mockery", "https://packagist.org/packages/mockery/mockery")) + expect(subject).to include(link("phpunit/phpunit", "https://packagist.org/packages/phpunit/phpunit")) + expect(subject).to include(link("symfony/css-selector", "https://packagist.org/packages/symfony/css-selector")) + expect(subject).to include(link("symfony/dom-crawler", "https://packagist.org/packages/symfony/dom-crawler")) + end + + it "doesn't link core dependencies" do + expect(subject).not_to include(link("php", "https://packagist.org/packages/php")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8f9d995fef603ea1e7c9e144b4b9a476282e051c --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -0,0 +1,59 @@ +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, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + 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 diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a60fda533d3e2126ece858762381516bf8b75918 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GemspecLinker, lib: true do + describe '.support?' do + it 'supports *.gemspec' do + expect(described_class.support?('gitlab_git.gemspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.gemspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "gitlab_git.gemspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Gem::Specification.new do |s| + s.name = 'gitlab_git' + s.version = `cat VERSION` + s.date = Time.now.strftime("%Y-%m-%d") + s.summary = "Gitlab::Git library" + s.description = "GitLab wrapper around git objects" + s.authors = ["Dmitriy Zaporozhets"] + s.email = 'dmitriy.zaporozhets@gmail.com' + s.license = 'MIT' + s.files = `git ls-files lib/`.split("\n") << 'VERSION' + s.homepage = + 'https://gitlab.com/gitlab-org/gitlab_git' + + s.add_dependency("github-linguist", "~> 4.7.0") + s.add_dependency("activesupport", "~> 4.0") + s.add_dependency("rugged", "~> 0.24.0") + s.add_runtime_dependency("charlock_holmes", "~> 0.7.3") + s.add_development_dependency("listen", "~> 3.0.6") + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("gitlab_git", "https://rubygems.org/gems/gitlab_git")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("github-linguist", "https://rubygems.org/gems/github-linguist")) + expect(subject).to include(link("activesupport", "https://rubygems.org/gems/activesupport")) + expect(subject).to include(link("rugged", "https://rubygems.org/gems/rugged")) + expect(subject).to include(link("charlock_holmes", "https://rubygems.org/gems/charlock_holmes")) + expect(subject).to include(link("listen", "https://rubygems.org/gems/listen")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a9ae721f05e3710e5585869405cd367361d3fa6a --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::GodepsJsonLinker, lib: true do + describe '.support?' do + it 'supports Godeps.json' do + expect(described_class.support?('Godeps.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Godeps.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Godeps.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "ImportPath": "gitlab.com/gitlab-org/gitlab-pages", + "GoVersion": "go1.5", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/kardianos/osext", + "Rev": "efacde03154693404c65e7aa7d461ac9014acd0c" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "github.com/stretchr/testify/require", + "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b" + }, + { + "ImportPath": "golang.org/x/crypto/ssh/terminal", + "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d" + }, + { + "ImportPath": "golang.org/x/net/http2", + "Rev": "b4e17d61b15679caf2335da776c614169a1b4643" + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the package name" do + expect(subject).to include(link("gitlab.com/gitlab-org/gitlab-pages", "http://gitlab.com/gitlab-org/gitlab-pages")) + end + + it "links dependencies" do + expect(subject).to include(link("github.com/kardianos/osext", "http://github.com/kardianos/osext")) + expect(subject).to include(link("github.com/stretchr/testify/assert", "http://github.com/stretchr/testify/tree/master/assert")) + expect(subject).to include(link("github.com/stretchr/testify/require", "http://github.com/stretchr/testify/tree/master/require")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..49d917ff80ba60a375409c0648e19335f2c01801 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do + describe '.support?' do + it 'supports package.json' do + expect(described_class.support?('package.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('package.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "package.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "module-name", + "version": "10.3.1", + "dependencies": { + "primus": "*", + "async": "~0.8.0", + "express": "4.2.x", + "winston": "git://github.com/flatiron/winston#master", + "bigpipe": "bigpipe/pagelet", + "plates": "https://github.com/flatiron/plates/tarball/master" + }, + "devDependencies": { + "vows": "^0.7.0", + "assume": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", + "pre-commit": "*" + }, + "license": "MIT" + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the module name" do + expect(subject).to include(link("module-name", "https://npmjs.com/package/module-name")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("primus", "https://npmjs.com/package/primus")) + expect(subject).to include(link("async", "https://npmjs.com/package/async")) + expect(subject).to include(link("express", "https://npmjs.com/package/express")) + expect(subject).to include(link("winston", "https://npmjs.com/package/winston")) + expect(subject).to include(link("bigpipe", "https://npmjs.com/package/bigpipe")) + expect(subject).to include(link("plates", "https://npmjs.com/package/plates")) + expect(subject).to include(link("vows", "https://npmjs.com/package/vows")) + expect(subject).to include(link("assume", "https://npmjs.com/package/assume")) + expect(subject).to include(link("pre-commit", "https://npmjs.com/package/pre-commit")) + end + + it "links GitHub repos" do + expect(subject).to include(link("bigpipe/pagelet", "https://github.com/bigpipe/pagelet")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..049306f9bd91e8a228541af3f2deef5da8557c2e --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodfileLinker, lib: true do + describe '.support?' do + it 'supports Podfile' do + expect(described_class.support?('Podfile')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('Podfile.lock')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Podfile" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + target 'MyApp' + pod 'AFNetworking', '~> 1.0' + pod "RestKit/CoreData" + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("RestKit/CoreData", "https://cocoapods.org/pods/RestKit")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a508def683f0e18a130da7def59626333c1a85ae --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecJsonLinker, lib: true do + describe '.support?' do + it 'supports *.podspec.json' do + expect(described_class.support?('Reachability.podspec.json')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.json.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "AFNetworking.podspec.json" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "name": "AFNetworking", + "version": "2.0.0", + "license": "MIT", + "summary": "A delightful iOS and OS X networking framework.", + "homepage": "https://github.com/AFNetworking/AFNetworking", + "authors": { + "Mattt Thompson": "m@mattt.me" + }, + "source": { + "git": "https://github.com/AFNetworking/AFNetworking.git", + "tag": "2.0.0", + "submodules": true + }, + "requires_arc": true, + "platforms": { + "ios": "6.0", + "osx": "10.8" + }, + "public_header_files": "AFNetworking/*.h", + "subspecs": [ + { + "name": "NSURLConnection", + "dependencies": { + "AFNetworking/Serialization": [ + + ], + "AFNetworking/Reachability": [ + + ], + "AFNetworking/Security": [ + + ] + }, + "source_files": [ + "AFNetworking/AFURLConnectionOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperation.{h,m}", + "AFNetworking/AFHTTPRequestOperationManager.{h,m}" + ] + } + ] + } + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + end + + it "links the license" do + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking/Serialization", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("AFNetworking/Reachability", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("AFNetworking/Security", "https://cocoapods.org/pods/AFNetworking")) + end + + it "doesn't link subspec names" do + expect(subject).not_to include(link("NSURLConnection", "https://cocoapods.org/pods/NSURLConnection")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..597a6393c18c306a2300cdf30fbe345cce618e54 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::PodspecLinker, lib: true do + describe '.support?' do + it 'supports *.podspec' do + expect(described_class.support?('Reachability.podspec')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('.podspec.example')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Reachability.podspec" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + Pod::Spec.new do |spec| + spec.name = 'Reachability' + spec.version = '3.1.0' + spec.license = { :type => 'BSD' } + spec.license = "MIT" + spec.license = { type: 'Apache-1.0' } + spec.homepage = 'https://github.com/tonymillion/Reachability' + spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' } + spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.' + spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' } + spec.source_files = 'Reachability.{h,m}' + spec.framework = 'SystemConfiguration' + + spec.dependency 'AFNetworking', '~> 1.0' + spec.dependency 'RestKit/CoreData', '~> 0.20.0' + spec.ios.dependency 'MBProgressHUD', '~> 0.5' + end + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links the gem name" do + expect(subject).to include(link("Reachability", "https://cocoapods.org/pods/Reachability")) + end + + it "links the license" do + expect(subject).to include(link("BSD", "http://spdx.org/licenses/BSD.html")) + expect(subject).to include(link("MIT", "http://spdx.org/licenses/MIT.html")) + expect(subject).to include(link("Apache-1.0", "http://spdx.org/licenses/Apache-1.0.html")) + end + + it "links dependencies" do + expect(subject).to include(link("AFNetworking", "https://cocoapods.org/pods/AFNetworking")) + expect(subject).to include(link("RestKit/CoreData", "https://cocoapods.org/pods/RestKit")) + expect(subject).to include(link("MBProgressHUD", "https://cocoapods.org/pods/MBProgressHUD")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a334bb38db149f878f6e3143e86be171603d0e9f --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do + describe '.support?' do + it 'supports requirements.txt' do + expect(described_class.support?('requirements.txt')).to be_truthy + end + + it 'supports doc-requirements.txt' do + expect(described_class.support?('doc-requirements.txt')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('requirements')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "requirements.txt" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # + ####### example-requirements.txt ####### + # + ###### Requirements without Version Specifiers ###### + nose + nose-cov + beautifulsoup4 + # + ###### Requirements with Version Specifiers ###### + # See https://www.python.org/dev/peps/pep-0440/#version-specifiers + docopt == 0.6.1 # Version Matching. Must be version 0.6.1 + keyring >= 4.1.1 # Minimum version 4.1.1 + coverage != 3.5 # Version Exclusion. Anything except version 3.5 + Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* + # + ###### Refer to other requirements files ###### + -r other-requirements.txt + # + # + ###### A particular file ###### + ./downloads/numpy-1.9.2-cp34-none-win32.whl + http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl + # + ###### Additional Requirements without Version Specifiers ###### + # Same as 1st section, just here to show that you can put things in any order. + rejected + green + # + + Jinja2>=2.3 + Pygments>=1.2 + Sphinx>=1.3 + docutils>=0.7 + markupsafe + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content, nowrap: false) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer" target="_blank">#{name}</a>} + end + + it "links dependencies" do + expect(subject).to include(link("nose", "https://pypi.python.org/pypi/nose")) + expect(subject).to include(link("nose-cov", "https://pypi.python.org/pypi/nose-cov")) + expect(subject).to include(link("beautifulsoup4", "https://pypi.python.org/pypi/beautifulsoup4")) + expect(subject).to include(link("docopt", "https://pypi.python.org/pypi/docopt")) + expect(subject).to include(link("keyring", "https://pypi.python.org/pypi/keyring")) + expect(subject).to include(link("coverage", "https://pypi.python.org/pypi/coverage")) + expect(subject).to include(link("Mopidy-Dirble", "https://pypi.python.org/pypi/Mopidy-Dirble")) + expect(subject).to include(link("rejected", "https://pypi.python.org/pypi/rejected")) + expect(subject).to include(link("green", "https://pypi.python.org/pypi/green")) + expect(subject).to include(link("Jinja2", "https://pypi.python.org/pypi/Jinja2")) + expect(subject).to include(link("Pygments", "https://pypi.python.org/pypi/Pygments")) + expect(subject).to include(link("Sphinx", "https://pypi.python.org/pypi/Sphinx")) + expect(subject).to include(link("docutils", "https://pypi.python.org/pypi/docutils")) + expect(subject).to include(link("markupsafe", "https://pypi.python.org/pypi/markupsafe")) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3d1cfbcfbf710082817d5f28750b420ec6aad08e --- /dev/null +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -0,0 +1,85 @@ +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 + + it 'links using GemspecLinker' do + blob_name = 'gitlab_git.gemspec' + + expect(described_class::GemspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PackageJsonLinker' do + blob_name = 'package.json' + + expect(described_class::PackageJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using ComposerJsonLinker' do + blob_name = 'composer.json' + + expect(described_class::ComposerJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodfileLinker' do + blob_name = 'Podfile' + + expect(described_class::PodfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodspecLinker' do + blob_name = 'Reachability.podspec' + + expect(described_class::PodspecLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using PodspecJsonLinker' do + blob_name = 'AFNetworking.podspec.json' + + expect(described_class::PodspecJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using CartfileLinker' do + blob_name = 'Cartfile' + + expect(described_class::CartfileLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using GodepsJsonLinker' do + blob_name = 'Godeps.json' + + expect(described_class::GodepsJsonLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + + it 'links using RequirementsTxtLinker' do + blob_name = 'requirements.txt' + + expect(described_class::RequirementsTxtLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end + end +end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 198ff977f2418f7729f35846ed5e4bea5dda9503..418521ae65061022cb1f821fa09fc958c271d4fa 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -1,26 +1,26 @@ require 'spec_helper' describe Gitlab::Diff::InlineDiffMarker, lib: true do - describe '#inline_diffs' do + describe '#mark' do context "when the rich text is html safe" do - let(:raw) { "abc 'def'" } - let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">'def'</span>}.html_safe } + let(:raw) { "abc <def>" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + let(:subject) { described_class.new(raw, rich).mark(inline_diffs) } it 'marks the inline diffs' do - expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>'d</span>ef'</span>}) + expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'><d</span>ef></span>}) expect(subject).to be_html_safe end end - context "when the text text is not html safe" do - let(:raw) { "abc 'def'" } + context "when the text is not html safe" do + let(:raw) { "abc <def>" } let(:inline_diffs) { [2..5] } - let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + let(:subject) { described_class.new(raw).mark(inline_diffs) } it 'marks the inline diffs' do - expect(subject).to eq(%{ab<span class='idiff left right'>c 'd</span>ef'}) + expect(subject).to eq(%{ab<span class='idiff left right'>c <d</span>ef>}) expect(subject).to be_html_safe end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index e49799ad10502bcd8d926db39d7c6f7cf63bdd88..4f6ae157d6e50d6e4bffa866ace0ba229c17df3d 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 @@ -57,4 +57,76 @@ describe Gitlab::Highlight, lib: true do end end end + + describe '#highlight' do + subject { described_class.highlight(file_name, file_content) } + + 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(%{<a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{<a href="mailto:hello@example.com">hello@example.com</a>}) + 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> + # <url>http://www.google.com</url> + CONTENT + end + + context "in a comment" do + it "links URLs" do + expect(subject).to include(%{URL in comment: <a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{Email in comment: <a href="mailto:hello@example.com">hello@example.com</a>}) + end + end + + context "in a string" do + it "links URLs" do + expect(subject).to include(%{URL in string: <a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>}) + end + + it "links emails" do + expect(subject).to include(%{Email in string: <a href="mailto:hello@example.com">hello@example.com</a>}) + end + end + + context 'in HTML/XML tags' do + it "links URLs" do + expect(subject).to include(%{<<a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a>>}) + expect(subject).to include(%{<url><a href="http://www.google.com" rel="nofollow noreferrer" target="_blank">http://www.google.com</a></url>}) + end + end + end + + 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 diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c77772b3f6e80b54724f94a33a76db467310bb1 --- /dev/null +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::StringRangeMarker, lib: true do + describe '#mark' do + context "when the rich text is html safe" do + let(:raw) { "abc <def>" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def"><def></span>}.html_safe } + let(:inline_diffs) { [2..5] } + subject do + described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT<dRIGHTef></span>}) + expect(subject).to be_html_safe + end + end + + context "when the rich text is not html safe" do + let(:raw) { "abc <def>" } + let(:inline_diffs) { [2..5] } + subject do + described_class.new(raw).mark(inline_diffs) do |text, left:, right:| + "LEFT#{text}RIGHT" + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{abLEFTc <dRIGHTef>}) + expect(subject).to be_html_safe + end + end + end +end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2f5cf6c6e3b61645320ac9169c94483aab6545f3 --- /dev/null +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::StringRegexMarker, lib: true do + describe '#mark' do + let(:raw) { %{"name": "AFNetworking"} } + let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } + subject do + described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| + %{<a href="#">#{text}</a>} + end + end + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) + expect(subject).to be_html_safe + end + end +end