pkgsrc-WIP-changes archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

py-docutils: bump patches up to 0.22.1rc1/SVN r10234



Module Name:	pkgsrc-wip
Committed By:	Thomas Klausner <wiz%NetBSD.org@localhost>
Pushed By:	wiz
Date:		Sun Sep 14 09:04:10 2025 +0200
Changeset:	25bae5260cbb8bc6d690d6bd5b63187a6521b844

Modified Files:
	py-docutils/distinfo
	py-docutils/patches/patch-docutils_nodes.py
	py-docutils/patches/patch-docutils_parsers_rst_states.py
	py-docutils/patches/patch-docutils_transforms_references.py
	py-docutils/patches/patch-docutils_writers_latex2e_____init____.py
Added Files:
	py-docutils/patches/patch-docutils_____init____.py
	py-docutils/patches/patch-docutils_parsers_rst_directives_body.py
	py-docutils/patches/patch-docutils_parsers_rst_directives_misc.py
	py-docutils/patches/patch-docutils_parsers_rst_directives_parts.py
	py-docutils/patches/patch-docutils_statemachine.py
	py-docutils/patches/patch-docutils_writers_latex2e_docutils.sty

Log Message:
py-docutils: bump patches up to 0.22.1rc1/SVN r10234

code changes only, not docs or tests

To see a diff of this commit:
https://wip.pkgsrc.org/cgi-bin/gitweb.cgi?p=pkgsrc-wip.git;a=commitdiff;h=25bae5260cbb8bc6d690d6bd5b63187a6521b844

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

diffstat:
 py-docutils/distinfo                               |  14 +-
 py-docutils/patches/patch-docutils_____init____.py |   0
 py-docutils/patches/patch-docutils_nodes.py        |   7 +-
 .../patch-docutils_parsers_rst_directives_body.py  |  44 +++
 .../patch-docutils_parsers_rst_directives_misc.py  |  13 +
 .../patch-docutils_parsers_rst_directives_parts.py |  15 +
 .../patches/patch-docutils_parsers_rst_states.py   | 392 +++++++++++++++----
 py-docutils/patches/patch-docutils_statemachine.py |  30 ++
 .../patch-docutils_transforms_references.py        |  33 ++
 .../patch-docutils_writers_latex2e_____init____.py | 424 +++++++++++++++++++++
 .../patch-docutils_writers_latex2e_docutils.sty    |  32 ++
 11 files changed, 932 insertions(+), 72 deletions(-)

diffs:
diff --git a/py-docutils/distinfo b/py-docutils/distinfo
index 67cf6b4783..542f85d5b9 100644
--- a/py-docutils/distinfo
+++ b/py-docutils/distinfo
@@ -3,13 +3,19 @@ $NetBSD: distinfo,v 1.33 2025/08/03 10:06:37 wiz Exp $
 BLAKE2s (docutils-0.22.tar.gz) = 20d7b105f2af0a2417ab1e3800120565ef7c3fc77da8dd4ebef852624b7b3eaa
 SHA512 (docutils-0.22.tar.gz) = 09082eb3bdd5f9b3e977d356740efee47725a50fbaca7bf35c7fddff06003c2b2177a38d160a9956f9e96261f881c0d870c0aa9fef84f90d0cac079ccc73669d
 Size (docutils-0.22.tar.gz) = 2277984 bytes
+SHA1 (patch-docutils_____init____.py) = da39a3ee5e6b4b0d3255bfef95601890afd80709
 SHA1 (patch-docutils_frontend.py) = e36ef1bbc98c2b01ae45341636a93a93e712b757
-SHA1 (patch-docutils_nodes.py) = 942cfbb6aa27313fac8b2be572c38833d5cbeae1
-SHA1 (patch-docutils_parsers_rst_states.py) = 2ae0fd135af2e4e999daf71be079335f3b6d527e
-SHA1 (patch-docutils_transforms_references.py) = 4997f5b060903359aed297ec74653093d6ef5cda
+SHA1 (patch-docutils_nodes.py) = 77d1ef24f35ac59abef0a12ab4d6e2097d2bd248
+SHA1 (patch-docutils_parsers_rst_directives_body.py) = 6b596cfe394299cce7609b2c1c5b8833b97dd18f
+SHA1 (patch-docutils_parsers_rst_directives_misc.py) = 86ba1bdcb00309e7bcde5980fee15ad4036e0c99
+SHA1 (patch-docutils_parsers_rst_directives_parts.py) = 709edad294ecfefd1013eaee886c3382a46699ea
+SHA1 (patch-docutils_parsers_rst_states.py) = 0e3c1925ffc2f38c3dc8d63c1215bdf51d8edc13
+SHA1 (patch-docutils_statemachine.py) = fb3e42652d16de8d9c26e8b77e83aef0ff6d6bc4
+SHA1 (patch-docutils_transforms_references.py) = e2cf86dfb87930def33b8371a7851efeb96a12cb
 SHA1 (patch-docutils_writers___html__base.py) = f432c8222ea1ef1f7181439e39caf130f014fa03
 SHA1 (patch-docutils_writers_html4css1_____init____.py) = b1a13109b56f1ba6922b1a67a2a52b3b34ace689
 SHA1 (patch-docutils_writers_html5__polyglot_____init____.py) = 5324c969d44395627942e9036770e7174313e30a
-SHA1 (patch-docutils_writers_latex2e_____init____.py) = ef56b1e93636673e1605f25344c4d744d6e4f943
+SHA1 (patch-docutils_writers_latex2e_____init____.py) = fe531918331bbacbe37d29503c43234fcfa2c30f
+SHA1 (patch-docutils_writers_latex2e_docutils.sty) = c1d1204f9e141dfb48f93dc8efa7649c80f4c514
 SHA1 (patch-docutils_writers_odf__odt_____init____.py) = 6244cfd4b2fa431afeb896116dd0a335d262df84
 SHA1 (patch-docutils_writers_xetex_____init____.py) = 4164cd7c8d9d8f1c501ba2d560a58ace6923abc1
diff --git a/py-docutils/patches/patch-docutils_____init____.py b/py-docutils/patches/patch-docutils_____init____.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/py-docutils/patches/patch-docutils_nodes.py b/py-docutils/patches/patch-docutils_nodes.py
index ea1f40df92..e1ce675896 100644
--- a/py-docutils/patches/patch-docutils_nodes.py
+++ b/py-docutils/patches/patch-docutils_nodes.py
@@ -2,9 +2,12 @@ $NetBSD$
 
 --- docutils/nodes.py.orig	2025-07-29 14:37:37.467805600 +0000
 +++ docutils/nodes.py
-@@ -820,18 +820,21 @@ class Element(Node):
+@@ -818,20 +818,23 @@ class Element(Node):
+         return self.parent[i-1] if i > 0 else None
+ 
      def section_hierarchy(self) -> list[section]:
-         """Return the element's section hierarchy.
+-        """Return the element's section hierarchy.
++        """Return the element's section anchestors.
  
 -        Return a list of all <section> elements containing `self`
 -        (including `self` if it is a <section>).
diff --git a/py-docutils/patches/patch-docutils_parsers_rst_directives_body.py b/py-docutils/patches/patch-docutils_parsers_rst_directives_body.py
new file mode 100644
index 0000000000..95dc2b2f1b
--- /dev/null
+++ b/py-docutils/patches/patch-docutils_parsers_rst_directives_body.py
@@ -0,0 +1,44 @@
+$NetBSD$
+
+--- docutils/parsers/rst/directives/body.py.orig	2025-07-29 14:37:38.097654300 +0000
++++ docutils/parsers/rst/directives/body.py
+@@ -19,9 +19,8 @@ from docutils.utils.code_analyzer import
+ 
+ 
+ class BasePseudoSection(Directive):
++    """Base class for Topic and Sidebar."""
+ 
+-    required_arguments = 1
+-    optional_arguments = 0
+     final_argument_whitespace = True
+     option_spec = {'class': directives.class_option,
+                    'name': directives.unchanged}
+@@ -31,8 +30,8 @@ class BasePseudoSection(Directive):
+     """Node class to be used (must be set in subclasses)."""
+ 
+     def run(self):
+-        if not (self.state_machine.match_titles
+-                or isinstance(self.state_machine.node, nodes.sidebar)):
++        if not isinstance(self.state_machine.node,
++                          (nodes.document, nodes.section, nodes.sidebar)):
+             raise self.error('The "%s" directive may not be used within '
+                              'topics or body elements.' % self.name)
+         self.assert_has_content()
+@@ -64,15 +63,14 @@ class BasePseudoSection(Directive):
+ 
+ class Topic(BasePseudoSection):
+ 
++    required_arguments = 1
+     node_class = nodes.topic
+ 
+ 
+ class Sidebar(BasePseudoSection):
+ 
+-    node_class = nodes.sidebar
+-
+-    required_arguments = 0
+     optional_arguments = 1
++    node_class = nodes.sidebar
+     option_spec = BasePseudoSection.option_spec | {
+                       'subtitle': directives.unchanged_required}
+ 
diff --git a/py-docutils/patches/patch-docutils_parsers_rst_directives_misc.py b/py-docutils/patches/patch-docutils_parsers_rst_directives_misc.py
new file mode 100644
index 0000000000..c4783e5a32
--- /dev/null
+++ b/py-docutils/patches/patch-docutils_parsers_rst_directives_misc.py
@@ -0,0 +1,13 @@
+$NetBSD$
+
+--- docutils/parsers/rst/directives/misc.py.orig	2025-07-29 14:37:38.096681000 +0000
++++ docutils/parsers/rst/directives/misc.py
+@@ -236,8 +236,6 @@ class Include(Directive):
+     def insert_into_input_lines(self, text: str) -> None:
+         """Insert file content into the rST input of the calling parser.
+ 
+-        Returns an empty list to comply with the API of `Directive.run()`.
+-
+         Provisional.
+         """
+         source = self.options['source']
diff --git a/py-docutils/patches/patch-docutils_parsers_rst_directives_parts.py b/py-docutils/patches/patch-docutils_parsers_rst_directives_parts.py
new file mode 100644
index 0000000000..6be7140786
--- /dev/null
+++ b/py-docutils/patches/patch-docutils_parsers_rst_directives_parts.py
@@ -0,0 +1,15 @@
+$NetBSD$
+
+--- docutils/parsers/rst/directives/parts.py.orig	2025-07-29 14:37:38.160271400 +0000
++++ docutils/parsers/rst/directives/parts.py
+@@ -43,8 +43,8 @@ class Contents(Directive):
+                    'class': directives.class_option}
+ 
+     def run(self):
+-        if not (self.state_machine.match_titles
+-                or isinstance(self.state_machine.node, nodes.sidebar)):
++        if not isinstance(self.state_machine.node,
++                          (nodes.document, nodes.section, nodes.sidebar)):
+             raise self.error('The "%s" directive may not be used within '
+                              'topics or body elements.' % self.name)
+         document = self.state_machine.document
diff --git a/py-docutils/patches/patch-docutils_parsers_rst_states.py b/py-docutils/patches/patch-docutils_parsers_rst_states.py
index 501f31ff9d..6ef9871d3f 100644
--- a/py-docutils/patches/patch-docutils_parsers_rst_states.py
+++ b/py-docutils/patches/patch-docutils_parsers_rst_states.py
@@ -2,14 +2,14 @@ $NetBSD$
 
 --- docutils/parsers/rst/states.py.orig	2025-07-29 14:37:37.894344600 +0000
 +++ docutils/parsers/rst/states.py
-@@ -104,6 +104,7 @@ from __future__ import annotations
- 
- __docformat__ = 'reStructuredText'
- 
-+import copy
+@@ -107,6 +107,7 @@ __docformat__ = 'reStructuredText'
  import re
  from types import FunctionType, MethodType
  from types import SimpleNamespace as Struct
++import warnings
+ 
+ from docutils import nodes, statemachine, utils
+ from docutils import ApplicationError, DataError
 @@ -121,6 +122,10 @@ from docutils.utils import split_escaped
  from docutils.utils._roman_numerals import (InvalidRomanNumeralError,
                                              RomanNumeral)
@@ -21,7 +21,23 @@ $NetBSD$
  
  class MarkupError(DataError): pass
  class UnknownInterpretedRoleError(DataError): pass
-@@ -151,16 +156,19 @@ class RSTStateMachine(StateMachineWS):
+@@ -136,6 +141,15 @@ class RSTStateMachine(StateMachineWS):
+ 
+     The entry point to reStructuredText parsing is the `run()` method.
+     """
++    section_level_offset: int = 0
++    """Correction term for section level determination in nested parsing.
++
++    Updated by `RSTState.nested_parse()` and used in
++    `RSTState.check_subsection()` to compensate differences when
++    nested parsing uses a detached base node with a document-wide
++    section title style hierarchy or the current node with a new,
++    independent title style hierarchy.
++    """
+ 
+     def run(self, input_lines, document, input_offset=0, match_titles=True,
+             inliner=None) -> None:
+@@ -151,16 +165,19 @@ class RSTStateMachine(StateMachineWS):
          if inliner is None:
              inliner = Inliner()
          inliner.init_customizations(document.settings)
@@ -45,59 +61,78 @@ $NetBSD$
          self.node = document
          results = StateMachineWS.run(self, input_lines, input_offset,
                                       input_source=document['source'])
-@@ -169,7 +177,6 @@ class RSTStateMachine(StateMachineWS):
+@@ -168,16 +185,23 @@ class RSTStateMachine(StateMachineWS):
+         self.node = self.memo = None    # remove unneeded references
  
  
- class NestedStateMachine(StateMachineWS):
+-class NestedStateMachine(StateMachineWS):
 -
++class NestedStateMachine(RSTStateMachine):
      """
      StateMachine run from within other StateMachine runs, to parse nested
      document structures.
-@@ -177,17 +184,28 @@ class NestedStateMachine(StateMachineWS)
+     """
  
++    def __init__(self, state_classes, initial_state,
++                 debug=False, parent_state_machine=None) -> None:
++
++        self.parent_state_machine = parent_state_machine
++        """The instance of the parent state machine."""
++
++        super().__init__(state_classes, initial_state, debug)
++
      def run(self, input_lines, input_offset, memo, node, match_titles=True):
          """
 -        Parse `input_lines` and populate a `docutils.nodes.document` instance.
 +        Parse `input_lines` and populate `node`.
-+
-+        Use a separate "title style hierarchy" if `node` is not
-+        attached to the document (changed in Docutils 0.23).
  
          Extend `StateMachineWS.run()`: set up document-wide data.
          """
