markup_helper.rb 7,0 КБ
Newer Older
Robert Speicher's avatar
Robert Speicher включено в состав коммита
1
2
require 'nokogiri'

Toon Claes's avatar
Toon Claes включено в состав коммита
3
module MarkupHelper
Toon Claes's avatar
Toon Claes включено в состав коммита
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  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

Riyad Preukschas's avatar
Riyad Preukschas включено в состав коммита
20
21
22
23
24
25
26
27
28
  # 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>").
randx's avatar
randx включено в состав коммита
29
  def link_to_gfm(body, url, html_options = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
30
    return '' if body.blank?
Robert Speicher's avatar
Robert Speicher включено в состав коммита
31

Nick Thomas's avatar
Nick Thomas включено в состав коммита
32
33
34
35
36
37
    context = {
      project: @project,
      current_user: (current_user if defined?(current_user)),
      pipeline: :single_line,
    }
    gfm_body = Banzai.render(body, context)
randx's avatar
randx включено в состав коммита
38

SAKATA Sinji's avatar
SAKATA Sinji включено в состав коммита
39
    fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
Robert Speicher's avatar
Robert Speicher включено в состав коммита
40
41
42
43
44
45
46
47
48
49
50
51
    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
randx's avatar
randx включено в состав коммита
52
53
    end

Robert Speicher's avatar
Robert Speicher включено в состав коммита
54
55
56
57
58
    # 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

Robert Speicher's avatar
Robert Speicher включено в состав коммита
59
    fragment.to_html.html_safe
randx's avatar
randx включено в состав коммита
60
  end
randx's avatar
randx включено в состав коммита
61

Douwe Maan's avatar
Douwe Maan включено в состав коммита
62
63
64
65
66
67
  # 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.
  def first_line_in_markdown(text, max_chars = nil, options = {})
    md = markdown(text, options).strip
Toon Claes's avatar
Toon Claes включено в состав коммита
68

Douwe Maan's avatar
Douwe Maan включено в состав коммита
69
    truncate_visible(md, max_chars || md.length) if md.present?
Toon Claes's avatar
Toon Claes включено в состав коммита
70
71
  end

Douwe Maan's avatar
Douwe Maan включено в состав коммита
72
  def markdown(text, context = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
73
    return '' unless text.present?
Robert Speicher's avatar
Robert Speicher включено в состав коммита
74

Douwe Maan's avatar
Douwe Maan включено в состав коммита
75
    context[:project] ||= @project
Toon Claes's avatar
Toon Claes включено в состав коммита
76
    html = markdown_unsafe(text, context)
Douwe Maan's avatar
Douwe Maan включено в состав коммита
77
    prepare_for_rendering(html, context)
Nick Thomas's avatar
Nick Thomas включено в состав коммита
78
  end
Dan Knox's avatar
Dan Knox включено в состав коммита
79

Nick Thomas's avatar
Nick Thomas включено в состав коммита
80
81
  def markdown_field(object, field)
    object = object.for_display if object.respond_to?(:for_display)
Toon Claes's avatar
Toon Claes включено в состав коммита
82
    return '' unless object.present?
Robert Speicher's avatar
Robert Speicher включено в состав коммита
83

Nick Thomas's avatar
Nick Thomas включено в состав коммита
84
    html = Banzai.render_field(object, field)
Douwe Maan's avatar
Douwe Maan включено в состав коммита
85
    prepare_for_rendering(html, object.banzai_render_context(field))
Robert Speicher's avatar
Robert Speicher включено в состав коммита
86
87
  end

Douwe Maan's avatar
Douwe Maan включено в состав коммита
88
  def markup(file_name, text, context = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
89
    context[:project] ||= @project
Douwe Maan's avatar
Douwe Maan включено в состав коммита
90
    html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
Douwe Maan's avatar
Douwe Maan включено в состав коммита
91
    prepare_for_rendering(html, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
92
93
  end

Douwe Maan's avatar
Douwe Maan включено в состав коммита
94
95
  def render_wiki_content(wiki_page)
    text = wiki_page.content
Toon Claes's avatar
Toon Claes включено в состав коммита
96
    return '' unless text.present?
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
97

Douwe Maan's avatar
Douwe Maan включено в состав коммита
98
    context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
uran's avatar
uran включено в состав коммита
99

Douwe Maan's avatar
Douwe Maan включено в состав коммита
100
101
102
103
104
105
106
107
108
    html =
      case wiki_page.format
      when :markdown
        markdown_unsafe(text, context)
      when :asciidoc
        asciidoc_unsafe(text)
      else
        wiki_page.formatted_content.html_safe
      end
Marin Jankovski's avatar
Marin Jankovski включено в состав коммита
109

Douwe Maan's avatar
Douwe Maan включено в состав коммита
110
    prepare_for_rendering(html, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
111
112
  end

Douwe Maan's avatar
Douwe Maan включено в состав коммита
113
  def markup_unsafe(file_name, text, context = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
114
    return '' unless text.present?
Douwe Maan's avatar
Douwe Maan включено в состав коммита
115

Toon Claes's avatar
Toon Claes включено в состав коммита
116
    if gitlab_markdown?(file_name)
Douwe Maan's avatar
Douwe Maan включено в состав коммита
117
      markdown_unsafe(text, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
118
    elsif asciidoc?(file_name)
Robert Speicher's avatar
Robert Speicher включено в состав коммита
119
      asciidoc_unsafe(text, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
120
121
    elsif plain?(file_name)
      content_tag :pre, class: 'plain-readme' do
Douwe Maan's avatar
Douwe Maan включено в состав коммита
122
        text
Toon Claes's avatar
Toon Claes включено в состав коммита
123
124
      end
    else
Robert Speicher's avatar
Robert Speicher включено в состав коммита
125
      other_markup_unsafe(file_name, text, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
126
127
    end
  rescue RuntimeError
Douwe Maan's avatar
Douwe Maan включено в состав коммита
128
129
130
    simple_format(text)
  end

James Lopez's avatar
James Lopez включено в состав коммита
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
  # 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

Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
153
154
155
156
157
158
159
  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
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
160
    truncated = false
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
161
162
163

    doc.traverse do |node|
      if node.text? || node.content.empty?
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
164
        if truncated
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
165
166
167
168
          node.remove
          next
        end

Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
169
170
171
172
173
174
        # Handle line breaks within a node
        if node.content.strip.lines.length > 1
          node.content = "#{node.content.lines.first.chomp}..."
          truncated = true
        end

Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
175
176
177
        num_remaining = max_chars - content_length
        if node.content.length > num_remaining
          node.content = node.content.truncate(num_remaining)
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
178
          truncated = true
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
179
180
181
        end
        content_length += node.content.length
      end
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
182
183

      truncated = truncate_if_block(node, truncated)
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
184
185
186
187
    end

    doc.to_html
  end
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
188
189
190
191
192

  # 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)
Douwe Maan's avatar
Douwe Maan включено в состав коммита
193
194
195
    return true if truncated

    if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
Stan Hu's avatar
Stan Hu включено в состав коммита
196
      node.inner_html = "#{node.inner_html}..." if node.next_sibling
Vinnie Okada's avatar
Vinnie Okada включено в состав коммита
197
198
199
200
201
      true
    else
      truncated
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets включено в состав коммита
202

Phil Hughes's avatar
Phil Hughes включено в состав коммита
203
  def markdown_toolbar_button(options = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
204
    data = options[:data].merge({ container: 'body' })
Phil Hughes's avatar
Phil Hughes включено в состав коммита
205
    content_tag :button,
Toon Claes's avatar
Toon Claes включено в состав коммита
206
207
      type: 'button',
      class: 'toolbar-btn js-md has-tooltip hidden-xs',
Phil Hughes's avatar
Phil Hughes включено в состав коммита
208
209
210
211
212
213
214
      tabindex: -1,
      data: data,
      title: options[:title],
      aria: { label: options[:title] } do
      icon(options[:icon])
    end
  end
Nick Thomas's avatar
Nick Thomas включено в состав коммита
215

Toon Claes's avatar
Toon Claes включено в состав коммита
216
217
218
219
  def markdown_unsafe(text, context = {})
    Banzai.render(text, context)
  end

Robert Speicher's avatar
Robert Speicher включено в состав коммита
220
221
  def asciidoc_unsafe(text, context = {})
    Gitlab::Asciidoc.render(text, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
222
223
  end

Robert Speicher's avatar
Robert Speicher включено в состав коммита
224
225
  def other_markup_unsafe(file_name, text, context = {})
    Gitlab::OtherMarkup.render(file_name, text, context)
Toon Claes's avatar
Toon Claes включено в состав коммита
226
227
  end

Douwe Maan's avatar
Douwe Maan включено в состав коммита
228
  def prepare_for_rendering(html, context = {})
Toon Claes's avatar
Toon Claes включено в состав коммита
229
    return '' unless html.present?
Douwe Maan's avatar
Douwe Maan включено в состав коммита
230

Nick Thomas's avatar
Nick Thomas включено в состав коммита
231
232
233
234
    context.merge!(
      current_user:   (current_user if defined?(current_user)),

      # RelativeLinkFilter
Toon Claes's avatar
Toon Claes включено в состав коммита
235
      commit:         @commit,
Nick Thomas's avatar
Nick Thomas включено в состав коммита
236
      project_wiki:   @project_wiki,
Toon Claes's avatar
Toon Claes включено в состав коммита
237
238
      ref:            @ref,
      requested_path: @path
Nick Thomas's avatar
Nick Thomas включено в состав коммита
239
240
    )

Douwe Maan's avatar
Douwe Maan включено в состав коммита
241
242
243
    html = Banzai.post_process(html, context)

    Hamlit::RailsHelpers.preserve(html)
Nick Thomas's avatar
Nick Thomas включено в состав коммита
244
  end
Toon Claes's avatar
Toon Claes включено в состав коммита
245
246

  extend self
randx's avatar
randx включено в состав коммита
247
end