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.

git-svn-id: http://svn.code.sf.net/p/docutils/code/trunk@10219 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
This commit is contained in:
milde
2025-08-21 15:18:05 +00:00
parent 02cfab2b1e
commit 1ea29dcb25
9 changed files with 365 additions and 20 deletions

View File

@@ -43,7 +43,8 @@ Release 0.23b0 (unpublished)
* 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.

View File

@@ -1714,6 +1714,8 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
'\\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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
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 @@ class LaTeXTranslator(writers.DoctreeTranslator):
# 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']

View File

@@ -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

View File

@@ -360,6 +360,7 @@ paragraph automatically, with or without \texttt{\textbackslash{}leavevmode}, so
\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

View File

@@ -271,7 +271,6 @@ reStructuredText construct.
\pagebreak[4] % start ToC on new page
\phantomsection\label{table-of-contents}
\renewcommand{\contentsname}{Table of Contents}
\tableofcontents

View File

@@ -268,7 +268,6 @@ reStructuredText construct.
\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

View File

@@ -135,7 +135,6 @@ reStructuredText construct.
\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 @@ this: %
\label{directives}%
}
\phantomsection\label{contents}
\begin{DUclass}{contents}
\begin{DUclass}{local}

View File

@@ -316,9 +316,11 @@ c4
\\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 @@ A paragraph with %
\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 @@ r"""
\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 @@ Y & N \\
\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}, [

View File

@@ -214,7 +214,6 @@ unnumbered section
------------------
""",
{'body': r"""
\phantomsection\label{contents}
\pdfbookmark[1]{Contents}{contents}
\tableofcontents
@@ -235,7 +234,6 @@ first section
-------------
""",
{'body': r"""
\phantomsection\label{contents}
\pdfbookmark[1]{Contents}{contents}
\tableofcontents
@@ -256,7 +254,6 @@ first section
-------------
""",
{'body': r"""
\phantomsection\label{contents}
\pdfbookmark[1]{Contents}{contents}
\setcounter{tocdepth}{1}
\tableofcontents
@@ -286,7 +283,6 @@ section not in local toc
\label{section-with-local-toc}%
}
\phantomsection\label{contents}
\mtcsettitle{secttoc}{}
\secttoc
@@ -596,7 +592,6 @@ first chapter
-------------
""",
{'body': r"""
\phantomsection\label{contents}
\pdfbookmark[1]{Contents}{contents}
\setcounter{tocdepth}{0}
\tableofcontents
@@ -703,7 +698,6 @@ Title 2
Paragraph 2.
""",
{'body': r"""
\phantomsection\label{table-of-contents}
\pdfbookmark[1]{Table of Contents}{table-of-contents}
\begin{DUclass}{contents}