Django

Code

root/djangoproject.com/django_website/apps/docs/smartypants.py

Revision 4789, 27.2 kB (checked in by jacob, 1 year ago)

Vastly improved the djangoproject.com doc system; it now draws from SVN directly (i.e. no more need for a hourly cron builder).

Line 
1 r"""
2 ==============
3 smartypants.py
4 ==============
5
6 ----------------------------
7 SmartyPants ported to Python
8 ----------------------------
9
10 Ported by `Chad Miller`_
11 Copyright (c) 2004 Chad Miller
12
13 original `SmartyPants`_ by `John Gruber`_
14 Copyright (c) 2003 John Gruber
15
16
17 Synopsis
18 ========
19
20 A smart-quotes plugin for Pyblosxom_.
21
22 The priginal "SmartyPants" is a free web publishing plug-in for Movable Type,
23 Blosxom, and BBEdit that easily translates plain ASCII punctuation characters
24 into "smart" typographic punctuation HTML entities.
25
26 This software, *smartypants.py*, endeavours to be a functional port of
27 SmartyPants to Python, for use with Pyblosxom_.
28
29
30 Description
31 ===========
32
33 SmartyPants can perform the following transformations:
34
35 - Straight quotes ( " and ' ) into "curly" quote HTML entities
36 - Backticks-style quotes (\`\`like this'') into "curly" quote HTML entities
37 - Dashes (``--`` and ``---``) into en- and em-dash entities
38 - Three consecutive dots (``...`` or ``. . .``) into an ellipsis entity
39
40 This means you can write, edit, and save your posts using plain old
41 ASCII straight quotes, plain dashes, and plain dots, but your published
42 posts (and final HTML output) will appear with smart quotes, em-dashes,
43 and proper ellipses.
44
45 SmartyPants does not modify characters within ``<pre>``, ``<code>``, ``<kbd>``,
46 ``<math>`` or ``<script>`` tag blocks. Typically, these tags are used to
47 display text where smart quotes and other "smart punctuation" would not be
48 appropriate, such as source code or example markup.
49
50
51 Backslash Escapes
52 =================
53
54 If you need to use literal straight quotes (or plain hyphens and
55 periods), SmartyPants accepts the following backslash escape sequences
56 to force non-smart punctuation. It does so by transforming the escape
57 sequence into a decimal-encoded HTML entity:
58
59 (FIXME:  table here.)
60
61 .. comment    It sucks that there's a disconnect between the visual layout and table markup when special characters are involved.
62 .. comment ======  =====  =========
63 .. comment Escape  Value  Character
64 .. comment ======  =====  =========
65 .. comment \\\\\\\\    &#92;  \\\\
66 .. comment \\\\"     &#34;  "
67 .. comment \\\\'     &#39;  '
68 .. comment \\\\.     &#46;  .
69 .. comment \\\\-     &#45;  \-
70 .. comment \\\\`     &#96;  \`
71 .. comment ======  =====  =========
72
73 This is useful, for example, when you want to use straight quotes as
74 foot and inch marks: 6'2" tall; a 17" iMac.
75
76 Options
77 =======
78
79 For Pyblosxom users, the ``smartypants_attributes`` attribute is where you
80 specify configuration options.
81
82 Numeric values are the easiest way to configure SmartyPants' behavior:
83
84 "0"
85         Suppress all transformations. (Do nothing.)
86 "1"
87         Performs default SmartyPants transformations: quotes (including
88         \`\`backticks'' -style), em-dashes, and ellipses. "``--``" (dash dash)
89         is used to signify an em-dash; there is no support for en-dashes.
90
91 "2"
92         Same as smarty_pants="1", except that it uses the old-school typewriter
93         shorthand for dashes:  "``--``" (dash dash) for en-dashes, "``---``"
94         (dash dash dash)
95         for em-dashes.
96
97 "3"
98         Same as smarty_pants="2", but inverts the shorthand for dashes:
99         "``--``" (dash dash) for em-dashes, and "``---``" (dash dash dash) for
100         en-dashes.
101
102 "-1"
103         Stupefy mode. Reverses the SmartyPants transformation process, turning
104         the HTML entities produced by SmartyPants into their ASCII equivalents.
105         E.g.  "&#8220;" is turned into a simple double-quote ("), "&#8212;" is
106         turned into two dashes, etc.
107
108
109 The following single-character attribute values can be combined to toggle
110 individual transformations from within the smarty_pants attribute. For
111 example, to educate normal quotes and em-dashes, but not ellipses or
112 \`\`backticks'' -style quotes:
113
114 ``py['smartypants_attributes'] = "1"``
115
116 "q"
117         Educates normal quote characters: (") and (').
118
119 "b"
120         Educates \`\`backticks'' -style double quotes.
121
122 "B"
123         Educates \`\`backticks'' -style double quotes and \`single' quotes.
124
125 "d"
126         Educates em-dashes.
127
128 "D"
129         Educates em-dashes and en-dashes, using old-school typewriter shorthand:
130         (dash dash) for en-dashes, (dash dash dash) for em-dashes.
131
132 "i"
133         Educates em-dashes and en-dashes, using inverted old-school typewriter
134         shorthand: (dash dash) for em-dashes, (dash dash dash) for en-dashes.
135
136 "e"
137         Educates ellipses.
138
139 "w"
140         Translates any instance of ``&quot;`` into a normal double-quote character.
141         This should be of no interest to most people, but of particular interest
142         to anyone who writes their posts using Dreamweaver, as Dreamweaver
143         inexplicably uses this entity to represent a literal double-quote
144         character. SmartyPants only educates normal quotes, not entities (because
145         ordinarily, entities are used for the explicit purpose of representing the
146         specific character they represent). The "w" option must be used in
147         conjunction with one (or both) of the other quote options ("q" or "b").
148         Thus, if you wish to apply all SmartyPants transformations (quotes, en-
149         and em-dashes, and ellipses) and also translate ``&quot;`` entities into
150         regular quotes so SmartyPants can educate them, you should pass the
151         following to the smarty_pants attribute:
152
153 The ``smartypants_forbidden_flavours`` list contains pyblosxom flavours for
154 which no Smarty Pants rendering will occur.
155
156
157 Caveats
158 =======
159
160 Why You Might Not Want to Use Smart Quotes in Your Weblog
161 ---------------------------------------------------------
162
163 For one thing, you might not care.
164
165 Most normal, mentally stable individuals do not take notice of proper
166 typographic punctuation. Many design and typography nerds, however, break
167 out in a nasty rash when they encounter, say, a restaurant sign that uses
168 a straight apostrophe to spell "Joe's".
169
170 If you're the sort of person who just doesn't care, you might well want to
171 continue not caring. Using straight quotes -- and sticking to the 7-bit
172 ASCII character set in general -- is certainly a simpler way to live.
173
174 Even if you I *do* care about accurate typography, you still might want to
175 think twice before educating the quote characters in your weblog. One side
176 effect of publishing curly quote HTML entities is that it makes your
177 weblog a bit harder for others to quote from using copy-and-paste. What
178 happens is that when someone copies text from your blog, the copied text
179 contains the 8-bit curly quote characters (as well as the 8-bit characters
180 for em-dashes and ellipses, if you use these options). These characters
181 are not standard across different text encoding methods, which is why they
182 need to be encoded as HTML entities.
183
184 People copying text from your weblog, however, may not notice that you're
185 using curly quotes, and they'll go ahead and paste the unencoded 8-bit
186 characters copied from their browser into an email message or their own
187 weblog. When pasted as raw "smart quotes", these characters are likely to
188 get mangled beyond recognition.
189
190 That said, my own opinion is that any decent text editor or email client
191 makes it easy to stupefy smart quote characters into their 7-bit
192 equivalents, and I don't consider it my problem if you're using an
193 indecent text editor or email client.
194
195
196 Algorithmic Shortcomings
197 ------------------------
198
199 One situation in which quotes will get curled the wrong way is when
200 apostrophes are used at the start of leading contractions. For example:
201
202 ``'Twas the night before Christmas.``
203
204 In the case above, SmartyPants will turn the apostrophe into an opening
205 single-quote, when in fact it should be a closing one. I don't think
206 this problem can be solved in the general case -- every word processor
207 I've tried gets this wrong as well. In such cases, it's best to use the
208 proper HTML entity for closing single-quotes (``&#8217;``) by hand.
209
210
211 Bugs
212 ====
213
214 To file bug reports or feature requests (other than topics listed in the
215 Caveats section above) please send email to: mailto:smartypantspy@chad.org
216
217 If the bug involves quotes being curled the wrong way, please send example
218 text to illustrate.
219
220 To Do list
221 ----------
222
223 - Provide a function for use within templates to quote anything at all.
224
225
226 Version History
227 ===============
228
229 1.5_1.5: Sat, 13 Aug 2005 15:50:24 -0400
230         - Fix bogus magical quotation when there is no hint that the
231           user wants it, e.g., in "21st century".  Thanks to Nathan Hamblen.
232         - Be smarter about quotes before terminating numbers in an en-dash'ed
233           range.
234
235 1.5_1.4: Thu, 10 Feb 2005 20:24:36 -0500
236         - Fix a date-processing bug, as reported by jacob childress.
237         - Begin a test-suite for ensuring correct output.
238         - Removed import of "string", since I didn't really need it.
239           (This was my first every Python program.  Sue me!)
240
241 1.5_1.3: Wed, 15 Sep 2004 18:25:58 -0400
242         - Abort processing if the flavour is in forbidden-list.  Default of
243           [ "rss" ]   (Idea of Wolfgang SCHNERRING.)
244         - Remove stray virgules from en-dashes.  Patch by Wolfgang SCHNERRING.
245
246 1.5_1.2: Mon, 24 May 2004 08:14:54 -0400
247         - Some single quotes weren't replaced properly.  Diff-tesuji played
248           by Benjamin GEIGER.
249
250 1.5_1.1: Sun, 14 Mar 2004 14:38:28 -0500
251         - Support upcoming pyblosxom 0.9 plugin verification feature.
252
253 1.5_1.0: Tue, 09 Mar 2004 08:08:35 -0500
254         - Initial release
255
256 Version Information
257 -------------------
258
259 Version numbers will track the SmartyPants_ version numbers, with the addition
260 of an underscore and the smartypants.py version on the end.
261
262 New versions will be available at `http://wiki.chad.org/SmartyPantsPy`_
263
264 .. _http://wiki.chad.org/SmartyPantsPy: http://wiki.chad.org/SmartyPantsPy
265
266 Authors
267 =======
268
269 `John Gruber`_ did all of the hard work of writing this software in Perl for
270 `Movable Type`_ and almost all of this useful documentation.  `Chad Miller`_
271 ported it to Python to use with Pyblosxom_.
272
273
274 Additional Credits
275 ==================
276
277 Portions of the SmartyPants original work are based on Brad Choate's nifty
278 MTRegex plug-in.  `Brad Choate`_ also contributed a few bits of source code to
279 this plug-in.  Brad Choate is a fine hacker indeed.
280
281 `Jeremy Hedley`_ and `Charles Wiltgen`_ deserve mention for exemplary beta
282 testing of the original SmartyPants.
283
284 `Rael Dornfest`_ ported SmartyPants to Blosxom.
285
286 .. _Brad Choate: http://bradchoate.com/
287 .. _Jeremy Hedley: http://antipixel.com/
288 .. _Charles Wiltgen: http://playbacktime.com/
289 .. _Rael Dornfest: http://raelity.org/
290
291
292 Copyright and License
293 =====================
294
295 SmartyPants_ license::
296
297         Copyright (c) 2003 John Gruber
298         (http://daringfireball.net/)
299         All rights reserved.
300
301         Redistribution and use in source and binary forms, with or without
302         modification, are permitted provided that the following conditions are
303         met:
304
305         *   Redistributions of source code must retain the above copyright
306                 notice, this list of conditions and the following disclaimer.
307
308         *   Redistributions in binary form must reproduce the above copyright
309                 notice, this list of conditions and the following disclaimer in
310                 the documentation and/or other materials provided with the
311                 distribution.
312
313         *   Neither the name "SmartyPants" nor the names of its contributors
314                 may be used to endorse or promote products derived from this
315                 software without specific prior written permission.
316
317         This software is provided by the copyright holders and contributors "as
318         is" and any express or implied warranties, including, but not limited
319         to, the implied warranties of merchantability and fitness for a
320         particular purpose are disclaimed. In no event shall the copyright
321         owner or contributors be liable for any direct, indirect, incidental,
322         special, exemplary, or consequential damages (including, but not
323         limited to, procurement of substitute goods or services; loss of use,
324         data, or profits; or business interruption) however caused and on any
325         theory of liability, whether in contract, strict liability, or tort
326         (including negligence or otherwise) arising in any way out of the use
327         of this software, even if advised of the possibility of such damage.
328
329
330 smartypants.py license::
331
332         smartypants.py is a derivative work of SmartyPants.
333         
334         Redistribution and use in source and binary forms, with or without
335         modification, are permitted provided that the following conditions are
336         met:
337
338         *   Redistributions of source code must retain the above copyright
339                 notice, this list of conditions and the following disclaimer.
340
341         *   Redistributions in binary form must reproduce the above copyright
342                 notice, this list of conditions and the following disclaimer in
343                 the documentation and/or other materials provided with the
344                 distribution.
345
346         This software is provided by the copyright holders and contributors "as
347         is" and any express or implied warranties, including, but not limited
348         to, the implied warranties of merchantability and fitness for a
349         particular purpose are disclaimed. In no event shall the copyright
350         owner or contributors be liable for any direct, indirect, incidental,
351         special, exemplary, or consequential damages (including, but not
352         limited to, procurement of substitute goods or services; loss of use,
353         data, or profits; or business interruption) however caused and on any
354         theory of liability, whether in contract, strict liability, or tort
355         (including negligence or otherwise) arising in any way out of the use
356         of this software, even if advised of the possibility of such damage.
357
358
359
360 .. _John Gruber: http://daringfireball.net/
361 .. _Chad Miller: http://web.chad.org/
362
363 .. _Pyblosxom: http://roughingit.subtlehints.net/pyblosxom
364 .. _SmartyPants: http://daringfireball.net/projects/smartypants/
365 .. _Movable Type: http://www.movabletype.org/
366
367 """
368
369 default_smartypants_attr = "1"
370
371 import re
372
373 tags_to_skip_regex = re.compile("<(/)?(?:pre|code|kbd|script|math)[^>]*>")
374
375
376 def verify_installation(request):
377         return 1
378         # assert the plugin is functional
379
380
381 def cb_story(args):
382         global default_smartypants_attr
383
384         try:
385                 forbidden_flavours = args["entry"]["smartypants_forbidden_flavours"]
386         except KeyError:
387                 forbidden_flavours = [ "rss" ]
388
389         try:
390                 attributes = args["entry"]["smartypants_attributes"]
391         except KeyError:
392                 attributes = default_smartypants_attr
393
394         if attributes is None:
395                 attributes = default_smartypants_attr
396
397         entryData = args["entry"].getData()
398
399         try:
400                 if args["request"]["flavour"] in forbidden_flavours:
401                         return
402         except KeyError:
403                 if "&lt;" in args["entry"]["body"][0:15]:  # sniff the stream
404                         return  # abort if it looks like escaped HTML.  FIXME
405
406         # FIXME: make these configurable, perhaps?
407         args["entry"]["body"] = smartyPants(entryData, attributes)
408         args["entry"]["title"] = smartyPants(args["entry"]["title"], attributes)
409
410
411 ### interal functions below here
412
413 def smartyPants(text, attr=default_smartypants_attr):
414         convert_quot = False  # should we translate &quot; entities into normal quotes?
415
416         # Parse attributes:
417         # 0 : do nothing
418         # 1 : set all
419         # 2 : set all, using old school en- and em- dash shortcuts
420         # 3 : set all, using inverted old school en and em- dash shortcuts
421         #
422         # q : quotes
423         # b : backtick quotes (``double'' only)
424         # B : backtick quotes (``double'' and `single')
425         # d : dashes
426         # D : old school dashes
427         # i : inverted old school dashes
428         # e : ellipses
429         # w : convert &quot; entities to " for Dreamweaver users
430
431         do_dashes = "0"
432         do_backticks = "0"
433         do_quotes = "0"
434         do_ellipses = "0"
435         do_stupefy = "0"
436
437         if attr == "0":
438                 # Do nothing.
439                 return text
440         elif attr == "1":
441                 do_quotes    = "1"
442                 do_backticks = "1"
443                 do_dashes    = "1"
444                 do_ellipses  = "1"
445         elif attr == "2":
446                 # Do everything, turn all options on, use old school dash shorthand.
447                 do_quotes    = "1"
448                 do_backticks = "1"
449                 do_dashes    = "2"
450                 do_ellipses  = "1"
451         elif attr == "3":
452                 # Do everything, turn all options on, use inverted old school dash shorthand.
453                 do_quotes    = "1"
454                 do_backticks = "1"
455                 do_dashes    = "3"
456                 do_ellipses  = "1"
457         elif attr == "-1":
458                 # Special "stupefy" mode.
459                 do_stupefy   = "1"
460         else:
461                 for c in attr:
462                         if c == "q": do_quotes = "1"
463                         elif c == "b": do_backticks = "1"
464                         elif c == "B": do_backticks = "2"
465                         elif c == "d": do_dashes = "1"
466                         elif c == "D": do_dashes = "2"
467                         elif c == "i": do_dashes = "3"
468                         elif c == "e": do_ellipses = "1"
469                         elif c == "w": convert_quot = "1"
470                         else:
471                                 pass
472                                 # ignore unknown option
473
474         tokens = _tokenize(text)
475         result = []
476         in_pre = False
477
478         prev_token_last_char = ""
479         # This is a cheat, used to get some context
480         # for one-character tokens that consist of
481         # just a quote char. What we do is remember
482         # the last character of the previous text
483         # token, to use as context to curl single-
484         # character quote tokens correctly.
485
486         for cur_token in tokens:
487                 if cur_token[0] == "tag":
488                         # Don't mess with quotes inside tags.
489                         result.append(cur_token[1])
490                         close_match = tags_to_skip_regex.match(cur_token[1])
491                         # if close_match is not None and close_match.group(1) == "":
492                         if close_match is not None:
493                                 in_pre = True
494                         else:
495                                 in_pre = False
496                 else:
497                         t = cur_token[1]
498                         last_char = t[-1:] # Remember last char of this token before processing.
499                         if not in_pre:
500                                 oldstr = t
501                                 t = processEscapes(t)
502
503                                 if convert_quot != "0":
504                                         t = re.sub('&quot;', '"', t)
505
506                                 if do_dashes != "0":
507                                         if do_dashes == "1":
508                                                 t = educateDashes(t)
509                                         if do_dashes == "2":
510                                                 t = educateDashesOldSchool(t)
511                                         if do_dashes == "3":
512                                                 t = educateDashesOldSchoolInverted(t)
513
514                                 if do_ellipses != "0":
515                                         t = educateEllipses(t)
516
517                                 # Note: backticks need to be processed before quotes.
518                                 if do_backticks != "0":
519                                         t = educateBackticks(t)
520
521                                 if do_backticks == "2":
522                                         t = educateSingleBackticks(t)
523
524                                 if do_quotes != "0":
525                                         if t == "'":
526                                                 # Special case: single-character ' token
527                                                 if re.match("\S", prev_token_last_char):
528                                                         t = "&#8217;"
529                                                 else:
530                                                         t = "&#8216;"
531                                         elif t == '"':
532                                                 # Special case: single-character " token
533                                                 if re.match("\S", prev_token_last_char):
534                                                         t = "&#8221;"
535                                                 else:
536                                                         t = "&#8220;"
537
538                                         else:
539                                                 # Normal case:
540                                                 t = educateQuotes(t)
541
542                                 if do_stupefy == "1":
543                                         t = stupefyEntities(t)
544
545                         prev_token_last_char = last_char
546                         result.append(t)
547
548         return "".join(result)
549
550
551 def educateQuotes(str):
552         """
553         Parameter:  String.
554         
555         Returns:        The string, with "educated" curly quote HTML entities.
556         
557         Example input:  "Isn't this fun?"
558         Example output: &#8220;Isn&#8217;t this fun?&#8221;
559         """
560
561         oldstr = str
562         punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
563
564         # Special case if the very first character is a quote
565         # followed by punctuation at a non-word-break. Close the quotes by brute force:
566         str = re.sub(r"""^'(?=%s\\B)""" % (punct_class,), r"""&#8217;""", str)
567         str = re.sub(r"""^"(?=%s\\B)""" % (punct_class,), r"""&#8221;""", str)
568
569         # Special case for double sets of quotes, e.g.:
570         #   <p>He said, "'Quoted' words in a larger quote."</p>
571         str = re.sub(r""""'(?=\w)""", """&#8220;&#8216;""", str)
572         str = re.sub(r"""'"(?=\w)""", """&#8216;&#8220;""", str)
573
574         # Special case for decade abbreviations (the '80s):
575         str = re.sub(r"""\b'(?=\d{2}s)""", r"""&#8217;""", str)
576
577         close_class = r"""[^\ \t\r\n\[\{\(\-]"""
578         dec_dashes = r"""&#8211;|&#8212;"""
579
580         # Get most opening single quotes:
581         opening_single_quotes_regex = re.compile(r"""
582                         (
583                                 \s          |   # a whitespace char, or
584                                 &nbsp;      |   # a non-breaking space entity, or
585                                 --          |   # dashes, or
586                                 &[mn]dash;  |   # named dash entities
587                                 %s          |   # or decimal entities
588                                 &\#x201[34];    # or hex
589                         )
590                         '                 # the quote
591                         (?=\w)            # followed by a word character
592                         """ % (dec_dashes,), re.VERBOSE)
593         str = opening_single_quotes_regex.sub(r"""\1&#8216;""", str)
594
595         closing_single_quotes_regex = re.compile(r"""
596                         (%s)
597                         '
598                         (?!\s | s\b | \d)
599                         """ % (close_class,), re.VERBOSE)
600         str = closing_single_quotes_regex.sub(r"""\1&#8217;""", str)
601
602         closing_single_quotes_regex = re.compile(r"""
603                         (%s)
604                         '
605                         (\s | s\b)
606                         """ % (close_class,), re.VERBOSE)
607         str = closing_single_quotes_regex.sub(r"""\1&#8217;\2""", str)
608
609         # Any remaining single quotes should be opening ones:
610         str = re.sub(r"""'""", r"""&#8216;""", str)
611
612         # Get most opening double quotes:
613         opening_double_quotes_regex = re.compile(r"""
614                         (
615                                 \s          |   # a whitespace char, or
616                                 &nbsp;      |   # a non-breaking space entity, or
617                                 --          |   # dashes, or
618                                 &[mn]dash;  |   # named dash entities
619                                 %s          |   # or decimal entities
620                                 &\#x201[34];    # or hex
621                         )
622                         "                 # the quote
623                         (?=\w)            # followed by a word character
624                         """ % (dec_dashes,), re.VERBOSE)
625         str = opening_double_quotes_regex.sub(r"""\1&#8220;""", str)
626
627         # Double closing quotes:
628         closing_double_quotes_regex = re.compile(r"""
629                         #(%s)?   # character that indicates the quote should be closing
630                         "
631                         (?=\s)
632                         """ % (close_class,), re.VERBOSE)
633         str = closing_double_quotes_regex.sub(r"""&#8221;""", str)
634
635         closing_double_quotes_regex = re.compile(r"""
636                         (%s)   # character that indicates the quote should be closing
637                         "
638                         """ % (close_class,), re.VERBOSE)
639         str = closing_double_quotes_regex.sub(r"""\1&#8221;""", str)
640
641         # Any remaining quotes should be opening ones.
642         str = re.sub(r'"', r"""&#8220;""", str)
643
644         return str
645
646
647 def educateBackticks(str):
648         """
649         Parameter:  String.
650         Returns:    The string, with ``backticks'' -style double quotes
651                     translated into HTML curly quote entities.
652         Example input:  ``Isn't this fun?''
653         Example output: &#8220;Isn't this fun?&#8221;
654         """
655
656         str = re.sub(r"""``""", r"""&#8220;""", str)
657         str = re.sub(r"""''""", r"""&#8221;""", str)
658         return str
659
660
661 def educateSingleBackticks(str):
662         """
663         Parameter:  String.
664         Returns:    The string, with `backticks' -style single quotes
665                     translated into HTML curly quote entities.
666         
667         Example input:  `Isn't this fun?'
668         Example output: &#8216;Isn&#8217;t this fun?&#8217;
669         """
670
671         str = re.sub(r"""`""", r"""&#8216;""", str)
672         str = re.sub(r"""'""", r"""&#8217;""", str)
673         return str
674
675
676 def educateDashes(str):
677         """
678         Parameter:  String.
679         
680         Returns:    The string, with each instance of "--" translated to
681                     an em-dash HTML entity.
682         """
683
684         str = re.sub(r"""---""", r"""&#8211;""", str) # en  (yes, backwards)
685         str = re.sub(r"""--""", r"""&#8212;""", str) # em (yes, backwards)
686         return str
687
688
689 def educateDashesOldSchool(str):
690         """
691         Parameter:  String.
692         
693         Returns:    The string, with each instance of "--" translated to
694                     an en-dash HTML entity, and each "---" translated to
695                     an em-dash HTML entity.
696         """
697
698         str = re.sub(r"""---""", r"""&#8212;""", str)    # em (yes, backwards)
699         str = re.sub(r"""--""", r"""&#8211;""", str)    # en (yes, backwards)
700         return str
701
702
703 def educateDashesOldSchoolInverted(str):
704         """