-         self.match_titles = match_titles
--        self.memo = memo
-+        self.memo = copy.copy(memo)
+@@ -185,8 +209,8 @@ class NestedStateMachine(StateMachineWS)
+         self.memo = memo
          self.document = memo.document
          self.attach_observer(self.document.note_source)
 -        self.reporter = memo.reporter
          self.language = memo.language
 +        self.reporter = self.document.reporter
          self.node = node
-+        if match_titles:
-+            # Start a new title style hierarchy if `node` is not
-+            # a descendant of the `document`:
-+            _root = node
-+            while _root.parent is not None:
-+                _root = _root.parent
-+            if _root != self.document:
-+                self.memo.title_styles = []
          results = StateMachineWS.run(self, input_lines, input_offset)
          assert results == [], ('NestedStateMachine.run() results should be '
-                                'empty!')
-@@ -214,9 +232,9 @@ class RSTState(StateWS):
+@@ -205,7 +229,7 @@ class RSTState(StateWS):
+     nested_sm = NestedStateMachine
+     nested_sm_cache = []
+ 
+-    def __init__(self, state_machine, debug=False) -> None:
++    def __init__(self, state_machine: RSTStateMachine, debug=False) -> None:
+         self.nested_sm_kwargs = {'state_classes': state_classes,
+                                  'initial_state': 'Body'}
+         StateWS.__init__(self, state_machine, debug)
+@@ -214,14 +238,21 @@ class RSTState(StateWS):
          StateWS.runtime_init(self)
          memo = self.state_machine.memo
          self.memo = memo
 -        self.reporter = memo.reporter
 -        self.inliner = memo.inliner
          self.document = memo.document
+-        self.parent = self.state_machine.node
 +        self.inliner = memo.inliner
 +        self.reporter = self.document.reporter
-         self.parent = self.state_machine.node
          # enable the reporter to determine source and source-line
          if not hasattr(self.reporter, 'get_source_and_line'):
-@@ -248,11 +266,40 @@ class RSTState(StateWS):
+             self.reporter.get_source_and_line = self.state_machine.get_source_and_line  # noqa:E501
+ 
++    @property
++    def parent(self) -> nodes.Element | None:
++        return self.state_machine.node
++
++    @parent.setter
++    def parent(self, value: nodes.Element):
++        self.state_machine.node = value
++
+     def goto_line(self, abs_line_offset) -> None:
+         """
+         Jump to input line `abs_line_offset`, ignoring jumps past the end.
+@@ -248,12 +279,45 @@ class RSTState(StateWS):
          """Called at beginning of file."""
          return [], []
  
@@ -109,7 +144,7 @@ $NetBSD$
 +    def nested_parse(self,
 +                     block: StringList,
 +                     input_offset: int,
-+                     node: nodes.Element,
++                     node: nodes.Element|None = None,
 +                     match_titles: bool = False,
 +                     state_machine_class: StateMachineWS|None = None,
 +                     state_machine_kwargs: dict|None = None
@@ -122,16 +157,12 @@ $NetBSD$
 +        :input_offset:
 +            Line number at start of the block.
 +        :node:
-+            Base node. Generated nodes will be appended to this node
-+            (unless a new section with lower level is encountered, see below).
++            Base node. Generated nodes will be appended to this node.
++            Default: the "current node" (`self.state_machine.node`).
 +        :match_titles:
 +            Allow section titles?
-+            If the base `node` is attached to the document, new sections will
-+            be appended according their level in the section hierarchy
-+            (moving up the tree).
-+            If the base `node` is *not* attached to the document,
-+            a separate section title style hierarchy is used for the nested
-+            parsing (all sections are subsections of the current section).
++            Caution: With a custom base node, this may lead to an invalid
++            or mixed up document tree. [#]_
 +        :state_machine_class:
 +            Default: `NestedStateMachine`.
 +        :state_machine_kwargs:
@@ -140,31 +171,91 @@ $NetBSD$
 +
 +        Create a new state-machine instance if required.
 +        Return new offset.
++
++        .. [#] See also ``test_parsers/test_rst/test_nested_parsing.py``
++               and Sphinx's `nested_parse_to_nodes()`__.
++
++        __ https://www.sphinx-doc.org/en/master/extdev/utils.html
++           #sphinx.util.parsing.nested_parse_to_nodes
          """
++        if node is None:
++            node = self.state_machine.node
          use_default = 0
          if state_machine_class is None:
-@@ -261,8 +308,6 @@ class RSTState(StateWS):
+             state_machine_class = self.nested_sm
+@@ -261,24 +325,54 @@ class RSTState(StateWS):
          if state_machine_kwargs is None:
              state_machine_kwargs = self.nested_sm_kwargs
              use_default += 1
 -        block_length = len(block)
 -
-         state_machine = None
+-        state_machine = None
++        my_state_machine = None
          if use_default == 2:
              try:
-@@ -272,8 +317,11 @@ class RSTState(StateWS):
-         if not state_machine:
-             state_machine = state_machine_class(debug=self.debug,
-                                                 **state_machine_kwargs)
-+        # run the statemachine and populate `node`:
+-                state_machine = self.nested_sm_cache.pop()
++                # get cached state machine, prevent others from using it
++                my_state_machine = self.nested_sm_cache.pop()
+             except IndexError:
+                 pass
+-        if not state_machine:
+-            state_machine = state_machine_class(debug=self.debug,
+-                                                **state_machine_kwargs)
+-        state_machine.run(block, input_offset, memo=self.memo,
+-                          node=node, match_titles=match_titles)
++        if not my_state_machine:
++            my_state_machine = state_machine_class(
++                                  debug=self.debug,
++                                  parent_state_machine=self.state_machine,
++                                  **state_machine_kwargs)
++        # Check if we may use sections (with a caveat for custom nodes
++        # that may be dummies to collect children):
++        if (node == self.state_machine.node
++                and not isinstance(node, (nodes.document, nodes.section))):
++            match_titles = False  # avoid invalid sections
++        if match_titles:
++            # Compensate mismatch of known title styles and number of
++            # parent sections of the base node if the document wide
++            # title styles are used with a detached base node or
++            # a new list of title styles with the current parent node:
++            l_node = len(node.section_hierarchy())
++            l_start = min(len(self.parent.section_hierarchy()),
++                          len(self.memo.title_styles))
++            my_state_machine.section_level_offset = l_start - l_node
++
++        # run the state machine and populate `node`:
 +        block_length = len(block)
-         state_machine.run(block, input_offset, memo=self.memo,
-                           node=node, match_titles=match_titles)
++        my_state_machine.run(block, input_offset, self.memo,
++                             node, match_titles)
++
++        if match_titles:
++            if node == self.state_machine.node:
++                # Pass on the new "current node" to parent state machines:
++                sm = self.state_machine
++                try:
++                    while True:
++                        sm.node = my_state_machine.node
++                        sm = sm.parent_state_machine
++                except AttributeError:
++                    pass
 +        # clean up
++        new_offset = my_state_machine.abs_line_offset()
          if use_default == 2:
-             self.nested_sm_cache.append(state_machine)
+-            self.nested_sm_cache.append(state_machine)
++            self.nested_sm_cache.append(my_state_machine)
          else:
