|
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 try: |
|
87 xrefs = roles.XRefRole()('doc', linktext, linktext, self.lineno, self.state) # Sphinx >= 1.0 |
|
88 except AttributeError: |
|
89 xrefs = roles.xfileref_role('doc', linktext, linktext, self.lineno, self.state) # Sphinx < 1.0 |
|
90 node.extend(xrefs[0]) |
|
91 node['version'] = arg0 |
|
92 else: |
|
93 node['version'] = "Development version" |
|
94 node['type'] = self.name |
|
95 if len(self.arguments) == 2: |
|
96 inodes, messages = self.state.inline_text(self.arguments[1], self.lineno+1) |
|
97 node.extend(inodes) |
|
98 if self.content: |
|
99 self.state.nested_parse(self.content, self.content_offset, node) |
|
100 ret = ret + messages |
|
101 env.note_versionchange(node['type'], node['version'], node, self.lineno) |
|
102 return ret |
|
103 |
|
104 |
|
105 class SuppressBlockquotes(transforms.Transform): |
|
106 """ |
|
107 Remove the default blockquotes that encase indented list, tables, etc. |
|
108 """ |
|
109 default_priority = 300 |
|
110 |
|
111 suppress_blockquote_child_nodes = ( |
|
112 nodes.bullet_list, |
|
113 nodes.enumerated_list, |
|
114 nodes.definition_list, |
|
115 nodes.literal_block, |
|
116 nodes.doctest_block, |
|
117 nodes.line_block, |
|
118 nodes.table |
|
119 ) |
|
120 |
|
121 def apply(self): |
|
122 for node in self.document.traverse(nodes.block_quote): |
|
123 if len(node.children) == 1 and isinstance(node.children[0], self.suppress_blockquote_child_nodes): |
|
124 node.replace_self(node.children[0]) |
|
125 |
|
126 class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): |
|
127 """ |
|
128 Django-specific reST to HTML tweaks. |
|
129 """ |
|
130 |
|
131 # Don't use border=1, which docutils does by default. |
|
132 def visit_table(self, node): |
|
133 self.body.append(self.starttag(node, 'table', CLASS='docutils')) |
|
134 |
|
135 # <big>? Really? |
|
136 def visit_desc_parameterlist(self, node): |
|
137 self.body.append('(') |
|
138 self.first_param = 1 |
|
139 |
|
140 def depart_desc_parameterlist(self, node): |
|
141 self.body.append(')') |
|
142 |
|
143 # |
|
144 # Don't apply smartypants to literal blocks |
|
145 # |
|
146 def visit_literal_block(self, node): |
|
147 self.no_smarty += 1 |
|
148 SmartyPantsHTMLTranslator.visit_literal_block(self, node) |
|
149 |
|
150 def depart_literal_block(self, node): |
|
151 SmartyPantsHTMLTranslator.depart_literal_block(self, node) |
|
152 self.no_smarty -= 1 |
|
153 |
|
154 # |
|
155 # Turn the "new in version" stuff (versionadded/versionchanged) into a |
|
156 # better callout -- the Sphinx default is just a little span, |
|
157 # which is a bit less obvious that I'd like. |
|
158 # |
|
159 # FIXME: these messages are all hardcoded in English. We need to change |
|
160 # that to accomodate other language docs, but I can't work out how to make |
|
161 # that work. |
|
162 # |
|
163 version_text = { |
|
164 'deprecated': 'Deprecated in Django %s', |
|
165 'versionchanged': 'Changed in Django %s', |
|
166 'versionadded': 'New in Django %s', |
|
167 } |
|
168 |
|
169 def visit_versionmodified(self, node): |
|
170 self.body.append( |
|
171 self.starttag(node, 'div', CLASS=node['type']) |
|
172 ) |
|
173 title = "%s%s" % ( |
|
174 self.version_text[node['type']] % node['version'], |
|
175 len(node) and ":" or "." |
|
176 ) |
|
177 self.body.append('<span class="title">%s</span> ' % title) |
|
178 |
|
179 def depart_versionmodified(self, node): |
|
180 self.body.append("</div>\n") |
|
181 |
|
182 # Give each section a unique ID -- nice for custom CSS hooks |
|
183 def visit_section(self, node): |
|
184 old_ids = node.get('ids', []) |
|
185 node['ids'] = ['s-' + i for i in old_ids] |
|
186 node['ids'].extend(old_ids) |
|
187 SmartyPantsHTMLTranslator.visit_section(self, node) |
|
188 node['ids'] = old_ids |
|
189 |
|
190 def parse_django_admin_node(env, sig, signode): |
|
191 command = sig.split(' ')[0] |
|
192 env._django_curr_admin_command = command |
|
193 title = "django-admin.py %s" % sig |
|
194 signode += addnodes.desc_name(title, title) |
|
195 return sig |
|
196 |
|
197 def parse_django_adminopt_node(env, sig, signode): |
|
198 """A copy of sphinx.directives.CmdoptionDesc.parse_signature()""" |
|
199 try: |
|
200 from sphinx.domains.std import option_desc_re # Sphinx >= 1.0 |
|
201 except ImportError: |
|
202 from sphinx.directives.desc import option_desc_re # Sphinx < 1.0 |
|
203 count = 0 |
|
204 firstname = '' |
|
205 for m in option_desc_re.finditer(sig): |
|
206 optname, args = m.groups() |
|
207 if count: |
|
208 signode += addnodes.desc_addname(', ', ', ') |
|
209 signode += addnodes.desc_name(optname, optname) |
|
210 signode += addnodes.desc_addname(args, args) |
|
211 if not count: |
|
212 firstname = optname |
|
213 count += 1 |
|
214 if not count: |
|
215 for m in simple_option_desc_re.finditer(sig): |
|
216 optname, args = m.groups() |
|
217 if count: |
|
218 signode += addnodes.desc_addname(', ', ', ') |
|
219 signode += addnodes.desc_name(optname, optname) |
|
220 signode += addnodes.desc_addname(args, args) |
|
221 if not count: |
|
222 firstname = optname |
|
223 count += 1 |
|
224 if not firstname: |
|
225 raise ValueError |
|
226 return firstname |
|
227 |
|
228 |
|
229 class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder): |
|
230 """ |
|
231 Subclass to add some extra things we need. |
|
232 """ |
|
233 |
|
234 name = 'djangohtml' |
|
235 |
|
236 def finish(self): |
|
237 super(DjangoStandaloneHTMLBuilder, self).finish() |
|
238 if json is None: |
|
239 self.warn("cannot create templatebuiltins.js due to missing simplejson dependency") |
|
240 return |
|
241 self.info(bold("writing templatebuiltins.js...")) |
|
242 try: |
|
243 # Sphinx < 1.0 |
|
244 xrefs = self.env.reftargets.items() |
|
245 templatebuiltins = dict([('ttags', [n for ((t,n),(l,a)) in xrefs |
|
246 if t == 'ttag' and |
|
247 l == 'ref/templates/builtins']), |
|
248 ('tfilters', [n for ((t,n),(l,a)) in xrefs |
|
249 if t == 'tfilter' and |
|
250 l == 'ref/templates/builtins'])]) |
|
251 except AttributeError: |
|
252 # Sphinx >= 1.0 |
|
253 xrefs = self.env.domaindata["std"]["objects"] |
|
254 templatebuiltins = dict([('ttags', [n for ((t,n), (l,a)) in xrefs.items() |
|
255 if t == 'templatetag' and |
|
256 l == 'ref/templates/builtins' ]), |
|
257 ('tfilters', [n for ((t,n), (l,a)) in xrefs.items() |
|
258 if t == 'templatefilter' and |
|
259 t == 'ref/templates/builtins'])]) |
|
260 outfilename = os.path.join(self.outdir, "templatebuiltins.js") |
|
261 f = open(outfilename, 'wb') |
|
262 f.write('var django_template_builtins = ') |
|
263 json.dump(templatebuiltins, f) |
|
264 f.write(';\n') |
|
265 f.close(); |