Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Vastly improved the djangoproject.com doc system; it now draws from S…

…VN directly (i.e. no more need for a hourly cron builder).

git-svn-id: http://code.djangoproject.com/svn/djangoproject.com@4789 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit adb1f18f46a9a36ed5fb0cdd433bb962cb5958f1 1 parent d854495
authored March 23, 2007
185  django_website/apps/docs/builder.py
... ...
@@ -0,0 +1,185 @@
  1
+"""
  2
+Code to do the ReST --> HTML generation for the docs.
  3
+"""
  4
+
  5
+import re
  6
+import compiler
  7
+import smartypants
  8
+from docutils import nodes
  9
+from docutils.core import publish_parts
  10
+from docutils.writers import html4css1
  11
+
  12
+def build_document(text):
  13
+    """
  14
+    Build a doc file into a dict of HTML bits.
  15
+    """
  16
+    return publish_parts(text, writer=DjangoHTMLWriter(), settings_overrides={'initial_header_level': 2})
  17
+
  18
+docstring_re = re.compile(r"([\"']{3})(.*?)(\1)", re.DOTALL|re.MULTILINE)
  19
+def build_model_document(text):
  20
+    """
  21
+    Build a test example into a dict of HTML bits.
  22
+    """
  23
+    # We need to parse the model file without actually executing it.
  24
+    tree = compiler.parse(text)
  25
+    
  26
+    # Get the title and blurb from the module's docstring
  27
+    title, blurb = tree.doc.strip().split('\n', 1)
  28
+    parts = publish_parts(blurb, writer=DjangoHTMLWriter(), settings_overrides={'initial_header_level': 2})
  29
+    parts["title"] = title
  30
+    
  31
+    # Walk the tree and parse out the bits we care about.
  32
+    visitor = compiler.walk(tree, ModelSourceVistor())
  33
+    parts["api_usage"] = visitor.doctest
  34
+    parts["models"] = visitor.models
  35
+    parts["newstyle"] = visitor.newstyle
  36
+    
  37
+    # Parse out the model source.
  38
+    if visitor.newstyle:
  39
+        model_source = text[:text.index("__test__")]        
  40
+    else:
  41
+        model_source = text[:text.index("API_TESTS")]
  42
+    parts["model_source"] = model_source.replace(tree.doc, "").replace('""""""\n', '\n').strip()
  43
+    
  44
+    return parts
  45
+
  46
+class ModelSourceVistor:
  47
+    """AST visitor for a model module."""
  48
+    
  49
+    def __init__(self):
  50
+        self.doctest = ""
  51
+        self.models = []
  52
+        self.newstyle = True
  53
+    
  54
+    def visitAssign(self, node):
  55
+        assname, valtree = node.getChildren()
  56
+        if assname.name == "__test__":
  57
+            self.doctest = valtree.getChildren()[1].value
  58
+        elif assname.name == "API_TESTS":
  59
+            self.newstyle = False
  60
+            self.doctest = valtree.value
  61
+            
  62
+    def visitClass(self, node):
  63
+        if node.bases and node.bases[0].attrname == "Model":
  64
+            self.models.append(node.name)
  65
+
  66
+class DjangoHTMLWriter(html4css1.Writer):
  67
+    """
  68
+    HTML writer that adds a "toc" key to the set of doc parts.
  69
+    """
  70
+    def __init__(self):
  71
+        html4css1.Writer.__init__(self)
  72
+        self.translator_class = DjangoHTMLTranslator
  73
+
  74
+    def translate(self):
  75
+        # build the document
  76
+        html4css1.Writer.translate(self)
  77
+
  78
+        # build the contents
  79
+        contents = self.build_contents(self.document)
  80
+        contents_doc = self.document.copy()
  81
+        contents_doc.children = contents
  82
+        contents_visitor = self.translator_class(contents_doc)
  83
+        contents_doc.walkabout(contents_visitor)
  84
+        self.parts['toc'] = "<ul class='toc'>%s</ul>" % ''.join(contents_visitor.fragment)
  85
+
  86
+    def build_contents(self, node, level=0):
  87
+        level += 1
  88
+        sections = []
  89
+        i = len(node) - 1
  90
+        while i >= 0 and isinstance(node[i], nodes.section):
  91
+            sections.append(node[i])
  92
+            i -= 1
  93
+        sections.reverse()
  94
+        entries = []
  95
+        autonum = 0
  96
+        depth = 4   # XXX FIXME
  97
+        for section in sections:
  98
+            title = section[0]
  99
+            entrytext = title
  100
+            try:
  101
+                reference = nodes.reference('', '', refid=section['ids'][0], *entrytext)
  102
