SF.net SVN: docutils:[10219] trunk/docutils

milde--- via Docutils-checkins <[email protected]> Thu, 21 Aug 2025 15:18:06 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10219
          http://sourceforge.net/p/docutils/code/10219
Author:   milde
Date:     2025-08-21 15:18:05 +0000 (Thu, 21 Aug 2025)
Log Message:
-----------
LaTeX writer: write "label" commands for elements with "ids".

Fix [bugs:#503].

Currently, internal cross-references to most body elements (admonitions, lists,
block_quotes, container, compound, ...) fail, because the "ids"
attribute of these elements is ignored.

Calls `ids_to_labels()` for (almost) all elements that may have one or more IDs.

TODO:
* test/revise citations and footnotes
* class handling for image and list items
  (use DUclass environment or DUrole function?)
* Currently, there is no way to specify a target name or custom class for
  the "docinfo" in the rST source.

Modified Paths:
--------------
    trunk/docutils/HISTORY.rst
    trunk/docutils/docutils/writers/latex2e/__init__.py
    trunk/docutils/test/functional/expected/latex_cornercases.tex
    trunk/docutils/test/functional/expected/latex_leavevmode.tex
    trunk/docutils/test/functional/expected/latex_memoir.tex
    trunk/docutils/test/functional/expected/standalone_rst_latex.tex
    trunk/docutils/test/functional/expected/standalone_rst_xetex.tex
    trunk/docutils/test/test_writers/test_latex2e.py
    trunk/docutils/test/test_writers/test_latex2e_parts.py

Modified: trunk/docutils/HISTORY.rst
===================================================================
--- trunk/docutils/HISTORY.rst	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/HISTORY.rst	2025-08-21 15:18:05 UTC (rev 10219)
@@ -43,7 +43,8 @@
 
 * docutils/writers/latex2e/__init__.py
 
-  - Prepend ``\phantomsection`` to labelled math-blocks.
+  - Add cross-reference anchors (``\phantomsection\label{...}``)
+    for elements with IDs (fixes bug #503).
   - Fix cross-reference anchor placement in figures, images,
     literal-blocks, tables, and (sub)titles.
   - Simplify code for nested image.

Modified: trunk/docutils/docutils/writers/latex2e/__init__.py
===================================================================
--- trunk/docutils/docutils/writers/latex2e/__init__.py	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/docutils/writers/latex2e/__init__.py	2025-08-21 15:18:05 UTC (rev 10219)
@@ -1714,6 +1714,8 @@
         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}')
 
@@ -1746,6 +1748,7 @@
         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}')
 
@@ -1754,6 +1757,7 @@
         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}')
 
@@ -1876,6 +1880,7 @@
     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)
 
@@ -1889,6 +1894,7 @@
         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:
@@ -1920,6 +1926,7 @@
         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')
 
@@ -1928,7 +1935,7 @@
         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:
@@ -2239,6 +2246,7 @@
         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}')
@@ -2263,8 +2271,8 @@
         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
@@ -2278,6 +2286,7 @@
             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')
@@ -2447,6 +2456,7 @@
     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:
@@ -2560,6 +2570,7 @@
                             '\\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)
@@ -2569,6 +2580,7 @@
         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:
@@ -2784,6 +2796,7 @@
     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')
 
@@ -2792,7 +2805,7 @@
         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
@@ -2927,10 +2940,13 @@
         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
@@ -2972,6 +2988,7 @@
         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')
@@ -2988,12 +3005,15 @@
 
     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)
@@ -3281,7 +3301,7 @@
 
         # 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']

Modified: trunk/docutils/test/functional/expected/latex_cornercases.tex
===================================================================
--- trunk/docutils/test/functional/expected/latex_cornercases.tex	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/functional/expected/latex_cornercases.tex	2025-08-21 15:18:05 UTC (rev 10219)
@@ -139,7 +139,6 @@
 These tests contain syntax elements and combinations which may cause
 trouble for the LaTeX writer.
 
-\phantomsection\label{contents}
 \pdfbookmark[1]{Contents}{contents}
 \tableofcontents
 