-@@ -293,9 +341,15 @@ class RSTState(StateWS):
+-            state_machine.unlink()
+-        new_offset = state_machine.abs_line_offset()
++            my_state_machine.unlink()
+         # No `block.parent` implies disconnected -- lines aren't in sync:
+         if block.parent and (len(block) - block_length) != 0:
+             # Adjustment for block if modified in nested parse:
+@@ -289,31 +383,45 @@ class RSTState(StateWS):
+                           blank_finish,
+                           blank_finish_state=None,
+                           extra_settings={},
+-                          match_titles=False,
++                          match_titles=False,  # deprecated, will be removed
                            state_machine_class=None,
                            state_machine_kwargs=None):
          """
@@ -180,9 +271,44 @@ $NetBSD$
 +        Return new offset and a boolean indicating whether there was a
 +        blank final line.
          """
++        if match_titles:
++            warnings.warn('The "match_titles" argument of '
++                          'parsers.rst.states.RSTState.nested_list_parse() '
++                          'will be ignored in Docutils 1.0 '
++                          'and removed in Docutils 2.0.',
++                          PendingDeprecationWarning, stacklevel=2)
          if state_machine_class is None:
              state_machine_class = self.nested_sm
-@@ -326,40 +380,45 @@ class RSTState(StateWS):
+         if state_machine_kwargs is None:
+             state_machine_kwargs = self.nested_sm_kwargs.copy()
+         state_machine_kwargs['initial_state'] = initial_state
+-        state_machine = state_machine_class(debug=self.debug,
+-                                            **state_machine_kwargs)
++        my_state_machine = state_machine_class(
++                               debug=self.debug,
++                               parent_state_machine=self.state_machine,
++                               **state_machine_kwargs)
+         if blank_finish_state is None:
+             blank_finish_state = initial_state
+-        state_machine.states[blank_finish_state].blank_finish = blank_finish
++        my_state_machine.states[blank_finish_state].blank_finish = blank_finish
+         for key, value in extra_settings.items():
+-            setattr(state_machine.states[initial_state], key, value)
+-        state_machine.run(block, input_offset, memo=self.memo,
+-                          node=node, match_titles=match_titles)
+-        blank_finish = state_machine.states[blank_finish_state].blank_finish
+-        state_machine.unlink()
+-        return state_machine.abs_line_offset(), blank_finish
++            setattr(my_state_machine.states[initial_state], key, value)
++        my_state_machine.run(block, input_offset, memo=self.memo,
++                             node=node, match_titles=match_titles)
++        blank_finish = my_state_machine.states[blank_finish_state].blank_finish
++        my_state_machine.unlink()
++        return my_state_machine.abs_line_offset(), blank_finish
+ 
+     def section(self, title, source, style, lineno, messages) -> None:
+         """Check for a valid subsection and create one if it checks out."""
+@@ -326,40 +434,60 @@ class RSTState(StateWS):
  
          When a new section is reached that isn't a subsection of the current
          section, set `self.parent` to the new section's parent section
@@ -195,7 +321,8 @@ $NetBSD$
 -        mylevel = len(parent_sections)
 -        # Determine the level of the new section:
 +        # current section level: (0 root, 1 section, 2 subsection, ...)
-+        oldlevel = len(parent_sections)
++        oldlevel = (len(parent_sections)
++                    + self.state_machine.section_level_offset)
 +        # new section level:
          try:  # check for existing title style
 -            level = title_styles.index(style) + 1
@@ -220,21 +347,36 @@ $NetBSD$
                  nodes.paragraph('', f'Established title styles: {styles}'),
                  line=lineno)
              return False
-         # Update parent state:
+-        # Update parent state:
 -        self.memo.section_level = level
 -        if level <= mylevel:
-+        if newlevel > len(title_styles):
-+            title_styles.append(style)
-+        self.memo.section_level = newlevel
-+        if newlevel > oldlevel:
-+            # new section is a subsection: get the current section or base node
-+            while self.parent.parent and not isinstance(
-+                      self.parent, (nodes.section, nodes.document)):
-+                self.parent = self.parent.parent
-+        else:
++        if newlevel <= oldlevel:
              # new section is sibling or higher up in the section hierarchy
 -            self.parent = parent_sections[level-1].parent
-+            self.parent = parent_sections[newlevel-1].parent
++            try:
++                new_parent = parent_sections[newlevel-oldlevel-1].parent
++            except IndexError:
++                styles = ' '.join('/'.join(style) for style in title_styles)
++                details = (f'The parent of level {newlevel} sections cannot'
++                           ' be reached. The parser is at section level'
++                           f' {oldlevel} but the current node has only'
++                           f' {len(parent_sections)} parent section(s).'
++                           '\nOne reason may be a high level'
++                           ' section used in a directive that parses its'
++                           ' content into a base node not attached to'
++                           ' the document\n(up to Docutils 0.21,'
++                           ' these sections were silently dropped).')
++                self.parent += self.reporter.error(
++                    f'A level {newlevel} section cannot be used here.',
++                    nodes.literal_block('', source),
++                    nodes.paragraph('', f'Established title styles: {styles}'),
++                    nodes.paragraph('', details),
++                    line=lineno)
++                return False
++            self.parent = new_parent
++            self.memo.section_level = newlevel - 1
++        if newlevel > len(title_styles):
++            title_styles.append(style)
          return True
  
      def title_inconsistent(self, sourcetext, lineno):
@@ -244,7 +386,25 @@ $NetBSD$
              'Title level inconsistent:', nodes.literal_block('', sourcetext),
              line=lineno)
          return error
-@@ -620,9 +679,9 @@ class Inliner:
+@@ -377,15 +505,8 @@ class RSTState(StateWS):
+         section_node += title_messages
+         self.document.note_implicit_target(section_node, section_node)
+         # Update state:
+-        self.state_machine.node = section_node
+-        # Also update the ".parent" attribute in all states.
+-        # This is a bit violent, but the state classes copy their .parent from
+-        # state_machine.node on creation, so we need to update them. We could
+-        # also remove RSTState.parent entirely and replace references to it
+-        # with statemachine.node, but that might break code downstream of
+-        # docutils.
+-        for s in self.state_machine.states.values():
+-            s.parent = section_node
++        self.parent = section_node
++        self.memo.section_level += 1
+ 
+     def paragraph(self, lines, lineno):
+         """
+@@ -620,9 +741,9 @@ class Inliner:
          :text: source string
          :lineno: absolute line number, cf. `statemachine.get_source_and_line()`
          """
@@ -255,7 +415,107 @@ $NetBSD$
          self.parent = parent
          pattern_search = self.patterns.initial.search
          dispatch = self.dispatch
