/apps/typogrify/templatetags/typogrify.py
Python | 325 lines | 289 code | 13 blank | 23 comment | 4 complexity | fd378a5b8180ea570670a102afe21cf1 MD5 | raw file
Possible License(s): BSD-3-Clause
- # -*- coding: utf-8 -*-
- import re
- from django.conf import settings
- from django import template
- from django.utils.safestring import mark_safe
- from django.utils.encoding import force_unicode
- register = template.Library()
- def amp(text):
- """Wraps apersands in HTML with ``<span class="amp">`` so they can be
- styled with CSS. Apersands are also normalized to ``&``. Requires
- ampersands to have whitespace or an `` `` on both sides.
- >>> amp('One & two')
- u'One <span class="amp">&</span> two'
- >>> amp('One & two')
- u'One <span class="amp">&</span> two'
- >>> amp('One & two')
- u'One <span class="amp">&</span> two'
- >>> amp('One & two')
- u'One <span class="amp">&</span> two'
- It won't mess up & that are already wrapped, in entities or URLs
- >>> amp('One <span class="amp">&</span> two')
- u'One <span class="amp">&</span> two'
- >>> amp('“this” & <a href="/?that&test">that</a>')
- u'“this” <span class="amp">&</span> <a href="/?that&test">that</a>'
- It should ignore standalone amps that are in attributes
- >>> amp('<link href="xyz.html" title="One & Two">xyz</link>')
- u'<link href="xyz.html" title="One & Two">xyz</link>'
- """
- text = force_unicode(text)
- # tag_pattern from http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
- # it kinda sucks but it fixes the standalone amps in attributes bug
- tag_pattern = '</?\w+((\s+\w+(\s*=\s*(?:".*?"|\'.*?\'|[^\'">\s]+))?)+\s*|\s*)/?>'
- amp_finder = re.compile(r"(\s| )(&|&|&\#38;)(\s| )")
- intra_tag_finder = re.compile(r'(?P<prefix>(%s)?)(?P<text>([^<]*))(?P<suffix>(%s)?)' % (tag_pattern, tag_pattern))
- def _amp_process(groups):
- prefix = groups.group('prefix') or ''
- text = amp_finder.sub(r"""\1<span class="amp">&</span>\3""", groups.group('text'))
- suffix = groups.group('suffix') or ''
- return prefix + text + suffix
- output = intra_tag_finder.sub(_amp_process, text)
- return mark_safe(output)
- amp.is_safe = True
- def caps(text):
- """Wraps multiple capital letters in ``<span class="caps">``
- so they can be styled with CSS.
- >>> caps("A message from KU")
- u'A message from <span class="caps">KU</span>'
- Uses the smartypants tokenizer to not screw with HTML or with tags it shouldn't.
- >>> caps("<PRE>CAPS</pre> more CAPS")
- u'<PRE>CAPS</pre> more <span class="caps">CAPS</span>'
- >>> caps("A message from 2KU2 with digits")
- u'A message from <span class="caps">2KU2</span> with digits'
- >>> caps("Dotted caps followed by spaces should never include them in the wrap D.O.T. like so.")
- u'Dotted caps followed by spaces should never include them in the wrap <span class="caps">D.O.T.</span> like so.'
- All caps with with apostrophes in them shouldn't break. Only handles dump apostrophes though.
- >>> caps("JIMMY'S")
- u'<span class="caps">JIMMY\\'S</span>'
- >>> caps("<i>D.O.T.</i>HE34T<b>RFID</b>")
- u'<i><span class="caps">D.O.T.</span></i><span class="caps">HE34T</span><b><span class="caps">RFID</span></b>'
- """
- text = force_unicode(text)
- try:
- import smartypants
- except ImportError:
- if settings.DEBUG:
- raise template.TemplateSyntaxError, "Error in {% caps %} filter: The Python SmartyPants library isn't installed."
- return text
- tokens = smartypants._tokenize(text)
- result = []
- in_skipped_tag = False
- cap_finder = re.compile(r"""(
- (\b[A-Z\d]* # Group 2: Any amount of caps and digits
- [A-Z]\d*[A-Z] # A cap string much at least include two caps (but they can have digits between them)
- [A-Z\d']*\b) # Any amount of caps and digits or dumb apostsrophes
- | (\b[A-Z]+\.\s? # OR: Group 3: Some caps, followed by a '.' and an optional space
- (?:[A-Z]+\.\s?)+) # Followed by the same thing at least once more
- (?:\s|\b|$))
- """, re.VERBOSE)
- def _cap_wrapper(matchobj):
- """This is necessary to keep dotted cap strings to pick up extra spaces"""
- if matchobj.group(2):
- return """<span class="caps">%s</span>""" % matchobj.group(2)
- else:
- if matchobj.group(3)[-1] == " ":
- caps = matchobj.group(3)[:-1]
- tail = ' '
- else:
- caps = matchobj.group(3)
- tail = ''
- return """<span class="caps">%s</span>%s""" % (caps, tail)
- tags_to_skip_regex = re.compile("<(/)?(?:pre|code|kbd|script|math)[^>]*>", re.IGNORECASE)
- for token in tokens:
- if token[0] == "tag":
- # Don't mess with tags.
- result.append(token[1])
- close_match = tags_to_skip_regex.match(token[1])
- if close_match and close_match.group(1) == None:
- in_skipped_tag = True
- else:
- in_skipped_tag = False
- else:
- if in_skipped_tag:
- result.append(token[1])
- else:
- result.append(cap_finder.sub(_cap_wrapper, token[1]))
- output = "".join(result)
- return mark_safe(output)
- caps.is_safe = True
- def initial_quotes(text):
- """Wraps initial quotes in ``class="dquo"`` for double quotes or
- ``class="quo"`` for single quotes. Works in these block tags ``(h1-h6, p, li, dt, dd)``
- and also accounts for potential opening inline elements ``a, em, strong, span, b, i``
- >>> initial_quotes('"With primes"')
- u'<span class="dquo">"</span>With primes"'
- >>> initial_quotes("'With single primes'")
- u'<span class="quo">\\'</span>With single primes\\''
- >>> initial_quotes('<a href="#">"With primes and a link"</a>')
- u'<a href="#"><span class="dquo">"</span>With primes and a link"</a>'
- >>> initial_quotes('“With smartypanted quotes”')
- u'<span class="dquo">“</span>With smartypanted quotes”'
- """
- text = force_unicode(text)
- quote_finder = re.compile(r"""((<(p|h[1-6]|li|dt|dd)[^>]*>|^) # start with an opening p, h1-6, li, dd, dt or the start of the string
- \s* # optional white space!
- (<(a|em|span|strong|i|b)[^>]*>\s*)*) # optional opening inline tags, with more optional white space for each.
- (("|“|&\#8220;)|('|‘|&\#8216;)) # Find me a quote! (only need to find the left quotes and the primes)
- # double quotes are in group 7, singles in group 8
- """, re.VERBOSE)
- def _quote_wrapper(matchobj):
- if matchobj.group(7):
- classname = "dquo"
- quote = matchobj.group(7)
- else:
- classname = "quo"
- quote = matchobj.group(8)
- return """%s<span class="%s">%s</span>""" % (matchobj.group(1), classname, quote)
- output = quote_finder.sub(_quote_wrapper, text)
- return mark_safe(output)
- initial_quotes.is_safe = True
- def smartypants(text):
- """Applies smarty pants to curl quotes.
- >>> smartypants('The "Green" man')
- u'The “Green” man'
- """
- text = force_unicode(text)
- try:
- import smartypants
- except ImportError:
- if settings.DEBUG:
- raise template.TemplateSyntaxError, "Error in {% smartypants %} filter: The Python smartypants library isn't installed."
- return text
- else:
- output = smartypants.smartyPants(text)
- return mark_safe(output)
- smartypants.is_safe = True
- def titlecase(text):
- """Support for titlecase.py's titlecasing
- >>> titlecase("this V that")
- u'This v That'
- >>> titlecase("this is just an example.com")
- u'This Is Just an example.com'
- """
- text = force_unicode(text)
- try:
- import titlecase
- except ImportError:
- if settings.DEBUG:
- raise template.TemplateSyntaxError, "Error in {% titlecase %} filter: The titlecase.py library isn't installed."
- return text
- else:
- return titlecase.titlecase(text)
- def typogrify(text):
- """The super typography filter
- Applies the following filters: widont, smartypants, caps, amp, initial_quotes
- >>> typogrify('<h2>"Jayhawks" & KU fans act extremely obnoxiously</h2>')
- u'<h2><span class="dquo">“</span>Jayhawks” <span class="amp">&</span> <span class="caps">KU</span> fans act extremely obnoxiously</h2>'
- Each filters properly handles autoescaping.
- >>> conditional_escape(typogrify('<h2>"Jayhawks" & KU fans act extremely obnoxiously</h2>'))
- u'<h2><span class="dquo">“</span>Jayhawks” <span class="amp">&</span> <span class="caps">KU</span> fans act extremely obnoxiously</h2>'
- """
- text = force_unicode(text)
- text = amp(text)
- text = widont(text)
- text = smartypants(text)
- text = caps(text)
- text = initial_quotes(text)
- text = mdash(text)
- return text
- def widont(text):
- """Replaces the space between the last two words in a string with `` ``
- Works in these block tags ``(h1-h6, p, li, dd, dt)`` and also accounts for
- potential closing inline elements ``a, em, strong, span, b, i``
- >>> widont('A very simple test')
- u'A very simple test'
- Single word items shouldn't be changed
- >>> widont('Test')
- u'Test'
- >>> widont(' Test')
- u' Test'
- >>> widont('<ul><li>Test</p></li><ul>')
- u'<ul><li>Test</p></li><ul>'
- >>> widont('<ul><li> Test</p></li><ul>')
- u'<ul><li> Test</p></li><ul>'
- >>> widont('<p>In a couple of paragraphs</p><p>paragraph two</p>')
- u'<p>In a couple of paragraphs</p><p>paragraph two</p>'
- >>> widont('<h1><a href="#">In a link inside a heading</i> </a></h1>')
- u'<h1><a href="#">In a link inside a heading</i> </a></h1>'
- >>> widont('<h1><a href="#">In a link</a> followed by other text</h1>')
- u'<h1><a href="#">In a link</a> followed by other text</h1>'
- Empty HTMLs shouldn't error
- >>> widont('<h1><a href="#"></a></h1>')
- u'<h1><a href="#"></a></h1>'
- >>> widont('<div>Divs get no love!</div>')
- u'<div>Divs get no love!</div>'
- >>> widont('<pre>Neither do PREs</pre>')
- u'<pre>Neither do PREs</pre>'
- >>> widont('<div><p>But divs with paragraphs do!</p></div>')
- u'<div><p>But divs with paragraphs do!</p></div>'
- """
- text = force_unicode(text)
- widont_finder = re.compile(r"""((?:</?(?:a|em|span|strong|i|b)[^>]*>)|[^<>\s]) # must be preceded by an approved inline opening or closing tag or a nontag/nonspace
- \s+ # the space to replace
- ([^<>\s]+ # must be followed by non-tag non-space characters
- \s* # optional white space!
- (</(a|em|span|strong|i|b)>\s*)* # optional closing inline tags with optional white space after each
- ((</(p|h[1-6]|li|dt|dd)>)|$)) # end with a closing p, h1-6, li or the end of the string
- """, re.VERBOSE)
- output = widont_finder.sub(r'\1 \2', text)
- return mark_safe(output)
- widont.is_safe = True
- def mdash(text):
- mdash_finder = re.compile(r"([^\.\!\?]\s)\-(\s[^\.\!\?])")
- return mark_safe(mdash_finder.sub(r'\1—\2', text))
- mdash.is_safe = True
- register.filter('amp', amp)
- register.filter('caps', caps)
- register.filter('initial_quotes', initial_quotes)
- register.filter('smartypants', smartypants)
- register.filter('titlecase', titlecase)
- register.filter('typogrify', typogrify)
- register.filter('widont', widont)
- register.filter('mdash', mdash)
- def _test():
- import doctest
- doctest.testmod()
- if __name__ == "__main__":
- _test()
- def check_letters(first, second):
- if first in u'CKz' or first in u'КС': # lat, cyr
- return '0'
- if first in u'Г':
- return '-0.25'
- elif first in u'T' or first in u'Т':
- return '-0.15'
- elif first in u'FPYW' or first in u'РУ':
- if second in u'iktl' or second in u'ёйб' or second.upper() == second:
- return '-0.1'
- return '-0.15'
- return '-.05'
- @register.filter
- def title_tracking(value):
- '''
- Set tracking for first letter of phrase. Decides by checking
- first and second letters by known patterns.
- Thinks that first letter is very big (200%).
- '''
- first, second = value[:2]
- width = check_letters(first, second)
- first = (u'<span class="first" style="letter-spacing:'
- '%sem">%s</span>' % (width, first))
- return mark_safe(first + value[1:])
- title_tracking.is_safe = True