djangodocs.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. """
  2. Sphinx plugins for Django documentation.
  3. """
  4. import json
  5. import os
  6. import re
  7. from docutils import nodes
  8. from docutils.parsers.rst import Directive, directives
  9. from docutils.statemachine import ViewList
  10. from sphinx import addnodes
  11. from sphinx.builders.html import StandaloneHTMLBuilder
  12. from sphinx.directives import CodeBlock
  13. from sphinx.domains.std import Cmdoption
  14. from sphinx.util.console import bold
  15. from sphinx.util.nodes import set_source_info
  16. from sphinx.writers.html import HTMLTranslator
  17. # RE for option descriptions without a '--' prefix
  18. simple_option_desc_re = re.compile(
  19. r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
  20. def setup(app):
  21. app.add_crossref_type(
  22. directivename="setting",
  23. rolename="setting",
  24. indextemplate="pair: %s; setting",
  25. )
  26. app.add_crossref_type(
  27. directivename="templatetag",
  28. rolename="ttag",
  29. indextemplate="pair: %s; template tag"
  30. )
  31. app.add_crossref_type(
  32. directivename="templatefilter",
  33. rolename="tfilter",
  34. indextemplate="pair: %s; template filter"
  35. )
  36. app.add_crossref_type(
  37. directivename="fieldlookup",
  38. rolename="lookup",
  39. indextemplate="pair: %s; field lookup type",
  40. )
  41. app.add_description_unit(
  42. directivename="django-admin",
  43. rolename="djadmin",
  44. indextemplate="pair: %s; django-admin command",
  45. parse_node=parse_django_admin_node,
  46. )
  47. app.add_directive('django-admin-option', Cmdoption)
  48. app.add_config_value('django_next_version', '0.0', True)
  49. app.add_directive('versionadded', VersionDirective)
  50. app.add_directive('versionchanged', VersionDirective)
  51. app.add_builder(DjangoStandaloneHTMLBuilder)
  52. # register the snippet directive
  53. app.add_directive('snippet', SnippetWithFilename)
  54. # register a node for snippet directive so that the xml parser
  55. # knows how to handle the enter/exit parsing event
  56. app.add_node(snippet_with_filename,
  57. html=(visit_snippet, depart_snippet_literal),
  58. latex=(visit_snippet_latex, depart_snippet_latex),
  59. man=(visit_snippet_literal, depart_snippet_literal),
  60. text=(visit_snippet_literal, depart_snippet_literal),
  61. texinfo=(visit_snippet_literal, depart_snippet_literal))
  62. app.set_translator('djangohtml', DjangoHTMLTranslator)
  63. app.set_translator('json', DjangoHTMLTranslator)
  64. app.add_node(
  65. ConsoleNode,
  66. html=(visit_console_html, None),
  67. latex=(visit_console_dummy, depart_console_dummy),
  68. man=(visit_console_dummy, depart_console_dummy),
  69. text=(visit_console_dummy, depart_console_dummy),
  70. texinfo=(visit_console_dummy, depart_console_dummy),
  71. )
  72. app.add_directive('console', ConsoleDirective)
  73. app.connect('html-page-context', html_page_context_hook)
  74. return {'parallel_read_safe': True}
  75. class snippet_with_filename(nodes.literal_block):
  76. """
  77. Subclass the literal_block to override the visit/depart event handlers
  78. """
  79. pass
  80. def visit_snippet_literal(self, node):
  81. """
  82. default literal block handler
  83. """
  84. self.visit_literal_block(node)
  85. def depart_snippet_literal(self, node):
  86. """
  87. default literal block handler
  88. """
  89. self.depart_literal_block(node)
  90. def visit_snippet(self, node):
  91. """
  92. HTML document generator visit handler
  93. """
  94. lang = self.highlightlang
  95. linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
  96. fname = node['filename']
  97. highlight_args = node.get('highlight_args', {})
  98. if 'language' in node:
  99. # code-block directives
  100. lang = node['language']
  101. highlight_args['force'] = True
  102. if 'linenos' in node:
  103. linenos = node['linenos']
  104. def warner(msg):
  105. self.builder.warn(msg, (self.builder.current_docname, node.line))
  106. highlighted = self.highlighter.highlight_block(node.rawsource, lang,
  107. warn=warner,
  108. linenos=linenos,
  109. **highlight_args)
  110. starttag = self.starttag(node, 'div', suffix='',
  111. CLASS='highlight-%s snippet' % lang)
  112. self.body.append(starttag)
  113. self.body.append('<div class="snippet-filename">%s</div>\n''' % (fname,))
  114. self.body.append(highlighted)
  115. self.body.append('</div>\n')
  116. raise nodes.SkipNode
  117. def visit_snippet_latex(self, node):
  118. """
  119. Latex document generator visit handler
  120. """
  121. code = node.rawsource.rstrip('\n')
  122. lang = self.hlsettingstack[-1][0]
  123. linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1
  124. fname = node['filename']
  125. highlight_args = node.get('highlight_args', {})
  126. if 'language' in node:
  127. # code-block directives
  128. lang = node['language']
  129. highlight_args['force'] = True
  130. if 'linenos' in node:
  131. linenos = node['linenos']
  132. def warner(msg):
  133. self.builder.warn(msg, (self.curfilestack[-1], node.line))
  134. hlcode = self.highlighter.highlight_block(code, lang, warn=warner,
  135. linenos=linenos,
  136. **highlight_args)
  137. self.body.append(
  138. '\n{\\colorbox[rgb]{0.9,0.9,0.9}'
  139. '{\\makebox[\\textwidth][l]'
  140. '{\\small\\texttt{%s}}}}\n' % (
  141. # Some filenames have '_', which is special in latex.
  142. fname.replace('_', r'\_'),
  143. )
  144. )
  145. if self.table:
  146. hlcode = hlcode.replace('\\begin{Verbatim}',
  147. '\\begin{OriginalVerbatim}')
  148. self.table.has_problematic = True
  149. self.table.has_verbatim = True
  150. hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
  151. hlcode = hlcode.rstrip() + '\n'
  152. self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' %
  153. (self.table and 'Original' or ''))
  154. # Prevent rawsource from appearing in output a second time.
  155. raise nodes.SkipNode
  156. def depart_snippet_latex(self, node):
  157. """
  158. Latex document generator depart handler.
  159. """
  160. pass
  161. class SnippetWithFilename(Directive):
  162. """
  163. The 'snippet' directive that allows to add the filename (optional)
  164. of a code snippet in the document. This is modeled after CodeBlock.
  165. """
  166. has_content = True
  167. optional_arguments = 1
  168. option_spec = {'filename': directives.unchanged_required}
  169. def run(self):
  170. code = '\n'.join(self.content)
  171. literal = snippet_with_filename(code, code)
  172. if self.arguments:
  173. literal['language'] = self.arguments[0]
  174. literal['filename'] = self.options['filename']
  175. set_source_info(self, literal)
  176. return [literal]
  177. class VersionDirective(Directive):
  178. has_content = True
  179. required_arguments = 1
  180. optional_arguments = 1
  181. final_argument_whitespace = True
  182. option_spec = {}
  183. def run(self):
  184. if len(self.arguments) > 1:
  185. msg = """Only one argument accepted for directive '{directive_name}::'.
  186. Comments should be provided as content,
  187. not as an extra argument.""".format(directive_name=self.name)
  188. raise self.error(msg)
  189. env = self.state.document.settings.env
  190. ret = []
  191. node = addnodes.versionmodified()
  192. ret.append(node)
  193. if self.arguments[0] == env.config.django_next_version:
  194. node['version'] = "Development version"
  195. else:
  196. node['version'] = self.arguments[0]
  197. node['type'] = self.name
  198. if self.content:
  199. self.state.nested_parse(self.content, self.content_offset, node)
  200. env.note_versionchange(node['type'], node['version'], node, self.lineno)
  201. return ret
  202. class DjangoHTMLTranslator(HTMLTranslator):
  203. """
  204. Django-specific reST to HTML tweaks.
  205. """
  206. # Don't use border=1, which docutils does by default.
  207. def visit_table(self, node):
  208. self.context.append(self.compact_p)
  209. self.compact_p = True
  210. self._table_row_index = 0 # Needed by Sphinx
  211. self.body.append(self.starttag(node, 'table', CLASS='docutils'))
  212. def depart_table(self, node):
  213. self.compact_p = self.context.pop()
  214. self.body.append('</table>\n')
  215. def visit_desc_parameterlist(self, node):
  216. self.body.append('(') # by default sphinx puts <big> around the "("
  217. self.first_param = 1
  218. self.optional_param_level = 0
  219. self.param_separator = node.child_text_separator
  220. self.required_params_left = sum(isinstance(c, addnodes.desc_parameter) for c in node.children)
  221. def depart_desc_parameterlist(self, node):
  222. self.body.append(')')
  223. #
  224. # Turn the "new in version" stuff (versionadded/versionchanged) into a
  225. # better callout -- the Sphinx default is just a little span,
  226. # which is a bit less obvious that I'd like.
  227. #
  228. # FIXME: these messages are all hardcoded in English. We need to change
  229. # that to accommodate other language docs, but I can't work out how to make
  230. # that work.
  231. #
  232. version_text = {
  233. 'versionchanged': 'Changed in Django %s',
  234. 'versionadded': 'New in Django %s',
  235. }
  236. def visit_versionmodified(self, node):
  237. self.body.append(
  238. self.starttag(node, 'div', CLASS=node['type'])
  239. )
  240. version_text = self.version_text.get(node['type'])
  241. if version_text:
  242. title = "%s%s" % (
  243. version_text % node['version'],
  244. ":" if node else "."
  245. )
  246. self.body.append('<span class="title">%s</span> ' % title)
  247. def depart_versionmodified(self, node):
  248. self.body.append("</div>\n")
  249. # Give each section a unique ID -- nice for custom CSS hooks
  250. def visit_section(self, node):
  251. old_ids = node.get('ids', [])
  252. node['ids'] = ['s-' + i for i in old_ids]
  253. node['ids'].extend(old_ids)
  254. super().visit_section(node)
  255. node['ids'] = old_ids
  256. def parse_django_admin_node(env, sig, signode):
  257. command = sig.split(' ')[0]
  258. env.ref_context['std:program'] = command
  259. title = "django-admin %s" % sig
  260. signode += addnodes.desc_name(title, title)
  261. return command
  262. class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
  263. """
  264. Subclass to add some extra things we need.
  265. """
  266. name = 'djangohtml'
  267. def finish(self):
  268. super().finish()
  269. self.info(bold("writing templatebuiltins.js..."))
  270. xrefs = self.env.domaindata["std"]["objects"]
  271. templatebuiltins = {
  272. "ttags": [
  273. n for ((t, n), (k, a)) in xrefs.items()
  274. if t == "templatetag" and k == "ref/templates/builtins"
  275. ],
  276. "tfilters": [
  277. n for ((t, n), (k, a)) in xrefs.items()
  278. if t == "templatefilter" and k == "ref/templates/builtins"
  279. ],
  280. }
  281. outfilename = os.path.join(self.outdir, "templatebuiltins.js")
  282. with open(outfilename, 'w') as fp:
  283. fp.write('var django_template_builtins = ')
  284. json.dump(templatebuiltins, fp)
  285. fp.write(';\n')
  286. class ConsoleNode(nodes.literal_block):
  287. """
  288. Custom node to override the visit/depart event handlers at registration
  289. time. Wrap a literal_block object and defer to it.
  290. """
  291. def __init__(self, litblk_obj):
  292. self.wrapped = litblk_obj
  293. def __getattr__(self, attr):
  294. if attr == 'wrapped':
  295. return self.__dict__.wrapped
  296. return getattr(self.wrapped, attr)
  297. def visit_console_dummy(self, node):
  298. """Defer to the corresponding parent's handler."""
  299. self.visit_literal_block(node)
  300. def depart_console_dummy(self, node):
  301. """Defer to the corresponding parent's handler."""
  302. self.depart_literal_block(node)
  303. def visit_console_html(self, node):
  304. """Generate HTML for the console directive."""
  305. if self.builder.name in ('djangohtml', 'json') and node['win_console_text']:
  306. # Put a mark on the document object signaling the fact the directive
  307. # has been used on it.
  308. self.document._console_directive_used_flag = True
  309. uid = node['uid']
  310. self.body.append('''\
  311. <div class="console-block" id="console-block-%(id)s">
  312. <input class="c-tab-unix" id="c-tab-%(id)s-unix" type="radio" name="console-%(id)s" checked>
  313. <label for="c-tab-%(id)s-unix" title="Linux/macOS">&#xf17c/&#xf179</label>
  314. <input class="c-tab-win" id="c-tab-%(id)s-win" type="radio" name="console-%(id)s">
  315. <label for="c-tab-%(id)s-win" title="Windows">&#xf17a</label>
  316. <section class="c-content-unix" id="c-content-%(id)s-unix">\n''' % {'id': uid})
  317. try:
  318. self.visit_literal_block(node)
  319. except nodes.SkipNode:
  320. pass
  321. self.body.append('</section>\n')
  322. self.body.append('<section class="c-content-win" id="c-content-%(id)s-win">\n' % {'id': uid})
  323. win_text = node['win_console_text']
  324. highlight_args = {'force': True}
  325. if 'linenos' in node:
  326. linenos = node['linenos']
  327. else:
  328. linenos = win_text.count('\n') >= self.highlightlinenothreshold - 1
  329. def warner(msg):
  330. self.builder.warn(msg, (self.builder.current_docname, node.line))
  331. highlighted = self.highlighter.highlight_block(
  332. win_text, 'doscon', warn=warner, linenos=linenos, **highlight_args
  333. )
  334. self.body.append(highlighted)
  335. self.body.append('</section>\n')
  336. self.body.append('</div>\n')
  337. raise nodes.SkipNode
  338. else:
  339. self.visit_literal_block(node)
  340. class ConsoleDirective(CodeBlock):
  341. """
  342. A reStructuredText directive which renders a two-tab code block in which
  343. the second tab shows a Windows command line equivalent of the usual
  344. Unix-oriented examples.
  345. """
  346. required_arguments = 0
  347. # The 'doscon' Pygments formatter needs a prompt like this. '>' alone
  348. # won't do it because then it simply paints the whole command line as a
  349. # grey comment with no highlighting at all.
  350. WIN_PROMPT = r'...\> '
  351. def run(self):
  352. def args_to_win(cmdline):
  353. changed = False
  354. out = []
  355. for token in cmdline.split():
  356. if token[:2] == './':
  357. token = token[2:]
  358. changed = True
  359. elif token[:2] == '~/':
  360. token = '%HOMEPATH%\\' + token[2:]
  361. changed = True
  362. elif token == 'make':
  363. token = 'make.bat'
  364. changed = True
  365. if '://' not in token and 'git' not in cmdline:
  366. out.append(token.replace('/', '\\'))
  367. changed = True
  368. else:
  369. out.append(token)
  370. if changed:
  371. return ' '.join(out)
  372. return cmdline
  373. def cmdline_to_win(line):
  374. if line.startswith('# '):
  375. return 'REM ' + args_to_win(line[2:])
  376. if line.startswith('$ # '):
  377. return 'REM ' + args_to_win(line[4:])
  378. if line.startswith('$ ./manage.py'):
  379. return 'manage.py ' + args_to_win(line[13:])
  380. if line.startswith('$ manage.py'):
  381. return 'manage.py ' + args_to_win(line[11:])
  382. if line.startswith('$ ./runtests.py'):
  383. return 'runtests.py ' + args_to_win(line[15:])
  384. if line.startswith('$ ./'):
  385. return args_to_win(line[4:])
  386. if line.startswith('$ python'):
  387. return 'py ' + args_to_win(line[8:])
  388. if line.startswith('$ '):
  389. return args_to_win(line[2:])
  390. return None
  391. def code_block_to_win(content):
  392. bchanged = False
  393. lines = []
  394. for line in content:
  395. modline = cmdline_to_win(line)
  396. if modline is None:
  397. lines.append(line)
  398. else:
  399. lines.append(self.WIN_PROMPT + modline)
  400. bchanged = True
  401. if bchanged:
  402. return ViewList(lines)
  403. return None
  404. env = self.state.document.settings.env
  405. self.arguments = ['console']
  406. lit_blk_obj = super().run()[0]
  407. # Only do work when the djangohtml HTML Sphinx builder is being used,
  408. # invoke the default behavior for the rest.
  409. if env.app.builder.name not in ('djangohtml', 'json'):
  410. return [lit_blk_obj]
  411. lit_blk_obj['uid'] = '%s' % env.new_serialno('console')
  412. # Only add the tabbed UI if there is actually a Windows-specific
  413. # version of the CLI example.
  414. win_content = code_block_to_win(self.content)
  415. if win_content is None:
  416. lit_blk_obj['win_console_text'] = None
  417. else:
  418. self.content = win_content
  419. lit_blk_obj['win_console_text'] = super().run()[0].rawsource
  420. # Replace the literal_node object returned by Sphinx's CodeBlock with
  421. # the ConsoleNode wrapper.
  422. return [ConsoleNode(lit_blk_obj)]
  423. def html_page_context_hook(app, pagename, templatename, context, doctree):
  424. # Put a bool on the context used to render the template. It's used to
  425. # control inclusion of console-tabs.css and activation of the JavaScript.
  426. # This way it's include only from HTML files rendered from reST files where
  427. # the ConsoleDirective is used.
  428. context['include_console_assets'] = getattr(doctree, '_console_directive_used_flag', False)