Modified: trunk/docutils/test/functional/expected/latex_leavevmode.tex
===================================================================
--- trunk/docutils/test/functional/expected/latex_leavevmode.tex	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/functional/expected/latex_leavevmode.tex	2025-08-21 15:18:05 UTC (rev 10219)
@@ -360,6 +360,7 @@
 \item[{Comment and Target}] \leavevmode
 % This is ignored.
 
+\phantomsection\label{foo}
 \begin{itemize}
 \item Comments and other “Invisible” nodes (substitution definitions,
 targets, pending) must be skipped when determining whether a

Modified: trunk/docutils/test/functional/expected/latex_memoir.tex
===================================================================
--- trunk/docutils/test/functional/expected/latex_memoir.tex	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/functional/expected/latex_memoir.tex	2025-08-21 15:18:05 UTC (rev 10219)
@@ -271,7 +271,6 @@
 
 \pagebreak[4] % start ToC on new page
 
-\phantomsection\label{table-of-contents}
 \renewcommand{\contentsname}{Table of Contents}
 \tableofcontents
 

Modified: trunk/docutils/test/functional/expected/standalone_rst_latex.tex
===================================================================
--- trunk/docutils/test/functional/expected/standalone_rst_latex.tex	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/functional/expected/standalone_rst_latex.tex	2025-08-21 15:18:05 UTC (rev 10219)
@@ -268,7 +268,6 @@
 
 \pagebreak[4] % start ToC on new page
 
-\phantomsection\label{table-of-contents}
 \pdfbookmark[1]{Table of Contents}{table-of-contents}
 \renewcommand{\contentsname}{Table of Contents}
 \tableofcontents

Modified: trunk/docutils/test/functional/expected/standalone_rst_xetex.tex
===================================================================
--- trunk/docutils/test/functional/expected/standalone_rst_xetex.tex	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/functional/expected/standalone_rst_xetex.tex	2025-08-21 15:18:05 UTC (rev 10219)
@@ -135,7 +135,6 @@
 
 \pagebreak[4] % start ToC on new page
 
-\phantomsection\label{table-of-contents}
 \pdfbookmark[1]{Table of Contents}{table-of-contents}
 
 \begin{DUclass}{contents}
@@ -787,7 +786,6 @@
   \label{directives}%
 }
 
-\phantomsection\label{contents}
 
 \begin{DUclass}{contents}
 \begin{DUclass}{local}

Modified: trunk/docutils/test/test_writers/test_latex2e.py
===================================================================
--- trunk/docutils/test/test_writers/test_latex2e.py	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/test_writers/test_latex2e.py	2025-08-21 15:18:05 UTC (rev 10219)
@@ -316,9 +316,11 @@
 \\hline
 \\end{longtable*}
 """],
+])
+
 # Test handling of IDs and custom class values
 # --------------------------------------------
-# targets with ID
+samples['IDs and classes'] = ({}, [
 ["""\
 A paragraph with _`inline target`.
 
