2015-05-29 01:56:30 -04:00
|
|
|
require 'nokogiri'
|
|
|
|
|
2012-08-02 20:28:02 -04:00
|
|
|
module GitlabMarkdownHelper
|
2012-08-23 14:10:06 -04:00
|
|
|
# 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>").
|
2012-08-02 20:28:02 -04:00
|
|
|
def link_to_gfm(body, url, html_options = {})
|
2012-09-10 11:32:31 -04:00
|
|
|
return "" if body.blank?
|
2012-09-19 19:42:26 -04:00
|
|
|
|
2015-04-10 12:30:02 -04:00
|
|
|
escaped_body = if body =~ /\A\<img/
|
2013-01-29 04:00:56 -05:00
|
|
|
body
|
|
|
|
else
|
|
|
|
escape_once(body)
|
|
|
|
end
|
|
|
|
|
2015-10-07 11:00:48 -04:00
|
|
|
user = current_user if defined?(current_user)
|
2015-12-15 09:51:16 -05:00
|
|
|
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
|
2012-08-02 20:28:02 -04:00
|
|
|
|
2015-10-01 00:09:07 -04:00
|
|
|
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
|
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
|
|
|
|
# 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
|
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
|
|
|
|
2015-08-27 16:09:01 -04:00
|
|
|
def markdown(text, context = {})
|
2015-10-07 09:17:43 -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
|
2013-03-14 02:31:08 -04:00
|
|
|
|
2016-02-21 19:21:28 -05:00
|
|
|
text = Banzai.pre_process(text, context)
|
|
|
|
|
2015-12-15 09:51:16 -05:00
|
|
|
html = Banzai.render(text, context)
|
2013-03-14 02:31:08 -04:00
|
|
|
|
2015-10-14 13:27:23 -04:00
|
|
|
context.merge!(
|
|
|
|
current_user: (current_user if defined?(current_user)),
|
2015-09-03 16:38:35 -04:00
|
|
|
|
2015-10-14 13:27:23 -04:00
|
|
|
# RelativeLinkFilter
|
|
|
|
requested_path: @path,
|
|
|
|
project_wiki: @project_wiki,
|
|
|
|
ref: @ref
|
2015-08-31 18:09:18 -04:00
|
|
|
)
|
|
|
|
|
2015-12-15 09:51:16 -05:00
|
|
|
Banzai.post_process(html, context)
|
2015-08-27 18:36:56 -04:00
|
|
|
end
|
|
|
|
|
2015-05-12 19:07:48 -04:00
|
|
|
def asciidoc(text)
|
2015-12-14 21:53:52 -05:00
|
|
|
Gitlab::Asciidoc.render(
|
|
|
|
text,
|
2015-10-14 14:18:49 -04:00
|
|
|
project: @project,
|
2016-02-01 11:07:59 -05:00
|
|
|
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,
|
2015-10-14 14:18:49 -04:00
|
|
|
current_user: (current_user if defined?(current_user)),
|
|
|
|
|
|
|
|
# RelativeLinkFilter
|
|
|
|
project_wiki: @project_wiki,
|
2015-05-12 19:07:48 -04:00
|
|
|
requested_path: @path,
|
2015-10-14 14:18:49 -04:00
|
|
|
ref: @ref,
|
|
|
|
commit: @commit
|
|
|
|
)
|
2015-05-12 19:07:48 -04:00
|
|
|
end
|
|
|
|
|
2014-10-11 22:17:02 -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.
|
2015-05-14 07:05:33 -04:00
|
|
|
def first_line_in_markdown(text, max_chars = nil, options = {})
|
|
|
|
md = markdown(text, options).strip
|
2014-10-11 22:17:02 -04:00
|
|
|
|
2014-10-13 00:07:18 -04:00
|
|
|
truncate_visible(md, max_chars || md.length) if md.present?
|
2014-09-12 06:45:00 -04:00
|
|
|
end
|
|
|
|
|
2013-03-14 02:31:08 -04:00
|
|
|
def render_wiki_content(wiki_page)
|
2015-05-12 19:07:48 -04:00
|
|
|
case wiki_page.format
|
|
|
|
when :markdown
|
2016-01-11 23:10:08 -05:00
|
|
|
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
|
2015-05-12 19:07:48 -04:00
|
|
|
when :asciidoc
|
|
|
|
asciidoc(wiki_page.content)
|
2013-03-14 02:31:08 -04:00
|
|
|
else
|
|
|
|
wiki_page.formatted_content.html_safe
|
|
|
|
end
|
|
|
|
end
|
2013-10-08 06:24:50 -04:00
|
|
|
|
2015-06-23 21:00:10 -04:00
|
|
|
MARKDOWN_TIPS = [
|
|
|
|
"End a line with two or more spaces for a line-break, or soft-return",
|
|
|
|
"Inline code can be denoted by `surrounding it with backticks`",
|
|
|
|
"Blocks of code can be denoted by three backticks ``` or four leading spaces",
|
|
|
|
"Emoji can be added by :emoji_name:, for example :thumbsup:",
|
|
|
|
"Notify other participants using @user_name",
|
|
|
|
"Notify a specific group using @group_name",
|
|
|
|
"Notify the entire team using @all",
|
|
|
|
"Reference an issue using a hash, for example issue #123",
|
|
|
|
"Reference a merge request using an exclamation point, for example MR !123",
|
|
|
|
"Italicize words or phrases using *asterisks* or _underscores_",
|
|
|
|
"Bold words or phrases using **double asterisks** or __double underscores__",
|
|
|
|
"Strikethrough words or phrases using ~~two tildes~~",
|
|
|
|
"Make a bulleted list using + pluses, - minuses, or * asterisks",
|
|
|
|
"Denote blockquotes using > at the beginning of a line",
|
|
|
|
"Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___"
|
|
|
|
].freeze
|
|
|
|
|
|
|
|
# Returns a random markdown tip for use as a textarea placeholder
|
|
|
|
def random_markdown_tip
|
2015-07-09 16:38:33 -04:00
|
|
|
MARKDOWN_TIPS.sample
|
2015-06-23 21:00:10 -04:00
|
|
|
end
|
|
|
|
|
2014-10-11 22:17:02 -04:00
|
|
|
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
|
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
|
|
|
|
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)
|
|
|
|
if node.element? && node.description.block? && !truncated
|
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
|
|
|
|
2015-04-20 18:47:22 -04: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
|
2014-10-20 05:56:01 -04:00
|
|
|
def cross_project_reference(project, entity)
|
2015-04-20 18:47:22 -04:00
|
|
|
if entity.respond_to?(:to_reference)
|
|
|
|
"#{project.to_reference}#{entity.to_reference}"
|
2014-10-20 05:56:01 -04:00
|
|
|
else
|
2015-04-20 18:47:22 -04:00
|
|
|
''
|
2014-10-20 05:56:01 -04:00
|
|
|
end
|
|
|
|
end
|
2012-08-02 20:28:02 -04:00
|
|
|
end
|