+            except IndexError:
  103
+                continue
  104
+            ref_id = self.document.set_id(reference)
  105
+            entry = nodes.paragraph('', '', reference)
  106
+            item = nodes.list_item('', entry)
  107
+            if level < depth:
  108
+                subsects = self.build_contents(section, level)
  109
+                item += subsects
  110
+            entries.append(item)
  111
+        if entries:
  112
+            contents = nodes.bullet_list('', *entries)
  113
+            return contents
  114
+        else:
  115
+            return []
  116
+
  117
+class DjangoHTMLTranslator(html4css1.HTMLTranslator):
  118
+    """
  119
+    reST -> HTML translator subclass that fixes up the parts of docutils I don't like.
  120
+    """
  121
+    
  122
+    # Prevent name attributes from being generated
  123
+    named_tags = []
  124
+    
  125
+    def __init__(self, document):
  126
+        html4css1.HTMLTranslator.__init__(self, document)
  127
+        self._in_literal = 0
  128
+    
  129
+    # Remove the default border=1 from <table>    
  130
+    def visit_table(self, node):
  131
+        self.body.append(self.starttag(node, 'table', CLASS='docutils'))
  132
+
  133
+    # Prevent <h3> from becoming <h3><a id>
  134
+    #def visit_title(self, node, move_ids=1):
  135
+    #    if isinstance(node.parent, nodes.Admonition):
  136
+    #        self.body.append(self.starttag(node, 'p', '', CLASS='admonition-title'))
  137
+    #        self.context.append("</p>\n")
  138
+    #    else:
  139
+    #        html4css1.HTMLTranslator.visit_title(self, node, move_ids=0)
  140
+    
  141
+    #
  142
+    # Apply smartypants to content when not inside literals
  143
+    #
  144
+    def visit_literal_block(self, node):
  145
+        self._in_literal += 1
  146
+        html4css1.HTMLTranslator.visit_literal_block(self, node)
  147
+
  148
+    def depart_literal_block(self, node):
  149
+        html4css1.HTMLTranslator.depart_literal_block(self, node)
  150
+        self._in_literal -= 1
  151
+     
  152
+    def visit_literal(self, node):
  153
+        self._in_literal += 1
  154
+        try:
  155
+            html4css1.HTMLTranslator.visit_literal(self, node)
  156
+        finally:
  157
+            self._in_literal -= 1
  158
+     
  159
+    def encode(self, text):
  160
+        text = html4css1.HTMLTranslator.encode(self, text)
  161
+        if self._in_literal <= 0:
  162
+            text = smartypants.smartyPants(text, "qde")
  163
+        return text
  164
+    
  165
+    #
  166
+    # Avoid <blockquote>s around merely indented nodes.
  167
+    # Adapted from http://thread.gmane.org/gmane.text.docutils.user/742/focus=804
  168
+    #
  169
+    
  170
+    _suppress_blockquote_child_nodes = (
  171
+        nodes.bullet_list, nodes.enumerated_list, nodes.definition_list,
  172
+        nodes.literal_block, nodes.doctest_block, nodes.line_block, nodes.table
  173
+    )
  174
+    def _bq_is_valid(self, node):
  175
+        return len(node.children) != 1 or not isinstance(node.children[0], self._suppress_blockquote_child_nodes)
  176
+                                        
  177
+    def visit_block_quote(self, node):
  178
+        if self._bq_is_valid(node):
  179
+            html4css1.HTMLTranslator.visit_block_quote(self, node)
  180
+
  181
+    def depart_block_quote(self, node):
  182
+        if self._bq_is_valid(node):
  183
+            html4css1.HTMLTranslator.depart_block_quote(self, node)
  184
+        
  185
+    
57  django_website/apps/docs/models.py
... ...
@@ -1,50 +1,19 @@
1 1
 from django.db import models
2 2
 
3  
-class Document(models.Model):
4  
-    title = models.CharField(maxlength=200)
5  
-    slug = models.CharField(maxlength=50, unique=True, prepopulate_from=('title',))
6  
-    doc_path = models.CharField(maxlength=200,
7  
-        help_text="Relative to the docs directory in django SVN. Leave off the file extension.")
8  
-    last_updated = models.DateTimeField(auto_now=True)
9  
-
  3
+class DocumentRelease(models.Model):
  4
+    version = models.CharField(maxlength=20, unique=True)
  5
+    repository_path = models.CharField(maxlength=50, help_text="(i.e. 'tags/releases/0.95' or 'branches/0.95-bugfixes')")
  6
+    release_date = models.DateField()
  7
+    
10 8
     class Meta:
