app/django/utils/html.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     1 """HTML utilities suitable for global use."""
       
     2 
       
     3 import re
       
     4 import string
       
     5 
       
     6 from django.utils.safestring import SafeData, mark_safe
       
     7 from django.utils.encoding import force_unicode
       
     8 from django.utils.functional import allow_lazy
       
     9 from django.utils.http import urlquote
       
    10 
       
    11 # Configuration for urlize() function.
       
    12 LEADING_PUNCTUATION  = ['(', '<', '&lt;']
       
    13 TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;']
       
    14 
       
    15 # List of possible strings used for bullets in bulleted lists.
       
    16 DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
       
    17 
       
    18 unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
       
    19 word_split_re = re.compile(r'(\s+)')
       
    20 punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
       
    21     ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
       
    22     '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
       
    23 simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
       
    24 link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
       
    25 html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
       
    26 hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
       
    27 trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
       
    28 del x # Temporary variable
       
    29 
       
    30 def escape(html):
       
    31     """Returns the given HTML with ampersands, quotes and carets encoded."""
       
    32     return mark_safe(force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
       
    33 escape = allow_lazy(escape, unicode)
       
    34 
       
    35 def conditional_escape(html):
       
    36     """
       
    37     Similar to escape(), except that it doesn't operate on pre-escaped strings.
       
    38     """
       
    39     if isinstance(html, SafeData):
       
    40         return html
       
    41     else:
       
    42         return escape(html)
       
    43 
       
    44 def linebreaks(value, autoescape=False):
       
    45     """Converts newlines into <p> and <br />s."""
       
    46     value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines
       
    47     paras = re.split('\n{2,}', value)
       
    48     if autoescape:
       
    49         paras = [u'<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras]
       
    50     else:
       
    51         paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
       
    52     return u'\n\n'.join(paras)
       
    53 linebreaks = allow_lazy(linebreaks, unicode)
       
    54 
       
    55 def strip_tags(value):
       
    56     """Returns the given HTML with all tags stripped."""
       
    57     return re.sub(r'<[^>]*?>', '', force_unicode(value))
       
    58 strip_tags = allow_lazy(strip_tags)
       
    59 
       
    60 def strip_spaces_between_tags(value):
       
    61     """Returns the given HTML with spaces between tags removed."""
       
    62     return re.sub(r'>\s+<', '><', force_unicode(value))
       
    63 strip_spaces_between_tags = allow_lazy(strip_spaces_between_tags, unicode)
       
    64 
       
    65 def strip_entities(value):
       
    66     """Returns the given HTML with all entities (&something;) stripped."""
       
    67     return re.sub(r'&(?:\w+|#\d+);', '', force_unicode(value))
       
    68 strip_entities = allow_lazy(strip_entities, unicode)
       
    69 
       
    70 def fix_ampersands(value):
       
    71     """Returns the given HTML with all unencoded ampersands encoded correctly."""
       
    72     return unencoded_ampersands_re.sub('&amp;', force_unicode(value))
       
    73 fix_ampersands = allow_lazy(fix_ampersands, unicode)
       
    74 
       
    75 def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
       
    76     """
       
    77     Converts any URLs in text into clickable links.
       
    78 
       
    79     Works on http://, https://, and www. links.  Links can have trailing
       
    80     punctuation (periods, commas, close-parens) and leading punctuation
       
    81     (opening parens) and it'll still do the right thing.
       
    82 
       
    83     If trim_url_limit is not None, the URLs in link text longer than this limit
       
    84     will truncated to trim_url_limit-3 characters and appended with an elipsis.
       
    85 
       
    86     If nofollow is True, the URLs in link text will get a rel="nofollow"
       
    87     attribute.
       
    88     """
       
    89     if autoescape:
       
    90         trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x)
       
    91     else:
       
    92         trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
       
    93     safe_input = isinstance(text, SafeData)
       
    94     words = word_split_re.split(force_unicode(text))
       
    95     nofollow_attr = nofollow and ' rel="nofollow"' or ''
       
    96     for i, word in enumerate(words):
       
    97         match = punctuation_re.match(word)
       
    98         if match:
       
    99             lead, middle, trail = match.groups()
       
   100             if safe_input:
       
   101                 middle = mark_safe(middle)
       
   102             if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \
       
   103                     len(middle) > 0 and middle[0] in string.ascii_letters + string.digits and \
       
   104                     (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
       
   105                 middle = 'http://%s' % middle
       
   106             if middle.startswith('http://') or middle.startswith('https://'):
       
   107                 url = urlquote(middle, safe='/&=:;#?+*')
       
   108                 if autoescape and not safe_input:
       
   109                     url = escape(url)
       
   110                 trimmed_url = trim_url(middle)
       
   111                 middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr,
       
   112                         trimmed_url)
       
   113             elif '@' in middle and not middle.startswith('www.') and \
       
   114                       not ':' in middle and simple_email_re.match(middle):
       
   115                 if autoescape:
       
   116                     middle = conditional_escape(middle)
       
   117                 middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
       
   118             if lead + middle + trail != word:
       
   119                 if autoescape and not safe_input:
       
   120                     lead, trail = escape(lead), escape(trail)
       
   121                 words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
       
   122             elif autoescape and not safe_input:
       
   123                 words[i] = escape(word)
       
   124         elif safe_input:
       
   125             words[i] = mark_safe(word)
       
   126         elif autoescape:
       
   127             words[i] = escape(word)
       
   128     return u''.join(words)
       
   129 urlize = allow_lazy(urlize, unicode)
       
   130 
       
   131 def clean_html(text):
       
   132     """
       
   133     Clean the given HTML.  Specifically, do the following:
       
   134         * Convert <b> and <i> to <strong> and <em>.
       
   135         * Encode all ampersands correctly.
       
   136         * Remove all "target" attributes from <a> tags.
       
   137         * Remove extraneous HTML, such as presentational tags that open and
       
   138           immediately close and <br clear="all">.
       
   139         * Convert hard-coded bullets into HTML unordered lists.
       
   140         * Remove stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the
       
   141           bottom of the text.
       
   142     """
       
   143     from django.utils.text import normalize_newlines
       
   144     text = normalize_newlines(force_unicode(text))
       
   145     text = re.sub(r'<(/?)\s*b\s*>', '<\\1strong>', text)
       
   146     text = re.sub(r'<(/?)\s*i\s*>', '<\\1em>', text)
       
   147     text = fix_ampersands(text)
       
   148     # Remove all target="" attributes from <a> tags.
       
   149     text = link_target_attribute_re.sub('\\1', text)
       
   150     # Trim stupid HTML such as <br clear="all">.
       
   151     text = html_gunk_re.sub('', text)
       
   152     # Convert hard-coded bullets into HTML unordered lists.
       
   153     def replace_p_tags(match):
       
   154         s = match.group().replace('</p>', '</li>')
       
   155         for d in DOTS:
       
   156             s = s.replace('<p>%s' % d, '<li>')
       
   157         return u'<ul>\n%s\n</ul>' % s
       
   158     text = hard_coded_bullets_re.sub(replace_p_tags, text)
       
   159     # Remove stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the bottom
       
   160     # of the text.
       
   161     text = trailing_empty_content_re.sub('', text)
       
   162     return text
       
   163 clean_html = allow_lazy(clean_html, unicode)