├── .gitignore
├── LICENSE.txt
├── __init__.py
├── readme.markdown
├── setup.py
└── typogrify
├── __init__.py
├── templatetags
├── __init__.py
└── typogrify_tags.py
├── tests
├── __init__.py
├── test_fuzzydate.py
├── test_tags.py
└── test_titlecase.py
└── titlecase.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.py?
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisdrackett/django-typogrify/fb47a3bbbf7adfa524879cb3e61517f8c3cb73dc/__init__.py
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typogrify import templatetags, titlecase
3 |
4 | __all__ = ['templatetags', 'titlecase']
5 |
--------------------------------------------------------------------------------
/typogrify/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisdrackett/django-typogrify/fb47a3bbbf7adfa524879cb3e61517f8c3cb73dc/typogrify/templatetags/__init__.py
--------------------------------------------------------------------------------
/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'(?P(%s)?)(?P([^<]*))(?P(%s)?)' % (tag_pattern, tag_pattern))
76 |
77 | def _amp_process(groups):
78 | prefix = groups.group('prefix') or ''
79 | text = amp_finder.sub(
80 | r"""\1&\3""", groups.group('text'))
81 | suffix = groups.group('suffix') or ''
82 | return prefix + text + suffix
83 | return intra_tag_finder.sub(_amp_process, text)
84 |
85 |
86 | @smart_filter
87 | def caps(text):
88 | """Wraps multiple capital letters in ````
89 | so they can be styled with CSS.
90 |
91 | >>> caps("A message from KU")
92 | u'A message from KU'
93 |
94 | Uses the smartypants tokenizer to not screw with HTML or with tags it shouldn't.
95 |
96 | >>> caps("
CAPS
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[\d]+)(?Pst|nd|rd|th)')
172 |
173 | def _suffix_process(groups):
174 | number = groups.group('number')
175 | suffix = groups.group('ord')
176 |
177 | return "%s%s" % (number, suffix)
178 | return suffix_finder.sub(_suffix_process, text)
179 |
180 |
181 | @smart_filter
182 | def initial_quotes(text):
183 | """Wraps initial quotes in ``class="dquo"`` for double quotes or
184 | ``class="quo"`` for single quotes. Works in these block tags ``(h1-h6, p, li, dt, dd)``
185 | and also accounts for potential opening inline elements ``a, em, strong, span, b, i``
186 |
187 | >>> initial_quotes('"With primes"')
188 | u'"With primes"'
189 | >>> initial_quotes("'With single primes'")
190 | u'\\'With single primes\\''
191 |
192 | >>> initial_quotes('"With primes and a link"')
193 | u'"With primes and a link"'
194 |
195 | >>> initial_quotes('“With smartypanted quotes”')
196 | u'“With smartypanted quotes”'
197 | """
198 |
199 | 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
200 | \s* # optional white space!
201 | (<(a|em|span|strong|i|b)[^>]*>\s*)*) # optional opening inline tags, with more optional white space for each.
202 | (("|“|&\#8220;)|('|‘|&\#8216;)) # Find me a quote! (only need to find the left quotes and the primes)
203 | # double quotes are in group 7, singles in group 8
204 | """, re.VERBOSE)
205 |
206 | def _quote_wrapper(matchobj):
207 | if matchobj.group(7):
208 | classname = "dquo"
209 | quote = matchobj.group(7)
210 | else:
211 | classname = "quo"
212 | quote = matchobj.group(8)
213 | return """%s%s""" % (matchobj.group(1), classname, quote)
214 | output = quote_finder.sub(_quote_wrapper, text)
215 | return output
216 |
217 |
218 | @smart_filter
219 | def smartypants(text):
220 | """Applies smarty pants to curl quotes.
221 |
222 | >>> smartypants('The "Green" man')
223 | u'The “Green” man'
224 | """
225 |
226 | return _smartypants.smartypants(text)
227 |
228 |
229 | @smart_filter
230 | def titlecase(text):
231 | """Support for titlecase.py's titlecasing
232 |
233 | >>> titlecase("this V that")
234 | u'This v That'
235 |
236 | >>> titlecase("this is just an example.com")
237 | u'This Is Just an example.com'
238 | """
239 |
240 | return _titlecase.titlecase(text)
241 |
242 |
243 | @smart_filter
244 | def widont(text):
245 | """Replaces the space between the last two words in a string with `` ``
246 | Works in these block tags ``(h1-h6, p, li, dd, dt)`` and also accounts for
247 | potential closing inline elements ``a, em, strong, span, b, i``
248 |
249 | >>> widont('A very simple test')
250 | u'A very simple test'
251 |
252 | Single word items shouldn't be changed
253 | >>> widont('Test')
254 | u'Test'
255 | >>> widont(' Test')
256 | u' Test'
257 | >>> widont('