11  
-        db_table = 'docs_documents'
12  
-        ordering = ('title',)
13  
-
  9
+        ordering = ('-release_date',)
  10
+        
14 11
     class Admin:
15  
-        fields = (
16  
-            (None, {'fields': ('title', 'slug', 'doc_path')}),
17  
-        )
18  
-        list_display = ('title', 'doc_path')
19  
-
  12
+        list_display = ("version", "repository_path", "release_date")
  13
+        
20 14
     def __str__(self):
21  
-        return self.title
22  
-
  15
+        return self.version
  16
+    
23 17
     def get_absolute_url(self):
24  
-        return "/documentation/%s/" % self.slug
25  
-
26  
-    def get_content(self):
27  
-        try:
28  
-            return self._doc_content
29  
-        except AttributeError:
30  
-            import os
31  
-            from django.conf import settings
32  
-            doc_path = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, "%s.html" % self.doc_path)
33  
-            if os.path.exists(doc_path):
34  
-                self._doc_content = open(doc_path).read()
35  
-            else:
36  
-                self._doc_content = ''
37  
-            return self._doc_content
38  
-
39  
-    def get_toc(self):
40  
-        try:
41  
-            return self._toc_content
42  
-        except AttributeError:
43  
-            import os
44  
-            from django.conf import settings
45  
-            toc_path = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, "%s_toc.html" % self.doc_path)
46  
-            if os.path.exists(toc_path):
47  
-                self._toc_content = open(toc_path).read()
48  
-            else:
49  
-                self._toc_content = ''
50  
-            return self._toc_content
  18
+        return "/documentation/%s/" % (self.version)
  19
