Awesome Truncation in Rails
For a project I’m working on, we’d like to have the rails truncate
helper truncate strings at word boundaries rather than at the in the middle of a word after exactly a certain number of characters.
Daniel Morrison’s awesome_truncate
provides the basic idea, but it has a few problems:
- it’s not compatible with Ruby 1.9
- it doesn’t support the new Rails 2.2 calling convention for
truncate
, in which options are passed as a hash rather than a list - you have to replace your calls to
truncate
with calls toawesome_truncate
In addition, our client asked us to ensure that there is never a single word on a line by itself at the end of the truncated string.
Solution
So, here is the solution I came up with. It overrides the default truncate
method with one which correctly refuses to split words in the middle, which can be called with either hash-style or list-style arguments, which supports three different ways of dealing with multibyte strings, and which supports a new :avoid_orphans
option for avoiding leaving a word on a line by itself.
1 module ActionView::Helpers::TextHelper 2 def truncate(text, *args) 3 options = args.extract_options! 4 5 # support either old or Rails 2.2 calling convention: 6 unless args.empty? 7 options[:length] = args[0] || 30 8 options[:omission] = args[1] || "..." 9 end 10 options.reverse_merge!(:length => 30, :omission => "...") 11 12 # support any of: 13 # * ruby 1.9 sane utf8 support 14 # * rails 2.1 workaround for crappy ruby 1.8 utf8 support 15 # * rails 2.2 workaround for crappy ruby 1.8 utf8 support 16 # hooray! 17 if text 18 chars = if text.respond_to?(:mb_chars) 19 text.mb_chars 20 elsif RUBY_VERSION < '1.9' 21 text.chars 22 else 23 text 24 end 25 26 omission = if options[:omission].respond_to?(:mb_chars) 27 options[:omission].mb_chars 28 elsif RUBY_VERSION < '1.9' 29 options[:omission].chars 30 else 31 options[:omission] 32 end 33 34 l = options[:length] - omission.length 35 if chars.length > options[:length] 36 result = (chars[/\A.{#{l}}\w*\;?/m][/.*[\w\;]/m]).to_s 37 ((options[:avoid_orphans] && result =~ /\A(.*?)\n+\W*\w*\W*\Z/m) ? $1 : result) + options[:omission] 38 else 39 text 40 end 41 end 42 end 43 end
I put this code into RAILS_ROOT/config/initializers/use_awesome_truncate.rb
; this may not be the most sane place for it, but it does work.
Of course, if you don’t want to override the default truncate, you could call the method something else and put it in ApplicationHelper
.
Does this look like too much code for the job? Yes. But most of the code is dedicated to dealing with multibyte string handling, which is unfortunately tricky. Once we get to Ruby 1.9, I understand things will get better, but of course we need to deal with the environment in which our code runs today.
Unit Test
Here is a test for the code, if you’re interested. I put it in test/unit/truncate_test.rb
.
1 require File.dirname(__FILE__) + '/../test_helper' 2 3 class TruncateTest < Test::Unit::TestCase 4 include ActionView::Helpers::TextHelper 5 6 def test_short 7 assert_equal "hello", truncate("hello") 8 assert_equal "hello there this is a test nothing more", truncate("hello there this is a test nothing more", 50) 9 end 10 11 def test_long 12 assert_equal "hello...", truncate("hello there", 3) 13 assert_equal "hello there this is a test nothing...", truncate("hello there this is a test nothing more than that my good man") 14 end 15 16 def test_multibyte 17 assert_equal "ɦɛĺłø...", truncate("ɦɛĺłø ŵőřļđ", 3) 18 assert_equal "ɦɛĺłø ŵőřļđ", truncate("ɦɛĺłø ŵőřļđ", 12) 19 end 20 21 def test_new_calling_convention 22 assert_equal "hello…", truncate("hello world", :length => 3, :omission => "…") 23 assert_equal "hello world", truncate("hello world", :omission => "…") 24 assert_equal "hello...", truncate("hello world", :length => 3) 25 end 26 27 def test_paragraph_truncate 28 assert_equal "This line stands alone.…", truncate("This line stands alone.\nNobody should see this.", :length => 28, :avoid_orphans => true, :omission => "…") 29 end 30 31 def test_multi_paragraph_truncate 32 (18..20).to_a.each do |len| 33 assert_equal "Para 1\n\nPara 2...", truncate("Para 1\n\nPara 2\n\nPara 3", :length => len, :avoid_orphans => true) 34 end 35 assert_equal "Para 1\n\nPara 2\n\nPara 3", truncate("Para 1\n\nPara 2\n\nPara 3", :length => 22, :avoid_orphans => true) 36 end 37 end
Enjoy, and feel free to post below if you have any further improvements or questions.
Update: Would you like to download the code? Yes, you would, it seems. How about the tests, as well?
Posted on Friday the 30th of January 2009 at 3:25 PM
- mlcastle posted this