-@@ -2420,7 +2479,7 @@ class Body(RSTState):
+@@ -1600,7 +1721,7 @@ class Body(RSTState):
+                   self.state_machine.input_lines[offset:],
+                   input_offset=self.state_machine.abs_line_offset() + 1,
+                   node=block, initial_state='LineBlock',
+-                  blank_finish=0)
++                  blank_finish=False)
+             self.goto_line(new_line_offset)
+         if not blank_finish:
+             self.parent += self.reporter.warning(
+@@ -1695,14 +1816,14 @@ class Body(RSTState):
+ 
+     def isolate_grid_table(self):
+         messages = []
+-        blank_finish = 1
++        blank_finish = True
+         try:
+             block = self.state_machine.get_text_block(flush_left=True)
+         except statemachine.UnexpectedIndentationError as err:
+             block, src, srcline = err.args
+             messages.append(self.reporter.error('Unexpected indentation.',
+                                                 source=src, line=srcline))
+-            blank_finish = 0
++            blank_finish = False
+         block.disconnect()
+         # for East Asian chars:
+         block.pad_double_width(self.double_width_pad_char)
+@@ -1710,24 +1831,26 @@ class Body(RSTState):
+         for i in range(len(block)):
+             block[i] = block[i].strip()
+             if block[i][0] not in '+|':  # check left edge
+-                blank_finish = 0
++                blank_finish = False
+                 self.state_machine.previous_line(len(block) - i)
+                 del block[i:]
+                 break
+         if not self.grid_table_top_pat.match(block[-1]):  # find bottom
+-            blank_finish = 0
+             # from second-last to third line of table:
+             for i in range(len(block) - 2, 1, -1):
+                 if self.grid_table_top_pat.match(block[i]):
+                     self.state_machine.previous_line(len(block) - i + 1)
+                     del block[i+1:]
++                    blank_finish = False
+                     break
+             else:
+-                messages.extend(self.malformed_table(block))
++                detail = 'Bottom border missing or corrupt.'
++                messages.extend(self.malformed_table(block, detail, i))
+                 return [], messages, blank_finish
+         for i in range(len(block)):     # check right edge
+             if len(block[i]) != width or block[i][-1] not in '+|':
+-                messages.extend(self.malformed_table(block))
++                detail = 'Right border not aligned or missing.'
++                messages.extend(self.malformed_table(block, detail, i))
+                 return [], messages, blank_finish
+         return block, messages, blank_finish
+ 
+@@ -1747,8 +1870,8 @@ class Body(RSTState):
+                 if len(line.strip()) != toplen:
+                     self.state_machine.next_line(i - start)
+                     messages = self.malformed_table(
+-                        lines[start:i+1], 'Bottom/header table border does '
+-                        'not match top border.')
++                        lines[start:i+1], 'Bottom border or header rule does '
++                        'not match top border.', i-start)
+                     return [], messages, i == limit or not lines[i+1].strip()
+                 found += 1
+                 found_at = i
+@@ -1757,17 +1880,16 @@ class Body(RSTState):
+                     break
+             i += 1
+         else:                           # reached end of input_lines
++            details = 'No bottom table border found'
+             if found:
+-                extra = ' or no blank line after table bottom'
++                details += ' or no blank line after table bottom'
+                 self.state_machine.next_line(found_at - start)
+                 block = lines[start:found_at+1]
+             else:
+-                extra = ''
+                 self.state_machine.next_line(i - start - 1)
+                 block = lines[start:]
+-            messages = self.malformed_table(
+-                block, 'No bottom table border found%s.' % extra)
+-            return [], messages, not extra
++            messages = self.malformed_table(block, details + '.')
++            return [], messages, not found
+         self.state_machine.next_line(end - start)
+         block = lines[start:end+1]
+         # for East Asian chars:
+@@ -2382,8 +2504,7 @@ class Body(RSTState):
+               self.state_machine.input_lines[offset:],
+               input_offset=self.state_machine.abs_line_offset() + 1,
+               node=self.parent, initial_state='Explicit',
+-              blank_finish=blank_finish,
+-              match_titles=self.state_machine.match_titles)
++              blank_finish=blank_finish)
+         self.goto_line(newline_offset)
+         if not blank_finish:
+             self.parent += self.unindent_warning('Explicit markup')
+@@ -2420,7 +2541,7 @@ class Body(RSTState):
              raise statemachine.TransitionCorrection('text')
          else:
              blocktext = self.state_machine.line
@@ -264,7 +524,7 @@ $NetBSD$
                    'Unexpected section title or transition.',
                    nodes.literal_block(blocktext, blocktext),
                    line=self.state_machine.abs_line_number())
-@@ -2775,7 +2834,7 @@ class Text(RSTState):
+@@ -2775,7 +2896,7 @@ class Text(RSTState):
              # if the error is in a table (try with test_tables.py)?
              # print("get_source_and_line", srcline)
              # print("abs_line_number", self.state_machine.abs_line_number())
@@ -273,7 +533,7 @@ $NetBSD$
                  'Unexpected section title.',
                  nodes.literal_block(blocktext, blocktext),
                  source=src, line=srcline)
-@@ -2977,7 +3036,7 @@ class Line(SpecializedText):
+@@ -2977,7 +3098,7 @@ class Line(SpecializedText):
              if len(overline.rstrip()) < 4:
                  self.short_overline(context, blocktext, lineno, 2)
              else:
@@ -282,7 +542,7 @@ $NetBSD$
                      'Incomplete section title.',
                      nodes.literal_block(blocktext, blocktext),
                      line=lineno)
-@@ -2991,7 +3050,7 @@ class Line(SpecializedText):
+@@ -2991,7 +3112,7 @@ class Line(SpecializedText):
              if len(overline.rstrip()) < 4:
                  self.short_overline(context, blocktext, lineno, 2)
              else:
@@ -291,7 +551,7 @@ $NetBSD$
                      'Missing matching underline for section title overline.',
                      nodes.literal_block(source, source),
                      line=lineno)
-@@ -3002,7 +3061,7 @@ class Line(SpecializedText):
+@@ -3002,7 +3123,7 @@ class Line(SpecializedText):
              if len(overline.rstrip()) < 4:
                  self.short_overline(context, blocktext, lineno, 2)
              else:
