Fixed #6799 - added an `end_text` argument to `truncate_words`/`truncate_html_words`.

This allows customizing the standard "..." end text. Yes, this is technically a
feature sneaking in after the deadline, but I just couldn't bring myself to punt
it again: we already used that excuse for not getting it into 1.1.

Thanks to Adam Fast and Travis Cline for work on this patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12431 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2010-02-14 18:36:48 +00:00
parent 7578981626
commit 1d078be448
2 changed files with 35 additions and 15 deletions

View File

@ -1,5 +1,4 @@
import re import re
from django.conf import settings
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
from django.utils.functional import allow_lazy from django.utils.functional import allow_lazy
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
@ -37,24 +36,25 @@ def wrap(text, width):
return u''.join(_generator()) return u''.join(_generator())
wrap = allow_lazy(wrap, unicode) wrap = allow_lazy(wrap, unicode)
def truncate_words(s, num): def truncate_words(s, num, end_text='...'):
"Truncates a string after a certain number of words." """Truncates a string after a certain number of words. Takes an optional
argument of what should be used to notify that the string has been
truncated, defaults to ellipsis (...)"""
s = force_unicode(s) s = force_unicode(s)
length = int(num) length = int(num)
words = s.split() words = s.split()
if len(words) > length: if len(words) > length:
words = words[:length] words = words[:length]
if not words[-1].endswith('...'): if not words[-1].endswith(end_text):
words.append('...') words.append(end_text)
return u' '.join(words) return u' '.join(words)
truncate_words = allow_lazy(truncate_words, unicode) truncate_words = allow_lazy(truncate_words, unicode)
def truncate_html_words(s, num): def truncate_html_words(s, num, end_text='...'):
""" """Truncates html to a certain number of words (not counting tags and
Truncates html to a certain number of words (not counting tags and
comments). Closes opened tags if they were correctly closed in the given comments). Closes opened tags if they were correctly closed in the given
html. html. Takes an optional argument of what should be used to notify that the
""" string has been truncated, defaults to ellipsis (...)."""
s = force_unicode(s) s = force_unicode(s)
length = int(num) length = int(num)
if length <= 0: if length <= 0:
@ -65,7 +65,7 @@ def truncate_html_words(s, num):
re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>') re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
# Count non-HTML words and keep note of open tags # Count non-HTML words and keep note of open tags
pos = 0 pos = 0
ellipsis_pos = 0 end_text_pos = 0
words = 0 words = 0
open_tags = [] open_tags = []
while words <= length: while words <= length:
@ -78,11 +78,11 @@ def truncate_html_words(s, num):
# It's an actual non-HTML word # It's an actual non-HTML word
words += 1 words += 1
if words == length: if words == length:
ellipsis_pos = pos end_text_pos = pos
continue continue
# Check for tag # Check for tag
tag = re_tag.match(m.group(0)) tag = re_tag.match(m.group(0))
if not tag or ellipsis_pos: if not tag or end_text_pos:
# Don't worry about non tags or tags after our truncate point # Don't worry about non tags or tags after our truncate point
continue continue
closing_tag, tagname, self_closing = tag.groups() closing_tag, tagname, self_closing = tag.groups()
@ -104,7 +104,9 @@ def truncate_html_words(s, num):
if words <= length: if words <= length:
# Don't try to close tags if we don't need to truncate # Don't try to close tags if we don't need to truncate
return s return s
out = s[:ellipsis_pos] + ' ...' out = s[:end_text_pos]
if end_text:
out += ' ' + end_text
# Close any tags still open # Close any tags still open
for tag in open_tags: for tag in open_tags:
out += '</%s>' % tag out += '</%s>' % tag

View File

@ -4,7 +4,7 @@ Tests for django.utils.
from unittest import TestCase from unittest import TestCase
from django.utils import html, checksums from django.utils import html, checksums, text
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
import timesince import timesince
@ -244,6 +244,24 @@ class TestUtilsSimpleLazyObject(TestCase):
s3 = copy.deepcopy(s) s3 = copy.deepcopy(s)
self.assertEqual(s3, complex_object()) self.assertEqual(s3, complex_object())
class TestUtilsText(TestCase):
def test_truncate_words(self):
self.assertEqual(u'The quick brown fox jumped over the lazy dog.',
text.truncate_words(u'The quick brown fox jumped over the lazy dog.', 10))
self.assertEqual(u'The quick brown fox ...',
text.truncate_words('The quick brown fox jumped over the lazy dog.', 4))
self.assertEqual(u'The quick brown fox ....',
text.truncate_words('The quick brown fox jumped over the lazy dog.', 4, '....'))
self.assertEqual(u'<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>',
text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 10))
self.assertEqual(u'<p><strong><em>The quick brown fox ...</em></strong></p>',
text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4))
self.assertEqual(u'<p><strong><em>The quick brown fox ....</em></strong></p>',
text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4, '....'))
self.assertEqual(u'<p><strong><em>The quick brown fox</em></strong></p>',
text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4, None))
if __name__ == "__main__": if __name__ == "__main__":
import doctest import doctest
doctest.testmod() doctest.testmod()