markup_helper.rb 7.7 KB
Newer Older
1 2
require 'nokogiri'

3
module MarkupHelper
4 5 6
  include ActionView::Helpers::TagHelper
  include ActionView::Context

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  def plain?(filename)
    Gitlab::MarkupHelper.plain?(filename)
  end

  def markup?(filename)
    Gitlab::MarkupHelper.markup?(filename)
  end

  def gitlab_markdown?(filename)
    Gitlab::MarkupHelper.gitlab_markdown?(filename)
  end

  def asciidoc?(filename)
    Gitlab::MarkupHelper.asciidoc?(filename)
  end

23
  # Use this in places where you would normally use link_to(gfm(...), ...).
24 25 26 27 28 29 30 31 32 33 34 35
  def link_to_markdown(body, url, html_options = {})
    return '' if body.blank?

    link_to_html(markdown(body, pipeline: :single_line), url, html_options)
  end

  def link_to_markdown_field(object, field, url, html_options = {})
    rendered_field = markdown_field(object, field)

    link_to_html(rendered_field, url, html_options)
  end

36 37 38 39
  # 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
40
  # not linked any more). link_to_html corrects that. It wraps all parts to
41 42
  # explicitly produce the correct linking behavior (i.e.
  # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
43 44
  def link_to_html(redacted, url, html_options = {})
    fragment = Nokogiri::HTML::DocumentFragment.parse(redacted)
45

46 47 48 49 50 51 52 53 54 55 56 57
    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
58 59
    end

60 61 62 63 64
    # 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

65
    fragment.to_html.html_safe
66
  end
R
randx 已提交
67

D
Douwe Maan 已提交
68 69 70 71
  # 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.
72 73
  def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
    md = markdown_field(object, attribute, options)
74

75 76 77 78 79 80 81
    text = truncate_visible(md, max_chars || md.length) if md.present?

    sanitize(
      text,
      tags: %w(a img gl-emoji b pre code p span),
      attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
    )
82 83
  end

D
Douwe Maan 已提交
84
  def markdown(text, context = {})
85
    return '' unless text.present?
86

87
    context[:project] ||= @project
88
    html = markdown_unsafe(text, context)
D
Douwe Maan 已提交
89
    prepare_for_rendering(html, context)
90
  end
91

92
  def markdown_field(object, field, context = {})
93
    object = object.for_display if object.respond_to?(:for_display)
94 95
    redacted_field_html = object.try(:"redacted_#{field}_html")

96
    return '' unless object.present?
97
    return redacted_field_html if redacted_field_html
98

99 100 101 102
    html = Banzai.render_field(object, field, context)
    context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)

    prepare_for_rendering(html, context)
103 104
  end

D
Douwe Maan 已提交
105
  def markup(file_name, text, context = {})
106
    context[:project] ||= @project
D
Douwe Maan 已提交
107
    html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
D
Douwe Maan 已提交
108
    prepare_for_rendering(html, context)
109 110
  end

D
Douwe Maan 已提交
111 112
  def render_wiki_content(wiki_page)
    text = wiki_page.content
113
    return '' unless text.present?
114

D
Douwe Maan 已提交
115
    context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
116

D
Douwe Maan 已提交
117 118 119 120 121 122 123 124 125
    html =
      case wiki_page.format
      when :markdown
        markdown_unsafe(text, context)
      when :asciidoc
        asciidoc_unsafe(text)
      else
        wiki_page.formatted_content.html_safe
      end
126

D
Douwe Maan 已提交
127
    prepare_for_rendering(html, context)
128 129
  end

D
Douwe Maan 已提交
130
  def markup_unsafe(file_name, text, context = {})
131
    return '' unless text.present?
D
Douwe Maan 已提交
132

133
    if gitlab_markdown?(file_name)
D
Douwe Maan 已提交
134
      markdown_unsafe(text, context)
135
    elsif asciidoc?(file_name)
136
      asciidoc_unsafe(text, context)
137 138
    elsif plain?(file_name)
      content_tag :pre, class: 'plain-readme' do
D
Douwe Maan 已提交
139
        text
140 141
      end
    else
142
      other_markup_unsafe(file_name, text, context)
143 144
    end
  rescue RuntimeError
D
Douwe Maan 已提交
145 146 147
    simple_format(text)
  end

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
  # 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
  def cross_project_reference(project, entity)
    if entity.respond_to?(:to_reference)
      entity.to_reference(project, full: true)
    else
      ''
    end
  end

170 171 172 173 174 175 176
  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
177
    truncated = false
178 179 180

    doc.traverse do |node|
      if node.text? || node.content.empty?
181
        if truncated
182 183 184 185
          node.remove
          next
        end

186 187 188 189 190 191
        # Handle line breaks within a node
        if node.content.strip.lines.length > 1
          node.content = "#{node.content.lines.first.chomp}..."
          truncated = true
        end

192 193 194
        num_remaining = max_chars - content_length
        if node.content.length > num_remaining
          node.content = node.content.truncate(num_remaining)
195
          truncated = true
196 197 198
        end
        content_length += node.content.length
      end
199 200

      truncated = truncate_if_block(node, truncated)
201 202 203 204
    end

    doc.to_html
  end
205 206 207 208 209

  # 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)
D
Douwe Maan 已提交
210 211 212
    return true if truncated

    if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
213
      node.inner_html = "#{node.inner_html}..." if node.next_sibling
214 215 216 217 218
      true
    else
      truncated
    end
  end
219

P
Phil Hughes 已提交
220
  def markdown_toolbar_button(options = {})
221
    data = options[:data].merge({ container: 'body' })
P
Phil Hughes 已提交
222
    content_tag :button,
223 224
      type: 'button',
      class: 'toolbar-btn js-md has-tooltip hidden-xs',
P
Phil Hughes 已提交
225 226 227 228
      tabindex: -1,
      data: data,
      title: options[:title],
      aria: { label: options[:title] } do
T
Tim Zallmann 已提交
229
      sprite_icon(options[:icon])
P
Phil Hughes 已提交
230 231
    end
  end
232

T
Toon Claes 已提交
233 234 235 236
  def markdown_unsafe(text, context = {})
    Banzai.render(text, context)
  end

237 238
  def asciidoc_unsafe(text, context = {})
    Gitlab::Asciidoc.render(text, context)
T
Toon Claes 已提交
239 240
  end

241 242
  def other_markup_unsafe(file_name, text, context = {})
    Gitlab::OtherMarkup.render(file_name, text, context)
T
Toon Claes 已提交
243 244
  end

D
Douwe Maan 已提交
245
  def prepare_for_rendering(html, context = {})
246
    return '' unless html.present?
D
Douwe Maan 已提交
247

248 249 250 251
    context.merge!(
      current_user:   (current_user if defined?(current_user)),

      # RelativeLinkFilter
252
      commit:         @commit,
253
      project_wiki:   @project_wiki,
254 255
      ref:            @ref,
      requested_path: @path
256 257
    )

D
Douwe Maan 已提交
258 259 260
    html = Banzai.post_process(html, context)

    Hamlit::RailsHelpers.preserve(html)
261
  end
T
Toon Claes 已提交
262 263

  extend self
264
end