diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6aeab7bb8ce..71f97fbb8c8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -136,9 +136,8 @@ module EventsHelper end def event_note(text) - text = first_line_in_markdown(text) - text = truncate(text, length: 150) - sanitize(markdown(text), tags: %w(a img b pre p)) + text = first_line_in_markdown(text, 150) + sanitize(text, tags: %w(a img b pre code p)) end def event_commit_title(message) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0365681a128..7d3cb749829 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -51,12 +51,14 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end - def first_line_in_markdown(text) - line = text.split("\n").detect do |i| - i.present? && markdown(i).present? - end - line += '...' unless line.nil? - line + # 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) + md = markdown(text).strip + + truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) @@ -204,4 +206,52 @@ module GitlabMarkdownHelper def correct_ref @ref ? @ref : "master" 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) + if node.element? && node.description.block? && !truncated + node.content = "#{node.content}..." if node.next_sibling + true + else + truncated + end + end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb new file mode 100644 index 00000000000..4de54d291f2 --- /dev/null +++ b/spec/helpers/events_helper_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe EventsHelper do + include ApplicationHelper + include GitlabMarkdownHelper + + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(event_note(input)).to match(input) + expect(event_note(input)).not_to match(/\.\.\.\z/) + end + + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with inline code' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' + + expect(event_note(input)).to match(expected) + end + + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = '
Code block...
' + + expect(event_note(input)).to match(expected) + end + + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = "#{text}#{text}#{text}#{text}" # 200 chars + expected = "#{text}#{text}".sub(/.{3}/, '...') + + expect(event_note(input)).to match(expected) + end + + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...' + + expect(event_note(input)).to match(link_url) + expect(event_note(input)).to match(expected_link_text) + end +end