BigW Consortium Gitlab

gitlab_markdown_helper.rb 7.16 KB
Newer Older
1
module GitlabMarkdownHelper
2
  include Gitlab::Markdown
3

4 5 6 7 8 9 10 11 12
  # Use this in places where you would normally use link_to(gfm(...), ...).
  #
  # It solves a problem occurring with nested links (i.e.
  # "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be
  # interpreted as intended. Browsers will parse something like
  # "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is
  # not linked any more). link_to_gfm corrects that. It wraps all parts to
  # explicitly produce the correct linking behavior (i.e.
  # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
13
  def link_to_gfm(body, url, html_options = {})
14
    return "" if body.blank?
15

16 17 18 19 20 21 22
    escaped_body = if body =~ /^\<img/
                     body
                   else
                     escape_once(body)
                   end

    gfm_body = gfm(escaped_body, html_options)
23 24 25 26 27 28 29

    gfm_body.gsub!(%r{<a.*?>.*?</a>}m) do |match|
      "</a>#{match}#{link_to("", url, html_options)[0..-5]}" # "</a>".length +1
    end

    link_to(gfm_body.html_safe, url, html_options)
  end
randx committed
30

31 32 33 34 35 36 37 38 39 40
  def markdown(text, options={})
    unless (@markdown and options == @options)
      @options = options
      gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, {
                            # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
                            filter_html: true,
                            with_toc_data: true,
                            hard_wrap: true,
                            safe_links_only: true
                          }.merge(options))
41
      @markdown = Redcarpet::Markdown.new(gitlab_renderer,
42 43 44 45 46 47 48 49 50 51 52
                      # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
                      no_intra_emphasis: true,
                      tables: true,
                      fenced_code_blocks: true,
                      autolink: true,
                      strikethrough: true,
                      lax_html_blocks: true,
                      space_after_headers: true,
                      superscript: true)
    end
    @markdown.render(text).html_safe
randx committed
53
  end
54 55 56 57 58 59 60 61

  def render_wiki_content(wiki_page)
    if wiki_page.format == :markdown
      markdown(wiki_page.content)
    else
      wiki_page.formatted_content.html_safe
    end
  end
62

63
  # text - whole text from a markdown file
Marin Jankovski committed
64 65 66
  # project_path_with_namespace - namespace/projectname, eg. gitlabhq/gitlabhq
  # ref - name of the branch or reference, eg. stable
  # requested_path - path of request, eg. doc/api/README.md, used in special case when path is pointing to the .md file were the original request is coming from
67
  # wiki - whether the markdown is from wiki or not
Marin Jankovski committed
68 69 70
  def create_relative_links(text, project, ref, requested_path, wiki = false)
    @path_to_satellite = project.satellite.path
    project_path_with_namespace = project.path_with_namespace
71
    paths = extract_paths(text)
Marin Jankovski committed
72
    paths.each do |file_path|
73 74 75 76 77 78 79 80 81 82 83
      original_file_path = extract(file_path)
      new_path = rebuild_path(project_path_with_namespace, original_file_path, requested_path, ref)
      if reference_path?(file_path)
        # Replacing old string with a new one that contains updated path
        # eg. [some document]: document.md will be replaced with [some document] /namespace/project/master/blob/document.md
        text.gsub!(file_path, file_path.gsub(original_file_path, "/#{new_path}"))
      else
        # Replacing old string with a new one with brackets ]() to prevent replacing occurence of a word
        # e.g. If we have a markdown like [test](test) this will replace ](test) and not the word test
        text.gsub!("](#{file_path})", "](/#{new_path})")
      end
84 85 86
    end
    text
  end
Marin Jankovski committed
87

88 89 90 91
  def extract_paths(markdown_text)
    all_markdown_paths = pick_out_paths(markdown_text)
    paths = remove_empty(all_markdown_paths)
    select_relative(paths)
Marin Jankovski committed
92 93
  end

94
  # Split the markdown text to each line and find all paths, this will match anything with - ]("some_text") and [some text]: file.md
95
  def pick_out_paths(markdown_text)
96 97 98
    inline_paths = markdown_text.split("\n").map { |text| text.scan(/\]\(([^(]+)\)/) }
    reference_paths = markdown_text.split("\n").map { |text| text.scan(/\[.*\]:.*/) }
    inline_paths + reference_paths
99 100 101 102 103 104 105
  end

  # Removes any empty result produced by not matching the regexp
  def remove_empty(paths)
    paths.reject{|l| l.empty? }.flatten
  end

106 107 108 109 110
  # If a path is a reference style link we need to omit ]:
  def extract(path)
    path.split("]: ").last
  end

111 112 113 114 115 116
  # Reject any path that contains ignored protocol
  # eg. reject "https://gitlab.org} but accept "doc/api/README.md"
  def select_relative(paths)
    paths.reject{|path| ignored_protocols.map{|protocol| path.include?(protocol)}.any?}
  end

117 118 119 120 121
  # Check whether a path is a reference-style link
  def reference_path?(path)
    path.include?("]: ")
  end

122 123 124 125
  def ignored_protocols
    ["http://","https://", "ftp://", "mailto:"]
  end

Marin Jankovski committed
126
  def rebuild_path(path_with_namespace, path, requested_path, ref)
127 128
    path.gsub!(/(#.*)/, "")
    id = $1 || ""
Marin Jankovski committed
129
    file_path = relative_file_path(path, requested_path)
Marin Jankovski committed
130 131
    [
      path_with_namespace,
Marin Jankovski committed
132 133
      path_with_ref(file_path, ref),
      file_path
134
    ].compact.join("/").gsub(/\/*$/, '') + id
Marin Jankovski committed
135 136
  end

137 138 139
  # Checks if the path exists in the repo
  # eg. checks if doc/README.md exists, if it doesn't then it is a wiki link
  def path_with_ref(path, ref)
Marin Jankovski committed
140
    if file_exists?(path)
141
      "#{local_path(path)}/#{correct_ref(ref)}"
Marin Jankovski committed
142 143 144 145 146
    else
      "wikis"
    end
  end

Marin Jankovski committed
147 148 149 150 151 152 153 154 155
  def relative_file_path(path, requested_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
156 157
  # If we are at doc/api and the README.md shown in below the tree view
  # this takes the rquest path(doc/api) and adds users.md so the path looks like doc/api/users.md
Marin Jankovski committed
158
  def build_nested_path(path, request_path)
159
    return request_path if path == ""
Marin Jankovski committed
160
    return path unless request_path
161 162 163 164 165 166 167 168
    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
Marin Jankovski committed
169 170
  end

Marin Jankovski committed
171
  def file_exists?(path)
172
    return false if path.nil?
173
    return @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any?
Marin Jankovski committed
174 175
  end

176 177 178
  # 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)
179 180
    return "tree" if @repository.tree(current_sha, path).entries.any?
    return "raw" if @repository.blob_at(current_sha, path).image?
181
    return "blob"
Marin Jankovski committed
182 183
  end

184 185
  def current_ref
    @commit.nil? ? "master" : @commit.id
Marin Jankovski committed
186 187
  end

188 189 190 191 192 193 194 195
  def current_sha
    if @commit
      @commit.id
    else
      @repository.head_commit.sha
    end
  end

196
  # We will assume that if no ref exists we can point to master
Marin Jankovski committed
197
  def correct_ref(ref)
198
    ref ? ref : "master"
Marin Jankovski committed
199
  end
200
end