Repository: sphinx-doc/sphinx

Issue:
LaTeX/ImageConverter: Regression with `data:` URIs
### Describe the bug

Images with `data:` URIs are not converted correctly in LaTeX output.

```rst
.. image:: data:image/png;base64,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
```

This is the error when running LaTeX:

```
! LaTeX Error: File `{data:image/png;base64,[...]}' not found.
```

### How to Reproduce

Try to build the LaTeX/PDF from https://github.com/mgeier/rsvgconverter-bug, after disabling the `rsvg_converter_bin` setting in `conf.py` (https://github.com/mgeier/rsvgconverter-bug/blob/8e107a6c7fca594a30c540117007716e967ee83e/conf.py#L5C1-L5C19).

According to `git bisect`, this is the culprit: 0a76199f994ea07ad8429fab3e1798fe01cdde80 (released in Sphinx 7.3.0).

### Environment Information

```text
Sphinx 7.3.0+
```


### Sphinx extensions

```python
['sphinxcontrib.rsvgconverter']
```


### Additional context

See #10073 and #10118, which might be related.

Discussion:
jayaddison: I'll remove the shoutiness from the pull request description; that was written at a moment where I'd emerged from confusion about what the `supported_data_uri_images` flag on a builder means.
jayaddison: @chrisjsewell if+when you have time, I'd appreciate review on this changeset:

* It resolves what seems like a regression: some output images began not to appear in (LaTeX builder) output.
* The images were specified in source documents using `data:` URIs (image encoded inline in-source).
* The fix is essentially to allow reading those inline images regardless of what the target output format is.
* The config setting `supported_data_uri_images` is interpreted as "output format _natively_ supports data: URIs" -- so in those cases we don't extract the inline image and write it to file, instead we simply pass the `data:` URI as-is into the written output format.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 675c7f77c..39a6d2c6a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -73,6 +73,9 @@ Bugs fixed
 * #12459: Add valid-type arguments to the ``linkcheck_rate_limit_timeout``
   configuration setting.
   Patch by James Addison.
+* #12331: Resolve data-URI-image-extraction regression from v7.3.0 affecting
+  builders without native support for data-URIs in their output format.
+  Patch by James Addison.
 
 Improvements
 ------------
diff --git a/doc/conf.py b/doc/conf.py
index 9ab88b48d..3fb4e8cea 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -185,8 +185,10 @@
     ('py:class', 'Node'),  # sphinx.domains.Domain
     ('py:class', 'NullTranslations'),  # gettext.NullTranslations
     ('py:class', 'RoleFunction'),  # sphinx.domains.Domain
+    ('py:class', 'RSTState'),  # sphinx.utils.parsing.nested_parse_to_nodes
     ('py:class', 'Theme'),  # sphinx.application.TemplateBridge
-    ('py:class', 'system_message'),  # sphinx.utils.docutils
+    ('py:class', 'StringList'),  # sphinx.utils.parsing.nested_parse_to_nodes
+    ('py:class', 'system_message'),  # sphinx.utils.docutils.SphinxDirective
     ('py:class', 'TitleGetter'),  # sphinx.domains.Domain
     ('py:class', 'XRefRole'),  # sphinx.domains.Domain
     ('py:class', 'docutils.nodes.Element'),
diff --git a/doc/extdev/markupapi.rst b/doc/extdev/markupapi.rst
index 87267d194..7aa632446 100644
--- a/doc/extdev/markupapi.rst
+++ b/doc/extdev/markupapi.rst
@@ -1,12 +1,53 @@
 Docutils markup API
 ===================
 
-This section describes the API for adding ReST markup elements (roles and
-directives).
+This section describes the API for adding reStructuredText markup elements
+(roles and directives).
+
 
 Roles
 -----
 
+Roles follow the interface described below.
+They have to be registered by an extension using
+:meth:`.Sphinx.add_role` or :meth:`.Sphinx.add_role_to_domain`.
+
+
+.. code-block:: python
+
+   def role_function(
+       role_name: str, raw_source: str, text: str,
+       lineno: int, inliner: Inliner,
+       options: dict = {}, content: list = [],
+   ) -> tuple[list[Node], list[system_message]]:
+       elements = []
+       messages = []
+       return elements, messages
+
+The *options* and *content* parameters are only used for custom roles
+created via the :dudir:`role` directive.
+The return value is a tuple of two lists,
+the first containing the text nodes and elements from the role,
+and the second containing any system messages generated.
+For more information, see the `custom role overview`_ from Docutils.
+
+.. _custom role overview: https://docutils.sourceforge.io/docs/howto/rst-roles.html
+
+
+Creating custom roles
+^^^^^^^^^^^^^^^^^^^^^
+
+Sphinx provides two base classes for creating custom roles,
+:class:`~sphinx.util.docutils.SphinxRole` and :class:`~sphinx.util.docutils.ReferenceRole`.
+
+These provide a class-based interface for creating roles,
+where the main logic must be implemented in your ``run()`` method.
+The classes provide a number of useful methods and attributes,
+such as ``self.text``, ``self.config``, and ``self.env``.
+The ``ReferenceRole`` class implements Sphinx's ``title <target>`` logic,
+exposing ``self.target`` and ``self.title`` attributes.
+This is useful for creating cross-reference roles.
+
 
 Directives
 ----------
@@ -85,68 +126,106 @@ using :meth:`.Sphinx.add_directive` or :meth:`.Sphinx.add_directive_to_domain`.
       The state and state machine which controls the parsing.  Used for
       ``nested_parse``.
 
+.. seealso::
+
+   `Creating directives`_ HOWTO of the Docutils documentation
+
+   .. _Creating directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html
+
+
+.. _parsing-directive-content-as-rest:
+
+Parsing directive content as reStructuredText
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-ViewLists
-^^^^^^^^^
+Many directives will contain more markup that must be parsed.
+To do this, use one of the following APIs from the :meth:`~Directive.run` method:
 
-Docutils represents document source lines in a class
-``docutils.statemachine.ViewList``.  This is a list with extended functionality
--- for one, slicing creates views of the original list, and also the list
-contains information about the source line numbers.
+* :py:meth:`.SphinxDirective.parse_content_to_nodes()`
+* :py:meth:`.SphinxDirective.parse_text_to_nodes()`
 
-The :attr:`Directive.content` attribute is a ViewList.  If you generate content
-to be parsed as ReST, you have to create a ViewList yourself.  Important for
-content generation are the following points:
+The first method parses all the directive's content as markup,
+whilst the second only parses the given *text* string.
+Both methods return the parsed Docutils nodes in a list.
 
-* The constructor takes a list of strings (lines) and a source (document) name.
+The methods are used as follows:
 
-* The ``.append()`` method takes a line and a source name as well.
+.. code-block:: python
 
+   def run(self) -> list[Node]:
+       # either
+       parsed = self.parse_content_to_nodes()
+       # or
+       parsed = self.parse_text_to_nodes('spam spam spam')
+       return parsed
 
-Parsing directive content as ReST
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. note::
+
+   The above utility methods were added in Sphinx 7.4.
+   Prior to Sphinx 7.4, the following methods should be used to parse content:
 
-Many directives will contain more markup that must be parsed.  To do this, use
-one of the following APIs from the :meth:`Directive.run` method:
+   * ``self.state.nested_parse``
+   * :func:`sphinx.util.nodes.nested_parse_with_titles` -- this allows titles in
+     the parsed content.
 
-* ``self.state.nested_parse``
-* :func:`sphinx.util.nodes.nested_parse_with_titles` -- this allows titles in
-  the parsed content.
+   .. code-block:: python
 
-Both APIs parse the content into a given node. They are used like this::
+      def run(self) -> list[Node]:
+          container = docutils.nodes.Element()
+          # either
+          nested_parse_with_titles(self.state, self.result, container)
+          # or
+          self.state.nested_parse(self.result, 0, container)
+          parsed = container.children
+          return parsed
 
-   node = docutils.nodes.paragraph()
-   # either
-   nested_parse_with_titles(self.state, self.result, node)
-   # or
-   self.state.nested_parse(self.result, 0, node)
+To parse inline markup,
+use :py:meth:`~sphinx.util.docutils.SphinxDirective.parse_inline()`.
+This must only be used for text which is a single line or paragraph,
+and does not contain any structural elements
+(headings, transitions, directives, etc).
 
 .. note::
 
-   ``sphinx.util.docutils.switch_source_input()`` allows to change a target file
-   during nested_parse.  It is useful to mixed contents.
-   For example, ``sphinx.ext.autodoc`` uses it to parse docstrings::
+   ``sphinx.util.docutils.switch_source_input()`` allows changing
+   the source (input) file during parsing content in a directive.
+   It is useful to parse mixed content, such as in ``sphinx.ext.autodoc``,
+   where it is used to parse docstrings.
+
+   .. code-block:: python
 
-       from sphinx.util.docutils import switch_source_input
+      from sphinx.util.docutils import switch_source_input
+      from sphinx.util.parsing import nested_parse_to_nodes
 
-       # Switch source_input between parsing content.
-       # Inside this context, all parsing errors and warnings are reported as
-       # happened in new source_input (in this case, ``self.result``).
-       with switch_source_input(self.state, self.result):
-           node = docutils.nodes.paragraph()
-           self.state.nested_parse(self.result, 0, node)
+      # Switch source_input between parsing content.
+      # Inside this context, all parsing errors and warnings are reported as
+      # happened in new source_input (in this case, ``self.result``).
+      with switch_source_input(self.state, self.result):
+          parsed = nested_parse_to_nodes(self.state, self.result)
 
    .. deprecated:: 1.7
 
       Until Sphinx 1.6, ``sphinx.ext.autodoc.AutodocReporter`` was used for this
       purpose.  It is replaced by ``switch_source_input()``.
 
-If you don't need the wrapping node, you can use any concrete node type and
-return ``node.children`` from the Directive.
 
+.. _ViewLists:
 
-.. seealso::
+ViewLists and StringLists
+^^^^^^^^^^^^^^^^^^^^^^^^^
 
-   `Creating directives`_ HOWTO of the Docutils documentation
+Docutils represents document source lines in a ``StringList`` class,
+which inherits from ``ViewList``, both in the ``docutils.statemachine`` module.
+This is a list with extended functionality,
+including that slicing creates views of the original list and
+that the list contains information about source line numbers.
+
+The :attr:`Directive.content` attribute is a ``StringList``.
+If you generate content to be parsed as reStructuredText,
+you have to create a ``StringList`` for the Docutils APIs.
+The utility functions provided by Sphinx handle this automatically.
+Important for content generation are the following points:
 
-.. _Creating directives: https://docutils.sourceforge.io/docs/howto/rst-directives.html
+* The ``ViewList`` constructor takes a list of strings (lines)
+  and a source (document) name.
+* The ``ViewList.append()`` method takes a line and a source name as well.
diff --git a/doc/extdev/utils.rst b/doc/extdev/utils.rst
index ff8dc4e27..9e10a0ded 100644
--- a/doc/extdev/utils.rst
+++ b/doc/extdev/utils.rst
@@ -3,6 +3,7 @@ Utilities
 
 Sphinx provides utility classes and functions to develop extensions.
 
+
 Base classes for components
 ---------------------------
 
@@ -30,12 +31,20 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily.
 .. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter
    :members:
 
+
 Utility components
 ------------------
 
 .. autoclass:: sphinx.events.EventManager
    :members:
 
+
+Utility functions
+-----------------
+
+.. autofunction:: sphinx.util.parsing.nested_parse_to_nodes
+
+
 Utility types
 -------------
 
diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py
index bfbf11657..de25ab06a 100644
--- a/sphinx/builders/__init__.py
+++ b/sphinx/builders/__init__.py
@@ -71,9 +71,9 @@ class Builder:
     #: The list of MIME types of image formats supported by the builder.
     #: Image files are searched in the order in which they appear here.
     supported_image_types: list[str] = []
-    #: The builder supports remote images or not.
+    #: The builder can produce output documents that may fetch external images when opened.
     supported_remote_images = False
-    #: The builder supports data URIs or not.
+    #: The file format produced by the builder allows images to be embedded using data-URIs.
     supported_data_uri_images = False
 
     def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py
index cf9b9d0f3..a9699f168 100644
--- a/sphinx/directives/__init__.py
+++ b/sphinx/directives/__init__.py
@@ -278,7 +278,8 @@ def run(self) -> list[Node]:
             # needed for association of version{added,changed} directives
             self.env.temp_data['object'] = self.names[0]
         self.before_content()
-        content_node = addnodes.desc_content('', *self.parse_content_to_nodes())
+        content_children = self.parse_content_to_nodes(allow_section_headings=True)
+        content_node = addnodes.desc_content('', *content_children)
         node.append(content_node)
         self.transform_content(content_node)
         self.env.app.emit('object-description-transform',
diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py
index 2e49461af..4a65945e0 100644
--- a/sphinx/domains/javascript.py
+++ b/sphinx/domains/javascript.py
@@ -311,7 +311,7 @@ def run(self) -> list[Node]:
         self.env.ref_context['js:module'] = mod_name
         no_index = 'no-index' in self.options or 'noindex' in self.options
 
-        content_nodes = self.parse_content_to_nodes()
+        content_nodes = self.parse_content_to_nodes(allow_section_headings=True)
 
         ret: list[Node] = []
         if not no_index:
diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py
index 22a23f226..75c2cddfb 100644
--- a/sphinx/domains/python/__init__.py
+++ b/sphinx/domains/python/__init__.py
@@ -416,7 +416,7 @@ def run(self) -> list[Node]:
         no_index = 'no-index' in self.options or 'noindex' in self.options
         self.env.ref_context['py:module'] = modname
 
-        content_nodes = self.parse_content_to_nodes()
+        content_nodes = self.parse_content_to_nodes(allow_section_headings=True)
 
         ret: list[Node] = []
         if not no_index:
diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py
index 5ac6ae5ac..504c95027 100644
--- a/sphinx/domains/std/__init__.py
+++ b/sphinx/domains/std/__init__.py
@@ -405,7 +405,9 @@ def run(self) -> list[Node]:
 
             if definition:
                 offset = definition.items[0][1]
-                definition_nodes = nested_parse_to_nodes(self.state, definition, offset=offset)
+                definition_nodes = nested_parse_to_nodes(
+                    self.state, definition, offset=offset, allow_section_headings=False,
+                )
             else:
                 definition_nodes = []
             termnodes.append(nodes.definition('', *definition_nodes))
diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py
index 2d2d7f6de..5bbb0d9dc 100644
--- a/sphinx/ext/autosummary/__init__.py
+++ b/sphinx/ext/autosummary/__init__.py
@@ -409,7 +409,8 @@ def append_row(*column_texts: str) -> None:
             for text in column_texts:
                 vl = StringList([text], f'{source}:{line}:<autosummary>')
                 with switch_source_input(self.state, vl):
-                    col_nodes = nested_parse_to_nodes(self.state, vl)
+                    col_nodes = nested_parse_to_nodes(self.state, vl,
+                                                      allow_section_headings=False)
                     if col_nodes and isinstance(col_nodes[0], nodes.paragraph):
                         node = col_nodes[0]
                     else:
diff --git a/sphinx/ext/ifconfig.py b/sphinx/ext/ifconfig.py
index 489920392..17331a0bd 100644
--- a/sphinx/ext/ifconfig.py
+++ b/sphinx/ext/ifconfig.py
@@ -47,7 +47,7 @@ def run(self) -> list[Node]:
         node.document = self.state.document
         self.set_source_info(node)
         node['expr'] = self.arguments[0]
-        node += self.parse_content_to_nodes()
+        node += self.parse_content_to_nodes(allow_section_headings=True)
         return [node]
 
 
diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py
index 589a8d6ab..b679481e8 100644
--- a/sphinx/transforms/post_transforms/images.py
+++ b/sphinx/transforms/post_transforms/images.py
@@ -115,10 +115,8 @@ class DataURIExtractor(BaseImageConverter):
     default_priority = 150
 
     def match(self, node: nodes.image) -> bool:
-        if not self.app.builder.supported_remote_images:
-            return False
         if self.app.builder.supported_data_uri_images is True:
-            return False
+            return False  # do not transform the image; data URIs are valid in the build output
         return node['uri'].startswith('data:')
 
     def handle(self, node: nodes.image) -> None:
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py
index 7b2364a84..3e018dd96 100644
--- a/sphinx/util/docutils.py
+++ b/sphinx/util/docutils.py
@@ -22,7 +22,7 @@
 from sphinx.errors import SphinxError
 from sphinx.locale import _, __
 from sphinx.util import logging
-from sphinx.util.parsing import inliner_parse_text, nested_parse_to_nodes
+from sphinx.util.parsing import nested_parse_to_nodes
 
 logger = logging.getLogger(__name__)
 report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(\\d+)?\\) ')
@@ -434,21 +434,53 @@ def get_location(self) -> str:
             return f'<unknown>:{line}'
         return ''
 
-    def parse_content_to_nodes(self) -> list[Node]:
-        """Parse the directive's content into nodes."""
-        return nested_parse_to_nodes(self.state, self.content, offset=self.content_offset)
+    def parse_content_to_nodes(self, allow_section_headings: bool = False) -> list[Node]:
+        """Parse the directive's content into nodes.
 
-    def parse_text_to_nodes(self, text: str = '', /, *, offset: int = -1) -> list[Node]:
+        :param allow_section_headings:
+            Are titles (sections) allowed in the directive's content?
+            Note that this option bypasses Docutils' usual checks on
+            doctree structure, and misuse of this option can lead to
+            an incoherent doctree. In Docutils, section nodes should
+            only be children of ``Structural`` nodes, which includes
+            ``document``, ``section``, and ``sidebar`` nodes.
+
+        .. versionadded:: 7.4
+        """
+        return nested_parse_to_nodes(
+            self.state,
+            self.content,
+            offset=self.content_offset,
+            allow_section_headings=allow_section_headings,
+        )
+
+    def parse_text_to_nodes(
+        self, text: str = '', /, *, offset: int = -1, allow_section_headings: bool = False,
+    ) -> list[Node]:
         """Parse *text* into nodes.
 
         :param text:
             Text, in string form. ``StringList`` is also accepted.
+        :param allow_section_headings:
+            Are titles (sections) allowed in *text*?
+            Note that this option bypasses Docutils' usual checks on
+            doctree structure, and misuse of this option can lead to
+            an incoherent doctree. In Docutils, section nodes should
+            only be children of ``Structural`` nodes, which includes
+            ``document``, ``section``, and ``sidebar`` nodes.
         :param offset:
             The offset of the content.
+
+        .. versionadded:: 7.4
         """
         if offset == -1:
             offset = self.content_offset
-        return nested_parse_to_nodes(self.state, text, offset=offset)
+        return nested_parse_to_nodes(
+            self.state,
+            text,
+            offset=offset,
+            allow_section_headings=allow_section_headings,
+        )
 
     def parse_inline(
         self, text: str, *, lineno: int = -1,
@@ -463,10 +495,12 @@ def parse_inline(
             The line number where the interpreted text begins.
         :returns:
             A list of nodes (text and inline elements) and a list of system_messages.
+
+        .. versionadded:: 7.4
         """
         if lineno == -1:
             lineno = self.lineno
-        return inliner_parse_text(text, state=self.state, lineno=lineno)
+        return self.state.inline_text(text, lineno)
 
 
 class SphinxRole:
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py
index 82672c2ff..9f3e827c8 100644
--- a/sphinx/util/nodes.py
+++ b/sphinx/util/nodes.py
@@ -334,7 +334,7 @@ def nested_parse_with_titles(state: RSTState, content: StringList, node: Node,
     context, such as docstrings.
 
     This function is retained for compatability and will be deprecated in
-    Sphinx 8. Prefer ``parse_block_text()``.
+    Sphinx 8. Prefer ``nested_parse_to_nodes()``.
     """
     with _fresh_title_style_context(state):
         ret = state.nested_parse(content, content_offset, node, match_titles=True)
diff --git a/sphinx/util/parsing.py b/sphinx/util/parsing.py
index 2cd395835..a8f937f8f 100644
--- a/sphinx/util/parsing.py
+++ b/sphinx/util/parsing.py
@@ -5,13 +5,13 @@
 import contextlib
 from typing import TYPE_CHECKING
 
-from docutils import nodes
+from docutils.nodes import Element, Node
 from docutils.statemachine import StringList, string2lines
 
 if TYPE_CHECKING:
     from collections.abc import Iterator
 
-    from docutils.parsers.rst.states import Inliner, RSTState, Struct
+    from docutils.parsers.rst.states import RSTState
 
 
 def nested_parse_to_nodes(
@@ -20,8 +20,9 @@ def nested_parse_to_nodes(
     *,
     source: str = '<generated text>',
     offset: int = 0,
+    allow_section_headings: bool = True,
     keep_title_context: bool = False,
-) -> list[nodes.Node]:  # Element | nodes.Text
+) -> list[Node]:  # Element | nodes.Text
     """Parse *text* into nodes.
 
     :param state:
@@ -32,6 +33,13 @@ def nested_parse_to_nodes(
         The text's source, used when creating a new ``StringList``.
     :param offset:
         The offset of the content.
+    :param allow_section_headings:
+        Are titles (sections) allowed in *text*?
+        Note that this option bypasses Docutils' usual checks on
+        doctree structure, and misuse of this option can lead to
+        an incoherent doctree. In Docutils, section nodes should
+        only be children of ``Structural`` nodes, which includes
+        ``document``, ``section``, and ``sidebar`` nodes.
     :param keep_title_context:
         If this is False (the default), then *content* is parsed as if it were
         an independent document, meaning that title decorations (e.g. underlines)
@@ -39,20 +47,22 @@ def nested_parse_to_nodes(
         This is useful when the parsed content comes from
         a completely different context, such as docstrings.
         If this is True, then title underlines must match those in
-        the surrounding document, otherwise errors will occur. TODO: check!
+        the surrounding document, otherwise the behaviour is undefined.
+
+    .. versionadded:: 7.4
     """
     document = state.document
     content = _text_to_string_list(
         text, source=source, tab_width=document.settings.tab_width,
     )
-    node = nodes.Element()  # Anonymous container for parsing
+    node = Element()  # Anonymous container for parsing
     node.document = document
 
     if keep_title_context:
-        state.nested_parse(content, offset, node, match_titles=True)
+        state.nested_parse(content, offset, node, match_titles=allow_section_headings)
     else:
         with _fresh_title_style_context(state):
-            state.nested_parse(content, offset, node, match_titles=True)
+            state.nested_parse(content, offset, node, match_titles=allow_section_headings)
     return node.children
 
 
@@ -73,20 +83,6 @@ def _fresh_title_style_context(state: RSTState) -> Iterator[None]:
         memo.section_level = surrounding_section_level
 
 
-def inliner_parse_text(
-    text: str, *, state: RSTState, lineno: int = 1,
-) -> tuple[list[nodes.Node], list[nodes.system_message]]:
-    """Parse *text* as inline nodes.
-
-    The text cannot contain any structural elements (headings, transitions,
-    directives, etc), so should be a simple line or paragraph of text.
-    """
-    inliner: Inliner = state.inliner
-    memo: Struct = state.memo
-    parent: nodes.Element = state.parent
-    return inliner.parse(text, lineno, memo, parent)
-
-
 def _text_to_string_list(
     text: str | StringList, /, *, source: str, tab_width: int,
 ) -> StringList:


Test patch:
diff --git a/tests/roots/test-images/index.rst b/tests/roots/test-images/index.rst
index 9b9aac1e5..f6d7160da 100644
--- a/tests/roots/test-images/index.rst
+++ b/tests/roots/test-images/index.rst
@@ -27,3 +27,8 @@ test-image
 
 .. non-exist remote image
 .. image:: http://localhost:7777/NOT_EXIST.PNG
+
+.. a self-contained image within a data URI
+   This image was generated using ImageMagick 6.9 with the command ``convert -pointsize 32 -font Noto-Sans-Egyptian-Hieroglyphs-Regular caption:$(printf '\U13080') -trim -border 2 -monochrome eoh.png``
+.. image:: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAjAQAAAADKt6U+AAAAAmJLR0QAAd2KE6QAAAAHdElNRQfoBQIVBgOBlOMTAAAAEGNhTnYAAAAtAAAAOwAAAAEAAAATst46RgAAAJtJREFUCNdNz70KwkAMAOA8iOhjuGh9HB9BCtoTHHwMH0Mc7KWTmx0dHDpovUk6HCil3sUmATHLR/4IAeJA+LEWPmbEeHJMWbTMZDA0CNFn8x1COFPaIHQ55R7hlZGdIjwj2aovRjJbhPvMLNN+r0g2vB7ByIWbHqqVh3LR3lhZWM0qYV8qjU6+lc4J7ZVx4SjEINBKOSinv/+YL1xvsJE6ztdqAAAADHRFWHRjYXB0aW9uAPCTgoD4hdKUAAAAD3RFWHRjYXB0aW9uOmxpbmVzADGoBz2RAAAAAElFTkSuQmCC
+   :alt: The Eye of Horus in a black font on a white background.
diff --git a/tests/test_builders/test_build_epub.py b/tests/test_builders/test_build_epub.py
index 6829f22dd..461ff8cc6 100644
--- a/tests/test_builders/test_build_epub.py
+++ b/tests/test_builders/test_build_epub.py
@@ -409,6 +409,7 @@ def test_copy_images(app, status, warning):
     images = {image.name for image in images_dir.rglob('*')}
     images.discard('python-logo.png')
     assert images == {
+        # 'ba30773957c3fe046897111afd65a80b81cad089.png',  # epub: image from data:image/png URI in source
         'img.png',
         'rimg.png',
         'rimg1.png',
diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py
index f385d1189..365b0110f 100644
--- a/tests/test_builders/test_build_html_image.py
+++ b/tests/test_builders/test_build_html_image.py
@@ -72,6 +72,7 @@ def test_copy_images(app, status, warning):
     images_dir = Path(app.outdir) / '_images'
     images = {image.name for image in images_dir.rglob('*')}
     assert images == {
+        # 'ba30773957c3fe046897111afd65a80b81cad089.png',  # html: image from data:image/png URI in source
         'img.png',
         'rimg.png',
         'rimg1.png',
diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py
index 2c3cae18b..88c83560b 100644
--- a/tests/test_builders/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -1711,6 +1711,7 @@ def test_copy_images(app, status, warning):
     }
     images.discard('sphinx.png')
     assert images == {
+        'ba30773957c3fe046897111afd65a80b81cad089.png',  # latex: image from data:image/png URI in source
         'img.pdf',
         'rimg.png',
         'testimäge.png',
diff --git a/tests/test_builders/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py
index f9effb2c0..3b8e27662 100644
--- a/tests/test_builders/test_build_texinfo.py
+++ b/tests/test_builders/test_build_texinfo.py
@@ -124,6 +124,7 @@ def test_copy_images(app, status, warning):
     images = {image.name for image in images_dir.rglob('*')}
     images.discard('python-logo.png')
     assert images == {
+        'ba30773957c3fe046897111afd65a80b81cad089.png',  # texinfo: image from data:image/png URI in source
         'img.png',
         'rimg.png',
         'testimäge.png',
diff --git a/tests/test_util/test_util_docutils_sphinx_directive.py b/tests/test_util/test_util_docutils_sphinx_directive.py
index 281f61f6b..3b6784b17 100644
--- a/tests/test_util/test_util_docutils_sphinx_directive.py
+++ b/tests/test_util/test_util_docutils_sphinx_directive.py
@@ -100,7 +100,7 @@ def test_sphinx_directive_parse_content_to_nodes():
     content = 'spam\n====\n\nEggs! *Lobster thermidor.*'
     directive.content = StringList(content.split('\n'), source='<source>')
 
-    parsed = directive.parse_content_to_nodes()
+    parsed = directive.parse_content_to_nodes(allow_section_headings=True)
     assert len(parsed) == 1
     node = parsed[0]
     assert isinstance(node, nodes.section)
@@ -115,7 +115,7 @@ def test_sphinx_directive_parse_text_to_nodes():
     directive = make_directive(env=SimpleNamespace())
     content = 'spam\n====\n\nEggs! *Lobster thermidor.*'
 
-    parsed = directive.parse_text_to_nodes(content)
+    parsed = directive.parse_text_to_nodes(content, allow_section_headings=True)
     assert len(parsed) == 1
     node = parsed[0]
     assert isinstance(node, nodes.section)