├── __init__.py
├── typogrify
├── templatetags
│ ├── __init__.py
│ └── typogrify_tags.py
├── __init__.py
├── tests
│ ├── test_tags.py
│ ├── __init__.py
│ ├── test_fuzzydate.py
│ └── test_titlecase.py
└── titlecase.py
├── .gitignore
├── setup.py
├── readme.markdown
└── LICENSE.txt
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/typogrify/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.py?
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/typogrify/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typogrify import templatetags, titlecase
3 |
4 | __all__ = ['templatetags', 'titlecase']
5 |
--------------------------------------------------------------------------------
/typogrify/tests/test_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import doctest
3 |
4 | from typogrify.templatetags.typogrify_tags import *
5 |
6 | if __name__ == "__main__":
7 | doctest.testmod()
8 |
--------------------------------------------------------------------------------
/typogrify/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typogrify.tests.test_fuzzydate import *
3 | from typogrify.tests.test_tags import *
4 | from typogrify.tests.test_titlecase import *
5 |
6 |
7 | __all__ = ['TestFuzzydate', 'TestTitlecase']
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from setuptools import setup
3 |
4 | VERSION = '1.3.3'
5 |
6 | setup(
7 | name='django-typogrify',
8 | version=VERSION,
9 | description='Make type not look like crap (for django apps)',
10 | author='Chris Drackett',
11 | author_email='chris@tiltshiftstudio.com',
12 | url='http://github.com/chrisdrackett/django-typogrify',
13 | packages=[
14 | "typogrify",
15 | "typogrify.templatetags",
16 | ],
17 | install_requires=[
18 | 'smartypants>=1.8.3', 'num2words'
19 | ],
20 | classifiers=[
21 | 'Development Status :: 5 - Production/Stable',
22 | 'Environment :: Web Environment',
23 | 'Intended Audience :: Developers',
24 | # 'Intended Audience :: Designers',
25 | 'Natural Language :: English',
26 | 'License :: OSI Approved :: BSD License',
27 | 'Operating System :: OS Independent',
28 | 'Programming Language :: Python',
29 | 'Framework :: Django',
30 | 'Topic :: Utilities',
31 | 'Topic :: Text Processing'
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/typogrify/tests/test_fuzzydate.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from datetime import datetime, timedelta
3 |
4 | from django.conf import settings
5 | from django.test import TestCase
6 | from typogrify.templatetags.typogrify_tags import fuzzydate
7 |
8 |
9 | class TestFuzzydate(TestCase):
10 |
11 | def setUp(self):
12 | settings.DATE_FORMAT = "F jS, Y"
13 |
14 | def test_returns_yesterday(self):
15 | yesterday = datetime.now() - timedelta(hours=24)
16 | self.assertEquals(fuzzydate(yesterday), "yesterday")
17 |
18 | two_days_ago = datetime.now() - timedelta(hours=48)
19 | self.assertNotEquals(fuzzydate(two_days_ago), "yesterday")
20 |
21 | def test_returns_today(self):
22 | today = datetime.now()
23 | self.assertEquals(fuzzydate(today), "today")
24 |
25 | def test_returns_tomorrow(self):
26 | tomorrow = datetime.now() + timedelta(hours=24)
27 | self.assertEquals(fuzzydate(tomorrow), "tomorrow")
28 |
29 | def test_formats_current_year(self):
30 | now = datetime.now()
31 | testdate = datetime.strptime("%s/10/10" % now.year, "%Y/%m/%d")
32 |
33 | expected = "October 10th"
34 | self.assertEquals(fuzzydate(testdate, 1), expected)
35 |
36 | def test_formats_other_years(self):
37 | testdate = datetime.strptime("1984/10/10", "%Y/%m/%d")
38 |
39 | expected = "October 10th, 1984"
40 | self.assertEquals(fuzzydate(testdate), expected)
41 |
--------------------------------------------------------------------------------
/typogrify/titlecase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # from __future__ import print_function
3 |
4 | # https://github.com/chrisdrackett/django-typogrify/blob/master/typogrify/titlecase.py
5 | # https://daringfireball.net/2008/05/title_case
6 |
7 | import re
8 | import sys
9 |
10 | SMALL = 'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?'
11 | PUNCT = "[!\"#$%&'‘()*+,-./:;?@[\\\\\\]_`{|}~]"
12 |
13 | SMALL_WORDS = re.compile(r'^(%s)$' % SMALL, re.I)
14 | INLINE_PERIOD = re.compile(r'[a-zA-Z][.][a-zA-Z]')
15 | UC_ELSEWHERE = re.compile(r'%s*?[a-zA-Z]+[A-Z]+?' % PUNCT)
16 | CAPFIRST = re.compile(r"^%s*?([A-Za-z])" % PUNCT)
17 | SMALL_FIRST = re.compile(r'^(%s*)(%s)\b' % (PUNCT, SMALL), re.I)
18 | SMALL_LAST = re.compile(r'\b(%s)%s?$' % (SMALL, PUNCT), re.I)
19 | SUBPHRASE = re.compile(r'([:.;?!][ ])(%s)' % SMALL)
20 |
21 |
22 | def titlecase(text):
23 | """
24 | Titlecases input text
25 |
26 | This filter changes all words to Title Caps, and attempts to be clever
27 | about *un*capitalizing SMALL words like a/an/the in the input.
28 |
29 | The list of "SMALL words" which are not capped comes from
30 | the New York Times Manual of Style, plus 'vs' and 'v'.
31 |
32 | """
33 |
34 | words = re.split('\s', text)
35 | line = []
36 | for word in words:
37 | if word.startswith('#') or \
38 | INLINE_PERIOD.search(word) or \
39 | UC_ELSEWHERE.match(word):
40 | line.append(word)
41 | continue
42 | if SMALL_WORDS.match(word):
43 | line.append(word.lower())
44 | continue
45 | line.append(CAPFIRST.sub(lambda m: m.group(0).upper(), word))
46 |
47 | line = " ".join(line)
48 |
49 | line = SMALL_FIRST.sub(lambda m: '%s%s' % (
50 | m.group(1),
51 | m.group(2).capitalize()
52 | ), line)
53 |
54 | line = SMALL_LAST.sub(lambda m: m.group(0).capitalize(), line)
55 |
56 | line = SUBPHRASE.sub(lambda m: '%s%s' % (
57 | m.group(1),
58 | m.group(2).capitalize()
59 | ), line)
60 |
61 | return line
62 |
63 |
64 | if __name__ == '__main__':
65 | if not sys.stdin.isatty():
66 | for line in sys.stdin:
67 | print(titlecase(line))
68 |
--------------------------------------------------------------------------------
/readme.markdown:
--------------------------------------------------------------------------------
1 | typogrify: Django template filters to make web typography easier
2 | ================================================================
3 |
4 | This application provides a set of custom filters for the Django
5 | template system which automatically apply various transformations to
6 | plain text in order to yield typographically-improved HTML.
7 |
8 | Requirements
9 | ============
10 |
11 | typogrify is designed to work with Django, and so requires a
12 | functioning installation of Django 0.96 or later.
13 |
14 | * **Django**: http://www.djangoproject.com/
15 |
16 | Installation
17 | ============
18 |
19 | 1. checkout the project into a folder called `typogrify` on your python path:
20 |
21 | git clone git@github.com:chrisdrackett/django-typogrify.git typogrify
22 |
23 | 2. Add 'typogrify' to your INSTALLED_APPS setting.
24 |
25 |
26 | Included filters
27 | ================
28 |
29 | amp
30 | ---
31 |
32 | Wraps ampersands in HTML with `` so they can be
33 | styled with CSS. Ampersands are also normalized to &. Requires
34 | ampersands to have whitespace or an on both sides. Will not
35 | change any ampersand which has already been wrapped in this fashion.
36 |
37 | caps
38 | ----
39 |
40 | Wraps multiple capital letters in `` so they can
41 | be styled with CSS.
42 |
43 | initial_quotes
44 | --------------
45 |
46 | Wraps initial quotes in `` for double quotes or
47 | `` for single quotes. Works inside these block
48 | elements:
49 |
50 | * h1, h2, h3, h4, h5, h6
51 | * p
52 | * li
53 | * dt
54 | * dd
55 |
56 | Also accounts for potential opening inline elements: a, em,
57 | strong, span, b, i.
58 |
59 | smartypants
60 | -----------
61 |
62 | * Straight quotes ( " and ' ) into “curly” quote HTML entities (‘ | ’ | “ | ”)
63 | * Backticks-style quotes (``like this'') into “curly” quote HTML entities (‘ | ’ | “ | ”)
64 | * Dashes (“--” and “---”) into n-dash and m-dash entities (– | —)
65 | * Three consecutive dots (“...”) into an ellipsis entity (…)
66 |
67 | widont
68 | ------
69 |
70 | Based on Shaun Inman's PHP utility of the same name, replaces the
71 | space between the last two words in a string with ` ` to avoid
72 | a final line of text with only one word.
73 |
74 | Works inside these block elements:
75 |
76 | * h1, h2, h3, h4, h5, h6
77 | * p
78 | * li
79 | * dt
80 | * dd
81 |
82 | Also accounts for potential closing inline elements: a, em,
83 | strong, span, b, i.
84 |
85 | titlecase
86 | ---------
87 |
88 | http://daringfireball.net/2008/05/title_case
89 |
90 | number_suffix
91 | -------------
92 |
93 | wraps number suffix's in `` so they can be styled.
94 |
95 | fuzzydate
96 | ---------
97 | (uses code from http://djangosnippets.org/snippets/1347/)
98 |
99 | Returns the date in a more human readable format:
100 |
101 | * Today
102 | * Yesterday
103 | * 4 days ago
104 | * 3 weeks ago
105 | * in 3 years
106 | * etc.
107 |
108 | typogrify
109 | ---------
110 |
111 | Applies all of the following filters, in order:
112 |
113 | * force_unicode (from django.utils.encoding)
114 | * amp
115 | * widont
116 | * smartypants
117 | * caps
118 | * initial_quotes
119 |
120 | Examples
121 | ========
122 |
123 | Apply all `django-typogrify` filters to template output:
124 |
125 | {% load typogrify_tags %}
126 | {{ blog_post.contents|typogrify }}
127 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Typogrify:
2 | ==========
3 |
4 | Copyright (c) 2007, Christian Metts
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are
9 | met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above
14 | copyright notice, this list of conditions and the following
15 | disclaimer in the documentation and/or other materials provided
16 | with the distribution.
17 | * Neither the name of the author nor the names of other
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 |
33 | Fuzzy Date / Typogrify Updates:
34 | ===============================
35 |
36 | Copyright (c) 2010, Chris Drackett
37 | All rights reserved.
38 |
39 | Redistribution and use in source and binary forms, with or without
40 | modification, are permitted provided that the following conditions are
41 | met:
42 |
43 | * Redistributions of source code must retain the above copyright
44 | notice, this list of conditions and the following disclaimer.
45 | * Redistributions in binary form must reproduce the above
46 | copyright notice, this list of conditions and the following
47 | disclaimer in the documentation and/or other materials provided
48 | with the distribution.
49 | * Neither the name of the author nor the names of other
50 | contributors may be used to endorse or promote products derived
51 | from this software without specific prior written permission.
52 |
53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
54 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
55 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
56 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
57 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
58 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
59 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
60 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
61 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
62 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
63 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
64 |
65 | titlecase.py
66 | ============
67 |
68 | Original Perl version by: John Gruber http://daringfireball.net/ 10 May 2008
69 | Python version by Stuart Colville http://muffinresearch.co.uk
70 | License: http://www.opensource.org/licenses/mit-license.php
--------------------------------------------------------------------------------
/typogrify/tests/test_titlecase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 |
4 | import unittest
5 |
6 | from typogrify.titlecase import titlecase
7 |
8 |
9 | class TestTitlecase(unittest.TestCase):
10 |
11 | """Tests to ensure titlecase follows all of the rules"""
12 |
13 | def test_q_and_a(self):
14 | """
15 | Testing: Q&A With Steve Jobs: 'That's What Happens In Technology'
16 | """
17 | text = titlecase(
18 | "Q&A with steve jobs: 'that's what happens in technology'"
19 | )
20 | result = "Q&A With Steve Jobs: 'That's What Happens in Technology'"
21 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
22 |
23 | def test_at_and_t(self):
24 | """Testing: What Is AT&T's Problem?"""
25 |
26 | text = titlecase("What is AT&T's problem?")
27 | result = "What Is AT&T's Problem?"
28 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
29 |
30 | def test_apple_deal(self):
31 | """Testing: Apple Deal With AT&T Falls Through"""
32 |
33 | text = titlecase("Apple deal with AT&T falls through")
34 | result = "Apple Deal With AT&T Falls Through"
35 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
36 |
37 | def test_this_v_that(self):
38 | """Testing: this v that"""
39 | text = titlecase("this v that")
40 | result = "This v That"
41 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
42 |
43 | def test_this_v_that2(self):
44 | """Testing: this v. that"""
45 |
46 | text = titlecase("this v. that")
47 | result = "This v. That"
48 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
49 |
50 | def test_this_vs_that(self):
51 | """Testing: this vs that"""
52 |
53 | text = titlecase("this vs that")
54 | result = "This vs That"
55 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
56 |
57 | def test_this_vs_that2(self):
58 | """Testing: this vs. that"""
59 |
60 | text = titlecase("this vs. that")
61 | result = "This vs. That"
62 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
63 |
64 | def test_apple_sec(self):
65 | """Testing: The SEC's Apple Probe: What You Need to Know"""
66 |
67 | text = titlecase("The SEC's Apple Probe: What You Need to Know")
68 | result = "The SEC's Apple Probe: What You Need to Know"
69 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
70 |
71 | def test_small_word_quoted(self):
72 | """Testing: 'by the Way, Small word at the start but within quotes.'"""
73 |
74 | text = titlecase(
75 | "'by the Way, small word at the start but within quotes.'"
76 | )
77 | result = "'By the Way, Small Word at the Start but Within Quotes.'"
78 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
79 |
80 | def test_small_word_end(self):
81 | """Testing: Small word at end is nothing to be afraid of"""
82 |
83 | text = titlecase("Small word at end is nothing to be afraid of")
84 | result = "Small Word at End Is Nothing to Be Afraid Of"
85 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
86 |
87 | def test_sub_phrase_small_word(self):
88 | """Testing: Starting Sub-Phrase With a Small Word: a Trick, Perhaps?"""
89 |
90 | text = titlecase(
91 | "Starting Sub-Phrase With a Small Word: a Trick, Perhaps?"
92 | )
93 | result = "Starting Sub-Phrase With a Small Word: A Trick, Perhaps?"
94 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
95 |
96 | def test_small_word_quotes(self):
97 | """Testing: Sub-Phrase With a Small Word in Quotes: 'a Trick..."""
98 |
99 | text = titlecase(
100 | "Sub-Phrase With a Small Word in Quotes: 'a Trick, Perhaps?'"
101 | )
102 | result = "Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'"
103 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
104 |
105 | def test_small_word_double_quotes(self):
106 | """Testing: Sub-Phrase With a Small Word in Quotes: \"a Trick..."""
107 | text = titlecase(
108 | 'Sub-Phrase With a Small Word in Quotes: "a Trick, Perhaps?"'
109 | )
110 | result = 'Sub-Phrase With a Small Word in Quotes: "A Trick, Perhaps?"'
111 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
112 |
113 | def test_nothing_to_be_afraid_of(self):
114 | """Testing: \"Nothing to Be Afraid of?\""""
115 | text = titlecase('"Nothing to Be Afraid of?"')
116 | result = '"Nothing to Be Afraid Of?"'
117 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
118 |
119 | def test_nothing_to_be_afraid_of2(self):
120 | """Testing: \"Nothing to Be Afraid Of?\""""
121 |
122 | text = titlecase('"Nothing to be Afraid Of?"')
123 | result = '"Nothing to Be Afraid Of?"'
124 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
125 |
126 | def test_a_thing(self):
127 | """Testing: a thing"""
128 |
129 | text = titlecase('a thing')
130 | result = 'A Thing'
131 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
132 |
133 | def test_vapourware(self):
134 | """Testing: 2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'"""
135 | text = titlecase(
136 | "2lmc Spool: 'gruber on OmniFocus and vapo(u)rware'"
137 | )
138 | result = "2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'"
139 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
140 |
141 | def test_domains(self):
142 | """Testing: this is just an example.com"""
143 | text = titlecase('this is just an example.com')
144 | result = 'This Is Just an example.com'
145 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
146 |
147 | def test_domains2(self):
148 | """Testing: this is something listed on an del.icio.us"""
149 |
150 | text = titlecase('this is something listed on del.icio.us')
151 | result = 'This Is Something Listed on del.icio.us'
152 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
153 |
154 | def test_itunes(self):
155 | """Testing: iTunes should be unmolested"""
156 |
157 | text = titlecase('iTunes should be unmolested')
158 | result = 'iTunes Should Be Unmolested'
159 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
160 |
161 | def test_thoughts_on_music(self):
162 | """Testing: Reading Between the Lines of Steve Jobs’s..."""
163 |
164 | text = titlecase(
165 | 'Reading between the lines of steve jobs’s ‘thoughts on music’'
166 | )
167 | result = 'Reading Between the Lines of Steve Jobs’s ‘Thoughts on '\
168 | 'Music’'
169 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
170 |
171 | def test_repair_perms(self):
172 | """Testing: Seriously, ‘Repair Permissions’ Is Voodoo"""
173 |
174 | text = titlecase('seriously, ‘repair permissions’ is voodoo')
175 | result = 'Seriously, ‘Repair Permissions’ Is Voodoo'
176 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
177 |
178 | def test_generalissimo(self):
179 | """Testing: Generalissimo Francisco Franco..."""
180 |
181 | text = titlecase(
182 | 'generalissimo francisco franco: still dead; kieren McCarthy: '
183 | 'still a jackass'
184 | )
185 | result = 'Generalissimo Francisco Franco: Still Dead; Kieren '\
186 | 'McCarthy: Still a Jackass'
187 | self.assertEqual(text, result, "%s should be: %s" % (text, result, ))
188 |
189 |
190 | if __name__ == '__main__':
191 | suite = unittest.TestLoader().loadTestsFromTestCase(TestTitlecase)
192 | unittest.TextTestRunner(verbosity=2).run(suite)
193 |
--------------------------------------------------------------------------------
/typogrify/templatetags/typogrify_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import calendar
3 | import re
4 | from datetime import date, timedelta
5 |
6 | import smartypants as _smartypants
7 | import typogrify.titlecase as _titlecase
8 | from num2words import num2words
9 |
10 | from django import template
11 | from django.conf import settings
12 | from django.utils.encoding import force_text
13 | from django.utils.html import conditional_escape
14 | from django.utils.safestring import mark_safe
15 | from django.utils.translation import ugettext, ungettext, get_language
16 |
17 | register = template.Library()
18 |
19 |
20 | __all__ = ['amp', 'caps', 'date', 'fuzzydate', 'initial_quotes',
21 | 'number_suffix', 'smart_filter', 'super_fuzzydate', 'titlecase',
22 | 'widont']
23 |
24 |
25 | def smart_filter(fn):
26 | '''
27 | Escapes filter's content based on template autoescape mode and marks output as safe
28 | '''
29 | def wrapper(text, autoescape=None):
30 | if autoescape:
31 | esc = conditional_escape
32 | else:
33 | esc = lambda x: x
34 |
35 | return mark_safe(fn(esc(text)))
36 | wrapper.needs_autoescape = True
37 |
38 | register.filter(fn.__name__, wrapper)
39 | return wrapper
40 |
41 |
42 | @smart_filter
43 | def amp(text, autoescape=None):
44 | """Wraps apersands in HTML with ```` so they can be
45 | styled with CSS. Apersands are also normalized to ``&``. Requires
46 | ampersands to have whitespace or an `` `` on both sides.
47 |
48 | >>> amp('One & two')
49 | u'One & two'
50 | >>> amp('One & two')
51 | u'One & two'
52 | >>> amp('One & two')
53 | u'One & two'
54 |
55 | >>> amp('One & two')
56 | u'One & two'
57 |
58 | It won't mess up & that are already wrapped, in entities or URLs
59 |
60 | >>> amp('One & two')
61 | u'One & two'
62 | >>> amp('“this” & that')
63 | u'“this” & that'
64 |
65 | It should ignore standalone amps that are in attributes
66 | >>> amp('xyz')
67 | u'xyz'
68 | """
69 |
70 | # tag_pattern from http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
71 | # it kinda sucks but it fixes the standalone amps in attributes bug
72 | tag_pattern = '?\w+((\s+\w+(\s*=\s*(?:".*?"|\'.*?\'|[^\'">\s]+))?)+\s*|\s*)/?>'
73 | amp_finder = re.compile(r"(\s| )(&|&|&\#38;)(\s| )")
74 | intra_tag_finder = re.compile(
75 | r'(?PCAPS
more CAPS")
97 | u'CAPS
more CAPS'
98 |
99 | >>> caps("A message from 2KU2 with digits")
100 | u'A message from 2KU2 with digits'
101 |
102 | >>> caps("Dotted caps followed by spaces should never include them in the wrap D.O.T. like so.")
103 | u'Dotted caps followed by spaces should never include them in the wrap D.O.T. like so.'
104 |
105 | All caps with with apostrophes in them shouldn't break. Only handles dump apostrophes though.
106 | >>> caps("JIMMY'S")
107 | u'JIMMY\\'S'
108 |
109 | >>> caps("D.O.T.HE34TRFID")
110 | u'D.O.T.HE34TRFID'
111 | """
112 |
113 | tokens = _smartypants._tokenize(text)
114 | result = []
115 | in_skipped_tag = False
116 |
117 | cap_finder = re.compile(r"""(
118 | (\b[A-Z\d]* # Group 2: Any amount of caps and digits
119 | [A-Z]\d*[A-Z] # A cap string must at least include two caps (but they can have digits between them)
120 | [A-Z\d']*\b) # Any amount of caps and digits or dumb apostsrophes
121 | | (\b[A-Z]+\.\s? # OR: Group 3: Some caps, followed by a '.' and an optional space
122 | (?:[A-Z]+\.\s?)+) # Followed by the same thing at least once more
123 | (?:\s|\b|$))
124 | """, re.VERBOSE)
125 |
126 | def _cap_wrapper(matchobj):
127 | """This is necessary to keep dotted cap strings to pick up extra spaces"""
128 | if matchobj.group(2):
129 | return """%s""" % matchobj.group(2)
130 | else:
131 | if matchobj.group(3)[-1] == " ":
132 | caps = matchobj.group(3)[:-1]
133 | tail = ' '
134 | else:
135 | caps = matchobj.group(3)
136 | tail = ''
137 | return """%s%s""" % (caps, tail)
138 |
139 | tags_to_skip_regex = re.compile(
140 | "<(/)?(?:pre|code|kbd|script|math)[^>]*>", re.IGNORECASE)
141 |
142 | for token in tokens:
143 | if token[0] == "tag":
144 | # Don't mess with tags.
145 | result.append(token[1])
146 | close_match = tags_to_skip_regex.match(token[1])
147 | if close_match and close_match.group(1) is None:
148 | in_skipped_tag = True
149 | else:
150 | in_skipped_tag = False
151 | else:
152 | if in_skipped_tag:
153 | result.append(token[1])
154 | else:
155 | result.append(cap_finder.sub(_cap_wrapper, token[1]))
156 | return "".join(result)
157 |
158 |
159 | @smart_filter
160 | def number_suffix(text):
161 | """Wraps date suffix in
162 | so they can be styled with CSS.
163 |
164 | >>> number_suffix("10th")
165 | u'10th'
166 |
167 | Uses the smartypants tokenizer to not screw with HTML or with tags it shouldn't.
168 |
169 | """
170 |
171 | suffix_finder = re.compile(r'(?P
In a couple of paragraphs
paragraph two
') 263 | u'In a couple of paragraphs
paragraph two
' 264 | 265 | >>> widont('Neither do PREs') 279 | u'
Neither do PREs' 280 | 281 | >>> widont('
But divs with paragraphs do!
But divs with paragraphs do!