@@ -335,6 +337,177 @@
 \phantomsection\label{block-target}
 \DUrole{custom}{\DUrole{paragraph}{Next paragraph.}}
 """],
+# admonition
+["""
+.. class:: cls1
+.. _label1:
+.. hint::
+   :name: label2
+   :class: cls2
+
+   Don't forget to breathe.
+""",
+r"""
+\phantomsection\label{label2}\label{label1}
+\begin{DUclass}{cls2}
+\begin{DUclass}{cls1}
+\begin{DUclass}{hint}
+\begin{DUadmonition}
+\DUtitle{Hint}
+
+Don't forget to breathe.
+\end{DUadmonition}
+\end{DUclass}
+\end{DUclass}
+\end{DUclass}
+"""],
+# block quote
+["""
+.. class:: cls1
+.. _label1:
+
+   Exlicit is better than implicit.
+
+   .. class:: attribute-cls cute
+   .. _a-tribution:
+
+   -- Zen of Python
+""",
+r"""
+\phantomsection\label{label1}
+\begin{DUclass}{cls1}
+\begin{quote}
+Exlicit is better than implicit.
+\nopagebreak
+
+\phantomsection\label{a-tribution}
+\begin{DUclass}{attribute-cls}
+\begin{DUclass}{cute}
+\raggedleft —Zen of Python
+\end{DUclass}
+\end{DUclass}
+\end{quote}
+\end{DUclass}
+"""],
+# bullet list
+["""
+.. class:: cls1
+.. _bullet1:
+
+* list item
+
+  .. class:: bullet-class
+  .. _b-item:
+
+* second bullet list item
+""",
+r"""
+\phantomsection\label{bullet1}
+\begin{DUclass}{cls1}
+\begin{itemize}
+\item list item
+
+\phantomsection\label{b-item}
+\item second bullet list item
+\end{itemize}
+\end{DUclass}
+"""],
+# definition list
+["""
+.. class:: def-list-class
+.. _def-list:
+
+definition
+  list
+
+  .. class:: def-item-class
+  .. _def-item:
+
+term
+  definition
+""",
+"""
+\\phantomsection\\label{def-list}
+\\begin{DUclass}{def-list-class}
+\\begin{description}
+\\item[{definition}] \n\
+list
+
+\\phantomsection\\label{def-item}
+\\item[{term}] \n\
+definition
+\\end{description}
+\\end{DUclass}
+"""],
+# enumerated list
+["""
+.. class:: cls1
+.. _enumerated1:
+
+#. list item
+
+   .. class:: e-item-class
+   .. _e-item:
+
+#. enumerated list item
+""",
+r"""
+\phantomsection\label{enumerated1}
+\begin{DUclass}{cls1}
+\begin{enumerate}
+\item list item
+
+\phantomsection\label{e-item}
+\item enumerated list item
+\end{enumerate}
+\end{DUclass}
+"""],
+# field list
+["""\
+Not a docinfo.
+
+.. class:: fieldlist-class
+.. _f-list:
+
+:field: list
+
+  .. class:: field-class
+  .. _f-list-item:
+
+:name: body
+""",
+r"""
+Not a docinfo.
+
+\phantomsection\label{f-list}
+\begin{DUclass}{fieldlist-class}
+\begin{DUfieldlist}
+\item[{field:}]
+list
+
+\phantomsection\label{f-list-item}
+\item[{name:}]
+body
+\end{DUfieldlist}
+\end{DUclass}
+"""],
+# line block
+["""\
+.. class:: lineblock-class
+.. _line-block:
+
+| line block
+| second line
+""",
+r"""
+\phantomsection\label{line-block}
+\begin{DUclass}{lineblock-class}
+\begin{DUlineblock}{0em}
+\item[] line block
+\item[] second line
+\end{DUlineblock}
+\end{DUclass}
+"""],
 # literal block
 ["""\
 .. class:: cls1
@@ -354,6 +527,28 @@
 \end{quote}
 \end{DUclass}
 """],
+# option list
+["""\
+.. class:: o-list-class
+.. _o-list:
+
+--an  option list
+
+  .. class:: option-class
+  .. _o-item:
+
+--another  option
+""",
+r"""
+\phantomsection\label{o-list}
+\begin{DUclass}{o-list-class}
+\begin{DUoptionlist}
+\item[-{}-an]  option list
+\phantomsection\label{o-item}
+\item[-{}-another]  option
+\end{DUoptionlist}
+\end{DUclass}
+"""],
 # table with IDs and custom + special class values
 ["""\
 .. class:: cls1
