gitlab-org--gitlab-foss/app/helpers/markup_helper.rb

293 lines
8.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require 'nokogiri'
module MarkupHelper
include ActionView::Helpers::TagHelper
Fix slow performance with compiling HAML templates In Rails 5, including `ActionView::Context` can have a significant and hidden performance penalty because this module also includes `ActionView::CompiledTemplates`. This means that any module that includes ActionView::Context becomes a descendant of `CompiledTemplates`. When a partial is rendered for the first time, it runs `ActionView::CompiledTemplates#module_eval`, which will evaluate a string that defines a new method for that partial. For example, the source of partial might be this string: ``` def _app_views_project_show_html_haml___12345(local_assigns, output) "hello world" end ``` When this string is evaluated, the Ruby interpreter will define the method and clear the global method cache for all descendants of `ActionView::CompiledTemplates`. Previous to this change, we inadvertently made a number of modules fall into this category: * GroupChildEntity * NoteUserEntity * Notify * MergeRequestUserEntity * AnalyticsCommitEntity * CommitEntity * UserEntity * Kaminari::Helpers::Paginator * CurrentUserEntity * ActionView::Base * ActionDispatch::DebugExceptions::DebugView * MarkupHelper * MergeRequestPresenter After this change: * Kaminari::Helpers::Paginator * ActionView::Base * ActionDispatch::DebugExceptions::DebugView Each time a partial is rendered for the first time, all methods for those modules will have to be redefined. This can exact a significant performance penalty. How bad is this penalty? Using the following benchmark script, we can use DTrace to sample the Ruby interpreter: ``` Benchmark.bm do |x| x.report do 1000.times do ActionView::CompiledTemplates.module_eval("def testme\nend") end end end ``` This revealed a 11x jump in the time spent in `core#define_method` alone. Rails 6 fixes this behavior by moving the `include CompiledTemplates` into ActionView::Base so that including `ActionView::Context` doesn't quietly affect other modules in this way. Closes https://gitlab.com/gitlab-org/gitlab-ee/issues/11198
2019-04-28 01:25:45 -04:00
include ::Gitlab::ActionViewOutput::Context
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(...), ...).
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
# 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>").
def link_to_html(redacted, url, html_options = {})
fragment = Nokogiri::HTML::DocumentFragment.parse(redacted)
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
end
# 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
fragment.to_html.html_safe
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.
def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
md = markdown_field(object, attribute, options)
return unless md.present?
tags = %w(a gl-emoji b pre code p span)
tags << 'img' if options[:allow_images]
text = truncate_visible(md, max_chars || md.length)
text = sanitize(
text,
tags: tags,
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
%w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title)
)
# 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
end
2017-04-21 11:17:19 -04:00
def markdown(text, context = {})
return '' unless text.present?
context[:project] ||= @project
context[:group] ||= @group
html = markdown_unsafe(text, context)
2017-04-28 10:57:17 -04:00
prepare_for_rendering(html, context)
end
def markdown_field(object, field, context = {})
object = object.for_display if object.respond_to?(:for_display)
redacted_field_html = object.try(:"redacted_#{field}_html")
return '' unless object.present?
return redacted_field_html if redacted_field_html
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)
end
2017-04-21 11:17:19 -04:00
def markup(file_name, text, context = {})
context[:project] ||= @project
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)
end
def render_wiki_content(wiki_page, context = {})
2017-04-21 11:17:19 -04:00
text = wiki_page.content
return '' unless text.present?
context.merge!(
pipeline: :wiki,
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
2018-08-15 16:51:29 -04:00
issuable_state_filter_enabled: true
)
2017-04-21 11:17:19 -04:00
html =
case wiki_page.format
when :markdown
markdown_unsafe(text, context)
when :asciidoc
asciidoc_unsafe(text)
else
wiki_page.formatted_content.html_safe
end
2017-04-28 10:57:17 -04:00
prepare_for_rendering(html, context)
end
2017-04-21 11:17:19 -04:00
def markup_unsafe(file_name, text, context = {})
return '' unless text.present?
2017-04-21 11:17:19 -04:00
if gitlab_markdown?(file_name)
2017-04-28 10:57:17 -04:00
markdown_unsafe(text, context)
elsif asciidoc?(file_name)
asciidoc_unsafe(text, context)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
2017-04-21 11:17:19 -04:00
text
end
else
other_markup_unsafe(file_name, text, context)
end
rescue RuntimeError
2017-04-21 11:17:19 -04:00
simple_format(text)
end
# 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
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
truncated = false
doc.traverse do |node|
if node.text? || node.content.empty?
if truncated
node.remove
next
end
# Handle line breaks within a node
if node.content.strip.lines.length > 1
node.content = "#{node.content.lines.first.chomp}..."
truncated = true
end
num_remaining = max_chars - content_length
if node.content.length > num_remaining
node.content = node.content.truncate(num_remaining)
truncated = true
end
content_length += node.content.length
end
truncated = truncate_if_block(node, truncated)
end
doc.to_html
end
# 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'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling
true
else
truncated
end
end
def strip_empty_link_tags(text)
scrubber = Loofah::Scrubber.new do |node|
node.remove if node.name == 'a' && node.content.blank?
end
sanitize text, scrubber: scrubber
end
2016-06-03 10:14:04 -04:00
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: 'body' })
2016-06-03 10:14:04 -04:00
content_tag :button,
type: 'button',
class: 'toolbar-btn js-md has-tooltip',
2016-06-03 10:14:04 -04:00
tabindex: -1,
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
2017-04-24 05:50:13 -04:00
def markdown_unsafe(text, context = {})
Banzai.render(text, context)
end
def asciidoc_unsafe(text, context = {})
Gitlab::Asciidoc.render(text, context)
2017-04-24 05:50:13 -04:00
end
def other_markup_unsafe(file_name, text, context = {})
Gitlab::OtherMarkup.render(file_name, text, context)
2017-04-24 05:50:13 -04:00
end
2017-04-28 10:57:17 -04:00
def prepare_for_rendering(html, context = {})
return '' unless html.present?
2017-04-21 11:17:19 -04:00
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
commit: @commit,
project_wiki: @project_wiki,
ref: @ref,
requested_path: @path
)
2017-04-28 10:57:17 -04:00
html = Banzai.post_process(html, context)
Hamlit::RailsHelpers.preserve(html)
end
2017-04-26 17:16:36 -04:00
extend self
end