Skip to content

Commit f1316bb

Browse files
committed
LaTeX: allow more cases of table nesting, fix #13646
Tables using longtable can now contain nested tables inclusive of those rendered by tabulary, up to the suppression of the latter horizontal lines due to an upstream LaTeX bug. A longtable can never itself be nested, and will fall-back to tabular. Formerly longtable would raise (in principle) an error if it contained any sort of nested table, but the detection of being a longtable was faulty if not specified as class option. Relates #6838.
1 parent a536639 commit f1316bb

File tree

6 files changed

+116
-47
lines changed

6 files changed

+116
-47
lines changed

doc/usage/restructuredtext/directives.rst

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,15 @@ Check the :confval:`latex_table_style`.
14721472
complex contents such as multiple paragraphs, blockquotes, lists, literal
14731473
blocks, will render correctly to LaTeX output.
14741474

1475+
.. versionchanged:: 8.3.0
1476+
1477+
The partial support for nesting a table in another has been extended.
1478+
Formerly Sphinx would raise an error if ``longtable`` class was specified
1479+
for a table containing a nested table, and some cases would not raise an
1480+
error at Sphinx level but fail at LaTeX level during PDF build. This is a
1481+
complex topic in LaTeX rendering and the output can sometimes be improved
1482+
via the :rst:dir:`tabularcolumns` directive.
1483+
14751484
.. rst:directive:: .. tabularcolumns:: column spec
14761485
14771486
This directive influences only the LaTeX output for the next table in
@@ -1489,40 +1498,38 @@ Check the :confval:`latex_table_style`.
14891498
:rst:dir:`tabularcolumns` conflicts with ``:widths:`` option of table
14901499
directives. If both are specified, ``:widths:`` option will be ignored.
14911500

1492-
Sphinx will render tables with more than 30 rows with ``longtable``.
1493-
Besides the ``l``, ``r``, ``c`` and ``p{width}`` column specifiers, one can
1494-
also use ``\X{a}{b}`` (new in version 1.5) which configures the column
1495-
width to be a fraction ``a/b`` of the total line width and ``\Y{f}`` (new
1496-
in version 1.6) where ``f`` is a decimal: for example ``\Y{0.2}`` means that
1497-
the column will occupy ``0.2`` times the line width.
1501+
Sphinx renders tables with at most 30 rows using ``tabulary``, and those
1502+
with more rows with ``longtable``.
14981503

1499-
When this directive is used for a table with at most 30 rows, Sphinx will
1500-
render it with ``tabulary``. One can then use specific column types ``L``
1501-
(left), ``R`` (right), ``C`` (centered) and ``J`` (justified). They have
1502-
the effect of a ``p{width}`` (i.e. each cell is a LaTeX ``\parbox``) with
1503-
the specified internal text alignment and an automatically computed
1504-
``width``.
1505-
1506-
.. warning::
1504+
``tabulary`` tries to compute automatically (internally to LaTeX) suitable
1505+
column widths. However, cells are then not allowed to contain
1506+
"problematic" elements such as lists, object descriptions,
1507+
blockquotes... Sphinx will fall back to using ``tabular`` if such a cell is
1508+
encountered (or a nested ``tabulary``). In such a case the table will have
1509+
a tendency to try to fill the whole available line width.
15071510

1508-
- Cells that contain list-like elements such as object descriptions,
1509-
blockquotes or any kind of lists are not compatible with the ``LRCJ``
1510-
column types. The column type must then be some ``p{width}`` with an
1511-
explicit ``width`` (or ``\X{a}{b}`` or ``\Y{f}``).
1511+
:rst:dir:`tabularcolumns` can help in coercing the usage of ``tabulary`` if
1512+
one is careful to not employ the ``tabulary`` column types (``L``, ``R``,
1513+
``C`` or ``J``) for those columns with at least one "problematic" cell, but
1514+
only LaTeX's ``p{<width>}`` or Sphinx ``\X`` and ``\Y`` (described next).
15121515

