body.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. # $Id: body.py 9030 2022-03-05 23:28:32Z milde $
  2. # Author: David Goodger <goodger@python.org>
  3. # Copyright: This module has been placed in the public domain.
  4. """
  5. Directives for additional body elements.
  6. See `docutils.parsers.rst.directives` for API details.
  7. """
  8. __docformat__ = 'reStructuredText'
  9. from docutils import nodes
  10. from docutils.parsers.rst import Directive
  11. from docutils.parsers.rst import directives
  12. from docutils.parsers.rst.roles import set_classes
  13. from docutils.utils.code_analyzer import Lexer, LexerError, NumberLines
  14. class BasePseudoSection(Directive):
  15. required_arguments = 1
  16. optional_arguments = 0
  17. final_argument_whitespace = True
  18. option_spec = {'class': directives.class_option,
  19. 'name': directives.unchanged}
  20. has_content = True
  21. node_class = None
  22. """Node class to be used (must be set in subclasses)."""
  23. def run(self):
  24. if not (self.state_machine.match_titles
  25. or isinstance(self.state_machine.node, nodes.sidebar)):
  26. raise self.error('The "%s" directive may not be used within '
  27. 'topics or body elements.' % self.name)
  28. self.assert_has_content()
  29. if self.arguments: # title (in sidebars optional)
  30. title_text = self.arguments[0]
  31. textnodes, messages = self.state.inline_text(
  32. title_text, self.lineno)
  33. titles = [nodes.title(title_text, '', *textnodes)]
  34. # Sidebar uses this code.
  35. if 'subtitle' in self.options:
  36. textnodes, more_messages = self.state.inline_text(
  37. self.options['subtitle'], self.lineno)
  38. titles.append(nodes.subtitle(self.options['subtitle'], '',
  39. *textnodes))
  40. messages.extend(more_messages)
  41. else:
  42. titles = []
  43. messages = []
  44. text = '\n'.join(self.content)
  45. node = self.node_class(text, *(titles + messages))
  46. node['classes'] += self.options.get('class', [])
  47. self.add_name(node)
  48. if text:
  49. self.state.nested_parse(self.content, self.content_offset, node)
  50. return [node]
  51. class Topic(BasePseudoSection):
  52. node_class = nodes.topic
  53. class Sidebar(BasePseudoSection):
  54. node_class = nodes.sidebar
  55. required_arguments = 0
  56. optional_arguments = 1
  57. option_spec = BasePseudoSection.option_spec.copy()
  58. option_spec['subtitle'] = directives.unchanged_required
  59. def run(self):
  60. if isinstance(self.state_machine.node, nodes.sidebar):
  61. raise self.error('The "%s" directive may not be used within a '
  62. 'sidebar element.' % self.name)
  63. if 'subtitle' in self.options and not self.arguments:
  64. raise self.error('The "subtitle" option may not be used '
  65. 'without a title.')
  66. return BasePseudoSection.run(self)
  67. class LineBlock(Directive):
  68. option_spec = {'class': directives.class_option,
  69. 'name': directives.unchanged}
  70. has_content = True
  71. def run(self):
  72. self.assert_has_content()
  73. block = nodes.line_block(classes=self.options.get('class', []))
  74. self.add_name(block)
  75. node_list = [block]
  76. for line_text in self.content:
  77. text_nodes, messages = self.state.inline_text(
  78. line_text.strip(), self.lineno + self.content_offset)
  79. line = nodes.line(line_text, '', *text_nodes)
  80. if line_text.strip():
  81. line.indent = len(line_text) - len(line_text.lstrip())
  82. block += line
  83. node_list.extend(messages)
  84. self.content_offset += 1
  85. self.state.nest_line_block_lines(block)
  86. return node_list
  87. class ParsedLiteral(Directive):
  88. option_spec = {'class': directives.class_option,
  89. 'name': directives.unchanged}
  90. has_content = True
  91. def run(self):
  92. set_classes(self.options)
  93. self.assert_has_content()
  94. text = '\n'.join(self.content)
  95. text_nodes, messages = self.state.inline_text(text, self.lineno)
  96. node = nodes.literal_block(text, '', *text_nodes, **self.options)
  97. node.line = self.content_offset + 1
  98. self.add_name(node)
  99. return [node] + messages
  100. class CodeBlock(Directive):
  101. """Parse and mark up content of a code block.
  102. Configuration setting: syntax_highlight
  103. Highlight Code content with Pygments?
  104. Possible values: ('long', 'short', 'none')
  105. """
  106. optional_arguments = 1
  107. option_spec = {'class': directives.class_option,
  108. 'name': directives.unchanged,
  109. 'number-lines': directives.unchanged # integer or None
  110. }
  111. has_content = True
  112. def run(self):
  113. self.assert_has_content()
  114. if self.arguments:
  115. language = self.arguments[0]
  116. else:
  117. language = ''
  118. set_classes(self.options)
  119. classes = ['code']
  120. if language:
  121. classes.append(language)
  122. if 'classes' in self.options:
  123. classes.extend(self.options['classes'])
  124. # set up lexical analyzer
  125. try:
  126. tokens = Lexer('\n'.join(self.content), language,
  127. self.state.document.settings.syntax_highlight)
  128. except LexerError as error:
  129. if self.state.document.settings.report_level > 2:
  130. # don't report warnings -> insert without syntax highlight
  131. tokens = Lexer('\n'.join(self.content), language, 'none')
  132. else:
  133. raise self.warning(error)
  134. if 'number-lines' in self.options:
  135. # optional argument `startline`, defaults to 1
  136. try:
  137. startline = int(self.options['number-lines'] or 1)
  138. except ValueError:
  139. raise self.error(':number-lines: with non-integer start value')
  140. endline = startline + len(self.content)
  141. # add linenumber filter:
  142. tokens = NumberLines(tokens, startline, endline)
  143. node = nodes.literal_block('\n'.join(self.content), classes=classes)
  144. self.add_name(node)
  145. # if called from "include", set the source
  146. if 'source' in self.options:
  147. node.attributes['source'] = self.options['source']
  148. # analyze content and add nodes for every token
  149. for classes, value in tokens:
  150. if classes:
  151. node += nodes.inline(value, value, classes=classes)
  152. else:
  153. # insert as Text to decrease the verbosity of the output
  154. node += nodes.Text(value)
  155. return [node]
  156. class MathBlock(Directive):
  157. option_spec = {'class': directives.class_option,
  158. 'name': directives.unchanged,
  159. # TODO: Add Sphinx' ``mathbase.py`` option 'nowrap'?
  160. # 'nowrap': directives.flag,
  161. }
  162. has_content = True
  163. def run(self):
  164. set_classes(self.options)
  165. self.assert_has_content()
  166. # join lines, separate blocks
  167. content = '\n'.join(self.content).split('\n\n')
  168. _nodes = []
  169. for block in content:
  170. if not block:
  171. continue
  172. node = nodes.math_block(self.block_text, block, **self.options)
  173. node.line = self.content_offset + 1
  174. self.add_name(node)
  175. _nodes.append(node)
  176. return _nodes
  177. class Rubric(Directive):
  178. required_arguments = 1
  179. optional_arguments = 0
  180. final_argument_whitespace = True
  181. option_spec = {'class': directives.class_option,
  182. 'name': directives.unchanged}
  183. def run(self):
  184. set_classes(self.options)
  185. rubric_text = self.arguments[0]
  186. textnodes, messages = self.state.inline_text(rubric_text, self.lineno)
  187. rubric = nodes.rubric(rubric_text, '', *textnodes, **self.options)
  188. self.add_name(rubric)
  189. return [rubric] + messages
  190. class BlockQuote(Directive):
  191. has_content = True
  192. classes = []
  193. def run(self):
  194. self.assert_has_content()
  195. elements = self.state.block_quote(self.content, self.content_offset)
  196. for element in elements:
  197. if isinstance(element, nodes.block_quote):
  198. element['classes'] += self.classes
  199. return elements
  200. class Epigraph(BlockQuote):
  201. classes = ['epigraph']
  202. class Highlights(BlockQuote):
  203. classes = ['highlights']
  204. class PullQuote(BlockQuote):
  205. classes = ['pull-quote']
  206. class Compound(Directive):
  207. option_spec = {'class': directives.class_option,
  208. 'name': directives.unchanged}
  209. has_content = True
  210. def run(self):
  211. self.assert_has_content()
  212. text = '\n'.join(self.content)
  213. node = nodes.compound(text)
  214. node['classes'] += self.options.get('class', [])
  215. self.add_name(node)
  216. self.state.nested_parse(self.content, self.content_offset, node)
  217. return [node]
  218. class Container(Directive):
  219. optional_arguments = 1
  220. final_argument_whitespace = True
  221. option_spec = {'name': directives.unchanged}
  222. has_content = True
  223. def run(self):
  224. self.assert_has_content()
  225. text = '\n'.join(self.content)
  226. try:
  227. if self.arguments:
  228. classes = directives.class_option(self.arguments[0])
  229. else:
  230. classes = []
  231. except ValueError:
  232. raise self.error(
  233. 'Invalid class attribute value for "%s" directive: "%s".'
  234. % (self.name, self.arguments[0]))
  235. node = nodes.container(text)
  236. node['classes'].extend(classes)
  237. self.add_name(node)
  238. self.state.nested_parse(self.content, self.content_offset, node)
  239. return [node]