Author: mtredinnick
Date: 2007-02-09 20:51:27 -0600 (Fri, 09 Feb 2007)
New Revision: 4468
Modified:
django/trunk/django/template/defaultfilters.py
django/trunk/django/utils/text.py
django/trunk/tests/regressiontests/defaultfilters/tests.py
Log:
Fixed #2027 -- added truncatewords_html filter that respects HTML tags whilst
truncating. Patch from SmileyChris.
Modified: django/trunk/django/template/defaultfilters.py
===================================================================
--- django/trunk/django/template/defaultfilters.py 2007-02-10 00:55:33 UTC
(rev 4467)
+++ django/trunk/django/template/defaultfilters.py 2007-02-10 02:51:27 UTC
(rev 4468)
@@ -119,6 +119,21 @@
value = str(value)
return truncate_words(value, length)
+def truncatewords_html(value, arg):
+ """
+ Truncates HTML after a certain number of words
+
+ Argument: Number of words to truncate after
+ """
+ from django.utils.text import truncate_html_words
+ try:
+ length = int(arg)
+ except ValueError: # invalid literal for int()
+ return value # Fail silently.
+ if not isinstance(value, basestring):
+ value = str(value)
+ return truncate_html_words(value, length)
+
def upper(value):
"Converts a string into all uppercase"
return value.upper()
@@ -534,6 +549,7 @@
register.filter(timeuntil)
register.filter(title)
register.filter(truncatewords)
+register.filter(truncatewords_html)
register.filter(unordered_list)
register.filter(upper)
register.filter(urlencode)
Modified: django/trunk/django/utils/text.py
===================================================================
--- django/trunk/django/utils/text.py 2007-02-10 00:55:33 UTC (rev 4467)
+++ django/trunk/django/utils/text.py 2007-02-10 02:51:27 UTC (rev 4468)
@@ -41,6 +41,66 @@
words.append('...')
return ' '.join(words)
+def truncate_html_words(s, num):
+ """
+ Truncates html to a certain number of words (not counting tags and
comments).
+ Closes opened tags if they were correctly closed in the given html.
+ """
+ length = int(num)
+ if length <= 0:
+ return ''
+ html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area',
'hr', 'input')
+ # Set up regular expressions
+ re_words = re.compile(r'&.*?;|<.*?>|([A-Za-z0-9][\w-]*)')
+ re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
+ # Count non-HTML words and keep note of open tags
+ pos = 0
+ ellipsis_pos = 0
+ words = 0
+ open_tags = []
+ while words <= length:
+ m = re_words.search(s, pos)
+ if not m:
+ # Checked through whole string
+ break
+ pos = m.end(0)
+ if m.group(1):
+ # It's an actual non-HTML word
+ words += 1
+ if words == length:
+ ellipsis_pos = pos
+ continue
+ # Check for tag
+ tag = re_tag.match(m.group(0))
+ if not tag or ellipsis_pos:
+ # Don't worry about non tags or tags after our truncate point
+ continue
+ closing_tag, tagname, self_closing = tag.groups()
+ tagname = tagname.lower() # Element names are always case-insensitive
+ if self_closing or tagname in html4_singlets:
+ pass
+ elif closing_tag:
+ # Check for match in open tags list
+ try:
+ i = open_tags.index(tagname)
+ except ValueError:
+ pass
+ else:
+ # SGML: An end tag closes, back to the matching start tag, all
unclosed intervening start tags with omitted end tags
+ open_tags = open_tags[i+1:]
+ else:
+ # Add it to the start of the open tags list
+ open_tags.insert(0, tagname)
+ if words <= length:
+ # Don't try to close tags if we don't need to truncate
+ return s
+ out = s[:ellipsis_pos] + ' ...'
+ # Close any tags still open
+ for tag in open_tags:
+ out += '</%s>' % tag
+ # Return string
+ return out
+
def get_valid_filename(s):
"""
Returns the given string converted to a string that can be used for a clean
Modified: django/trunk/tests/regressiontests/defaultfilters/tests.py
===================================================================
--- django/trunk/tests/regressiontests/defaultfilters/tests.py 2007-02-10
00:55:33 UTC (rev 4467)
+++ django/trunk/tests/regressiontests/defaultfilters/tests.py 2007-02-10
02:51:27 UTC (rev 4468)
@@ -87,7 +87,21 @@
>>> truncatewords('A sentence with a few words in it', 'not a number')
'A sentence with a few words in it'
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>',
0)
+''
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>',
2)
+'<p>one <a href="#">two ...</a></p>'
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>',
4)
+'<p>one <a href="#">two - three <br>four ...</a></p>'
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>',
5)
+'<p>one <a href="#">two - three <br>four</a> five</p>'
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>',
100)
+'<p>one <a href="#">two - three <br>four</a> five</p>'
+
>>> upper('Mixed case input')
'MIXED CASE INPUT'
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---