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