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
-~----------~----~----~----~------~----~------~--~---

Reply via email to