Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Fixed #19496 -- Added truncatechars_html filter. #1689

Closed
wants to merge 12 commits into from

3 participants

@saturn597

truncatechars_html was incorrectly adding the '...' to text under the truncate length but only by 3 or fewer characters.

>>> truncatechars_html('abcd', 4)
u'a...'

It now correctly returns:

>>> truncatechars_html('abcd', 4)
u'abcd'
@timgraham timgraham referenced this pull request
Closed

truncatechars_html #1126

docs/releases/1.6.txt
@@ -372,6 +372,11 @@ Minor features
default value.
+* ``truncatechars_html`` template filter. This new filter truncates a string
@timgraham Owner

1.6 is feature frozen, so this should be moved to the 1.7 release notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
docs/ref/templates/builtins.txt
@@ -2143,6 +2143,26 @@ If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``.
.. templatefilter:: truncatewords
@timgraham Owner

there's an extra heading for truncatewords here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@timgraham timgraham commented on the diff
docs/ref/templates/builtins.txt
@@ -2143,6 +2143,26 @@ If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``.
.. templatefilter:: truncatewords
+.. templatefilter:: truncatechars_html
+
+truncatechars_html
+^^^^^^^^^^^^^^^^^^
+
@timgraham Owner

needs .. versionadded:: 1.7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/utils/text.py
@@ -115,7 +123,73 @@ def chars(self, num, truncate=None):
# Return the original string since no truncation was necessary
return text
- chars = allow_lazy(chars)
+
+ def _html_chars(self, length, truncate, text, truncate_len):
@timgraham Owner

could this be refactored so there's less duplicated logic between _html_chars and _html_words, please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@timgraham
Owner

merged in f94f466, thanks.

@timgraham timgraham closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 10, 2013
  1. @slurms

    Add truncatechars_html filter.

    slurms authored
Commits on May 18, 2013
  1. truncatechars_html now working

    Martin Warne authored
  2. Merge branch 'master' of github.com:martmatwarne/django

    Martin Warne authored
    Conflicts:
    	docs/releases/1.6.txt
Commits on Sep 25, 2013
  1. @saturn597
Commits on Sep 29, 2013
  1. @saturn597

    Fixed 19496

    saturn597 authored
    truncatechars_html was incorrectly adding the '...' to text under the
    truncate length but only by 3 or fewer characters.
  2. @saturn597
Commits on Oct 9, 2013
  1. @saturn597
Commits on Nov 14, 2013
  1. @saturn597

    Fixed #19496 -- removed duplicate truncation logic

    saturn597 authored
    Collapsed _html_words and _html_chars into a single function,
    _truncate_html.
  2. @saturn597
  3. @saturn597

    Merge branch 'master' of https://github.com/django/django into 19496

    saturn597 authored
    Conflicts:
    	django/utils/text.py
    	docs/releases/1.6.txt
    	docs/releases/1.7.txt
  4. @saturn597
  5. @saturn597

    Minor fix to tests.

    saturn597 authored
