PageRenderTime 50ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/typogrify/templatetags/typogrify.py

https://bitbucket.org/resplin/byteflow
Python | 325 lines | 289 code | 13 blank | 23 comment | 4 complexity | fd378a5b8180ea570670a102afe21cf1 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. # -*- coding: utf-8 -*-
  2. import re
  3. from django.conf import settings
  4. from django import template
  5. from django.utils.safestring import mark_safe
  6. from django.utils.encoding import force_unicode
  7. register = template.Library()
  8. def amp(text):
  9. """Wraps apersands in HTML with ``<span class="amp">`` so they can be
  10. styled with CSS. Apersands are also normalized to ``&amp;``. Requires
  11. ampersands to have whitespace or an ``&nbsp;`` on both sides.
  12. >>> amp('One & two')
  13. u'One <span class="amp">&amp;</span> two'
  14. >>> amp('One &amp; two')
  15. u'One <span class="amp">&amp;</span> two'
  16. >>> amp('One &#38; two')
  17. u'One <span class="amp">&amp;</span> two'
  18. >>> amp('One&nbsp;&amp;&nbsp;two')
  19. u'One&nbsp;<span class="amp">&amp;</span>&nbsp;two'
  20. It won't mess up & that are already wrapped, in entities or URLs
  21. >>> amp('One <span class="amp">&amp;</span> two')
  22. u'One <span class="amp">&amp;</span> two'
  23. >>> amp('&ldquo;this&rdquo; & <a href="/?that&amp;test">that</a>')
  24. u'&ldquo;this&rdquo; <span class="amp">&amp;</span> <a href="/?that&amp;test">that</a>'
  25. It should ignore standalone amps that are in attributes
  26. >>> amp('<link href="xyz.html" title="One & Two">xyz</link>')
  27. u'<link href="xyz.html" title="One & Two">xyz</link>'
  28. """
  29. text = force_unicode(text)
  30. # tag_pattern from http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
  31. # it kinda sucks but it fixes the standalone amps in attributes bug
  32. tag_pattern = '</?\w+((\s+\w+(\s*=\s*(?:".*?"|\'.*?\'|[^\'">\s]+))?)+\s*|\s*)/?>'
  33. amp_finder = re.compile(r"(\s|&nbsp;)(&|&amp;|&\#38;)(\s|&nbsp;)")
  34. intra_tag_finder = re.compile(r'(?P<prefix>(%s)?)(?P<text>([^<]*))(?P<suffix>(%s)?)' % (tag_pattern, tag_pattern))
  35. def _amp_process(groups):
  36. prefix = groups.group('prefix') or ''
  37. text = amp_finder.sub(r"""\1<span class="amp">&amp;</span>\3""", groups.group('text'))
  38. suffix = groups.group('suffix') or ''
  39. return prefix + text + suffix
  40. output = intra_tag_finder.sub(_amp_process, text)
  41. return mark_safe(output)
  42. amp.is_safe = True
  43. def caps(text):
  44. """Wraps multiple capital letters in ``<span class="caps">``
  45. so they can be styled with CSS.
  46. >>> caps("A message from KU")
  47. u'A message from <span class="caps">KU</span>'
  48. Uses the smartypants tokenizer to not screw with HTML or with tags it shouldn't.
  49. >>> caps("<PRE>CAPS</pre> more CAPS")
  50. u'<PRE>CAPS</pre> more <span class="caps">CAPS</span>'
  51. >>> caps("A message from 2KU2 with digits")
  52. u'A message from <span class="caps">2KU2</span> with digits'
  53. >>> caps("Dotted caps followed by spaces should never include them in the wrap D.O.T. like so.")
  54. u'Dotted caps followed by spaces should never include them in the wrap <span class="caps">D.O.T.</span> like so.'
  55. All caps with with apostrophes in them shouldn't break. Only handles dump apostrophes though.
  56. >>> caps("JIMMY'S")
  57. u'<span class="caps">JIMMY\\'S</span>'
  58. >>> caps("<i>D.O.T.</i>HE34T<b>RFID</b>")
  59. u'<i><span class="caps">D.O.T.</span></i><span class="caps">HE34T</span><b><span class="caps">RFID</span></b>'
  60. """
  61. text = force_unicode(text)
  62. try:
  63. import smartypants
  64. except ImportError:
  65. if settings.DEBUG:
  66. raise template.TemplateSyntaxError, "Error in {% caps %} filter: The Python SmartyPants library isn't installed."
  67. return text
  68. tokens = smartypants._tokenize(text)
  69. result = []
  70. in_skipped_tag = False
  71. cap_finder = re.compile(r"""(
  72. (\b[A-Z\d]* # Group 2: Any amount of caps and digits
  73. [A-Z]\d*[A-Z] # A cap string much at least include two caps (but they can have digits between them)
  74. [A-Z\d']*\b) # Any amount of caps and digits or dumb apostsrophes
  75. | (\b[A-Z]+\.\s? # OR: Group 3: Some caps, followed by a '.' and an optional space
  76. (?:[A-Z]+\.\s?)+) # Followed by the same thing at least once more
  77. (?:\s|\b|$))
  78. """, re.VERBOSE)
  79. def _cap_wrapper(matchobj):
  80. """This is necessary to keep dotted cap strings to pick up extra spaces"""
  81. if matchobj.group(2):
  82. return """<span class="caps">%s</span>""" % matchobj.group(2)
  83. else:
  84. if matchobj.group(3)[-1] == " ":
  85. caps = matchobj.group(3)[:-1]
  86. tail = ' '
  87. else:
  88. caps = matchobj.group(3)
  89. tail = ''
  90. return """<span class="caps">%s</span>%s""" % (caps, tail)
  91. tags_to_skip_regex = re.compile("<(/)?(?:pre|code|kbd|script|math)[^>]*>", re.IGNORECASE)
  92. for token in tokens:
  93. if token[0] == "tag":
  94. # Don't mess with tags.
  95. result.append(token[1])
  96. close_match = tags_to_skip_regex.match(token[1])
  97. if close_match and close_match.group(1) == None:
  98. in_skipped_tag = True
  99. else:
  100. in_skipped_tag = False
  101. else:
  102. if in_skipped_tag:
  103. result.append(token[1])
  104. else:
  105. result.append(cap_finder.sub(_cap_wrapper, token[1]))
  106. output = "".join(result)
  107. return mark_safe(output)
  108. caps.is_safe = True
  109. def initial_quotes(text):
  110. """Wraps initial quotes in ``class="dquo"`` for double quotes or
  111. ``class="quo"`` for single quotes. Works in these block tags ``(h1-h6, p, li, dt, dd)``
  112. and also accounts for potential opening inline elements ``a, em, strong, span, b, i``
  113. >>> initial_quotes('"With primes"')
  114. u'<span class="dquo">"</span>With primes"'
  115. >>> initial_quotes("'With single primes'")
  116. u'<span class="quo">\\'</span>With single primes\\''
  117. >>> initial_quotes('<a href="#">"With primes and a link"</a>')
  118. u'<a href="#"><span class="dquo">"</span>With primes and a link"</a>'
  119. >>> initial_quotes('&#8220;With smartypanted quotes&#8221;')
  120. u'<span class="dquo">&#8220;</span>With smartypanted quotes&#8221;'
  121. """
  122. text = force_unicode(text)
  123. 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
  124. \s* # optional white space!
  125. (<(a|em|span|strong|i|b)[^>]*>\s*)*) # optional opening inline tags, with more optional white space for each.
  126. (("|&ldquo;|&\#8220;)|('|&lsquo;|&\#8216;)) # Find me a quote! (only need to find the left quotes and the primes)
  127. # double quotes are in group 7, singles in group 8
  128. """, re.VERBOSE)
  129. def _quote_wrapper(matchobj):
  130. if matchobj.group(7):
  131. classname = "dquo"
  132. quote = matchobj.group(7)
  133. else:
  134. classname = "quo"
  135. quote = matchobj.group(8)
  136. return """%s<span class="%s">%s</span>""" % (matchobj.group(1), classname, quote)
  137. output = quote_finder.sub(_quote_wrapper, text)
  138. return mark_safe(output)
  139. initial_quotes.is_safe = True
  140. def smartypants(text):
  141. """Applies smarty pants to curl quotes.
  142. >>> smartypants('The "Green" man')
  143. u'The &#8220;Green&#8221; man'
  144. """
  145. text = force_unicode(text)
  146. try:
  147. import smartypants
  148. except ImportError:
  149. if settings.DEBUG:
  150. raise template.TemplateSyntaxError, "Error in {% smartypants %} filter: The Python smartypants library isn't installed."
  151. return text
  152. else:
  153. output = smartypants.smartyPants(text)
  154. return mark_safe(output)
  155. smartypants.is_safe = True
  156. def titlecase(text):
  157. """Support for titlecase.py's titlecasing
  158. >>> titlecase("this V that")
  159. u'This v That'
  160. >>> titlecase("this is just an example.com")
  161. u'This Is Just an example.com'
  162. """
  163. text = force_unicode(text)
  164. try:
  165. import titlecase
  166. except ImportError:
  167. if settings.DEBUG:
  168. raise template.TemplateSyntaxError, "Error in {% titlecase %} filter: The titlecase.py library isn't installed."
  169. return text
  170. else:
  171. return titlecase.titlecase(text)
  172. def typogrify(text):
  173. """The super typography filter
  174. Applies the following filters: widont, smartypants, caps, amp, initial_quotes
  175. >>> typogrify('<h2>"Jayhawks" & KU fans act extremely obnoxiously</h2>')
  176. u'<h2><span class="dquo">&#8220;</span>Jayhawks&#8221; <span class="amp">&amp;</span> <span class="caps">KU</span> fans act extremely&nbsp;obnoxiously</h2>'
  177. Each filters properly handles autoescaping.
  178. >>> conditional_escape(typogrify('<h2>"Jayhawks" & KU fans act extremely obnoxiously</h2>'))
  179. u'<h2><span class="dquo">&#8220;</span>Jayhawks&#8221; <span class="amp">&amp;</span> <span class="caps">KU</span> fans act extremely&nbsp;obnoxiously</h2>'
  180. """
  181. text = force_unicode(text)
  182. text = amp(text)
  183. text = widont(text)
  184. text = smartypants(text)
  185. text = caps(text)
  186. text = initial_quotes(text)
  187. text = mdash(text)
  188. return text
  189. def widont(text):
  190. """Replaces the space between the last two words in a string with ``&nbsp;``
  191. Works in these block tags ``(h1-h6, p, li, dd, dt)`` and also accounts for
  192. potential closing inline elements ``a, em, strong, span, b, i``
  193. >>> widont('A very simple test')
  194. u'A very simple&nbsp;test'
  195. Single word items shouldn't be changed
  196. >>> widont('Test')
  197. u'Test'
  198. >>> widont(' Test')
  199. u' Test'
  200. >>> widont('<ul><li>Test</p></li><ul>')
  201. u'<ul><li>Test</p></li><ul>'
  202. >>> widont('<ul><li> Test</p></li><ul>')
  203. u'<ul><li> Test</p></li><ul>'
  204. >>> widont('<p>In a couple of paragraphs</p><p>paragraph two</p>')
  205. u'<p>In a couple of&nbsp;paragraphs</p><p>paragraph&nbsp;two</p>'
  206. >>> widont('<h1><a href="#">In a link inside a heading</i> </a></h1>')
  207. u'<h1><a href="#">In a link inside a&nbsp;heading</i> </a></h1>'
  208. >>> widont('<h1><a href="#">In a link</a> followed by other text</h1>')
  209. u'<h1><a href="#">In a link</a> followed by other&nbsp;text</h1>'
  210. Empty HTMLs shouldn't error
  211. >>> widont('<h1><a href="#"></a></h1>')
  212. u'<h1><a href="#"></a></h1>'
  213. >>> widont('<div>Divs get no love!</div>')
  214. u'<div>Divs get no love!</div>'
  215. >>> widont('<pre>Neither do PREs</pre>')
  216. u'<pre>Neither do PREs</pre>'
  217. >>> widont('<div><p>But divs with paragraphs do!</p></div>')
  218. u'<div><p>But divs with paragraphs&nbsp;do!</p></div>'
  219. """
  220. text = force_unicode(text)
  221. 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
  222. \s+ # the space to replace
  223. ([^<>\s]+ # must be followed by non-tag non-space characters
  224. \s* # optional white space!
  225. (</(a|em|span|strong|i|b)>\s*)* # optional closing inline tags with optional white space after each
  226. ((</(p|h[1-6]|li|dt|dd)>)|$)) # end with a closing p, h1-6, li or the end of the string
  227. """, re.VERBOSE)
  228. output = widont_finder.sub(r'\1&nbsp;\2', text)
  229. return mark_safe(output)
  230. widont.is_safe = True
  231. def mdash(text):
  232. mdash_finder = re.compile(r"([^\.\!\?]\s)\-(\s[^\.\!\?])")
  233. return mark_safe(mdash_finder.sub(r'\1&mdash;\2', text))
  234. mdash.is_safe = True
  235. register.filter('amp', amp)
  236. register.filter('caps', caps)
  237. register.filter('initial_quotes', initial_quotes)
  238. register.filter('smartypants', smartypants)
  239. register.filter('titlecase', titlecase)
  240. register.filter('typogrify', typogrify)
  241. register.filter('widont', widont)
  242. register.filter('mdash', mdash)
  243. def _test():
  244. import doctest
  245. doctest.testmod()
  246. if __name__ == "__main__":
  247. _test()
  248. def check_letters(first, second):
  249. if first in u'CKz' or first in u'КС': # lat, cyr
  250. return '0'
  251. if first in u'Г':
  252. return '-0.25'
  253. elif first in u'T' or first in u'Т':
  254. return '-0.15'
  255. elif first in u'FPYW' or first in u'РУ':
  256. if second in u'iktl' or second in u'ёйб' or second.upper() == second:
  257. return '-0.1'
  258. return '-0.15'
  259. return '-.05'
  260. @register.filter
  261. def title_tracking(value):
  262. '''
  263. Set tracking for first letter of phrase. Decides by checking
  264. first and second letters by known patterns.
  265. Thinks that first letter is very big (200%).
  266. '''
  267. first, second = value[:2]
  268. width = check_letters(first, second)
  269. first = (u'<span class="first" style="letter-spacing:'
  270. '%sem">%s</span>' % (width, first))
  271. return mark_safe(first + value[1:])
  272. title_tracking.is_safe = True