From 5b2a42a091b2300ae1962b158b1496ac160c9e0f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 11 Oct 2014 21:17:02 -0500 Subject: [PATCH 1/2] Preserve link href in truncated note view Notes on the dashboard views are truncated to 150 characters; this change ensures that when a link's text is truncated it still points to the correct URL. --- app/helpers/events_helper.rb | 5 ++-- app/helpers/gitlab_markdown_helper.rb | 43 ++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6aeab7bb8ce..100dde10273 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 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..27d8aee830c 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -51,12 +51,21 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end - def first_line_in_markdown(text) - line = text.split("\n").detect do |i| + # 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) + line = text.split("\n").find do |i| i.present? && markdown(i).present? end - line += '...' unless line.nil? - line + + if line + md = markdown(line) + truncated = truncate_visible(md, max_chars || md.length) + end + + truncated end def render_wiki_content(wiki_page) @@ -204,4 +213,30 @@ 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 + + doc.traverse do |node| + if node.text? || node.content.empty? + if content_length >= max_chars + node.remove + next + end + + num_remaining = max_chars - content_length + if node.content.length > num_remaining + node.content = node.content.truncate(num_remaining) + end + content_length += node.content.length + end + end + + doc.to_html + end end From b3c70d001d7371e8952cd7be879e727b5ee4155a Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 12 Oct 2014 23:07:18 -0500 Subject: [PATCH 2/2] Improve dashboard note view and add tests Update the `#first_line_in_markdown` method so that the first line of parsed text is displayed more reliably, and the continuation indicators ("...") are displayed in all cases where the note is truncated. Also add Rspec tests for `EventsHelper#event_note`. --- app/helpers/events_helper.rb | 2 +- app/helpers/gitlab_markdown_helper.rb | 35 ++++++++++++------ spec/helpers/events_helper_spec.rb | 52 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 spec/helpers/events_helper_spec.rb diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 100dde10273..71f97fbb8c8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -137,7 +137,7 @@ module EventsHelper def event_note(text) text = first_line_in_markdown(text, 150) - sanitize(text, tags: %w(a img b pre p)) + 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 27d8aee830c..7d3cb749829 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -56,16 +56,9 @@ module GitlabMarkdownHelper # +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) - line = text.split("\n").find do |i| - i.present? && markdown(i).present? - end + md = markdown(text).strip - if line - md = markdown(line) - truncated = truncate_visible(md, max_chars || md.length) - end - - truncated + truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) @@ -221,22 +214,44 @@ module GitlabMarkdownHelper 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 content_length >= max_chars + 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