Loading django/template/defaultfilters.py +17 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,23 @@ def truncatechars(value, arg): return Truncator(value).chars(length) @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) @register.filter(is_safe=True) @stringfilter def truncatewords(value, arg): Loading django/utils/text.py +33 −20 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ capfirst = allow_lazy(capfirst, six.text_type) # 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) re_newlines = re.compile(r'\r\n|\r') # Used in normalize_newlines re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') Loading Loading @@ -82,7 +83,7 @@ class Truncator(SimpleLazyObject): 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. Loading @@ -101,7 +102,15 @@ class Truncator(SimpleLazyObject): 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): Loading @@ -119,7 +128,6 @@ class Truncator(SimpleLazyObject): # Return the original string since no truncation was necessary return text chars = allow_lazy(chars) def words(self, num, truncate=None, html=False): """ Loading @@ -129,7 +137,7 @@ class Truncator(SimpleLazyObject): """ 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) Loading @@ -145,40 +153,45 @@ class Truncator(SimpleLazyObject): 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_words if words else re_chars 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() Loading @@ -199,10 +212,10 @@ class Truncator(SimpleLazyObject): 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 Loading docs/ref/templates/builtins.txt +20 −0 Original line number Diff line number Diff line Loading @@ -2172,6 +2172,26 @@ For example:: If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``. .. templatefilter:: truncatechars_html truncatechars_html ^^^^^^^^^^^^^^^^^^ .. 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 Loading docs/releases/1.7.txt +4 −1 Original line number Diff line number Diff line Loading @@ -657,7 +657,7 @@ 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 Loading @@ -668,6 +668,9 @@ Templates otherwise. It also now accepts an optional ``using`` keyword argument to control which cache it uses. * The new :tfilter:`truncatechars_html` filter truncates a string to be no longer than the specified number of characters, taking HTML into account. Requests ^^^^^^^^ Loading tests/defaultfilters/tests.py +19 −2 Original line number Diff line number Diff line Loading @@ -13,8 +13,8 @@ from django.template.defaultfilters import ( 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.utils import six Loading Loading @@ -195,6 +195,23 @@ class DefaultFiltersTests(TestCase): '¿Cómo está?</i>', 3), '<i>Buenos días! ¿Có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 Loading Loading
django/template/defaultfilters.py +17 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,23 @@ def truncatechars(value, arg): return Truncator(value).chars(length) @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) @register.filter(is_safe=True) @stringfilter def truncatewords(value, arg): Loading
django/utils/text.py +33 −20 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ capfirst = allow_lazy(capfirst, six.text_type) # 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) re_newlines = re.compile(r'\r\n|\r') # Used in normalize_newlines re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') Loading Loading @@ -82,7 +83,7 @@ class Truncator(SimpleLazyObject): 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. Loading @@ -101,7 +102,15 @@ class Truncator(SimpleLazyObject): 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): Loading @@ -119,7 +128,6 @@ class Truncator(SimpleLazyObject): # Return the original string since no truncation was necessary return text chars = allow_lazy(chars) def words(self, num, truncate=None, html=False): """ Loading @@ -129,7 +137,7 @@ class Truncator(SimpleLazyObject): """ 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) Loading @@ -145,40 +153,45 @@ class Truncator(SimpleLazyObject): 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_words if words else re_chars 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() Loading @@ -199,10 +212,10 @@ class Truncator(SimpleLazyObject): 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 Loading
docs/ref/templates/builtins.txt +20 −0 Original line number Diff line number Diff line Loading @@ -2172,6 +2172,26 @@ For example:: If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel i..."``. .. templatefilter:: truncatechars_html truncatechars_html ^^^^^^^^^^^^^^^^^^ .. 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 Loading
docs/releases/1.7.txt +4 −1 Original line number Diff line number Diff line Loading @@ -657,7 +657,7 @@ 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 Loading @@ -668,6 +668,9 @@ Templates otherwise. It also now accepts an optional ``using`` keyword argument to control which cache it uses. * The new :tfilter:`truncatechars_html` filter truncates a string to be no longer than the specified number of characters, taking HTML into account. Requests ^^^^^^^^ Loading
tests/defaultfilters/tests.py +19 −2 Original line number Diff line number Diff line Loading @@ -13,8 +13,8 @@ from django.template.defaultfilters import ( 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.utils import six Loading Loading @@ -195,6 +195,23 @@ class DefaultFiltersTests(TestCase): '¿Cómo está?</i>', 3), '<i>Buenos días! ¿Có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 Loading