# HG changeset patch # User neko259 # Date 2015-02-27 20:29:13 # Node ID 328430277e12e0c6efa8504f5f6a101f945efd17 # Parent defa7113e2f3304e389dafe523890e4942d531da Truncate posts by lines diff --git a/boards/templates/boards/post.html b/boards/templates/boards/post.html --- a/boards/templates/boards/post.html +++ b/boards/templates/boards/post.html @@ -63,7 +63,7 @@
{% autoescape off %} {% if truncated %} - {{ post.get_text|truncatewords_html:50 }} + {{ post.get_text|truncatewords_html:50|truncate_lines:3 }} {% else %} {{ post.get_text }} {% endif %} diff --git a/boards/templatetags/board.py b/boards/templatetags/board.py --- a/boards/templatetags/board.py +++ b/boards/templatetags/board.py @@ -1,6 +1,12 @@ +import re from django.shortcuts import get_object_or_404 from django import template +ELLIPSIZER = '...' + +REGEX_LINES = re.compile(r'(
)', re.U | re.S) +REGEX_TAG = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) + register = template.Library() @@ -72,3 +78,68 @@ def post_view(post, moderator=False, nee 'truncated': truncated, 'opening_post_id': opening_post_id, } + + +@register.filter(is_safe=True) +def truncate_lines(text, length): + if length <= 0: + return '' + + html4_singlets = ( + 'br', 'col', 'link', 'base', 'img', + 'param', 'area', 'hr', 'input' + ) + + # Count non-HTML chars/words and keep note of open tags + pos = 0 + end_text_pos = 0 + current_len = 0 + open_tags = [] + + while current_len <= length: + m = REGEX_LINES.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 or char + current_len += 1 + if current_len == length: + end_text_pos = pos + continue + # Check for tag + tag = REGEX_TAG.match(m.group(0)) + if not tag or current_len >= length: + # Don't worry about non tags or tags after our truncate point + continue + closing_tag, tagname, self_closing = tag.groups() + # Element names are always case-insensitive + tagname = tagname.lower() + 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 current_len <= length: + return text + out = text[:end_text_pos] + + if not out.endswith(ELLIPSIZER): + out += ELLIPSIZER + # Close any tags still open + for tag in open_tags: + out += '' % tag + # Return string + return out