2018-08-18 07:19:57 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2015-05-29 01:56:30 -04:00
|
|
|
require 'nokogiri'
|
|
|
|
|
2017-04-03 05:55:15 -04:00
|
|
|
module MarkupHelper
|
2019-07-04 10:11:44 -04:00
|
|
|
include ActionView::Helpers::TextHelper
|
2020-01-29 13:08:47 -05:00
|
|
|
include ActionView::Context
|
2017-05-18 15:06:38 -04:00
|
|
|
|
2017-04-03 06:04:42 -04:00
|
|
|
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
|
|
|
|
|
2012-08-23 14:10:06 -04:00
|
|
|
# Use this in places where you would normally use link_to(gfm(...), ...).
|
2017-08-23 12:53:29 -04:00
|
|
|
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
|
|
|
|
|
2012-08-23 14:10:06 -04:00
|
|
|
# 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
|
2017-08-23 12:53:29 -04:00
|
|
|
# not linked any more). link_to_html corrects that. It wraps all parts to
|
2012-08-23 14:10:06 -04:00
|
|
|
# explicitly produce the correct linking behavior (i.e.
|
|
|
|
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
|
2017-08-23 12:53:29 -04:00
|
|
|
def link_to_html(redacted, url, html_options = {})
|
|
|
|
fragment = Nokogiri::HTML::DocumentFragment.parse(redacted)
|
2012-09-19 19:42:26 -04:00
|
|
|
|
2015-05-29 01:56:30 -04:00
|
|
|
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
|
2019-11-02 11:06:18 -04:00
|
|
|
# Traverse the fragment's first generation of children looking for
|
|
|
|
# either pure text or emojis, wrapping anything found in the
|
|
|
|
# requested link
|
2015-05-29 01:56:30 -04:00
|
|
|
fragment.children.each do |node|
|
2019-11-02 11:06:18 -04:00
|
|
|
if node.text?
|
|
|
|
node.replace(link_to(node.text, url, html_options))
|
|
|
|
elsif node.name == 'gl-emoji'
|
|
|
|
node.replace(link_to(node.to_html.html_safe, url, html_options))
|
|
|
|
end
|
2015-05-29 01:56:30 -04:00
|
|
|
end
|
2012-08-02 20:28:02 -04:00
|
|
|
end
|
|
|
|
|
2015-08-27 17:28:45 -04:00
|
|
|
# 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
|
|
|
|
|
2015-05-29 01:56:30 -04:00
|
|
|
fragment.to_html.html_safe
|
2012-08-02 20:28:02 -04:00
|
|
|
end
|
2012-08-08 04:52:09 -04:00
|
|
|
|
2017-04-21 11:17:19 -04:00
|
|
|
# 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.
|
2017-11-06 11:52:56 -05:00
|
|
|
def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
|
2020-01-23 07:08:38 -05:00
|
|
|
md = markdown_field(object, attribute, options.merge(post_process: false))
|
2019-02-08 07:19:53 -05:00
|
|
|
return unless md.present?
|
2017-04-06 10:47:52 -04:00
|
|
|
|
2020-03-04 04:08:20 -05:00
|
|
|
tags = %w(a gl-emoji b strong i em pre code p span)
|
2018-09-12 18:47:21 -04:00
|
|
|
tags << 'img' if options[:allow_images]
|
2017-11-06 11:52:56 -05:00
|
|
|
|
2018-09-12 18:47:21 -04:00
|
|
|
text = truncate_visible(md, max_chars || md.length)
|
2020-01-23 07:08:38 -05:00
|
|
|
text = prepare_for_rendering(text, markdown_field_render_context(object, attribute, options))
|
2018-09-12 18:47:21 -04:00
|
|
|
text = sanitize(
|
2017-11-06 11:52:56 -05:00
|
|
|
text,
|
2018-09-12 18:47:21 -04:00
|
|
|
tags: tags,
|
2019-04-24 10:09:36 -04:00
|
|
|
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
|
2020-03-04 04:08:20 -05:00
|
|
|
%w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title data-html)
|
2017-11-06 11:52:56 -05:00
|
|
|
)
|
2018-09-12 18:47:21 -04:00
|
|
|
|
|
|
|
# since <img> tags are stripped, this can leave empty <a> tags hanging around
|
|
|
|
# (as our markdown wraps images in links)
|
|
|
|
options[:allow_images] ? text : strip_empty_link_tags(text).html_safe
|
2017-04-06 10:47:52 -04:00
|
|
|
end
|
|
|
|
|
2017-04-21 11:17:19 -04:00
|
|
|
def markdown(text, context = {})
|
2017-04-24 05:25:03 -04:00
|
|
|
return '' unless text.present?
|
2015-09-03 16:38:35 -04:00
|
|
|
|
2015-10-14 13:27:23 -04:00
|
|
|
context[:project] ||= @project
|
2017-12-01 04:21:05 -05:00
|
|
|
context[:group] ||= @group
|
|
|
|
|
2017-04-24 06:27:58 -04:00
|
|
|
html = markdown_unsafe(text, context)
|
2017-04-28 10:57:17 -04:00
|
|
|
prepare_for_rendering(html, context)
|
2016-10-06 17:17:11 -04:00
|
|
|
end
|
2013-03-14 02:31:08 -04:00
|
|
|
|
2017-11-06 11:52:56 -05:00
|
|
|
def markdown_field(object, field, context = {})
|
2016-10-06 17:17:11 -04:00
|
|
|
object = object.for_display if object.respond_to?(:for_display)
|
2017-04-24 05:25:03 -04:00
|
|
|
return '' unless object.present?
|
2015-09-03 16:38:35 -04:00
|
|
|
|
2020-01-23 07:08:38 -05:00
|
|
|
redacted_field_html = object.try(:"redacted_#{field}_html")
|
|
|
|
return redacted_field_html if redacted_field_html
|
2017-11-06 11:52:56 -05:00
|
|
|
|
2020-01-23 07:08:38 -05:00
|
|
|
render_markdown_field(object, field, context)
|
2015-08-27 18:36:56 -04:00
|
|
|
end
|
|
|
|
|
2017-04-21 11:17:19 -04:00
|
|
|
def markup(file_name, text, context = {})
|
2017-04-06 10:47:52 -04:00
|
|
|
context[:project] ||= @project
|
2021-06-01 17:10:06 -04:00
|
|
|
context[:text_source] ||= :blob
|
2017-04-21 11:17:19 -04:00
|
|
|
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
|
2017-04-28 10:57:17 -04:00
|
|
|
prepare_for_rendering(html, context)
|
2017-04-06 10:47:52 -04:00
|
|
|
end
|
|
|
|
|
2018-09-02 11:24:27 -04:00
|
|
|
def render_wiki_content(wiki_page, context = {})
|
2017-04-21 11:17:19 -04:00
|
|
|
text = wiki_page.content
|
2017-04-24 05:25:03 -04:00
|
|
|
return '' unless text.present?
|
2014-10-11 22:17:02 -04:00
|
|
|
|
2021-01-27 07:09:01 -05:00
|
|
|
context = render_wiki_content_context(@wiki, wiki_page, context)
|
|
|
|
html = markup_unsafe(wiki_page.path, text, context)
|
2013-10-08 06:24:50 -04:00
|
|
|
|
2017-04-28 10:57:17 -04:00
|
|
|
prepare_for_rendering(html, context)
|
2017-04-06 10:47:52 -04:00
|
|
|
end
|
|
|
|
|
2017-04-21 11:17:19 -04:00
|
|
|
def markup_unsafe(file_name, text, context = {})
|
2017-04-24 05:25:03 -04:00
|
|
|
return '' unless text.present?
|
2017-04-21 11:17:19 -04:00
|
|
|
|
2017-04-03 06:04:42 -04:00
|
|
|
if gitlab_markdown?(file_name)
|
2017-04-28 10:57:17 -04:00
|
|
|
markdown_unsafe(text, context)
|
2017-04-03 06:04:42 -04:00
|
|
|
elsif asciidoc?(file_name)
|
2017-05-02 10:52:19 -04:00
|
|
|
asciidoc_unsafe(text, context)
|
2017-04-03 06:04:42 -04:00
|
|
|
elsif plain?(file_name)
|
2019-07-04 10:30:45 -04:00
|
|
|
plain_unsafe(text)
|
2017-04-03 06:04:42 -04:00
|
|
|
else
|
2017-05-02 10:52:19 -04:00
|
|
|
other_markup_unsafe(file_name, text, context)
|
2017-04-03 06:04:42 -04:00
|
|
|
end
|
2020-01-16 07:08:32 -05:00
|
|
|
rescue StandardError => e
|
2020-02-06 13:08:54 -05:00
|
|
|
Gitlab::ErrorTracking.track_exception(e, project_id: @project&.id, file_name: file_name)
|
2020-01-16 07:08:32 -05:00
|
|
|
|
2017-04-21 11:17:19 -04:00
|
|
|
simple_format(text)
|
|
|
|
end
|
|
|
|
|
2016-12-21 11:41:33 -05:00
|
|
|
# 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
|
|
|
|
|
2014-10-11 22:17:02 -04:00
|
|
|
private
|
|
|
|
|
2020-11-30 13:09:46 -05:00
|
|
|
def render_wiki_content_context(wiki, wiki_page, context)
|
|
|
|
context.merge(
|
|
|
|
pipeline: :wiki,
|
|
|
|
wiki: wiki,
|
|
|
|
repository: wiki.repository,
|
|
|
|
page_slug: wiki_page.slug,
|
|
|
|
issuable_state_filter_enabled: true
|
|
|
|
).merge(render_wiki_content_context_container(wiki))
|
|
|
|
end
|
|
|
|
|
|
|
|
def render_wiki_content_context_container(wiki)
|
|
|
|
{ project: wiki.container }
|
|
|
|
end
|
|
|
|
|
2014-10-11 22:17:02 -04:00
|
|
|
# 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
|
2014-10-13 00:07:18 -04:00
|
|
|
truncated = false
|
2014-10-11 22:17:02 -04:00
|
|
|
|
|
|
|
doc.traverse do |node|
|
|
|
|
if node.text? || node.content.empty?
|
2014-10-13 00:07:18 -04:00
|
|
|
if truncated
|
2014-10-11 22:17:02 -04:00
|
|
|
node.remove
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
2014-10-13 00:07:18 -04:00
|
|
|
# Handle line breaks within a node
|
|
|
|
if node.content.strip.lines.length > 1
|
|
|
|
node.content = "#{node.content.lines.first.chomp}..."
|
|
|
|
truncated = true
|
|
|
|
end
|
|
|
|
|
2014-10-11 22:17:02 -04:00
|
|
|
num_remaining = max_chars - content_length
|
|
|
|
if node.content.length > num_remaining
|
|
|
|
node.content = node.content.truncate(num_remaining)
|
2014-10-13 00:07:18 -04:00
|
|
|
truncated = true
|
2014-10-11 22:17:02 -04:00
|
|
|
end
|
2018-01-11 11:34:01 -05:00
|
|
|
|
2014-10-11 22:17:02 -04:00
|
|
|
content_length += node.content.length
|
|
|
|
end
|
2014-10-13 00:07:18 -04:00
|
|
|
|
|
|
|
truncated = truncate_if_block(node, truncated)
|
2014-10-11 22:17:02 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
doc.to_html
|
|
|
|
end
|
2014-10-13 00:07:18 -04:00
|
|
|
|
|
|
|
# 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)
|
2017-03-13 18:39:22 -04:00
|
|
|
return true if truncated
|
|
|
|
|
|
|
|
if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
|
2015-09-23 00:56:27 -04:00
|
|
|
node.inner_html = "#{node.inner_html}..." if node.next_sibling
|
2014-10-13 00:07:18 -04:00
|
|
|
true
|
|
|
|
else
|
|
|
|
truncated
|
|
|
|
end
|
|
|
|
end
|
2014-10-20 05:56:01 -04:00
|
|
|
|
2018-09-12 18:47:21 -04:00
|
|
|
def strip_empty_link_tags(text)
|
|
|
|
scrubber = Loofah::Scrubber.new do |node|
|
2020-03-04 04:08:20 -05:00
|
|
|
node.remove if node.name == 'a' && node.children.empty?
|
2018-09-12 18:47:21 -04:00
|
|
|
end
|
|
|
|
|
2019-04-11 08:24:13 -04:00
|
|
|
sanitize text, scrubber: scrubber
|
2018-09-12 18:47:21 -04:00
|
|
|
end
|
|
|
|
|
2016-06-03 10:14:04 -04:00
|
|
|
def markdown_toolbar_button(options = {})
|
2017-04-24 05:25:03 -04:00
|
|
|
data = options[:data].merge({ container: 'body' })
|
2016-06-03 10:14:04 -04:00
|
|
|
content_tag :button,
|
2017-04-24 05:25:03 -04:00
|
|
|
type: 'button',
|
2021-08-11 17:10:33 -04:00
|
|
|
class: 'gl-button btn btn-default-tertiary btn-icon js-md has-tooltip',
|
2016-06-03 10:14:04 -04:00
|
|
|
data: data,
|
|
|
|
title: options[:title],
|
|
|
|
aria: { label: options[:title] } do
|
2017-11-06 07:04:55 -05:00
|
|
|
sprite_icon(options[:icon])
|
2016-06-03 10:14:04 -04:00
|
|
|
end
|
|
|
|
end
|
2016-10-06 17:17:11 -04:00
|
|
|
|
2017-04-24 05:50:13 -04:00
|
|
|
def markdown_unsafe(text, context = {})
|
|
|
|
Banzai.render(text, context)
|
|
|
|
end
|
|
|
|
|
2017-05-02 10:52:19 -04:00
|
|
|
def asciidoc_unsafe(text, context = {})
|
2019-06-14 03:53:08 -04:00
|
|
|
context.merge!(
|
|
|
|
commit: @commit,
|
|
|
|
ref: @ref,
|
|
|
|
requested_path: @path
|
|
|
|
)
|
2017-05-02 10:52:19 -04:00
|
|
|
Gitlab::Asciidoc.render(text, context)
|
2017-04-24 05:50:13 -04:00
|
|
|
end
|
|
|
|
|
2019-07-04 10:30:45 -04:00
|
|
|
def plain_unsafe(text)
|
|
|
|
content_tag :pre, class: 'plain-readme' do
|
|
|
|
text
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-05-02 10:52:19 -04:00
|
|
|
def other_markup_unsafe(file_name, text, context = {})
|
|
|
|
Gitlab::OtherMarkup.render(file_name, text, context)
|
2017-04-24 05:50:13 -04:00
|
|
|
end
|
|
|
|
|
2020-01-23 07:08:38 -05:00
|
|
|
def render_markdown_field(object, field, context = {})
|
|
|
|
post_process = context.delete(:post_process)
|
|
|
|
post_process = true if post_process.nil?
|
|
|
|
|
|
|
|
html = Banzai.render_field(object, field, context)
|
|
|
|
|
|
|
|
return html unless post_process
|
|
|
|
|
|
|
|
prepare_for_rendering(html, markdown_field_render_context(object, field, context))
|
|
|
|
end
|
|
|
|
|
|
|
|
def markdown_field_render_context(object, field, base_context = {})
|
|
|
|
return base_context unless object.respond_to?(:banzai_render_context)
|
|
|
|
|
|
|
|
base_context.reverse_merge(object.banzai_render_context(field))
|
|
|
|
end
|
|
|
|
|
2017-04-28 10:57:17 -04:00
|
|
|
def prepare_for_rendering(html, context = {})
|
2017-04-24 05:25:03 -04:00
|
|
|
return '' unless html.present?
|
2017-04-21 11:17:19 -04:00
|
|
|
|
2019-06-20 04:02:33 -04:00
|
|
|
context.reverse_merge!(
|
2018-04-03 09:45:17 -04:00
|
|
|
current_user: (current_user if defined?(current_user)),
|
2016-10-06 17:17:11 -04:00
|
|
|
|
2020-01-14 16:07:45 -05:00
|
|
|
# RepositoryLinkFilter and UploadLinkFilter
|
2017-04-24 06:36:59 -04:00
|
|
|
commit: @commit,
|
2020-06-03 20:08:17 -04:00
|
|
|
wiki: @wiki,
|
2017-04-24 06:36:59 -04:00
|
|
|
ref: @ref,
|
|
|
|
requested_path: @path
|
2016-10-06 17:17:11 -04:00
|
|
|
)
|
|
|
|
|
2017-04-28 10:57:17 -04:00
|
|
|
html = Banzai.post_process(html, context)
|
|
|
|
|
|
|
|
Hamlit::RailsHelpers.preserve(html)
|
2016-10-06 17:17:11 -04:00
|
|
|
end
|
2017-04-26 17:16:36 -04:00
|
|
|
|
|
|
|
extend self
|
2012-08-02 20:28:02 -04:00
|
|
|
end
|
2020-11-30 13:09:46 -05:00
|
|
|
|
2021-05-11 17:10:21 -04:00
|
|
|
MarkupHelper.prepend_mod_with('MarkupHelper')
|