1513-
- Literal blocks do not work with ``tabulary`` at all. Sphinx will
1514-
fall back to ``tabular`` or ``longtable`` environments and generate a
1515-
suitable column specification.
1516+
Literal blocks do not work at all with ``tabulary``. Sphinx will fall back
1517+
to ``tabular`` or ``longtable`` environments depending on the number of
1518+
rows. It will employ the :rst:dir:`tabularcolumns` specification only if it
1519+
contains no usage of the ``tabulary`` specific types.
15161520

1517-
In absence of the :rst:dir:`tabularcolumns` directive, and for a table with at
1518-
most 30 rows and no problematic cells as described in the above warning,
1519-
Sphinx uses ``tabulary`` and the ``J`` column-type for every column.
1521+
Besides the LaTeX ``l``, ``r``, ``c`` and ``p{width}`` column specifiers,
1522+
one can also use ``\X{a}{b}`` which configures the column width to be a
1523+
fraction ``a/b`` of the total line width and ``\Y{f}`` where ``f`` is a
1524+
decimal: for example ``\Y{0.2}`` means that the column will occupy ``0.2``
1525+
times the line width.
15201526

15211527
.. versionchanged:: 1.6
15221528

1523-
Formerly, the ``L`` column-type was used (text is flushed-left). To revert
1524-
to this, include ``\newcolumntype{T}{L}`` in the LaTeX preamble, as in fact
1525-
Sphinx uses ``T`` and sets it by default to be an alias of ``J``.
1529+
Use ``J`` (justified) by default with ``tabulary``, not ``L``
1530+
(flushed-left). To revert, include ``\newcolumntype{T}{L}`` in the LaTeX
1531+
preamble, as in fact Sphinx uses ``T`` and sets it by default to be an
1532+
alias of ``J``.
15261533

15271534
.. hint::
15281535

sphinx/templates/latex/tabulary.tex.jinja

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<% if 'nocolorrows' in table.styles -%>
2222
\sphinxthistablewithnocolorrowsstyle
2323
<% endif -%>
24+
<% if table.is_nested -%>
25+
\sphinxthistabularywithnohlinesifinlongtable
26+
<% endif -%>
2427
<% if table.align -%>
2528
<%- if table.align in ('center', 'default') -%>
2629
\centering

