Added directives and autodocumenters for template tags and filters in a custom extens...
[philo.git] / docs / _ext / djangodocs.py
1 """
2 Sphinx plugins for Django documentation.
3 """
4 import os
5 import re
6
7 from docutils import nodes, transforms
8 try:
9     import json
10 except ImportError:
11     try:
12         import simplejson as json
13     except ImportError:
14         try:
15             from django.utils import simplejson as json
16         except ImportError:
17             json = None
18
19 from sphinx import addnodes, roles
20 from sphinx.builders.html import StandaloneHTMLBuilder
21 from sphinx.writers.html import SmartyPantsHTMLTranslator
22 from sphinx.util.console import bold
23 from sphinx.util.compat import Directive
24
25 # RE for option descriptions without a '--' prefix
26 simple_option_desc_re = re.compile(
27     r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
28
29 def setup(app):
30     app.add_crossref_type(
31         directivename = "setting",
32         rolename      = "setting",
33         indextemplate = "pair: %s; setting",
34     )
35     #app.add_crossref_type(
36     #    directivename = "templatetag",
37     #    rolename      = "ttag",
38     #    indextemplate = "pair: %s; template tag"
39     #)
40     #app.add_crossref_type(
41     #    directivename = "templatefilter",
42     #    rolename      = "tfilter",
43     #    indextemplate = "pair: %s; template filter"
44     #)
45     app.add_crossref_type(
46         directivename = "fieldlookup",
47         rolename      = "lookup",
48         indextemplate = "pair: %s; field lookup type",
49     )
50     app.add_description_unit(
51         directivename = "django-admin",
52         rolename      = "djadmin",
53         indextemplate = "pair: %s; django-admin command",
54         parse_node    = parse_django_admin_node,
55     )
56     app.add_description_unit(
57         directivename = "django-admin-option",
58         rolename      = "djadminopt",
59         indextemplate = "pair: %s; django-admin command-line option",
60         parse_node    = parse_django_adminopt_node,
61     )
62     app.add_config_value('django_next_version', '0.0', True)
63     app.add_directive('versionadded', VersionDirective)
64     app.add_directive('versionchanged', VersionDirective)
65     app.add_transform(SuppressBlockquotes)
66     app.add_builder(DjangoStandaloneHTMLBuilder)
67
68
69 class VersionDirective(Directive):
70     has_content = True
71     required_arguments = 1
72     optional_arguments = 1
73     final_argument_whitespace = True
74     option_spec = {}
75
76     def run(self):
77         env = self.state.document.settings.env
78         arg0 = self.arguments[0]
79         is_nextversion = env.config.django_next_version == arg0
80         ret = []
81         node = addnodes.versionmodified()
82         ret.append(node)
83         if not is_nextversion:
84             if len(self.arguments) == 1:
85                 linktext = 'Please, see the release notes </releases/%s>' % (arg0)
86                 xrefs = roles.XRefRole()('doc', linktext, linktext, self.lineno, self.state)
87                 node.extend(xrefs[0])
88             node['version'] = arg0
89         else:
90             node['version'] = "Development version"
91         node['type'] = self.name
92         if len(self.arguments) == 2:
93             inodes, messages = self.state.inline_text(self.arguments[1], self.lineno+1)
94             node.extend(inodes)
95             if self.content:
96                 self.state.nested_parse(self.content, self.content_offset, node)
97             ret = ret + messages
98         env.note_versionchange(node['type'], node['version'], node, self.lineno)
99         return ret
100
101
102 class SuppressBlockquotes(transforms.Transform):
103     """
104     Remove the default blockquotes that encase indented list, tables, etc.
105     """
106     default_priority = 300
107
108     suppress_blockquote_child_nodes = (
109         nodes.bullet_list,
110         nodes.enumerated_list,
111         nodes.definition_list,
112         nodes.literal_block,
113         nodes.doctest_block,
114         nodes.line_block,
115         nodes.table
116     )
117
118     def apply(self):
119         for node in self.document.traverse(nodes.block_quote):
120             if len(node.children) == 1 and isinstance(node.children[0], self.suppress_blockquote_child_nodes):
121                 node.replace_self(node.children[0])
122
123 class DjangoHTMLTranslator(SmartyPantsHTMLTranslator):
124     """
125     Django-specific reST to HTML tweaks.
126     """
127
128     # Don't use border=1, which docutils does by default.
129     def visit_table(self, node):
130         self.body.append(self.starttag(node, 'table', CLASS='docutils'))
131
132     # <big>? Really?
133     def visit_desc_parameterlist(self, node):
134         self.body.append('(')
135         self.first_param = 1
136
137     def depart_desc_parameterlist(self, node):
138         self.body.append(')')
139
140     #
141     # Don't apply smartypants to literal blocks
142     #
143     def visit_literal_block(self, node):
144         self.no_smarty += 1
145         SmartyPantsHTMLTranslator.visit_literal_block(self, node)
146
147     def depart_literal_block(self, node):
148         SmartyPantsHTMLTranslator.depart_literal_block(self, node)
149         self.no_smarty -= 1
150
151     #
152     # Turn the "new in version" stuff (versionadded/versionchanged) into a
153     # better callout -- the Sphinx default is just a little span,
154     # which is a bit less obvious that I'd like.
155     #
156     # FIXME: these messages are all hardcoded in English. We need to change
157     # that to accomodate other language docs, but I can't work out how to make
158     # that work.
159     #
160     version_text = {
161         'deprecated':       'Deprecated in Django %s',
162         'versionchanged':   'Changed in Django %s',
163         'versionadded':     'New in Django %s',
164     }
165
166     def visit_versionmodified(self, node):
167         self.body.append(
168             self.starttag(node, 'div', CLASS=node['type'])
169         )
170         title = "%s%s" % (
171             self.version_text[node['type']] % node['version'],
172             len(node) and ":" or "."
173         )
174         self.body.append('<span class="title">%s</span> ' % title)
175
176     def depart_versionmodified(self, node):
177         self.body.append("</div>\n")
178
179     # Give each section a unique ID -- nice for custom CSS hooks
180     def visit_section(self, node):
181         old_ids = node.get('ids', [])
182         node['ids'] = ['s-' + i for i in old_ids]
183         node['ids'].extend(old_ids)
184         SmartyPantsHTMLTranslator.visit_section(self, node)
185         node['ids'] = old_ids
186
187 def parse_django_admin_node(env, sig, signode):
188     command = sig.split(' ')[0]
189     env._django_curr_admin_command = command
190     title = "django-admin.py %s" % sig
191     signode += addnodes.desc_name(title, title)
192     return sig
193
194 def parse_django_adminopt_node(env, sig, signode):
195     """A copy of sphinx.directives.CmdoptionDesc.parse_signature()"""
196     from sphinx.domains.std import option_desc_re
197     count = 0
198     firstname = ''
199     for m in option_desc_re.finditer(sig):
200         optname, args = m.groups()
201         if count:
202             signode += addnodes.desc_addname(', ', ', ')
203         signode += addnodes.desc_name(optname, optname)
204         signode += addnodes.desc_addname(args, args)
205         if not count:
206             firstname = optname
207         count += 1
208     if not count:
209         for m in simple_option_desc_re.finditer(sig):
210             optname, args = m.groups()
211             if count:
212                 signode += addnodes.desc_addname(', ', ', ')
213             signode += addnodes.desc_name(optname, optname)
214             signode += addnodes.desc_addname(args, args)
215             if not count:
216                 firstname = optname
217             count += 1
218     if not firstname:
219         raise ValueError
220     return firstname
221
222
223 class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
224     """
225     Subclass to add some extra things we need.
226     """
227
228     name = 'djangohtml'
229
230     def finish(self):
231         super(DjangoStandaloneHTMLBuilder, self).finish()
232         if json is None:
233             self.warn("cannot create templatebuiltins.js due to missing simplejson dependency")
234             return
235         self.info(bold("writing templatebuiltins.js..."))
236         xrefs = self.env.domaindata["std"]["objects"]
237         templatebuiltins = {
238             "ttags": [n for ((t, n), (l, a)) in xrefs.items()
239                         if t == "templatetag" and l == "ref/templates/builtins"],
240             "tfilters": [n for ((t, n), (l, a)) in xrefs.items()
241                         if t == "templatefilter" and l == "ref/templates/builtins"],
242         }
243         outfilename = os.path.join(self.outdir, "templatebuiltins.js")
244         f = open(outfilename, 'wb')
245         f.write('var django_template_builtins = ')
246         json.dump(templatebuiltins, f)
247         f.write(';\n')
248         f.close();