diff --git a/py-docutils/patches/patch-docutils_statemachine.py b/py-docutils/patches/patch-docutils_statemachine.py
new file mode 100644
index 0000000000..4fab9d76d9
--- /dev/null
+++ b/py-docutils/patches/patch-docutils_statemachine.py
@@ -0,0 +1,30 @@
+$NetBSD$
+
+--- docutils/statemachine.py.orig	2025-07-29 14:37:38.508729200 +0000
++++ docutils/statemachine.py
+@@ -140,7 +140,6 @@ class StateMachine:
+         - `initial_state`: a string, the class name of the initial state.
+         - `debug`: a boolean; produce verbose output if true (nonzero).
+         """
+-
+         self.input_lines = None
+         """`StringList` of input lines (without newlines).
+         Filled by `self.run()`."""
+@@ -1406,7 +1405,7 @@ class StringList(ViewList):
+             stripped = line.lstrip()
+             if not stripped:            # blank line
+                 if until_blank:
+-                    blank_finish = 1
++                    blank_finish = True
+                     break
+             elif block_indent is None:
+                 line_indent = len(line) - len(stripped)
+@@ -1416,7 +1415,7 @@ class StringList(ViewList):
+                     indent = min(indent, line_indent)
+             end += 1
+         else:
+-            blank_finish = 1            # block ends at end of lines
++            blank_finish = True            # block ends at end of lines
+         block = self[start:end]
+         if first_indent is not None and block:
+             block.data[0] = block.data[0][first_indent:]
diff --git a/py-docutils/patches/patch-docutils_transforms_references.py b/py-docutils/patches/patch-docutils_transforms_references.py
index 42fc8cd17f..3c6113f79d 100644
--- a/py-docutils/patches/patch-docutils_transforms_references.py
+++ b/py-docutils/patches/patch-docutils_transforms_references.py
@@ -18,3 +18,36 @@ $NetBSD$
                  msgid = self.document.set_id(msg)
                  for ref in self.document.autofootnote_refs[i:]:
                      if ref.resolved or ref.hasattr('refname'):
+@@ -953,11 +955,29 @@ class DanglingReferencesVisitor(nodes.Sp
+         if refname in self.document.nameids:
+             msg = self.document.reporter.error(
+                 'Duplicate target name, cannot be used as a unique '
+-                'reference: "%s".' % (node['refname']), base_node=node)
++                f'reference: "{refname}".', base_node=node)
+         else:
++            if '<' in refname or '>' in refname:
++                hint = 'Did you want to embed a URI or alias?'
++                if '<' not in refname:
++                    hint += '\nOpening bracket missing.'
++                elif ' <' not in refname:
++                    hint += ('\nThe embedded reference must be preceded'
++                             ' by whitespace.')
++                if '>' not in refname:
++                    hint += '\nClosing bracket missing.'
++                elif not refname.endswith('>'):
++                    hint += ('\nThe embedded reference must be the last text'
++                             ' before the end string.')
++                if '< ' in refname or ' >' in refname:
++                    hint += ('\nWhitespace around the embedded reference'
++                             ' is not allowed.')
++                details = [nodes.paragraph('', hint)]
++            else:
++                details = []
+             msg = self.document.reporter.error(
+-                f'Unknown target name: "{node["refname"]}".',
+-                base_node=node)
++                      f'Unknown target name: "{refname}".',
++                      *details, base_node=node)
+         msgid = self.document.set_id(msg)
+         prb = nodes.problematic(node.rawsource, node.rawsource, refid=msgid)
+         try:
diff --git a/py-docutils/patches/patch-docutils_writers_latex2e_____init____.py b/py-docutils/patches/patch-docutils_writers_latex2e_____init____.py
index de12c9684a..d8c16a03e8 100644
--- a/py-docutils/patches/patch-docutils_writers_latex2e_____init____.py
+++ b/py-docutils/patches/patch-docutils_writers_latex2e_____init____.py
@@ -78,3 +78,427 @@ $NetBSD$
           ('Specify style and database(s) for bibtex, for example '
            '"--use-bibtex=unsrt,mydb1,mydb2". Provisional!',
            ['--use-bibtex'],
+@@ -938,24 +937,25 @@ class Table:
+         return ''
+ 
+     def get_opening(self, width=r'\linewidth'):
++        opening = []
++        nr_of_cols = len(self._col_specs)
+         align_map = {'left': '[l]',
+                      'center': '[c]',
+                      'right': '[r]',
+                      None: ''}
+         align = align_map.get(self.get('align'))
+         latex_type = self.get_latex_type()
+-        if align and latex_type not in ("longtable", "longtable*"):
+-            opening = [r'\noindent\makebox[\linewidth]%s{%%' % (align,),
+-                       r'\begin{%s}' % (latex_type,)]
+-        else:
+-            opening = [r'\begin{%s}%s' % (latex_type, align)]
++        if align and not latex_type.startswith("longtable"):
++            opening.append(r'\noindent\makebox[\linewidth]%s{%%' % align)
++            align = ''
+         if not self.colwidths_auto:
+             if self.borders == 'standard' and not self.legacy_column_widths:
+-                opening.insert(-1, r'\setlength{\DUtablewidth}'
++                opening.append(r'\setlength{\DUtablewidth}'
+                                r'{\dimexpr%s-%i\arrayrulewidth\relax}%%'
+-                               % (width, len(self._col_specs)+1))
++                               % (width, nr_of_cols+1))
+             else:
+-                opening.insert(-1, r'\setlength{\DUtablewidth}{%s}%%' % width)
++                opening.append(r'\setlength{\DUtablewidth}{%s}%%' % width)
++        opening.append(r'\begin{%s}%s' % (latex_type, align))
+         return '\n'.join(opening)
+ 
+     def get_closing(self):
+@@ -966,7 +966,7 @@ class Table:
+         #     closing.append(r'\hline')
+         closing.append(r'\end{%s}' % self.get_latex_type())
+         if (self.get('align')
+-            and self.get_latex_type() not in ("longtable", "longtable*")):
++            and not self.get_latex_type().startswith("longtable")):
+             closing.append('}')
+         return '\n'.join(closing)
+ 
+@@ -1359,7 +1359,7 @@ class LaTeXTranslator(writers.DoctreeTra
+             else:
+                 # require a minimal version:
+                 self.fallbacks['_docutils.sty'] = (
+-                    r'\usepackage{docutils}[2024-09-24]')
++                    r'\usepackage{docutils}[2025-08-06]')
+ 
+         self.stylesheet = [self.stylesheet_call(path)
+                            for path in stylesheet_list]
+@@ -1381,7 +1381,8 @@ class LaTeXTranslator(writers.DoctreeTra
+     # -----------------
+ 
+     def stylesheet_call(self, path):
+-        """Return code to reference or embed stylesheet file `path`"""
++        """Return code to reference or embed stylesheet file `path`."""
++
+         path = Path(path)
+         # is it a package (no extension or *.sty) or "normal" tex code:
+         is_package = path.suffix in ('.sty', '')
+@@ -1560,19 +1561,25 @@ class LaTeXTranslator(writers.DoctreeTra
+                                    id for id in node['ids']))
+ 
+     def ids_to_labels(self, node, set_anchor=True, protect=False,
+-                      newline=False) -> list[str]:
++                      newline=False, pre_nl=False) -> list[str]:
+         """Return label definitions for all ids of `node`.
+ 
+         If `set_anchor` is True, an anchor is set with \\phantomsection.
+         If `protect` is True, the \\label cmd is made robust.
+         If `newline` is True, a newline is added if there are labels.
++        If `pre_nl` is True, a newline is prepended if there are labels.
++
++        Provisional.
+         """
+         prefix = '\\protect' if protect else ''
+-        labels = [prefix + '\\label{%s}' % id for id in node['ids']]
+-        if set_anchor and labels:
+-            labels.insert(0, '\\phantomsection')
+-        if newline and labels:
+-            labels.append('\n')
++        labels = [f'{prefix}\\label{{{id}}}' for id in node['ids']]
++        if labels:
++            if set_anchor:
++                labels.insert(0, '\\phantomsection')
++            if newline:
++                labels.append('\n')
++            if pre_nl:
++                labels.insert(0, '\n')
+         return labels
+ 
+     def set_align_from_classes(self, node) -> None:
+@@ -1707,6 +1714,8 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.provide_fallback('admonition')
+         if 'error' in node['classes']:
+             self.provide_fallback('error')
++        if not isinstance(node, nodes.system_message):
++            self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.out.append('\\begin{DUadmonition}')
+ 
+@@ -1739,6 +1748,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.depart_docinfo_item(node)
+ 
+     def visit_block_quote(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.out.append('\\begin{quote}')
+ 
+@@ -1747,6 +1757,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.duclass_close(node)
+ 
+     def visit_bullet_list(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.out.append('\\begin{itemize}')
+ 
+@@ -1771,7 +1782,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.out.append('}')
+ 
+     def visit_caption(self, node) -> None:
+-        self.out.append('\n\\caption{')
++        self.out.append('\\caption{')
+         self.visit_inline(node)
+ 
+     def depart_caption(self, node) -> None:
+@@ -1869,6 +1880,7 @@ class LaTeXTranslator(writers.DoctreeTra
+     def visit_compound(self, node) -> None:
+         if isinstance(node.parent, nodes.compound):
+             self.out.append('\n')
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         node['classes'].insert(0, 'compound')
+         self.duclass_open(node)
+ 
+@@ -1882,6 +1894,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.depart_docinfo_item(node)
+ 
+     def visit_container(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+ 
+     def depart_container(self, node) -> None:
+@@ -1913,6 +1926,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         pass
+ 
+     def visit_definition_list(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.out.append('\\begin{description}\n')
+ 
+@@ -1921,7 +1935,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.duclass_close(node)
+ 
+     def visit_definition_list_item(self, node) -> None:
+-        pass
++        self.out += self.ids_to_labels(node, newline=True)
+ 
+     def depart_definition_list_item(self, node) -> None:
+         if node.next_node(descend=False, siblings=True) is not None:
+@@ -2232,6 +2246,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         label = r'%s\%s{%s}%s' % (prefix, enumtype, counter_name, suffix)
+         self._enumeration_counters.append(label)
+ 
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         if enum_level <= 4:
+             self.out.append('\\begin{enumerate}')
+@@ -2256,8 +2271,8 @@ class LaTeXTranslator(writers.DoctreeTra
+         self._enumeration_counters.pop()
+ 
+     def visit_field(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         # output is done in field_body, field_name
+-        pass
+ 
+     def depart_field(self, node) -> None:
+         pass
+@@ -2271,6 +2286,7 @@ class LaTeXTranslator(writers.DoctreeTra
+             self.out.append(r'\\'+'\n')
+ 
+     def visit_field_list(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         if self.out is not self.docinfo:
+             self.provide_fallback('fieldlist')
+@@ -2297,6 +2313,7 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+     def visit_figure(self, node) -> None:
+         self.requirements['float'] = PreambleCmds.float
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         # The 'align' attribute sets the "outer alignment",
+         # for "inner alignment" use LaTeX default alignment (similar to HTML)
+@@ -2305,10 +2322,9 @@ class LaTeXTranslator(writers.DoctreeTra
+             # The LaTeX "figure" environment always uses the full linewidth,
+             # so "outer alignment" is ignored. Just write a comment.
+             # TODO: use the wrapfigure environment?
+-            self.out.append('\\begin{figure} %% align = "%s"\n' % alignment)
++            self.out.append('\\begin{figure} %% align = "%s"' % alignment)
+         else:
+-            self.out.append('\\begin{figure}\n')
+-        self.out += self.ids_to_labels(node, newline=True)
++            self.out.append('\\begin{figure}')
+ 
+     def depart_figure(self, node) -> None:
+         self.out.append('\\end{figure}\n')
+@@ -2335,9 +2351,6 @@ class LaTeXTranslator(writers.DoctreeTra
+                 num = '[%s]' % num
+             self.out.append('%%\n\\DUfootnotetext{%s}{%s}{%s}{' %
+                             (node['ids'][0], backref, self.encode(num)))
+-            if node['ids'] == [nodes.make_id(n) for n in node['names']]:
+-                # autonumber-label: create anchor
+-                self.out += self.ids_to_labels(node)
+             # prevent spurious whitespace if footnote starts with paragraph:
+             if len(node) > 1 and isinstance(node[1], nodes.paragraph):
+                 self.out.append('%')
+@@ -2441,6 +2454,9 @@ class LaTeXTranslator(writers.DoctreeTra
+         return f'{value}\\DU{unit}dimen'
+ 
+     def visit_image(self, node) -> None:
++        # <image> can be inline element, body element, or nested in a <figure>
++        # in all three cases the <image> may also be nested in a <reference>
++        # TODO: "classes" attribute currently ignored!
+         self.requirements['graphicx'] = self.graphicx_package
+         attrs = node.attributes
+         # convert image URI to filesystem path, do not adjust relative path:
+@@ -2485,13 +2501,14 @@ class LaTeXTranslator(writers.DoctreeTra
+         if 'width' in attrs:
+             include_graphics_options.append(
+                 f"width={self.to_latex_length(attrs['width'], node)}")
++        pre.append(''.join(self.ids_to_labels(node, newline=True)))
+         if not (self.is_inline(node)
+-                or isinstance(node.parent, (nodes.figure, nodes.compound))):
++                or isinstance(node.parent, nodes.compound)):
+             pre.append('\n')
+-        if not (self.is_inline(node)
+-                or isinstance(node.parent, nodes.figure)):
++        if not self.is_inline(node):
+             post.append('\n')
+         pre.reverse()
++        # now insert image code
+         self.out.extend(pre)
+         if imagepath.suffix == '.svg' and 'svg' in self.settings.stylesheet:
+             cmd = 'includesvg'
+@@ -2504,7 +2521,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.out.extend(post)
+ 
+     def depart_image(self, node) -> None:
+-        self.out += self.ids_to_labels(node, newline=True)
++        pass
+ 
+     def visit_inline(self, node) -> None:
+         # This function is also called by the visiting functions for
+@@ -2512,8 +2529,9 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+         # Handle "ids" attribute:
+         # do we need a \phantomsection?
+-        set_anchor = not (isinstance(node.parent, (nodes.caption, nodes.title))
+-                          or isinstance(node, nodes.caption))
++        anchor_nodes = (nodes.caption, nodes.subtitle, nodes.title)
++        set_anchor = not (isinstance(node.parent, anchor_nodes)
++                          or isinstance(node, anchor_nodes))
+         add_newline = isinstance(node, nodes.paragraph)
+         self.out += self.ids_to_labels(node, set_anchor, newline=add_newline)
+         # Handle "classes" attribute:
+@@ -2552,6 +2570,7 @@ class LaTeXTranslator(writers.DoctreeTra
+                             '\\begin{DUlineblock}{\\DUlineblockindent}\n')
+             # In rST, nested line-blocks cannot be given class arguments
+         else:
++            self.out += self.ids_to_labels(node, pre_nl=True)
+             self.duclass_open(node)
+             self.out.append('\\begin{DUlineblock}{0em}\n')
+             self.insert_align_declaration(node)
+@@ -2561,6 +2580,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.duclass_close(node)
+ 
+     def visit_list_item(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.out.append('\n\\item ')
+ 
+     def depart_list_item(self, node) -> None:
+@@ -2626,8 +2646,8 @@ class LaTeXTranslator(writers.DoctreeTra
+         _use_listings = (literal_env == 'lstlisting') and _use_env
+ 
+         # Labels and classes:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+-        self.out += self.ids_to_labels(node, newline=True)
+         # Highlight code?
+         if (not _plaintext
+             and 'code' in node['classes']
+@@ -2728,16 +2748,20 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+     def visit_math_block(self, node) -> None:
+         self.requirements['amsmath'] = r'\usepackage{amsmath}'
++        math_env = pick_math_environment(node.astext())
++        self.out.append('%\n')
++        if node['ids'] and math_env.endswith('*'):  # non-numbered equation
++            self.out.append('\\phantomsection\n')
+         for cls in node['classes']:
+             self.provide_fallback('inline')
+-            self.out.append(r'\DUrole{%s}{' % cls)
+-        math_env = pick_math_environment(node.astext())
+-        self.out += [f'%\n\\begin{{{math_env}}}\n',
++            self.out.append(f'\\DUrole{{{cls}}}{{%\n')
++        self.out += [f'\\begin{{{math_env}}}\n',
+                      node.astext().translate(unichar2tex.uni2tex_table),
+                      '\n',
+                      *self.ids_to_labels(node, set_anchor=False, newline=True),
+                      f'\\end{{{math_env}}}']
+-        self.out.append('}' * len(node['classes']))
++        if node['classes']:
++            self.out.append('\n' + '}' * len(node['classes']))
+         raise nodes.SkipNode  # content already processed
+ 
+     def depart_math_block(self, node) -> None:
+@@ -2771,6 +2795,7 @@ class LaTeXTranslator(writers.DoctreeTra
+     def visit_option_list(self, node) -> None:
+         self.provide_fallback('providelength', '_providelength')
+         self.provide_fallback('optionlist')
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.out.append('\\begin{DUoptionlist}\n')
+ 
+@@ -2779,7 +2804,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.duclass_close(node)
+ 
+     def visit_option_list_item(self, node) -> None:
+-        pass
++        self.out += self.ids_to_labels(node, newline=True)
+ 
+     def depart_option_list_item(self, node) -> None:
+         pass
+@@ -2871,7 +2896,7 @@ class LaTeXTranslator(writers.DoctreeTra
+                          ord('%'): '\\%',
+                          ord('\\'): '\\\\',
+                          }
+-        if not (self.is_inline(node) or isinstance(node.parent, nodes.figure)):
++        if not self.is_inline(node):
+             self.out.append('\n')
+         # external reference (URL)
+         if 'refuri' in node:
+@@ -2901,7 +2926,7 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+     def depart_reference(self, node) -> None:
+         self.out.append('}')
+-        if not (self.is_inline(node) or isinstance(node.parent, nodes.figure)):
++        if not self.is_inline(node):
+             self.out.append('\n')
+ 
+     def visit_revision(self, node) -> None:
+@@ -2914,10 +2939,13 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.provide_fallback('rubric')
+         # class wrapper would interfere with ``\section*"`` type commands
+         # (spacing/indent of first paragraph)
+-        self.out.append('\n\\DUrubric{')
++        self.out += self.ids_to_labels(node, pre_nl=True)
++        self.duclass_open(node)
++        self.out.append('\\DUrubric{')
+ 
+     def depart_rubric(self, node) -> None:
+         self.out.append('}\n')
++        self.duclass_close(node)
+ 
+     def visit_section(self, node) -> None:
+         # Update counter-prefix for compound enumerators
+@@ -2959,6 +2987,7 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.section_level -= 1
+ 
+     def visit_sidebar(self, node) -> None:
++        self.out += self.ids_to_labels(node, pre_nl=True)
+         self.duclass_open(node)
+         self.requirements['color'] = PreambleCmds.color
+         self.provide_fallback('sidebar')
+@@ -2975,12 +3004,15 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+     def visit_attribution(self, node) -> None:
+         prefix, suffix = self.attribution_formats[self.settings.attribution]
+-        self.out.append('\\nopagebreak\n\n\\raggedleft ')
+-        self.out.append(prefix)
++        self.out.append('\\nopagebreak\n')
++        self.out += self.ids_to_labels(node, pre_nl=True)
++        self.duclass_open(node)
++        self.out.append(f'\\raggedleft {prefix}')
+         self.context.append(suffix)
+ 
+     def depart_attribution(self, node) -> None:
+         self.out.append(self.context.pop() + '\n')
++        self.duclass_close(node)
+ 
+     def visit_status(self, node) -> None:
+         self.visit_docinfo_item(node)
+@@ -3056,7 +3088,6 @@ class LaTeXTranslator(writers.DoctreeTra
+         self.depart_admonition(node)
+ 
+     def visit_table(self, node) -> None:
+-        self.duclass_open(node)
+         self.requirements['table'] = PreambleCmds.table
+         if not self.settings.legacy_column_widths:
+             self.requirements['table1'] = PreambleCmds.table_columnwidth
+@@ -3088,9 +3119,9 @@ class LaTeXTranslator(writers.DoctreeTra
+         # if it has no caption/title.
+         # See visit_thead() for tables with caption.
+         if not self.active_table.caption:
+-            self.out.extend(self.ids_to_labels(
+-                node, set_anchor=len(self.table_stack) != 1,
+-                newline=True))
++            set_anchor = (len(self.table_stack) != 1)
++            self.out += self.ids_to_labels(node, set_anchor, pre_nl=True)
++        self.duclass_open(node)
+         # TODO: Don't use a longtable or add \noindent before
+         #       the next paragraph, when in a "compound paragraph".
+         #       Start a new line or a new paragraph?
+@@ -3269,7 +3300,7 @@ class LaTeXTranslator(writers.DoctreeTra
+ 
+         # labels and PDF bookmark (sidebar entry)
+         self.out.append('\n')  # start new paragraph
+-        if node['names']:  # don't add labels just for auto-ids
++        if len(node['names']) > 1:  # don't add labels just for the auto-id
+             self.out += self.ids_to_labels(node, newline=True)
+         if (isinstance(node.next_node(), nodes.title)
+             and 'local' not in node['classes']
diff --git a/py-docutils/patches/patch-docutils_writers_latex2e_docutils.sty b/py-docutils/patches/patch-docutils_writers_latex2e_docutils.sty
new file mode 100644
index 0000000000..a08394b8be
--- /dev/null
+++ b/py-docutils/patches/patch-docutils_writers_latex2e_docutils.sty
@@ -0,0 +1,32 @@
+$NetBSD$
+
+--- docutils/writers/latex2e/docutils.sty.orig	2025-07-29 14:37:37.623939000 +0000
++++ docutils/writers/latex2e/docutils.sty
+@@ -22,7 +22,7 @@
+ 
+ \NeedsTeXFormat{LaTeX2e}
+ \ProvidesPackage{docutils}
+-  [2024-09-24 macros for Docutils LaTeX output]
++  [2025-08-06 macros for Docutils LaTeX output]
+ 
+ % Helpers
+ % -------
+@@ -124,15 +124,15 @@
+ 
+ % footnotes::
+ 
+-% numerical or symbol footnotes with hyperlinks and backlinks
++% numbered or symbol footnotes with hyperlinks and backlinks
+ \providecommand*{\DUfootnotemark}[3]{%
+   \raisebox{1em}{\hypertarget{#1}{}}%
+-  \hyperlink{#2}{\textsuperscript{#3}}%
++  \hyperref[#2]{\textsuperscript{#3}}%
+ }
+ \providecommand{\DUfootnotetext}[4]{%
+   \begingroup%
+   \renewcommand{\thefootnote}{%
+-    \protect\raisebox{1em}{\protect\hypertarget{#1}{}}%
++    \protect\phantomsection\protect\label{#1}
+     \protect\hyperlink{#2}{#3}}%
+   \footnotetext{#4}%
+   \endgroup%


Home | Main Index | Thread Index | Old Index