sphinx/texinputs/sphinxlatextables.sty

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
% The method here is with no changes to neither writer nor templates.
4848
\newif\ifspx@intable
4949
\newif\ifspx@thistableisnested
50+
% Try to allow nested tables in a longtable. But tabulary causes problems.
51+
\newif\ifspx@longtable
5052
%
5153
% Also provides user command (see docs)
5254
% - \sphixncolorblend
@@ -115,6 +117,7 @@
115117
\edef\sphinxbaselineskip{\dimexpr\the\dimexpr\baselineskip\relax\relax}%
116118
\spx@inframedtrue % message to sphinxheavybox
117119
\spx@table@setnestedflags
120+
\spx@longtabletrue
118121
}
119122
% Compatibility with caption package
120123
\def\sphinxthelongtablecaptionisattop{%
@@ -128,7 +131,10 @@
128131
\def\sphinxatlongtableend{\@nobreakfalse % latex3/latex2e#173
129132
\prevdepth\z@\vskip\sphinxtablepost\relax}%
130133
% B. Table with tabular or tabulary
131-
\def\sphinxattablestart{\par\vskip\dimexpr\sphinxtablepre\relax
134+
\def\sphinxattablestart{\par
135+
\ifvmode % guard agains being nested in a table cell
136+
\vskip\dimexpr\sphinxtablepre\relax
137+
\fi
132138
\spx@inframedtrue % message to sphinxheavybox
133139
\spx@table@setnestedflags
134140
}%
@@ -142,7 +148,12 @@
142148
\spx@intabletrue
143149
\fi
144150
}%
145-
\let\sphinxattableend\sphinxatlongtableend
151+
\def\sphinxattableend{%
152+
\@nobreakfalse % <- probably unneeded as this is not a longtable
153+
\ifvmode % guard against being nested in a table cell
154+
\prevdepth\z@\vskip\sphinxtablepost\relax
155+
\fi
156+
}%
146157
% This is used by tabular and tabulary templates
147158
\newcommand*\sphinxcapstartof[1]{%
148159
\vskip\parskip
@@ -1083,14 +1094,20 @@ local use of booktabs table style}%
10831094
10841095
% borderless style
10851096
\def\sphinxthistablewithborderlessstyle{%
1097+
\sphinxthistablewithnohlines
1098+
\def\spx@arrayrulewidth{\z@}%
1099+
}%
1100+
\def\sphinxthistablewithnohlines{%
10861101
\let\sphinxhline \@empty
10871102
\let\sphinxcline \@gobble
10881103
\let\sphinxvlinecrossing\@gobble
10891104
\let\sphinxfixclines \@gobble
10901105
\let\spx@toprule \@empty
10911106
\let\sphinxmidrule \@empty
10921107
\let\sphinxbottomrule \@empty
1093-
\def\spx@arrayrulewidth{\z@}%
1108+
}%
1109+
\def\sphinxthistabularywithnohlinesifinlongtable{%
1110+
\ifspx@longtable\sphinxthistablewithnohlines\fi
10941111
}%
10951112
10961113
% colorrows style

sphinx/writers/latex.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ def __init__(self, node: Element) -> None:
134134
self.has_problematic = False
135135
self.has_oldproblematic = False
136136
self.has_verbatim = False
137+
# cf https://github.com/sphinx-doc/sphinx/issues/13646#issuecomment-2958309632
138+
self.is_nested = False
137139
self.entry_needs_linetrimming = 0
138140
self.caption: list[str] = []
139141
self.stubs: list[int] = []
@@ -147,29 +149,47 @@ def __init__(self, node: Element) -> None:
147149
self.cell_id = 0 # last assigned cell_id
148150

149151
def is_longtable(self) -> bool:
150-
"""True if and only if table uses longtable environment."""
152+
"""True if and only if table uses longtable environment.
153+
154+
In absence of longtable class can only be used trustfully on departing
155+
the table, as the number of rows is not known until then.
156+
"""
151157
return self.row > 30 or 'longtable' in self.classes
152158

153159
def get_table_type(self) -> str:
154160
"""Returns the LaTeX environment name for the table.
155161
162+
It is used at time of ``depart_table()`` and again via ``get_colspec()``.
156163
The class currently supports:
157164
158165
* longtable
159166
* tabular
160167
* tabulary
161168
"""
162-
if self.is_longtable():
169+
if self.is_longtable() and not self.is_nested:
163170
return 'longtable'
164171
elif self.has_verbatim:
165172
return 'tabular'
166173
elif self.colspec:
167-
return 'tabulary'
174+
if any(c in 'LRCJT' for c in self.colspec):
175+
# tabulary would complain "no suitable columns" if none of its
176+
# column type were used so we ensure at least one matches.
177+
# It is responsability of user to make sure not to use tabulary
178+
# column types for a column containing a problematic cell.
179+
return 'tabulary'
180+
else:
181+
return 'tabular'
168182
elif self.has_problematic or (
169183
self.colwidths and 'colwidths-given' in self.classes
170184
):
171185
return 'tabular'
172186
else:
187+
# A nested tabulary in a longtable can not use any \hline's,
188+
# i.e. it can not use "booktabs" or "standard" styles (due to a
189+
# LaTeX upstream bug we do not try to solve). But we can't know
190+
# here if it ends up in a tabular or longtable. So it is via
191+
# LaTeX macros inserted by the tabulary template that the problem
192+
# will be solved.
173193
return 'tabulary'
174194

175195
def get_colspec(self) -> str:
@@ -179,6 +199,7 @@ def get_colspec(self) -> str:
179199
180200
.. note::
181201
202+
This is used by the template renderer at time of depart_table().
182203
The ``\\X`` and ``T`` column type specifiers are defined in
183204
``sphinxlatextables.sty``.
184205
"""
@@ -1146,23 +1167,17 @@ def visit_tabular_col_spec(self, node: Element) -> None:
11461167
raise nodes.SkipNode
11471168

11481169
def visit_table(self, node: Element) -> None:
1149-
if len(self.tables) == 1:
1150-
assert self.table is not None
1151-
if self.table.get_table_type() == 'longtable':
1152-
raise UnsupportedError(
1153-
'%s:%s: longtable does not support nesting a table.'
1154-
% (self.curfilestack[-1], node.line or '')
1155-
)
1156-
# change type of parent table to tabular
1157-
# see https://groups.google.com/d/msg/sphinx-users/7m3NeOBixeo/9LKP2B4WBQAJ
1158-
self.table.has_problematic = True
1159-
elif len(self.tables) > 2:
1170+
table = Table(node)
1171+
assert table is not None
1172+
if len(self.tables) >= 1:
1173+
table.is_nested = True
1174+
# TODO: do we want > 2, > 1, or actually nothing here?
1175+
if len(self.tables) > 2:
11601176
raise UnsupportedError(
11611177
'%s:%s: deeply nested tables are not implemented.'
11621178
% (self.curfilestack[-1], node.line or '')
11631179
)
11641180

1165-
table = Table(node)
11661181
self.tables.append(table)
11671182
if table.colsep is None:
11681183
table.colsep = '|' * (
@@ -1191,6 +1206,25 @@ def depart_table(self, node: Element) -> None:
11911206
assert self.table is not None
11921207
labels = self.hypertarget_to(node)
11931208
table_type = self.table.get_table_type()
1209+
if table_type == 'tabulary':
1210+
if len(self.tables) > 1:
1211+
# tell parents to not be tabulary
1212+
for _ in self.tables[:-1]:
1213+
_.has_problematic = True
1214+
else:
1215+
if self.table.colspec:
1216+
if any(c in self.table.colspec for c in 'LRJCT'):
1217+
logger.warning(
1218+
__(
1219+
'colspec %s was given which uses '
1220+
'tabulary syntax. But this table can not be '
1221+
'rendered as a tabulary; colspec will be ignored.'
1222+
),
1223+
self.table.colspec[:-1],
1224+
type='latex',
1225+
location=node,
1226+
)
1227+
self.table.colspec = ''
11941228
table = self.render(
11951229
table_type + '.tex.jinja', {'table': self.table, 'labels': labels}
11961230
)

tests/roots/test-latex-table/expects/tabularcolumn.tex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
\sphinxthistablewithglobalstyle
55
\sphinxthistablewithnovlinesstyle
66
\centering
7-
\begin{tabulary}{\linewidth}[t]{cc}
7+
\begin{tabular}[t]{cc}
88
\sphinxtoprule
99
\sphinxstyletheadfamily
1010
\sphinxAtStartPar
@@ -36,6 +36,6 @@
3636
cell3\sphinxhyphen{}2
3737
\\
3838
\sphinxbottomrule
39-
\end{tabulary}
39+
\end{tabular}
4040
\sphinxtableafterendhook\par
4141
\sphinxattableend\end{savenotes}

tests/roots/test-root/markup.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ Tables with multirow and multicol:
229229
| +---+ |
230230
+---+---+
231231

232+
.. rst-class:: longtable
233+
234+
+---+---+
235+
| +---+ |
236+
| | h | |
237+
| +---+ |
238+
+---+---+
239+
232240
.. list-table::
233241
:header-rows: 0
234242

0 commit comments

Comments
 (0)