| 1 | """
|
|---|
| 2 | Sphinx plugins for Django documentation. Version GW 2015-11-14
|
|---|
| 3 | """
|
|---|
| 4 | import json
|
|---|
| 5 | import os
|
|---|
| 6 | import re
|
|---|
| 7 |
|
|---|
| 8 | from docutils import nodes
|
|---|
| 9 | from docutils.parsers.rst import directives
|
|---|
| 10 | from sphinx import __version__ as sphinx_ver, addnodes
|
|---|
| 11 | from sphinx.builders.html import StandaloneHTMLBuilder
|
|---|
| 12 | from sphinx.util.compat import Directive
|
|---|
| 13 | from sphinx.util.console import bold
|
|---|
| 14 | from sphinx.util.nodes import set_source_info
|
|---|
| 15 | from sphinx.writers.html import SmartyPantsHTMLTranslator
|
|---|
| 16 |
|
|---|
| 17 | # RE for option descriptions without a '--' prefix
|
|---|
| 18 | simple_option_desc_re = re.compile(
|
|---|
| 19 | r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 | def 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 |
|
|---|
| 72 | class snippet_with_filename(nodes.literal_block):
|
|---|
| 73 | """
|
|---|
| 74 | Subclass the literal_block to override the visit/depart event handlers
|
|---|
| 75 | """
|
|---|
| 76 | pass
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 | def visit_snippet_literal(self, node):
|
|---|
| 80 | """
|
|---|
| 81 | default literal block handler
|
|---|
| 82 | """
|
|---|
| 83 | self.visit_literal_block(node)
|
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 | def depart_snippet_literal(self, node):
|
|---|
| 87 | """
|
|---|
| 88 | default literal block handler
|
|---|
| 89 | """
|
|---|
| 90 | self.depart_literal_block(node)
|
|---|
| 91 |
|
|---|
| 92 |
|
|---|
| 93 | def 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 |
|
|---|
| 124 | def 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 | self.body.append('\n{\\colorbox[rgb]{0.9,0.9,0.9}'
|
|---|
| 155 | '{\\makebox[\\textwidth][l]'
|
|---|
| 156 | '{\\small\\texttt{%s}}}}\n' % (fname,))
|
|---|
| 157 |
|
|---|
| 158 | if self.table:
|
|---|
| 159 | hlcode = hlcode.replace('\\begin{Verbatim}',
|
|---|
| 160 | '\\begin{OriginalVerbatim}')
|
|---|
| 161 | self.table.has_problematic = True
|
|---|
| 162 | self.table.has_verbatim = True
|
|---|
| 163 |
|
|---|
| 164 | hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
|
|---|
| 165 | hlcode = hlcode.rstrip() + '\n'
|
|---|
| 166 | self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
|
|---|
| 167 | (self.table and 'Original' or ''))
|
|---|
| 168 |
|
|---|
| 169 | self.verbatim = None # unneeded? -- GW
|
|---|
| 170 |
|
|---|
| 171 | # End moved code -- GW
|
|---|
| 172 | #----------------------------------
|
|---|
| 173 |
|
|---|
| 174 | # Added
|
|---|
| 175 | raise nodes.SkipNode # prevents rawsource from appearing in output a second time
|
|---|
| 176 |
|
|---|
| 177 | def depart_snippet_latex(self, node):
|
|---|
| 178 | """
|
|---|
| 179 | Latex document generator depart handler.
|
|---|
| 180 | """
|
|---|
| 181 | # Code moved to visit_snippet_latex.
|
|---|
| 182 |
|
|---|
| 183 | pass
|
|---|
| 184 |
|
|---|
| 185 |
|
|---|
| 186 | class SnippetWithFilename(Directive):
|
|---|
| 187 | """
|
|---|
| 188 | The 'snippet' directive that allows to add the filename (optional)
|
|---|
| 189 | of a code snippet in the document. This is modeled after CodeBlock.
|
|---|
| 190 | """
|
|---|
| 191 | has_content = True
|
|---|
| 192 | optional_arguments = 1
|
|---|
| 193 | option_spec = {'filename': directives.unchanged_required}
|
|---|
| 194 |
|
|---|
| 195 | def run(self):
|
|---|
| 196 | code = '\n'.join(self.content)
|
|---|
| 197 |
|
|---|
| 198 | literal = snippet_with_filename(code, code)
|
|---|
| 199 | if self.arguments:
|
|---|
| 200 | literal['language'] = self.arguments[0]
|
|---|
| 201 | literal['filename'] = self.options['filename']
|
|---|
| 202 | set_source_info(self, literal)
|
|---|
| 203 | return [literal]
|
|---|
| 204 |
|
|---|
| 205 |
|
|---|
| 206 | class VersionDirective(Directive):
|
|---|
| 207 | has_content = True
|
|---|
| 208 | required_arguments = 1
|
|---|
| 209 | optional_arguments = 1
|
|---|
| 210 | final_argument_whitespace = True
|
|---|
| 211 | option_spec = {}
|
|---|
| 212 |
|
|---|
| 213 | def run(self):
|
|---|
| 214 | if len(self.arguments) > 1:
|
|---|
| 215 | msg = """Only one argument accepted for directive '{directive_name}::'.
|
|---|
| 216 | Comments should be provided as content,
|
|---|
| 217 | not as an extra argument.""".format(directive_name=self.name)
|
|---|
| 218 | raise self.error(msg)
|
|---|
| 219 |
|
|---|
| 220 | env = self.state.document.settings.env
|
|---|
| 221 | ret = []
|
|---|
| 222 | node = addnodes.versionmodified()
|
|---|
| 223 | ret.append(node)
|
|---|
| 224 |
|
|---|
| 225 | if self.arguments[0] == env.config.django_next_version:
|
|---|
| 226 | node['version'] = "Development version"
|
|---|
| 227 | else:
|
|---|
| 228 | node['version'] = self.arguments[0]
|
|---|
| 229 |
|
|---|
| 230 | node['type'] = self.name
|
|---|
| 231 | if self.content:
|
|---|
| 232 | self.state.nested_parse(self.content, self.content_offset, node)
|
|---|
| 233 | env.note_versionchange(node['type'], node['version'], node, self.lineno)
|
|---|
| 234 | return ret
|
|---|
| 235 |
|
|---|
| 236 |
|
|---|
| 237 | class DjangoHTMLTranslator(SmartyPantsHTMLTranslator):
|
|---|
| 238 | """
|
|---|
| 239 | Django-specific reST to HTML tweaks.
|
|---|
| 240 | """
|
|---|
| 241 |
|
|---|
| 242 | # Don't use border=1, which docutils does by default.
|
|---|
| 243 | def visit_table(self, node):
|
|---|
| 244 | self.context.append(self.compact_p)
|
|---|
| 245 | self.compact_p = True
|
|---|
| 246 | self._table_row_index = 0 # Needed by Sphinx
|
|---|
| 247 | self.body.append(self.starttag(node, 'table', CLASS='docutils'))
|
|---|
| 248 |
|
|---|
| 249 | def depart_table(self, node):
|
|---|
| 250 | self.compact_p = self.context.pop()
|
|---|
| 251 | self.body.append('</table>\n')
|
|---|
| 252 |
|
|---|
| 253 | def visit_desc_parameterlist(self, node):
|
|---|
| 254 | self.body.append('(') # by default sphinx puts <big> around the "("
|
|---|
| 255 | self.first_param = 1
|
|---|
| 256 | self.optional_param_level = 0
|
|---|
| 257 | self.param_separator = node.child_text_separator
|
|---|
| 258 | self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
|
|---|
| 259 | for c in node.children])
|
|---|
| 260 |
|
|---|
| 261 | def depart_desc_parameterlist(self, node):
|
|---|
| 262 | self.body.append(')')
|
|---|
| 263 |
|
|---|
| 264 | if sphinx_ver < '1.0.8':
|
|---|
| 265 | #
|
|---|
| 266 | # Don't apply smartypants to literal blocks
|
|---|
| 267 | #
|
|---|
| 268 | def visit_literal_block(self, node):
|
|---|
| 269 | self.no_smarty += 1
|
|---|
| 270 | SmartyPantsHTMLTranslator.visit_literal_block(self, node)
|
|---|
| 271 |
|
|---|
| 272 | def depart_literal_block(self, node):
|
|---|
| 273 | SmartyPantsHTMLTranslator.depart_literal_block(self, node)
|
|---|
| 274 | self.no_smarty -= 1
|
|---|
| 275 |
|
|---|
| 276 | #
|
|---|
| 277 | # Turn the "new in version" stuff (versionadded/versionchanged) into a
|
|---|
| 278 | # better callout -- the Sphinx default is just a little span,
|
|---|
| 279 | # which is a bit less obvious that I'd like.
|
|---|
| 280 | #
|
|---|
| 281 | # FIXME: these messages are all hardcoded in English. We need to change
|
|---|
| 282 | # that to accommodate other language docs, but I can't work out how to make
|
|---|
| 283 | # that work.
|
|---|
| 284 | #
|
|---|
| 285 | version_text = {
|
|---|
| 286 | 'versionchanged': 'Changed in Django %s',
|
|---|
| 287 | 'versionadded': 'New in Django %s',
|
|---|
| 288 | }
|
|---|
| 289 |
|
|---|
| 290 | def visit_versionmodified(self, node):
|
|---|
| 291 | self.body.append(
|
|---|
| 292 | self.starttag(node, 'div', CLASS=node['type'])
|
|---|
| 293 | )
|
|---|
| 294 | version_text = self.version_text.get(node['type'])
|
|---|
| 295 | if version_text:
|
|---|
| 296 | title = "%s%s" % (
|
|---|
| 297 | version_text % node['version'],
|
|---|
| 298 | ":" if len(node) else "."
|
|---|
| 299 | )
|
|---|
| 300 | self.body.append('<span class="title">%s</span> ' % title)
|
|---|
| 301 |
|
|---|
| 302 | def depart_versionmodified(self, node):
|
|---|
| 303 | self.body.append("</div>\n")
|
|---|
| 304 |
|
|---|
| 305 | # Give each section a unique ID -- nice for custom CSS hooks
|
|---|
| 306 | def visit_section(self, node):
|
|---|
| 307 | old_ids = node.get('ids', [])
|
|---|
| 308 | node['ids'] = ['s-' + i for i in old_ids]
|
|---|
| 309 | node['ids'].extend(old_ids)
|
|---|
| 310 | SmartyPantsHTMLTranslator.visit_section(self, node)
|
|---|
| 311 | node['ids'] = old_ids
|
|---|
| 312 |
|
|---|
| 313 |
|
|---|
| 314 | def parse_django_admin_node(env, sig, signode):
|
|---|
| 315 | command = sig.split(' ')[0]
|
|---|
| 316 | env._django_curr_admin_command = command
|
|---|
| 317 | title = "django-admin %s" % sig
|
|---|
| 318 | signode += addnodes.desc_name(title, title)
|
|---|
| 319 | return sig
|
|---|
| 320 |
|
|---|
| 321 |
|
|---|
| 322 | def parse_django_adminopt_node(env, sig, signode):
|
|---|
| 323 | """A copy of sphinx.directives.CmdoptionDesc.parse_signature()"""
|
|---|
| 324 | from sphinx.domains.std import option_desc_re
|
|---|
| 325 | count = 0
|
|---|
| 326 | firstname = ''
|
|---|
| 327 | for m in option_desc_re.finditer(sig):
|
|---|
| 328 | optname, args = m.groups()
|
|---|
| 329 | if count:
|
|---|
| 330 | signode += addnodes.desc_addname(', ', ', ')
|
|---|
| 331 | signode += addnodes.desc_name(optname, optname)
|
|---|
| 332 | signode += addnodes.desc_addname(args, args)
|
|---|
| 333 | if not count:
|
|---|
| 334 | firstname = optname
|
|---|
| 335 | count += 1
|
|---|
| 336 | if not count:
|
|---|
| 337 | for m in simple_option_desc_re.finditer(sig):
|
|---|
| 338 | optname, args = m.groups()
|
|---|
| 339 | if count:
|
|---|
| 340 | signode += addnodes.desc_addname(', ', ', ')
|
|---|
| 341 | signode += addnodes.desc_name(optname, optname)
|
|---|
| 342 | signode += addnodes.desc_addname(args, args)
|
|---|
| 343 | if not count:
|
|---|
| 344 | firstname = optname
|
|---|
| 345 | count += 1
|
|---|
| 346 | if not firstname:
|
|---|
| 347 | raise ValueError
|
|---|
| 348 | return firstname
|
|---|
| 349 |
|
|---|
| 350 |
|
|---|
| 351 | class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
|
|---|
| 352 | """
|
|---|
| 353 | Subclass to add some extra things we need.
|
|---|
| 354 | """
|
|---|
| 355 |
|
|---|
| 356 | name = 'djangohtml'
|
|---|
| 357 |
|
|---|
| 358 | def finish(self):
|
|---|
| 359 | super(DjangoStandaloneHTMLBuilder, self).finish()
|
|---|
| 360 | self.info(bold("writing templatebuiltins.js..."))
|
|---|
| 361 | xrefs = self.env.domaindata["std"]["objects"]
|
|---|
| 362 | templatebuiltins = {
|
|---|
| 363 | "ttags": [n for ((t, n), (l, a)) in xrefs.items()
|
|---|
| 364 | if t == "templatetag" and l == "ref/templates/builtins"],
|
|---|
| 365 | "tfilters": [n for ((t, n), (l, a)) in xrefs.items()
|
|---|
| 366 | if t == "templatefilter" and l == "ref/templates/builtins"],
|
|---|
| 367 | }
|
|---|
| 368 | outfilename = os.path.join(self.outdir, "templatebuiltins.js")
|
|---|
| 369 | with open(outfilename, 'w') as fp:
|
|---|
| 370 | fp.write('var django_template_builtins = ')
|
|---|
| 371 | json.dump(templatebuiltins, fp)
|
|---|
| 372 | fp.write(';\n')
|
|---|