@@ -377,6 +572,145 @@
 \end{DUclass}
 \end{DUclass}
 """],
+# directives
+["""\
+.. compound::
+   :class: compoundclass
+   :name: com-pound
+
+   Compound paragraph 1
+
+   Compound paragraph 2
+
+.. container:: containerclass
+   :name: con-tainer
+
+   Container paragraph 1
+
+   Container paragraph 2
+""",
+r"""
+\phantomsection\label{com-pound}
+\begin{DUclass}{compound}
+\begin{DUclass}{compoundclass}
+Compound paragraph 1
+
+Compound paragraph 2
+\end{DUclass}
+\end{DUclass}
+
+\phantomsection\label{con-tainer}
+\begin{DUclass}{containerclass}
+Container paragraph 1
+
+Container paragraph 2
+\end{DUclass}
+"""],
+# figures and images
+["""\
+.. figure:: parrot.png
+   :figclass: figureclass
+   :figname: fig-ure
+
+   .. class:: f-caption-class
+   .. _f-caption:
+
+   A figure with caption
+
+   .. class:: legend-class
+   .. _le-gend:
+
+   A figure legend
+
+.. image:: parrot.png
+   :class: imgclass TODO ignored!
+   :name: i-mage
+   :target: example.org/parrots
+""",
+r"""
+\phantomsection\label{fig-ure}
+\begin{DUclass}{figureclass}
+\begin{figure}
+\noindent\makebox[\linewidth][c]{\includegraphics{parrot.png}}
+\caption{\label{f-caption}\DUrole{f-caption-class}{A figure with caption}}
+\begin{DUlegend}
+\phantomsection\label{le-gend}
+\DUrole{legend-class}{A figure legend}
+\end{DUlegend}
+\end{figure}
+\end{DUclass}
+
+\phantomsection\label{i-mage}
+\href{example.org/parrots}{\includegraphics{parrot.png}}
+"""],
+["""\
+.. math:: x = 2^4
+   :class: mathclass
+   :name: math-block
+
+.. note:: a specific admonition
+   :class: noteclass
+   :name: my-note
+
+.. _my-raw:
+.. raw:: latex pseudoxml xml
+   :class: rawclass
+
+   \\LaTeX
+
+.. sidebar:: sidebar title
+   :class: sideclass
+   :name: side-bar
+
+   sidebar content
+
+.. topic:: topic heading
+   :class: topicclass
+   :name: to-pic
+
+   topic content
+""",
+r"""%
+\phantomsection
+\DUrole{mathclass}{%
+\begin{equation*}
+x = 2^4
+\label{math-block}
+\end{equation*}
+}
+\phantomsection\label{my-note}
+\begin{DUclass}{noteclass}
+\begin{DUclass}{note}
+\begin{DUadmonition}
+\DUtitle{Note}
+
+a specific admonition
+\end{DUadmonition}
+\end{DUclass}
+\end{DUclass}
+
+\phantomsection\label{my-raw}\DUrole{rawclass}{\LaTeX}
+
+\phantomsection\label{side-bar}
+\begin{DUclass}{sideclass}
+\DUsidebar{
+\DUtitle{sidebar title}
+
+sidebar content
+}
+\end{DUclass}
+
+\phantomsection\label{to-pic}
+\begin{DUclass}{topic}
+\begin{DUclass}{topicclass}
+\begin{quote}
+\DUtitle{topic heading}
+
+topic content
+\end{quote}
+\end{DUclass}
+\end{DUclass}
+"""],
 ])
 
 samples['latex_sectnum'] = ({'sectnum_xform': False}, [

Modified: trunk/docutils/test/test_writers/test_latex2e_parts.py
===================================================================
--- trunk/docutils/test/test_writers/test_latex2e_parts.py	2025-08-20 12:04:54 UTC (rev 10218)
+++ trunk/docutils/test/test_writers/test_latex2e_parts.py	2025-08-21 15:18:05 UTC (rev 10219)
@@ -214,7 +214,6 @@
 ------------------
 """,
  {'body': r"""
-\phantomsection\label{contents}
 \pdfbookmark[1]{Contents}{contents}
 \tableofcontents
 
@@ -235,7 +234,6 @@
 -------------
 """,
  {'body': r"""
-\phantomsection\label{contents}
 \pdfbookmark[1]{Contents}{contents}
 \tableofcontents
 
@@ -256,7 +254,6 @@
 -------------
 """,
  {'body': r"""
-\phantomsection\label{contents}
 \pdfbookmark[1]{Contents}{contents}
 \setcounter{tocdepth}{1}
 \tableofcontents
@@ -286,7 +283,6 @@
   \label{section-with-local-toc}%
 }
 
-\phantomsection\label{contents}
 \mtcsettitle{secttoc}{}
 \secttoc
 
@@ -596,7 +592,6 @@
 -------------
 """,
  {'body': r"""
-\phantomsection\label{contents}
 \pdfbookmark[1]{Contents}{contents}
 \setcounter{tocdepth}{0}
 \tableofcontents
@@ -703,7 +698,6 @@
 Paragraph 2.
 """,
  {'body': r"""
-\phantomsection\label{table-of-contents}
 \pdfbookmark[1]{Table of Contents}{table-of-contents}
 
 \begin{DUclass}{contents}

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.



_______________________________________________
Docutils-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/docutils-checkins