Skip to content

Commit b5b383f

Browse files
authored
Rewrite the "Docutils markup API" page (#12505)
1 parent 24a0385 commit b5b383f

File tree

5 files changed

+144
-46
lines changed

5 files changed

+144
-46
lines changed

doc/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@
185185
('py:class', 'Node'), # sphinx.domains.Domain
186186
('py:class', 'NullTranslations'), # gettext.NullTranslations
187187
('py:class', 'RoleFunction'), # sphinx.domains.Domain
188+
('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes
188189
('py:class', 'Theme'), # sphinx.application.TemplateBridge
189-
('py:class', 'system_message'), # sphinx.utils.docutils
190+
('py:class', 'StringList'), # sphinx.utils.parsing.nested_parse_to_nodes
191+
('py:class', 'system_message'), # sphinx.utils.docutils.SphinxDirective
190192
('py:class', 'TitleGetter'), # sphinx.domains.Domain
191193
('py:class', 'XRefRole'), # sphinx.domains.Domain
192194
('py:class', 'docutils.nodes.Element'),

doc/extdev/markupapi.rst

Lines changed: 120 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,53 @@
11
Docutils markup API
22
===================
33

4-
This section describes the API for adding ReST markup elements (roles and
5-
directives).
4+
This section describes the API for adding reStructuredText markup elements
5+
(roles and directives).
6+
67

78
Roles
89
-----
910

11+
Roles follow the interface described below.
12+
They have to be registered by an extension using
13+
:meth:`.Sphinx.add_role` or :meth:`.Sphinx.add_role_to_domain`.
14+
15+
16+
.. code-block:: python
17+
18+
def role_function(
19+
role_name: str, raw_source: str, text: str,
20+
lineno: int, inliner: Inliner,
21+
options: dict = {}, content: list = [],
22+
) -> tuple[list[Node], list[system_message]]:
23+
elements = []
24+
messages = []
25+
return elements, messages
26+
27+
The *options* and *content* parameters are only used for custom roles
28+
created via the :dudir:`role` directive.
29+
The return value is a tuple of two lists,
30+
the first containing the text nodes and elements from the role,
31+
and the second containing any system messages generated.
32+
For more information, see the `custom role overview`_ from Docutils.
33+
34+
.. _custom role overview: https://docutils.sourceforge.io/docs/howto/rst-roles.html
35+
36+
37+
Creating custom roles
38+
^^^^^^^^^^^^^^^^^^^^^
39+
40+
Sphinx provides two base classes for creating custom roles,
41+
:class:`~sphinx.util.docutils.SphinxRole` and :class:`~sphinx.util.docutils.ReferenceRole`.
42+
43+
These provide a class-based interface for creating roles,
44+
where the main logic must be implemented in your ``run()`` method.
45+
The classes provide a number of useful methods and attributes,
46+
such as ``self.text``, ``self.config``, and ``self.env``.
47+
The ``ReferenceRole`` class implements Sphinx's ``title <target>`` logic,
48+
exposing ``self.target`` and ``self.title`` attributes.
49+
This is useful for creating cross-reference roles.
50+
1051

1152
Directives
1253
----------
@@ -85,68 +126,106 @@ using :meth:`.Sphinx.add_directive` or :meth:`.Sphinx.add_directive_to_domain`.
85126
The state and state machine which controls the parsing. Used for
86127
``nested_parse``.
87128

129+
.. seealso::
130+
131+
`Creating directives`_ HOWTO of the Docutils documentation
132+
133+
.. _Creating directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html
134+
135+
136+
.. _parsing-directive-content-as-rest:
137+
138+
Parsing directive content as reStructuredText
139+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88140

89-
ViewLists
90-
^^^^^^^^^
141+
Many directives will contain more markup that must be parsed.
142+
To do this, use one of the following APIs from the :meth:`~Directive.run` method:
91143

92-
Docutils represents document source lines in a class
93-
``docutils.statemachine.ViewList``. This is a list with extended functionality
94-
-- for one, slicing creates views of the original list, and also the list
95-
contains information about the source line numbers.
144+
* :py:meth:`.SphinxDirective.parse_content_to_nodes()`
145+
* :py:meth:`.SphinxDirective.parse_text_to_nodes()`
96146

97-
The :attr:`Directive.content` attribute is a ViewList. If you generate content
98-
to be parsed as ReST, you have to create a ViewList yourself. Important for
99-
content generation are the following points:
147+
The first method parses all the directive's content as markup,
148+
whilst the second only parses the given *text* string.
149+
Both methods return the parsed Docutils nodes in a list.
100150

101-
* The constructor takes a list of strings (lines) and a source (document) name.
151+
The methods are used as follows:
102152

103-
* The ``.append()`` method takes a line and a source name as well.
153+
.. code-block:: python
104154
155+
def run(self) -> list[Node]:
156+
# either
157+
parsed = self.parse_content_to_nodes()
158+
# or
159+
parsed = self.parse_text_to_nodes('spam spam spam')
160+
return parsed
105161
106-
Parsing directive content as ReST
107-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
162+
.. note::
163+
164+
The above utility methods were added in Sphinx 7.4.
165+
Prior to Sphinx 7.4, the following methods should be used to parse content:
108166

109-
Many directives will contain more markup that must be parsed. To do this, use
110-
one of the following APIs from the :meth:`Directive.run` method:
167+
* ``self.state.nested_parse``
168+
* :func:`sphinx.util.nodes.nested_parse_with_titles` -- this allows titles in
169+
the parsed content.
111170

112-
* ``self.state.nested_parse``
113-
* :func:`sphinx.util.nodes.nested_parse_with_titles` -- this allows titles in
114-
the parsed content.
171+
.. code-block:: python
115172
116-
Both APIs parse the content into a given node. They are used like this::
173+
def run(self) -> list[Node]:
174+
container = docutils.nodes.Element()
175+
# either
176+
nested_parse_with_titles(self.state, self.result, container)
177+
# or
178+
self.state.nested_parse(self.result, 0, container)
179+
parsed = container.children
180+
return parsed
117181
118-
node = docutils.nodes.paragraph()
119-
# either
120-
nested_parse_with_titles(self.state, self.result, node)
121-
# or
122-
self.state.nested_parse(self.result, 0, node)
182+
To parse inline markup,
183+
use :py:meth:`~sphinx.util.docutils.SphinxDirective.parse_inline()`.
184+
This must only be used for text which is a single line or paragraph,
185+
and does not contain any structural elements
186+
(headings, transitions, directives, etc).
123187

124188
.. note::
125189

126-
``sphinx.util.docutils.switch_source_input()`` allows to change a target file
127-
during nested_parse. It is useful to mixed contents.
128-
For example, ``sphinx.ext.autodoc`` uses it to parse docstrings::
190+
``sphinx.util.docutils.switch_source_input()`` allows changing
191+
the source (input) file during parsing content in a directive.
192+
It is useful to parse mixed content, such as in ``sphinx.ext.autodoc``,
193+
where it is used to parse docstrings.
194+
195+
.. code-block:: python
129196
130-
from sphinx.util.docutils import switch_source_input
197+
from sphinx.util.docutils import switch_source_input
198+
from sphinx.util.parsing import nested_parse_to_nodes
131199
132-
# Switch source_input between parsing content.
133-
# Inside this context, all parsing errors and warnings are reported as
134-
# happened in new source_input (in this case, ``self.result``).
135-
with switch_source_input(self.state, self.result):
136-
node = docutils.nodes.paragraph()
137-
self.state.nested_parse(self.result, 0, node)
200+
# Switch source_input between parsing content.
201+
# Inside this context, all parsing errors and warnings are reported as
202+
# happened in new source_input (in this case, ``self.result``).
203+
with switch_source_input(self.state, self.result):
204+
parsed = nested_parse_to_nodes(self.state, self.result)
138205
139206
.. deprecated:: 1.7
140207

141208
Until Sphinx 1.6, ``sphinx.ext.autodoc.AutodocReporter`` was used for this
142209
purpose. It is replaced by ``switch_source_input()``.
143210

144-
If you don't need the wrapping node, you can use any concrete node type and
145-
return ``node.children`` from the Directive.
146211

212+
.. _ViewLists:
147213

148-
.. seealso::
214+
ViewLists and StringLists
215+
^^^^^^^^^^^^^^^^^^^^^^^^^
149216

150-
`Creating directives`_ HOWTO of the Docutils documentation
217+
Docutils represents document source lines in a ``StringList`` class,
218+
which inherits from ``ViewList``, both in the ``docutils.statemachine`` module.
219+
This is a list with extended functionality,
220+
including that slicing creates views of the original list and
221+
that the list contains information about source line numbers.
222+
223+
The :attr:`Directive.content` attribute is a ``StringList``.
224+
If you generate content to be parsed as reStructuredText,
225+
you have to create a ``StringList`` for the Docutils APIs.
226+
The utility functions provided by Sphinx handle this automatically.
227+
Important for content generation are the following points:
151228

152-
.. _Creating directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html
229+
* The ``ViewList`` constructor takes a list of strings (lines)
230+
and a source (document) name.
231+
* The ``ViewList.append()`` method takes a line and a source name as well.

doc/extdev/utils.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Utilities
33

44
Sphinx provides utility classes and functions to develop extensions.
55

6+
67
Base classes for components
78
---------------------------
89

@@ -30,12 +31,20 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily.
3031
.. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter
3132
:members:
3233

34+
3335
Utility components
3436
------------------
3537

3638
.. autoclass:: sphinx.events.EventManager
3739
:members:
3840

41+
42+
Utility functions
43+
-----------------
44+
45+
.. autofunction:: sphinx.util.parsing.nested_parse_to_nodes
46+
47+
3948
Utility types
4049
-------------
4150

sphinx/util/docutils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ def parse_content_to_nodes(self, allow_section_headings: bool = False) -> list[N
444444
an incoherent doctree. In Docutils, section nodes should
445445
only be children of ``Structural`` nodes, which includes
446446
``document``, ``section``, and ``sidebar`` nodes.
447+
448+
.. versionadded:: 7.4
447449
"""
448450
return nested_parse_to_nodes(
449451
self.state,
@@ -468,6 +470,8 @@ def parse_text_to_nodes(
468470
``document``, ``section``, and ``sidebar`` nodes.
469471
:param offset:
470472
The offset of the content.
473+
474+
.. versionadded:: 7.4
471475
"""
472476
if offset == -1:
473477
offset = self.content_offset
@@ -491,6 +495,8 @@ def parse_inline(
491495
The line number where the interpreted text begins.
492496
:returns:
493497
A list of nodes (text and inline elements) and a list of system_messages.
498+
499+
.. versionadded:: 7.4
494500
"""
495501
if lineno == -1:
496502
lineno = self.lineno

sphinx/util/parsing.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import contextlib
66
from typing import TYPE_CHECKING
77

8-
from docutils import nodes
8+
from docutils.nodes import Element, Node
99
from docutils.statemachine import StringList, string2lines
1010

1111
if TYPE_CHECKING:
@@ -22,7 +22,7 @@ def nested_parse_to_nodes(
2222
offset: int = 0,
2323
allow_section_headings: bool = True,
2424
keep_title_context: bool = False,
25-
) -> list[nodes.Node]: # Element | nodes.Text
25+
) -> list[Node]: # Element | nodes.Text
2626
"""Parse *text* into nodes.
2727
2828
:param state:
@@ -47,13 +47,15 @@ def nested_parse_to_nodes(
4747
This is useful when the parsed content comes from
4848
a completely different context, such as docstrings.
4949
If this is True, then title underlines must match those in
50-
the surrounding document, otherwise errors will occur. TODO: check!
50+
the surrounding document, otherwise the behaviour is undefined.
51+
52+
.. versionadded:: 7.4
5153
"""
5254
document = state.document
5355
content = _text_to_string_list(
5456
text, source=source, tab_width=document.settings.tab_width,
5557
)
56-
node = nodes.Element() # Anonymous container for parsing
58+
node = Element() # Anonymous container for parsing
5759
node.document = document
5860

5961
if keep_title_context:

0 commit comments

Comments
 (0)