Extract handling of relative file links to RelativeLinkFilter
This commit is contained in:
parent
6fdc51f81d
commit
f7adac87fe
4 changed files with 126 additions and 145 deletions
|
@ -72,146 +72,6 @@ module GitlabMarkdownHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO (rspeicher): This should be its own filter
|
|
||||||
def create_relative_links(text)
|
|
||||||
paths = extract_paths(text)
|
|
||||||
|
|
||||||
paths.uniq.each do |file_path|
|
|
||||||
# If project does not have repository
|
|
||||||
# its nothing to rebuild
|
|
||||||
#
|
|
||||||
# TODO: pass project variable to markdown helper instead of using
|
|
||||||
# instance variable. Right now it generates invalid path for pages out
|
|
||||||
# of project scope. Example: search results where can be rendered markdown
|
|
||||||
# from different projects
|
|
||||||
if @repository && @repository.exists? && !@repository.empty?
|
|
||||||
new_path = rebuild_path(file_path)
|
|
||||||
# Finds quoted path so we don't replace other mentions of the string
|
|
||||||
# eg. "doc/api" will be replaced and "/home/doc/api/text" won't
|
|
||||||
text.gsub!("\"#{file_path}\"", "\"/#{new_path}\"")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
text
|
|
||||||
end
|
|
||||||
|
|
||||||
def extract_paths(text)
|
|
||||||
links = substitute_links(text)
|
|
||||||
image_links = substitute_image_links(text)
|
|
||||||
links + image_links
|
|
||||||
end
|
|
||||||
|
|
||||||
def substitute_links(text)
|
|
||||||
links = text.scan(/<a href=\"([^"]*)\">/)
|
|
||||||
relative_links = links.flatten.reject{ |link| link_to_ignore? link }
|
|
||||||
relative_links
|
|
||||||
end
|
|
||||||
|
|
||||||
def substitute_image_links(text)
|
|
||||||
links = text.scan(/<img src=\"([^"]*)\"/)
|
|
||||||
relative_links = links.flatten.reject{ |link| link_to_ignore? link }
|
|
||||||
relative_links
|
|
||||||
end
|
|
||||||
|
|
||||||
def link_to_ignore?(link)
|
|
||||||
if link =~ /\A\#\w+/
|
|
||||||
# ignore anchors like <a href="#my-header">
|
|
||||||
true
|
|
||||||
else
|
|
||||||
ignored_protocols.map{ |protocol| link.include?(protocol) }.any?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ignored_protocols
|
|
||||||
["http://","https://", "ftp://", "mailto:", "smb://"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def rebuild_path(file_path)
|
|
||||||
file_path = file_path.dup
|
|
||||||
file_path.gsub!(/(#.*)/, "")
|
|
||||||
id = $1 || ""
|
|
||||||
file_path = relative_file_path(file_path)
|
|
||||||
file_path = sanitize_slashes(file_path)
|
|
||||||
|
|
||||||
[
|
|
||||||
Gitlab.config.gitlab.relative_url_root,
|
|
||||||
@project.path_with_namespace,
|
|
||||||
path_with_ref(file_path),
|
|
||||||
file_path
|
|
||||||
].compact.join("/").gsub(/\A\/*|\/*\z/, '') + id
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize_slashes(path)
|
|
||||||
path[0] = "" if path.start_with?("/")
|
|
||||||
path.chop if path.end_with?("/")
|
|
||||||
path
|
|
||||||
end
|
|
||||||
|
|
||||||
def relative_file_path(path)
|
|
||||||
requested_path = @path
|
|
||||||
nested_path = build_nested_path(path, requested_path)
|
|
||||||
return nested_path if file_exists?(nested_path)
|
|
||||||
path
|
|
||||||
end
|
|
||||||
|
|
||||||
# Covering a special case, when the link is referencing file in the same directory eg:
|
|
||||||
# If we are at doc/api/README.md and the README.md contains relative links like [Users](users.md)
|
|
||||||
# this takes the request path(doc/api/README.md), and replaces the README.md with users.md so the path looks like doc/api/users.md
|
|
||||||
# If we are at doc/api and the README.md shown in below the tree view
|
|
||||||
# this takes the request path(doc/api) and adds users.md so the path looks like doc/api/users.md
|
|
||||||
def build_nested_path(path, request_path)
|
|
||||||
return request_path if path == ""
|
|
||||||
return path unless request_path
|
|
||||||
if local_path(request_path) == "tree"
|
|
||||||
base = request_path.split("/").push(path)
|
|
||||||
base.join("/")
|
|
||||||
else
|
|
||||||
base = request_path.split("/")
|
|
||||||
base.pop
|
|
||||||
base.push(path).join("/")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks if the path exists in the repo
|
|
||||||
# eg. checks if doc/README.md exists, if not then link to blob
|
|
||||||
def path_with_ref(path)
|
|
||||||
if file_exists?(path)
|
|
||||||
"#{local_path(path)}/#{correct_ref}"
|
|
||||||
else
|
|
||||||
"blob/#{correct_ref}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_exists?(path)
|
|
||||||
return false if path.nil?
|
|
||||||
@repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check if the path is pointing to a directory(tree) or a file(blob)
|
|
||||||
# eg. doc/api is directory and doc/README.md is file
|
|
||||||
def local_path(path)
|
|
||||||
return "tree" if @repository.tree(current_sha, path).entries.any?
|
|
||||||
return "raw" if @repository.blob_at(current_sha, path).image?
|
|
||||||
"blob"
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_sha
|
|
||||||
if @commit
|
|
||||||
@commit.id
|
|
||||||
elsif @repository && !@repository.empty?
|
|
||||||
if @ref
|
|
||||||
@repository.commit(@ref).try(:sha)
|
|
||||||
else
|
|
||||||
@repository.head_commit.sha
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# We will assume that if no ref exists we can point to master
|
|
||||||
def correct_ref
|
|
||||||
@ref ? @ref : "master"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
|
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
|
||||||
|
|
|
@ -15,6 +15,7 @@ module Gitlab
|
||||||
autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
|
autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
|
||||||
autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
|
autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
|
||||||
autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
|
autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
|
||||||
|
autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
|
||||||
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
|
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
|
||||||
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
|
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
|
||||||
autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
|
autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
|
||||||
|
@ -64,7 +65,12 @@ module Gitlab
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
only_path: options[:reference_only_path],
|
only_path: options[:reference_only_path],
|
||||||
project: project,
|
project: project,
|
||||||
reference_class: html_options[:class]
|
reference_class: html_options[:class],
|
||||||
|
|
||||||
|
# RelativeLinkFilter
|
||||||
|
ref: @ref,
|
||||||
|
requested_path: @path,
|
||||||
|
project_wiki: @project_wiki
|
||||||
}
|
}
|
||||||
|
|
||||||
result = pipeline.call(text, context)
|
result = pipeline.call(text, context)
|
||||||
|
@ -91,6 +97,8 @@ module Gitlab
|
||||||
[
|
[
|
||||||
Gitlab::Markdown::SanitizationFilter,
|
Gitlab::Markdown::SanitizationFilter,
|
||||||
|
|
||||||
|
Gitlab::Markdown::RelativeLinkFilter,
|
||||||
|
|
||||||
Gitlab::Markdown::EmojiFilter,
|
Gitlab::Markdown::EmojiFilter,
|
||||||
Gitlab::Markdown::TableOfContentsFilter,
|
Gitlab::Markdown::TableOfContentsFilter,
|
||||||
Gitlab::Markdown::AutolinkFilter,
|
Gitlab::Markdown::AutolinkFilter,
|
||||||
|
|
117
lib/gitlab/markdown/relative_link_filter.rb
Normal file
117
lib/gitlab/markdown/relative_link_filter.rb
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that "fixes" relative links to files in a repository.
|
||||||
|
#
|
||||||
|
# Context options:
|
||||||
|
# :commit
|
||||||
|
# :project
|
||||||
|
# :project_wiki
|
||||||
|
# :requested_path
|
||||||
|
# :ref
|
||||||
|
class RelativeLinkFilter < HTML::Pipeline::Filter
|
||||||
|
|
||||||
|
def call
|
||||||
|
if !project_wiki && repository.try(:exists?) && !repository.empty?
|
||||||
|
doc.search('a').each do |el|
|
||||||
|
process_link_attr el.attribute('href')
|
||||||
|
end
|
||||||
|
|
||||||
|
doc.search('img').each do |el|
|
||||||
|
process_link_attr el.attribute('src')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
doc
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def process_link_attr(html_attr)
|
||||||
|
return if html_attr.blank?
|
||||||
|
|
||||||
|
uri = URI(html_attr.value)
|
||||||
|
if uri.relative? && uri.path.present?
|
||||||
|
html_attr.value = rebuild_relative_uri(uri).to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_relative_uri(uri)
|
||||||
|
file_path = relative_file_path(uri.path)
|
||||||
|
|
||||||
|
uri.path = [
|
||||||
|
relative_url_root,
|
||||||
|
project.path_with_namespace,
|
||||||
|
path_type(file_path),
|
||||||
|
ref || 'master', # assume that if no ref exists we can point to master
|
||||||
|
file_path
|
||||||
|
].compact.join('/').squeeze('/').chomp('/')
|
||||||
|
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
|
def relative_file_path(path)
|
||||||
|
nested_path = build_nested_path(path, requested_path)
|
||||||
|
file_exists?(nested_path) ? nested_path : path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Covering a special case, when the link is referencing file in the same
|
||||||
|
# directory.
|
||||||
|
# If we are at doc/api/README.md and the README.md contains relative
|
||||||
|
# links like [Users](users.md), this takes the request
|
||||||
|
# path(doc/api/README.md) and replaces the README.md with users.md so the
|
||||||
|
# path looks like doc/api/users.md.
|
||||||
|
# If we are at doc/api and the README.md shown in below the tree view
|
||||||
|
# this takes the request path(doc/api) and adds users.md so the path
|
||||||
|
# looks like doc/api/users.md
|
||||||
|
def build_nested_path(path, request_path)
|
||||||
|
return request_path if path.empty?
|
||||||
|
return path unless request_path
|
||||||
|
|
||||||
|
parts = request_path.split('/')
|
||||||
|
parts.pop if path_type(request_path) != 'tree'
|
||||||
|
parts.push(path).join('/')
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_exists?(path)
|
||||||
|
return false if path.nil?
|
||||||
|
repository.blob_at(current_sha, path).present? ||
|
||||||
|
repository.tree(current_sha, path).entries.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if the path is pointing to a directory(tree) or a file(blob)
|
||||||
|
# eg. doc/api is directory and doc/README.md is file.
|
||||||
|
def path_type(path)
|
||||||
|
return 'tree' if repository.tree(current_sha, path).entries.any?
|
||||||
|
return 'raw' if repository.blob_at(current_sha, path).try(:image?)
|
||||||
|
'blob'
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_sha
|
||||||
|
if commit
|
||||||
|
commit.id
|
||||||
|
elsif ref
|
||||||
|
repository.commit(ref).try(:sha)
|
||||||
|
else
|
||||||
|
repository.head_commit.sha
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relative_url_root
|
||||||
|
Gitlab.config.gitlab.relative_url_root.presence || '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
[:commit, :project, :project_wiki, :requested_path, :ref].each do |name|
|
||||||
|
define_method(name) do
|
||||||
|
context[name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def repository
|
||||||
|
return if project.nil?
|
||||||
|
project.repository
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,10 +36,6 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
|
||||||
end
|
end
|
||||||
|
|
||||||
def postprocess(full_document)
|
def postprocess(full_document)
|
||||||
unless @template.instance_variable_get("@project_wiki") || @project.nil?
|
|
||||||
full_document = h.create_relative_links(full_document)
|
|
||||||
end
|
|
||||||
|
|
||||||
h.gfm_with_options(full_document, @options)
|
h.gfm_with_options(full_document, @options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue