Ticket #23751: djangodocs-latex-pdf-fix-gw-2015-11-14C.py

File djangodocs-latex-pdf-fix-gw-2015-11-14C.py, 12.5 KB (added by Graham Wideman, 8 years ago)

djangodocs.py with even better fixed visit_snippet_latex() and depart_snippet_latex

Line 
1"""
2Sphinx plugins for Django documentation. Version GW 2015-11-14C
3"""
4import json
5import os
6import re
7
8from docutils import nodes
9from docutils.parsers.rst import directives
10from sphinx import __version__ as sphinx_ver, addnodes
11from sphinx.builders.html import StandaloneHTMLBuilder
12from sphinx.util.compat import Directive
13from sphinx.util.console import bold
14from sphinx.util.nodes import set_source_info
15from sphinx.writers.html import SmartyPantsHTMLTranslator
16
17# RE for option descriptions without a '--' prefix
18simple_option_desc_re = re.compile(
19 r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
20
21
22def setup(app):
23 app.add_crossref_type(
24 directivename="setting",
25 rolename="setting",
26 indextemplate="pair: %s; setting",
27 )
28 app.add_crossref_type(
29 directivename="templatetag",
30 rolename="ttag",
31 indextemplate="pair: %s; template tag"
32 )
33 app.add_crossref_type(
34 directivename="templatefilter",
35 rolename="tfilter",
36 indextemplate="pair: %s; template filter"
37 )
38 app.add_crossref_type(
39 directivename="fieldlookup",
40 rolename="lookup",
41 indextemplate="pair: %s; field lookup type",
42 )
43 app.add_description_unit(
44 directivename="django-admin",
45 rolename="djadmin",
46 indextemplate="pair: %s; django-admin command",
47 parse_node=parse_django_admin_node,
48 )
49 app.add_description_unit(
50 directivename="django-admin-option",
51 rolename="djadminopt",
52 indextemplate="pair: %s; django-admin command-line option",
53 parse_node=parse_django_adminopt_node,
54 )
55 app.add_config_value('django_next_version', '0.0', True)
56 app.add_directive('versionadded', VersionDirective)
57 app.add_directive('versionchanged', VersionDirective)
58 app.add_builder(DjangoStandaloneHTMLBuilder)
59
60 # register the snippet directive
61 app.add_directive('snippet', SnippetWithFilename)
62 # register a node for snippet directive so that the xml parser
63 # knows how to handle the enter/exit parsing event
64 app.add_node(snippet_with_filename,
65 html=(visit_snippet , depart_snippet_literal),
66 latex=(visit_snippet_latex , depart_snippet_latex),
67 man=(visit_snippet_literal , depart_snippet_literal),
68 text=(visit_snippet_literal , depart_snippet_literal),
69 texinfo=(visit_snippet_literal , depart_snippet_literal))
70
71
72class snippet_with_filename(nodes.literal_block):
73 """
74 Subclass the literal_block to override the visit/depart event handlers
75 """
76 pass
77
78
79def visit_snippet_literal(self, node):
80 """
81 default literal block handler
82 """
83 self.visit_literal_block(node)
84
85
86def depart_snippet_literal(self, node):
87 """
88 default literal block handler
89 """
90 self.depart_literal_block(node)
91
92
93def visit_snippet(self, node):
94 """
95 HTML document generator visit handler
96 """
97 lang = self.highlightlang
98 linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
99 fname = node['filename']
100 highlight_args = node.get('highlight_args', {})
101 if 'language' in node:
102 # code-block directives
103 lang = node['language']
104 highlight_args['force'] = True
105 if 'linenos' in node:
106 linenos = node['linenos']
107
108 def warner(msg):
109 self.builder.warn(msg, (self.builder.current_docname, node.line))
110
111 highlighted = self.highlighter.highlight_block(node.rawsource, lang,
112 warn=warner,
113 linenos=linenos,
114 **highlight_args)
115 starttag = self.starttag(node, 'div', suffix='',
116 CLASS='highlight-%s' % lang)
117 self.body.append(starttag)
118 self.body.append('<div class="snippet-filename">%s</div>\n''' % (fname,))
119 self.body.append(highlighted)
120 self.body.append('</div>\n')
121 raise nodes.SkipNode
122
123
124def visit_snippet_latex(self, node):
125 """
126 Latex document generator visit handler
127 """
128 # self.verbatim = ''
129
130 #----------------------------------
131 # Moved from depart_snippet_latex -- GW
132
133 # code = self.verbatim.rstrip('\n')
134 code = node.rawsource.rstrip('\n') # GW added
135
136 lang = self.hlsettingstack[-1][0]
137 linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
138 fname = node['filename']
139 highlight_args = node.get('highlight_args', {})
140 if 'language' in node:
141 # code-block directives
142 lang = node['language']
143 highlight_args['force'] = True
144 if 'linenos' in node:
145 linenos = node['linenos']
146
147 def warner(msg):
148 self.builder.warn(msg, (self.curfilestack[-1], node.line))
149
150 hlcode = self.highlighter.highlight_block(code, lang, warn=warner,
151 linenos=linenos,
152 **highlight_args)
153
154 fname2 = fname.replace('_', r'\_') # because some filenames have '_', which is special in latex
155
156 self.body.append('\n{\\colorbox[rgb]{0.9,0.9,0.9}'
157 '{\\makebox[\\textwidth][l]'
158 '{\\small\\texttt{%s}}}}\n' % (fname2,))
159
160# self.body.append('\n{\\colorbox[rgb]{0.9,0.9,0.9}'
161# '{\\makebox[\\textwidth][l]'
162# '{\\small\\texttt{%s}}}}\n' % (fname,))
163
164 if self.table:
165 hlcode = hlcode.replace('\\begin{Verbatim}',
166 '\\begin{OriginalVerbatim}')
167 self.table.has_problematic = True
168 self.table.has_verbatim = True
169
170 hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
171 hlcode = hlcode.rstrip() + '\n'
172 self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
173 (self.table and 'Original' or ''))
174
175 self.verbatim = None # unneeded? -- GW
176
177 # End moved code -- GW
178 #----------------------------------
179
180 # Added
181 raise nodes.SkipNode # prevents rawsource from appearing in output a second time
182
183def depart_snippet_latex(self, node):
184 """
185 Latex document generator depart handler.
186 """
187 # Code moved to visit_snippet_latex.
188
189 pass
190
191
192class SnippetWithFilename(Directive):
193 """
194 The 'snippet' directive that allows to add the filename (optional)
195 of a code snippet in the document. This is modeled after CodeBlock.
196 """
197 has_content = True
198 optional_arguments = 1
199 option_spec = {'filename': directives.unchanged_required}
200
201 def run(self):
202 code = '\n'.join(self.content)
203
204 literal = snippet_with_filename(code, code)
205 if self.arguments:
206 literal['language'] = self.arguments[0]
207 literal['filename'] = self.options['filename']
208 set_source_info(self, literal)
209 return [literal]
210
211
212class VersionDirective(Directive):
213 has_content = True
214 required_arguments = 1
215 optional_arguments = 1
216 final_argument_whitespace = True
217 option_spec = {}
218
219 def run(self):
220 if len(self.arguments) > 1:
221 msg = """Only one argument accepted for directive '{directive_name}::'.
222 Comments should be provided as content,
223 not as an extra argument.""".format(directive_name=self.name)
224 raise self.error(msg)
225
226 env = self.state.document.settings.env
227 ret = []
228 node = addnodes.versionmodified()
229 ret.append(node)
230
231 if self.arguments[0] == env.config.django_next_version:
232 node['version'] = "Development version"
233 else:
234 node['version'] = self.arguments[0]
235
236 node['type'] = self.name
237 if self.content:
238 self.state.nested_parse(self.content, self.content_offset, node)
239 env.note_versionchange(node['type'], node['version'], node, self.lineno)
240 return ret
241
242
243class DjangoHTMLTranslator(SmartyPantsHTMLTranslator):
244 """
245 Django-specific reST to HTML tweaks.
246 """
247
248 # Don't use border=1, which docutils does by default.
249 def visit_table(self, node):
250 self.context.append(self.compact_p)
251 self.compact_p = True
252 self._table_row_index = 0 # Needed by Sphinx
253 self.body.append(self.starttag(node, 'table', CLASS='docutils'))
254
255 def depart_table(self, node):
256 self.compact_p = self.context.pop()
257 self.body.append('</table>\n')
258
259 def visit_desc_parameterlist(self, node):
260 self.body.append('(') # by default sphinx puts <big> around the "("
261 self.first_param = 1
262 self.optional_param_level = 0
263 self.param_separator = node.child_text_separator
264 self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
265 for c in node.children])
266
267 def depart_desc_parameterlist(self, node):
268 self.body.append(')')
269
270 if sphinx_ver < '1.0.8':
271 #
272 # Don't apply smartypants to literal blocks
273 #
274 def visit_literal_block(self, node):
275 self.no_smarty += 1
276 SmartyPantsHTMLTranslator.visit_literal_block(self, node)
277
278 def depart_literal_block(self, node):
279 SmartyPantsHTMLTranslator.depart_literal_block(self, node)
280 self.no_smarty -= 1
281
282 #
283 # Turn the "new in version" stuff (versionadded/versionchanged) into a
284 # better callout -- the Sphinx default is just a little span,
285 # which is a bit less obvious that I'd like.
286 #
287 # FIXME: these messages are all hardcoded in English. We need to change
288 # that to accommodate other language docs, but I can't work out how to make
289 # that work.
290 #
291 version_text = {
292 'versionchanged': 'Changed in Django %s',
293 'versionadded': 'New in Django %s',
294 }
295
296 def visit_versionmodified(self, node):
297 self.body.append(
298 self.starttag(node, 'div', CLASS=node['type'])
299 )
300 version_text = self.version_text.get(node['type'])
301 if version_text:
302 title = "%s%s" % (
303 version_text % node['version'],
304 ":" if len(node) else "."
305 )
306 self.body.append('<span class="title">%s</span> ' % title)
307
308 def depart_versionmodified(self, node):
309 self.body.append("</div>\n")
310
311 # Give each section a unique ID -- nice for custom CSS hooks
312 def visit_section(self, node):
313 old_ids = node.get('ids', [])
314 node['ids'] = ['s-' + i for i in old_ids]
315 node['ids'].extend(old_ids)
316 SmartyPantsHTMLTranslator.visit_section(self, node)
317 node['ids'] = old_ids
318
319
320def parse_django_admin_node(env, sig, signode):
321 command = sig.split(' ')[0]
322 env._django_curr_admin_command = command
323 title = "django-admin %s" % sig
324 signode += addnodes.desc_name(title, title)
325 return sig
326
327
328def parse_django_adminopt_node(env, sig, signode):
329 """A copy of sphinx.directives.CmdoptionDesc.parse_signature()"""
330 from sphinx.domains.std import option_desc_re
331 count = 0
332 firstname = ''
333 for m in option_desc_re.finditer(sig):
334 optname, args = m.groups()
335 if count:
336 signode += addnodes.desc_addname(', ', ', ')
337 signode += addnodes.desc_name(optname, optname)
338 signode += addnodes.desc_addname(args, args)
339 if not count:
340 firstname = optname
341 count += 1
342 if not count:
343 for m in simple_option_desc_re.finditer(sig):
344 optname, args = m.groups()
345 if count:
346 signode += addnodes.desc_addname(', ', ', ')
347 signode += addnodes.desc_name(optname, optname)
348 signode += addnodes.desc_addname(args, args)
349 if not count:
350 firstname = optname
351 count += 1
352 if not firstname:
353 raise ValueError
354 return firstname
355
356
357class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
358 """
359 Subclass to add some extra things we need.
360 """
361
362 name = 'djangohtml'
363
364 def finish(self):
365 super(DjangoStandaloneHTMLBuilder, self).finish()
366 self.info(bold("writing templatebuiltins.js..."))
367 xrefs = self.env.domaindata["std"]["objects"]
368 templatebuiltins = {
369 "ttags": [n for ((t, n), (l, a)) in xrefs.items()
370 if t == "templatetag" and l == "ref/templates/builtins"],
371 "tfilters": [n for ((t, n), (l, a)) in xrefs.items()
372 if t == "templatefilter" and l == "ref/templates/builtins"],
373 }
374 outfilename = os.path.join(self.outdir, "templatebuiltins.js")
375 with open(outfilename, 'w') as fp:
376 fp.write('var django_template_builtins = ')
377 json.dump(templatebuiltins, fp)
378 fp.write(';\n')
Back to Top