1# $Id: body.py 10293 2026-01-21 15:18:32Z milde $
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
4
5"""
6Directives for additional body elements.
7
8See `docutils.parsers.rst.directives` for API details.
9"""
10
11__docformat__ = 'reStructuredText'
12
13
14from docutils import nodes
15from docutils.parsers.rst import Directive
16from docutils.parsers.rst import directives
17from docutils.parsers.rst.roles import normalize_options
18from docutils.utils.code_analyzer import Lexer, LexerError, NumberLines
19
20
21class BasePseudoSection(Directive):
22 """Base class for Topic and Sidebar."""
23
24 final_argument_whitespace = True
25 option_spec = {'class': directives.class_option,
26 'name': directives.unchanged}
27 has_content = True
28
29 node_class = None
30 """Node class to be used (must be set in subclasses)."""
31
32 invalid_parents = (nodes.SubStructural, nodes.Bibliographic,
33 nodes.Decorative, nodes.Body, nodes.Part, nodes.topic)
34 """
35 Node categories where topics and sidebars are invalid children.
36
37 Sidebars are only valid in <document> and <section> elements,
38 topics also in <sidebar> elements. However, during parsing,
39 there may be wrapper nodes (like `sphinx.addnodes.only`).
40 """
41
42 def run(self):
43 if (not isinstance(self.state_machine.node,
44 (nodes.Root, nodes.section, nodes.sidebar))
45 and isinstance(self.state_machine.node, self.invalid_parents)):
46 raise self.error('The "%s" directive may not be used within '
47 'topics or body elements.' % self.name)
48 self.assert_has_content()
49 if self.arguments: # title (in sidebars optional)
50 title_text = self.arguments[0]
51 textnodes, messages = self.state.inline_text(
52 title_text, self.lineno)
53 titles = [nodes.title(title_text, '', *textnodes)]
54 # Sidebar uses this code.
55 if 'subtitle' in self.options:
56 textnodes, more_messages = self.state.inline_text(
57 self.options['subtitle'], self.lineno)
58 titles.append(nodes.subtitle(self.options['subtitle'], '',
59 *textnodes))
60 messages.extend(more_messages)
61 else:
62 titles = []
63 messages = []
64 text = '\n'.join(self.content)
65 node = self.node_class(text, *(titles + messages))
66 node['classes'] += self.options.get('class', [])
67 (node.source,
68 node.line) = self.state_machine.get_source_and_line(self.lineno)
69 self.add_name(node)
70 if text:
71 self.state.nested_parse(self.content, self.content_offset, node)
72 return [node]
73
74
75class Topic(BasePseudoSection):
76
77 required_arguments = 1
78 node_class = nodes.topic
79
80
81class Sidebar(BasePseudoSection):
82
83 optional_arguments = 1
84 node_class = nodes.sidebar
85 option_spec = BasePseudoSection.option_spec | {
86 'subtitle': directives.unchanged_required}
87
88 def run(self):
89 if isinstance(self.state_machine.node, nodes.sidebar):
90 raise self.error('The "%s" directive may not be used within a '
91 'sidebar element.' % self.name)
92 if 'subtitle' in self.options and not self.arguments:
93 raise self.error('The "subtitle" option may not be used '
94 'without a title.')
95
96 return BasePseudoSection.run(self)
97
98
99class LineBlock(Directive):
100 """Legacy directive for line blocks.
101
102 Use is deprecated in favour of the line block syntax,
103 cf. `parsers.rst.states.Body.line_block()`.
104 """
105
106 option_spec = {'class': directives.class_option,
107 'name': directives.unchanged}
108 has_content = True
109
110 def run(self):
111 self.assert_has_content()
112 block = nodes.line_block(classes=self.options.get('class', []))
113 (block.source,
114 block.line) = self.state_machine.get_source_and_line(self.lineno)
115 self.add_name(block)
116 node_list = [block]
117 for i, line_text in enumerate(self.content):
118 text_nodes, messages = self.state.inline_text(
119 line_text.strip(), self.lineno + self.content_offset)
120 line = nodes.line(line_text, '', *text_nodes)
121 line.source = block.source
122 line.line = block.line + i
123 if line_text.strip():
124 line.indent = len(line_text) - len(line_text.lstrip())
125 block += line
126 node_list.extend(messages)
127 self.content_offset += 1
128 self.state.nest_line_block_lines(block)
129 return node_list
130
131
132class ParsedLiteral(Directive):
133
134 option_spec = {'class': directives.class_option,
135 'name': directives.unchanged}
136 has_content = True
137
138 def run(self):
139 options = normalize_options(self.options)
140 self.assert_has_content()
141 text = '\n'.join(self.content)
142 text_nodes, messages = self.state.inline_text(text, self.lineno)
143 node = nodes.literal_block(text, '', *text_nodes, **options)
144 node.line = self.content_offset + 1
145 self.add_name(node)
146 return [node] + messages
147
148
149class CodeBlock(Directive):
150 """Parse and mark up content of a code block.
151
152 Configuration setting: syntax_highlight
153 Highlight Code content with Pygments?
154 Possible values: ('long', 'short', 'none')
155
156 """
157 optional_arguments = 1
158 option_spec = {'class': directives.class_option,
159 'name': directives.unchanged,
160 'number-lines': directives.value_or((None,), int),
161 }
162 has_content = True
163
164 def run(self):
165 self.assert_has_content()
166 if self.arguments:
167 language = self.arguments[0]
168 else:
169 language = ''
170 options = normalize_options(self.options)
171 classes = ['code']
172 if language:
173 classes.append(language)
174 if 'classes' in options:
175 classes.extend(options['classes'])
176
177 # set up lexical analyzer
178 try:
179 tokens = Lexer('\n'.join(self.content), language,
180 self.state.document.settings.syntax_highlight)
181 except LexerError as error:
182 if self.state.document.settings.report_level > 2:
183 # don't report warnings -> insert without syntax highlight
184 tokens = Lexer('\n'.join(self.content), language, 'none')
185 else:
186 raise self.warning(error)
187
188 if 'number-lines' in options:
189 startline = self.options['number-lines']
190 if startline is None:
191 startline = 1
192 endline = startline + len(self.content)
193 # add linenumber filter:
194 tokens = NumberLines(tokens, startline, endline)
195
196 node = nodes.literal_block('\n'.join(self.content), classes=classes)
197 self.add_name(node)
198 # if called from "include", set the source
199 if 'source' in options:
200 node.attributes['source'] = options['source']
201 # analyze content and add nodes for every token
202 for classes, value in tokens:
203 if classes:
204 node += nodes.inline(value, value, classes=classes)
205 else:
206 # insert as Text to decrease the verbosity of the output
207 node += nodes.Text(value)
208
209 return [node]
210
211
212class MathBlock(Directive):
213
214 option_spec = {'class': directives.class_option,
215 'name': directives.unchanged,
216 # TODO: Add Sphinx' ``mathbase.py`` option 'nowrap'?
217 # 'nowrap': directives.flag,
218 }
219 has_content = True
220
221 def run(self):
222 options = normalize_options(self.options)
223 self.assert_has_content()
224 # join lines, separate blocks
225 content = '\n'.join(self.content).split('\n\n')
226 _nodes = []
227 for block in content:
228 if not block:
229 continue
230 node = nodes.math_block(self.block_text, block, **options)
231 (node.source,
232 node.line) = self.state_machine.get_source_and_line(self.lineno)
233 self.add_name(node)
234 _nodes.append(node)
235 return _nodes
236
237
238class Rubric(Directive):
239
240 required_arguments = 1
241 optional_arguments = 0
242 final_argument_whitespace = True
243 option_spec = {'class': directives.class_option,
244 'name': directives.unchanged}
245
246 def run(self):
247 options = normalize_options(self.options)
248 rubric_text = self.arguments[0]
249 textnodes, messages = self.state.inline_text(rubric_text, self.lineno)
250 rubric = nodes.rubric(rubric_text, '', *textnodes, **options)
251 (rubric.source,
252 rubric.line) = self.state_machine.get_source_and_line(self.lineno)
253 self.add_name(rubric)
254 return [rubric] + messages
255
256
257class BlockQuote(Directive):
258
259 has_content = True
260 classes = []
261
262 def run(self):
263 self.assert_has_content()
264 elements = self.state.block_quote(self.content, self.content_offset)
265 for element in elements:
266 if isinstance(element, nodes.block_quote):
267 element['classes'] += self.classes
268 return elements
269
270
271class Epigraph(BlockQuote):
272
273 classes = ['epigraph']
274
275
276class Highlights(BlockQuote):
277
278 classes = ['highlights']
279
280
281class PullQuote(BlockQuote):
282
283 classes = ['pull-quote']
284
285
286class Compound(Directive):
287
288 option_spec = {'class': directives.class_option,
289 'name': directives.unchanged}
290 has_content = True
291
292 def run(self):
293 self.assert_has_content()
294 text = '\n'.join(self.content)
295 node = nodes.compound(text)
296 node['classes'] += self.options.get('class', [])
297 (node.source,
298 node.line) = self.state_machine.get_source_and_line(self.lineno)
299 self.add_name(node)
300 self.state.nested_parse(self.content, self.content_offset, node)
301 return [node]
302
303
304class Container(Directive):
305
306 optional_arguments = 1
307 final_argument_whitespace = True
308 option_spec = {'name': directives.unchanged}
309 has_content = True
310
311 def run(self):
312 self.assert_has_content()
313 text = '\n'.join(self.content)
314 try:
315 if self.arguments:
316 classes = directives.class_option(self.arguments[0])
317 else:
318 classes = []
319 except ValueError:
320 raise self.error(
321 'Invalid class attribute value for "%s" directive: "%s".'
322 % (self.name, self.arguments[0]))
323 node = nodes.container(text)
324 node['classes'].extend(classes)
325 (node.source,
326 node.line) = self.state_machine.get_source_and_line(self.lineno)
327 self.add_name(node)
328 self.state.nested_parse(self.content, self.content_offset, node)
329 return [node]