diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 4cbc2930e25..f466815ae58 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -440,10 +440,20 @@ def depart_thead(self, node: nodes.thead) -> None: self.unrestrict(node) def depart_table(self, node: nodes.table) -> None: - tbody = next(node.findall(nodes.tbody)) - for footnote in reversed(self.table_footnotes): - fntext = footnotetext('', *footnote.children, ids=footnote['ids']) - tbody.insert(0, fntext) + tbody = next(node.findall(nodes.tbody), None) + if tbody is not None: + for footnote in reversed(self.table_footnotes): + fntext = footnotetext('', *footnote.children, ids=footnote['ids']) + tbody.insert(0, fntext) + else: + # If there is no tbody (e.g. a table with only header rows), + # place any collected footnotes after the table node instead. + table_parent = node.parent + if table_parent is not None: + idx = table_parent.index(node) + for i, footnote in enumerate(self.table_footnotes): + fntext = footnotetext('', *footnote.children, ids=footnote['ids']) + table_parent.insert(idx + i + 1, fntext) self.table_footnotes = [] diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 71ee6d16d9f..1bd40b2cc13 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -16,6 +16,7 @@ import docutils import pygments import pytest +from docutils import nodes from sphinx.builders.latex import default_latex_documents from sphinx.config import Config @@ -2097,6 +2098,47 @@ def test_latex_nested_tables(app: SphinxTestApp) -> None: assert app.warning.getvalue() == '' +def test_latex_table_empty_body() -> None: + """Regression test for issue #14271. + + Tables with header rows but no body rows (as produced by e.g. + myst_parser from Markdown) should not crash the LaTeX builder + with a StopIteration in LaTeXFootnoteVisitor.depart_table. + """ + from docutils.utils import new_document + + from sphinx.builders.latex.transforms import LaTeXFootnoteVisitor + + document = new_document('') + + # Build a table node with thead but no tbody, as myst_parser would + # generate from a Markdown table with only a header row. + table = nodes.table() + tgroup = nodes.tgroup(cols=2) + table += tgroup + tgroup += nodes.colspec(colwidth=50) + tgroup += nodes.colspec(colwidth=50) + thead = nodes.thead() + tgroup += thead + row = nodes.row() + thead += row + entry1 = nodes.entry() + entry1 += nodes.paragraph(text='Header 1') + row += entry1 + entry2 = nodes.entry() + entry2 += nodes.paragraph(text='Header 2') + row += entry2 + + section = nodes.section() + section += table + document += section + + # This should not raise StopIteration + visitor = LaTeXFootnoteVisitor(document, []) + visitor.table_footnotes = [] + table.walkabout(visitor) + + @pytest.mark.sphinx('latex', testroot='latex-container') def test_latex_container(app: SphinxTestApp) -> None: app.build(force_all=True)