This page is out of date. Refresh to see the latest.
View
16 django/template/defaultfilters.py
@@ -281,6 +281,22 @@ def truncatechars(value, arg):
@register.filter(is_safe=True)
@stringfilter
+def truncatechars_html(value, arg):
+ """
+ Truncates HTML after a certain number of chars.
+
+ Argument: Number of chars to truncate after.
+
+ Newlines in the HTML are preserved.
+ """
+ try:
+ length = int(arg)
+ except ValueError: # invalid literal for int()
+ return value # Fail silently.
+ return Truncator(value).chars(length, html=True, truncate='...')
+
+@register.filter(is_safe=True)
+@stringfilter
def truncatewords(value, arg):
"""
Truncates a string after a certain number of words.
View
55 django/utils/text.py
@@ -23,6 +23,7 @@
# Set up regular expressions
re_words = re.compile(r'<.*?>|((?:\w[-\w]*|&.*?;)+)', re.U | re.S)
+re_chars = re.compile(r'<.*?>|(.)', re.U | re.S)
re_tag = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S)
@@ -79,7 +80,7 @@ def add_truncation_text(self, text, truncate=None):
return text
return '%s%s' % (text, truncate)
- def chars(self, num, truncate=None):
+ def chars(self, num, truncate=None, html=False):
"""
Returns the text truncated to be no longer than the specified number
of characters.
@@ -98,7 +99,15 @@ def chars(self, num, truncate=None):
truncate_len -= 1
if truncate_len == 0:
break
+ if html:
+ return self._truncate_html(length, truncate, text, truncate_len, False)
+ return self._text_chars(length, truncate, text, truncate_len)
+ chars = allow_lazy(chars)
+ def _text_chars(self, length, truncate, text, truncate_len):
+ """
+ Truncates a string after a certain number of chars.
+ """
s_len = 0
end_index = None
for i, char in enumerate(text):
@@ -116,7 +125,6 @@ def chars(self, num, truncate=None):
# Return the original string since no truncation was necessary
return text
- chars = allow_lazy(chars)
def words(self, num, truncate=None, html=False):
"""
@@ -126,7 +134,7 @@ def words(self, num, truncate=None, html=False):
"""
length = int(num)
if html:
- return self._html_words(length, truncate)
+ return self._truncate_html(length, truncate, self._wrapped, length, True)
return self._text_words(length, truncate)
words = allow_lazy(words)
@@ -142,40 +150,47 @@ def _text_words(self, length, truncate):
return self.add_truncation_text(' '.join(words), truncate)
return ' '.join(words)
- def _html_words(self, length, truncate):
+ def _truncate_html(self, length, truncate, text, truncate_len, words):
"""
- 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.
+ Truncates HTML to a certain number of chars (not counting tags and
+ comments), or, if words is True, then to a certain number of words.
+ Closes opened tags if they were correctly closed in the given HTML.
Newlines in the HTML are preserved.
"""
- if length <= 0:
+ if words and length <= 0:
return ''
+
html4_singlets = (
'br', 'col', 'link', 'base', 'img',
'param', 'area', 'hr', 'input'
)
- # Count non-HTML words and keep note of open tags
+
+ # Count non-HTML chars/words and keep note of open tags
pos = 0
end_text_pos = 0
- words = 0
+ current_len = 0
open_tags = []
- while words <= length:
- m = re_words.search(self._wrapped, pos)
+
+ regex = re_chars
+ if words:
+ regex = re_words
+
+ while current_len <= length:
+ m = regex.search(text, 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:
+ # It's an actual non-HTML word or char
+ current_len += 1
+ if current_len == truncate_len:
end_text_pos = pos
continue
# Check for tag
tag = re_tag.match(m.group(0))
- if not tag or end_text_pos:
+ if not tag or current_len >= truncate_len:
# Don't worry about non tags or tags after our truncate point
continue
closing_tag, tagname, self_closing = tag.groups()
@@ -196,10 +211,10 @@ def _html_words(self, length, truncate):
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 self._wrapped
- out = self._wrapped[:end_text_pos]
+
+ if current_len <= length:
+ return text
+ out = text[:end_text_pos]
truncate_text = self.add_truncation_text('', truncate)
if truncate_text:
out += truncate_text
View
20 docs/ref/templates/builtins.txt
@@ -2164,6 +2164,26 @@ For example::
If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``.
+.. templatefilter:: truncatechars_html
+
+truncatechars_html
+^^^^^^^^^^^^^^^^^^
+
@timgraham Owner

needs .. versionadded:: 1.7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+.. versionadded:: 1.7
+
+Similar to :tfilter:`truncatechars`, except that it is aware of HTML tags. Any
+tags that are opened in the string and not closed before the truncation point,
+are closed immediately after the truncation.
+
+For example::
+
+ {{ value|truncatechars_html:9 }}
+
+If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
+``"<p>Joel i...</p>"``.
+
+Newlines in the HTML content will be preserved.
+
.. templatefilter:: truncatewords
truncatewords
View
7 docs/releases/1.7.txt
@@ -455,12 +455,17 @@ Templates
* :func:`django.shortcuts.render()`
* :func:`django.shortcuts.render_to_response()`
-* The :tfilter:`time` filter now accepts timzone-related :ref:`format
+* The :tfilter:`time` filter now accepts timezone-related :ref:`format
specifiers <date-and-time-formatting-specifiers>` ``'e'``, ``'O'`` , ``'T'``
and ``'Z'`` and is able to digest :ref:`time-zone-aware
<naive_vs_aware_datetimes>` ``datetime`` instances performing the expected
rendering.
+* A new filter, ``truncatechars_html``, has been added. It truncates a string
+ to be no longer than the specified number of characters, taking HTML into
+ account. Truncated strings end with a translatable ellipsis sequence ("...").
+ See the documentation for :tfilter:`truncatechars_html` for more details.
+
* The :ttag:`cache` tag will now try to use the cache called
"template_fragments" if it exists and fall back to using the default cache
otherwise. It also now accepts an optional ``using`` keyword argument to
View
21 tests/defaultfilters/tests.py
@@ -13,8 +13,8 @@
linebreaks_filter, linenumbers, ljust, lower, make_list,
phone2numeric_filter, pluralize, removetags, rjust, slice_filter, slugify,
stringformat, striptags, time, timesince_filter, timeuntil_filter, title,
- truncatewords, truncatewords_html, unordered_list, upper, urlencode,
- urlize, urlizetrunc, wordcount, wordwrap, yesno,
+ truncatechars_html, truncatewords, truncatewords_html, unordered_list,
+ upper, urlencode, urlize, urlizetrunc, wordcount, wordwrap, yesno,
)
from django.test import TestCase
from django.test.utils import TransRealMixin
@@ -196,6 +196,23 @@ def test_truncatewords_html(self):
'&#x00bf;C&oacute;mo est&aacute;?</i>', 3),
'<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo ...</i>')
+ def test_truncatechars_html(self):
+ self.assertEqual(truncatechars_html(
+ '<p>one <a href="#">two - three <br>four</a> five</p>', 0), '...')
+ self.assertEqual(truncatechars_html('<p>one <a href="#">two - '\
+ 'three <br>four</a> five</p>', 6),
+ '<p>one...</p>')
+ self.assertEqual(truncatechars_html(
+ '<p>one <a href="#">two - three <br>four</a> five</p>', 11),
+ '<p>one <a href="#">two ...</a></p>')
+ self.assertEqual(truncatechars_html(
+ '<p>one <a href="#">two - three <br>four</a> five</p>', 100),
+ '<p>one <a href="#">two - three <br>four</a> five</p>')
+ self.assertEqual(truncatechars_html(
+ '<b>\xc5ngstr\xf6m</b> was here', 5), '<b>\xc5n...</b>')
+ self.assertEqual(truncatechars_html(
+ 'a<b>b</b>c', 3), 'a<b>b</b>c')
+
def test_upper(self):
self.assertEqual(upper('Mixed case input'), 'MIXED CASE INPUT')
# lowercase e umlaut
Something went wrong with that request. Please try again.