Read about our upcoming Code of Conduct on this issue

gitlab_markdown_helper.rb 6.11 KB
Newer Older
1
2
require 'nokogiri'

3
module GitlabMarkdownHelper
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
    context = {
      project: @project,
      current_user: (current_user if defined?(current_user)),
      pipeline: :single_line,
    }
    gfm_body = Banzai.render(body, context)
22

SAKATA Sinji's avatar
SAKATA Sinji committed
23
    fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
24
25
26
27
28
29
30
31
32
33
34
35
    if fragment.children.size == 1 && fragment.children[0].name == 'a'
      # Fragment has only one node, and it's a link generated by `gfm`.
      # Replace it with our requested link.
      text = fragment.children[0].text
      fragment.children[0].replace(link_to(text, url, html_options))
    else
      # Traverse the fragment's first generation of children looking for pure
      # text, wrapping anything found in the requested link
      fragment.children.each do |node|
        next unless node.text?
        node.replace(link_to(node.text, url, html_options))
      end
36
37
    end

38
39
40
41
42
    # Add any custom CSS classes to the GFM-generated reference links
    if html_options[:class]
      fragment.css('a.gfm').add_class(html_options[:class])
    end

43
    fragment.to_html.html_safe
44
  end
randx's avatar
randx committed
45

46
  def markdown(text, context = {})
Douwe Maan's avatar
Douwe Maan committed
47
    return "" unless text.present?
48

49
    context[:project] ||= @project
50

51
    html = Banzai.render(text, context)
52
53
    banzai_postprocess(html, context)
  end
54

55
56
57
  def markdown_field(object, field)
    object = object.for_display if object.respond_to?(:for_display)
    return "" unless object.present?
58

59
60
    html = Banzai.render_field(object, field)
    banzai_postprocess(html, object.banzai_render_context(field))
61
62
  end

63
  def asciidoc(text)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
64
65
    Gitlab::Asciidoc.render(
      text,
66
      project:      @project,
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
      current_user: (current_user if defined?(current_user)),

      # RelativeLinkFilter
      project_wiki:   @project_wiki,
      requested_path: @path,
      ref:            @ref,
      commit:         @commit
    )
  end

  def other_markup(file_name, text)
    Gitlab::OtherMarkup.render(
      file_name,
      text,
      project:      @project,
82
83
84
85
      current_user: (current_user if defined?(current_user)),

      # RelativeLinkFilter
      project_wiki:   @project_wiki,
86
      requested_path: @path,
87
88
89
      ref:            @ref,
      commit:         @commit
    )
90
91
  end

92
93
94
95
  # Return the first line of +text+, up to +max_chars+, after parsing the line
  # as Markdown.  HTML tags in the parsed output are not counted toward the
  # +max_chars+ limit.  If the length limit falls within a tag's contents, then
  # the tag contents are truncated without removing the closing tag.
96
97
  def first_line_in_markdown(text, max_chars = nil, options = {})
    md = markdown(text, options).strip
98

99
    truncate_visible(md, max_chars || md.length) if md.present?
100
101
  end

102
  def render_wiki_content(wiki_page)
103
104
    case wiki_page.format
    when :markdown
105
      markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
106
107
    when :asciidoc
      asciidoc(wiki_page.content)
108
109
110
111
    else
      wiki_page.formatted_content.html_safe
    end
  end
112

113
114
115
116
117
118
119
  private

  # Return +text+, truncated to +max_chars+ characters, excluding any HTML
  # tags.
  def truncate_visible(text, max_chars)
    doc = Nokogiri::HTML.fragment(text)
    content_length = 0
120
    truncated = false
121
122
123

    doc.traverse do |node|
      if node.text? || node.content.empty?
124
        if truncated
125
126
127
128
          node.remove
          next
        end

129
130
131
132
133
134
        # Handle line breaks within a node
        if node.content.strip.lines.length > 1
          node.content = "#{node.content.lines.first.chomp}..."
          truncated = true
        end

135
136
137
        num_remaining = max_chars - content_length
        if node.content.length > num_remaining
          node.content = node.content.truncate(num_remaining)
138
          truncated = true
139
140
141
        end
        content_length += node.content.length
      end
142
143

      truncated = truncate_if_block(node, truncated)
144
145
146
147
    end

    doc.to_html
  end
148
149
150
151
152
153

  # Used by #truncate_visible.  If +node+ is the first block element, and the
  # text hasn't already been truncated, then append "..." to the node contents
  # and return true.  Otherwise return false.
  def truncate_if_block(node, truncated)
    if node.element? && node.description.block? && !truncated
154
      node.inner_html = "#{node.inner_html}..." if node.next_sibling
155
156
157
158
159
      true
    else
      truncated
    end
  end
160

161
162
163
164
165
166
167
168
169
170
171
172
173
174
  # Returns the text necessary to reference `entity` across projects
  #
  # project - Project to reference
  # entity  - Object that responds to `to_reference`
  #
  # Examples:
  #
  #   cross_project_reference(project, project.issues.first)
  #   # => 'namespace1/project1#123'
  #
  #   cross_project_reference(project, project.merge_requests.first)
  #   # => 'namespace1/project1!345'
  #
  # Returns a String
175
  def cross_project_reference(project, entity)
176
    if entity.respond_to?(:to_reference)
177
      entity.to_reference(project)
178
    else
179
      ''
180
181
    end
  end
182
183
184
185
186
187
188
189
190
191
192
193
194

  def markdown_toolbar_button(options = {})
    data = options[:data].merge({ container: "body" })
    content_tag :button,
      type: "button",
      class: "toolbar-btn js-md has-tooltip hidden-xs",
      tabindex: -1,
      data: data,
      title: options[:title],
      aria: { label: options[:title] } do
      icon(options[:icon])
    end
  end
195
196
197
198
199
200
201
202
203
204
205
206
207
208

  # Calls Banzai.post_process with some common context options
  def banzai_postprocess(html, context)
    context.merge!(
      current_user:   (current_user if defined?(current_user)),

      # RelativeLinkFilter
      requested_path: @path,
      project_wiki:   @project_wiki,
      ref:            @ref
    )

    Banzai.post_process(html, context)
  end
209
end