+    
0  django_website/apps/docs/parts/__init__.py
No changes.
242  django_website/apps/docs/parts/build_documentation.py
... ...
@@ -1,242 +0,0 @@
1  
-"""
2  
-Script to build the documentation for Django from ReST -> HTML.
3  
-
4  
-Builds each text file in sys.argv (or settings.DJANGO_DOCUMENT_ROOT_PATH) into
5  
-two files: a ".html" file with the document contents and a "_toc.html" file
6  
-with the TOC.
7  
-"""
8  
-
9  
-from django.conf import settings
10  
-from django import template
11  
-from django.db.models import get_app, get_models
12  
-from docutils import nodes, utils
13  
-from docutils.core import publish_parts
14  
-from docutils.writers import html4css1
15  
-import glob, inspect, os, re, sys
16  
-
17  
-SETTINGS = {
18  
-    'initial_header_level': 2
19  
-}
20  
-
21  
-MODEL_DOC_TEMPLATE = """
22  
-<div class="document" id="model-{{ model_name }}">
23  
-
24  
-<h1 class="title">{{ title }}</h1>
25  
-{{ blurb }}
26  
-
27  
-<h2 id="model-source-code">Model source code</h2>
28  
-<pre class="literal-block">{{ model_source|escape }}</pre>
29  
-
30  
-<h2 id="sample-usage">Sample API usage</h2>
31  
-<p>This sample code assumes the above model{{ models|pluralize }} {% if models|pluralize %}have{% else %}has{% endif %}
32  
-been saved in a file <tt class="docutils literal"><span class="pre">mysite/models.py</span></tt>.
33  
-<pre class="literal-block">&gt;&gt;&gt; from mysite.models import {% for model in models %}{{ model.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
34  
-{{ api_usage|escape }}</pre>
35  
-</div>
36  
-"""
37  
-
38  
-MODEL_TOC = """
39  
-<ul>
40  
-<li><a href="#model-source-code">Model source code</a></li>
41  
-<li><a href="#api-reference">API reference</a></li>
42  
-<li><a href="#sample-usage">Sample API usage</a></li>
43  
-</ul>
44  
-"""
45  
-
46  
-def build_documents():
47  
-    writer = DjangoHTMLWriter()
48  
-    for fname in glob.glob1(settings.DJANGO_DOCUMENT_ROOT_PATH, "*.txt"):
49  
-        in_file = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, fname)
50  
-        out_file = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, os.path.splitext(fname)[0] + ".html")
51  
-        toc_file = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, os.path.splitext(fname)[0] + "_toc.html")
52  
-        parts = publish_parts(
53  
-            open(in_file).read(),
54  
-            source_path=in_file,
55  
-            destination_path=out_file,
56  
-            writer=writer,
57  
-            settings_overrides=SETTINGS,
58  
-        )
59  
-        open(out_file, 'w').write(parts['html_body'])
60  
-        open(toc_file, 'w').write(parts['toc'])
61  
-
62  
-def build_test_documents():
63  
-    sys.path.insert(0, settings.DJANGO_TESTS_PATH)
64  
-    writer = DjangoHTMLWriter()
65  
-    import runtests
66  
-
67  
-    # An empty access of the settings to force the default options to be
68  
-    # installed prior to assigning to them.
69  
-    settings.INSTALLED_APPS
70  
-
71  
-    # Manually set INSTALLED_APPS to point to the test models.
72  
-    test_apps = [runtests.MODEL_TESTS_DIR_NAME + '.' + app for (loc, app) in runtests.get_test_models() if loc != runtests.REGRESSION_TESTS_DIR_NAME]
73  
-    settings.INSTALLED_APPS = runtests.ALWAYS_INSTALLED_APPS + test_apps
74  
-
75  
-    # Some of the test models need to know whether the docs are being built.
76  
-    settings.BUILDING_DOCS = True
77  
-
78  
-    for app_name in test_apps:
79  
-        model_name = app_name.split(".")[-1]
80  
-        mod = get_app(model_name)
81  
-
82  
-        out_file = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, 'model_' + model_name + '.html')
83  
-        toc_file = os.path.join(settings.DJANGO_DOCUMENT_ROOT_PATH, 'model_' + model_name + '_toc.html')
84  
-
85  
-        # Clean up the title and blurb.
86  
-        try:
87  
-            title, blurb = mod.__doc__.strip().split('\n', 1)
88  
-        except ValueError:
89  
-            sys.stderr.write("title and blurb not found in %s model test.\n" % model_name)
90  
-            continue
91  
-        parts = publish_parts(
92  
-            blurb,
93  
-            source_path=mod.__file__,
94  
-            destination_path=out_file,
95  
-            writer=writer,
96  
-            settings_overrides=SETTINGS,
97  
-        )
98  
-        blurb = parts["html_body"]
99  
-        try:
100  
-            api_usage = mod.API_TESTS
101  
-        except AttributeError:
102  
-            continue # This model didn't have API_TESTS.
103  
-
104  
-        # Get the source code of the model, without the docstring or the
105  
-        # API_TESTS variable.
106  
-        model_source = inspect.getsource(mod)
107  
-        model_source = model_source.replace(mod.__doc__, '')
108  
-        model_source = model_source.replace(mod.API_TESTS, '')
109  
-        model_source = model_source.replace('""""""\n', '\n')
110  
-        model_source = re.sub(r'(?s)API_TESTS = .*', '', model_source)
111  
-        model_source = model_source.strip()
112  
-
113  
-        models = []
114  
-        for m in get_models(mod):
115  
-            models.append({
116  
-                'name': m._meta.object_name,
117  
-                'module_name': m._meta.module_name,
118  
-            })
119  
-
120  
-        # Run this through the template system.
121  
-        t = template.Template(MODEL_DOC_TEMPLATE)
122  
-        c = template.Context(locals())
123  
-        html = t.render(c)
124  
-
125  
-        try:
126  
-            fp = open(out_file, 'w')
127  
-        except IOError:
128  
-            sys.stderr.write("Couldn't write to %s.\n" % out_file)
129  
-            continue
130  
-        else:
131  
-            fp.write(html)
132  
-            fp.close()
133  
-
134  
-        try:
135  
-            fp = open(toc_file, 'w')
136  
-        except IOError:
137  
-            sys.stderr.write("Couldn't write to %s.\n" % toc_file)
138  
-            continue
139  
-        else:
140  
-            fp.write(MODEL_TOC)
141  
-            fp.close()
142  
-
143  
-class DjangoHTMLWriter(html4css1.Writer):
144  
-    def __init__(self):
145  
-        html4css1.Writer.__init__(self)
146  
-        self.translator_class = DjangoHTMLTranslator
147  
-
148  
-    def translate(self):
149  
-        # build the document
150  
-        html4css1.Writer.translate(self)
151  
-
152  
-        # build the contents
153  
-        contents = self.build_contents(self.document)
154  
-        contents_doc = self.document.copy()
155  
-        contents_doc.children = contents
156  
-        contents_visitor = self.translator_class(contents_doc)
157  
-        contents_doc.walkabout(contents_visitor)
158  
-        self.parts['toc'] = "<ul class='toc'>%s</ul>" % ''.join(contents_visitor.fragment)
159  
-
160  
-    def build_contents(self, node, level=0):
161  
-        level += 1
162  
-        sections = []
163  
-        i = len(node) - 1
164  
-        while i >= 0 and isinstance(node[i], nodes.section):
165  
-            sections.append(node[i])
166  
-            i -= 1
167  
-        sections.reverse()
168  
-        entries = []
169  
-        autonum = 0
170  
-        depth = 4   # XXX FIXME
171  
-        for section in sections:
172  
-            title = section[0]
173  
-            entrytext = title
174  
-            try:
175  
-                reference = nodes.reference('', '', refid=section['ids'][0], *entrytext)
176  
-            except IndexError:
177  
-                continue
178  
-            ref_id = self.document.set_id(reference)
179  
-            entry = nodes.paragraph('', '', reference)
180  
-            item = nodes.list_item('', entry)
181  
-            if level < depth:
182  
-                subsects = self.build_contents(section, level)
183  
-                item += subsects
184  
-            entries.append(item)
185  
-        if entries:
186  
-            contents = nodes.bullet_list('', *entries)
187  
-            return contents
188  
-        else:
189  
-            return []
190  
-
191  
-class DjangoHTMLTranslator(html4css1.HTMLTranslator):
192  
-    def visit_table(self, node):
193  
-        """Remove the damn border=1 from the standard HTML writer"""
194  
-        self.body.append(self.starttag(node, 'table', CLASS='docutils'))
195  
-
196  
-    def visit_title(self, node):
197  
-        """Coppied from html4css1.Writer wholesale just to get rid of the <a name=> crap.  Fun, eh?"""
198  
-        check_id = 0
199  
-        close_tag = '</p>\n'
200  
-        if isinstance(node.parent, nodes.topic):
201  
-            self.body.append(
202  
-                  self.starttag(node, 'p', '', CLASS='topic-title first'))
203  
-            check_id = 1
204  
-        elif isinstance(node.parent, nodes.sidebar):
205  
-            self.body.append(
206  
-                  self.starttag(node, 'p', '', CLASS='sidebar-title'))
207  
-            check_id = 1
208  
-        elif isinstance(node.parent, nodes.Admonition):
209  
-            self.body.append(
210  
-                  self.starttag(node, 'p', '', CLASS='admonition-title'))
211  
-            check_id = 1
212  
-        elif isinstance(node.parent, nodes.table):
213  
-            self.body.append(
214  
-                  self.starttag(node, 'caption', ''))
215  
-            check_id = 1
216  
-            close_tag = '</caption>\n'
217  
-        elif isinstance(node.parent, nodes.document):
218  
-            self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
219  
-            self.context.append('</h1>\n<h2 class="deck">This covers Django version 0.95 and the development version. Old docs: <a href="/documentation/0_90/">0.90</a>, <a href="/documentation/0_91/">0.91</a></h2>\n')
220  
-            self.in_document_title = len(self.body)
221  
-        else:
222  
-            assert isinstance(node.parent, nodes.section)
223  
-            h_level = self.section_level + self.initial_header_level - 1
224  
-            atts = {}
225  
-            if (len(node.parent) >= 2 and
226  
-                isinstance(node.parent[1], nodes.subtitle)):
227  
-                atts['CLASS'] = 'with-subtitle'
228  
-            node.ids = node.parent['ids']
229  
-            self.body.append(self.starttag(node, 'h%s' % h_level, '', **atts))
230  
-            self.context.append('</h%s>\n' % (h_level))
231  
-        if check_id:
232  
-            if node.parent['ids']:
233  
-                self.body.append(
234  
-                    self.starttag({}, 'a', '', name=node.parent['ids'][0]))
235  
-                self.context.append('</a>' + close_tag)
236  
-            else:
237  
-                self.context.append(close_tag)
238  
-
239  
-
240  
-if __name__ == "__main__":
241  
-    build_documents()
242  
-    build_test_documents()
20  django_website/apps/docs/parts/flatten_docs.py
... ...
@@ -1,20 +0,0 @@
1  
-"""
2  
-Utility that converts all current Django documents to flat pages.
3  
-
4  
-We use this to flatten/freeze the current Django documentation for
5  
-a particular version.
6  
-"""
7  
-
8  
-from django_website.apps.docs.models import Document
9  
-from django.contrib.flatpages.models import FlatPage
10  
-
11  
-for doc in Document.objects.all():
12  
-    f = FlatPage(
13  
-        url='/documentation/0_91/%s/' % doc.slug,
14  
-        title='Documentation (version 0.91) | %s' % doc.title,
15  
-        content='%s</div><div id="content-related" class="sidebar"><h2>Contents</h2>%s' % (doc.get_content(), doc.get_toc()),
16  
-        enable_comments=False,
17  
-        template_name='flatfiles/legacy_docs',
18  
-        registration_required=False)
19  
-    f.save()
20  
-    f.site_set = [1]
879  django_website/apps/docs/smartypants.py
... ...
@@ -0,0 +1,879 @@
  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
+