Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Fixed

- Captioned figures inside `prf:` directive boxes breaking the LaTeX/PDF build with `LaTeX Error: Not in outer par mode`, fixing [\#169](https://github.com/executablebooks/sphinx-proof/issues/169).
- Figures inside every `prf:` box, numbered or `:nonumber:`, are now written as `\begin{figure}[H]` so they stay in place, as Sphinx already does for figures inside its own admonitions
- HTML output is unchanged
Comment on lines +7 to +9

## v0.4.0 (2026-01-27)

### Added
Expand Down
20 changes: 16 additions & 4 deletions sphinx_proof/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,23 @@
latex_admonition_end = "\\end{sphinxadmonition}" + CR


def visit_latex_admonition(self) -> None:
self.body.append(latex_admonition_start)
# Write figures inside the box as [H] rather than floats, which LaTeX
# rejects there, as Sphinx does for its own admonitions
self.no_latex_floats += 1


def depart_latex_admonition(self) -> None:
self.body.append(latex_admonition_end)
self.no_latex_floats -= 1


def visit_enumerable_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
self.body.append(latex_admonition_start)
visit_latex_admonition(self)
else:
self.body.append(self.starttag(node, "div", CLASS="admonition"))

Expand All @@ -42,7 +54,7 @@ def depart_enumerable_node(self, node: Node) -> None:
number = get_node_number(self, node, countertyp)
idx = list_rindex(self.body, latex_admonition_start) + 2
self.body.insert(idx, f"{realtyp.title()} {number}")
self.body.append(latex_admonition_end)
depart_latex_admonition(self)
else:
# Find index in list of 'Proof #'
number = get_node_number(self, node, countertyp)
Expand All @@ -55,7 +67,7 @@ def visit_unenumerable_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
self.body.append(latex_admonition_start)
visit_latex_admonition(self)
else:
self.body.append(self.starttag(node, "div", CLASS="admonition"))

Expand All @@ -66,7 +78,7 @@ def depart_unenumerable_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
idx = list_rindex(self.body, latex_admonition_start) + 2
self.body.insert(idx, f"{realtyp.title()}")
self.body.append(latex_admonition_end)
depart_latex_admonition(self)
else:
# use the id to find the correct title location
search_str = f'<p class="admonition-title" id="{id}">'
Expand Down
10 changes: 10 additions & 0 deletions tests/books/test-latexfigure/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Test root for captioned figures inside prf directive boxes in LaTeX output
# (https://github.com/executablebooks/sphinx-proof/issues/169). The boxes are
# in a chapter, figures.rst, as in a book: boxes in the root document itself
# lose their numbers in LaTeX output.

project = "sphinx-proof Test"
copyright = "2020, QuantEcon Developers"
author = "QuantEcon Developers"

extensions = ["sphinx_proof"]
67 changes: 67 additions & 0 deletions tests/books/test-latexfigure/figures.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Figures
=======

.. prf:theorem:: A numbered theorem
:label: thm-numbered

.. figure:: plot.png
:name: fig-numbered-theorem

Figure in a numbered theorem

.. prf:lemma:: An unnumbered lemma
:label: lem-nonumber
:nonumber:

.. figure:: plot.png
:name: fig-unnumbered-lemma

Figure in an unnumbered lemma

.. prf:proof::

A proof with a figure.

.. figure:: plot.png
:name: fig-proof

Figure in a proof

.. note::

.. figure:: plot.png
:name: fig-note

Figure in a note

.. prf:theorem:: A theorem with a nested lemma
:label: thm-nested

.. prf:lemma:: A lemma inside a theorem
:label: lem-nested
:nonumber:

.. figure:: plot.png
:name: fig-nested-lemma

Figure in a lemma inside a theorem

.. figure:: plot.png
:name: fig-after-nested-lemma

Figure in a theorem after its nested lemma

.. note::

.. prf:definition:: A definition inside a note
:label: def-in-note

.. figure:: plot.png
:name: fig-definition-in-note

Figure in a definition inside a note

.. figure:: plot.png
:name: fig-outside

Figure outside any box
6 changes: 6 additions & 0 deletions tests/books/test-latexfigure/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Figures in proof boxes
======================

.. toctree::

figures
Binary file added tests/books/test-latexfigure/plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions tests/test_latex.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from TexSoup import TexSoup
import pytest

Expand All @@ -12,3 +14,51 @@ def test_latex_build(app, file_regression):
# get content markup
file_content = TexSoup(path_algo_directive.read_text(encoding="utf8"))
file_regression.check(str(file_content.document), extension=".tex", encoding="utf8")


LATEX_FIGURE = re.compile(
r"\\begin\{figure\}\[(?P<align>[^\]]*)\](?P<body>.*?)\\end\{figure\}", re.DOTALL
)
# A figure's :name: becomes \label{\detokenize{<docname>:<name>}}
LATEX_FIGURE_NAME = re.compile(r"\\label\{\\detokenize\{[^:}]*:(?P<name>[^}]+)\}\}")


def latex_figures(tex):
"""Map each figure's name to (figure placement, sphinxadmonition depth).

A figure without a name is collected under None.
"""
figures = {}
for figure in LATEX_FIGURE.finditer(tex):
before = tex[: figure.start()]
depth = before.count(r"\begin{sphinxadmonition}") - before.count(
r"\end{sphinxadmonition}"
)
name = LATEX_FIGURE_NAME.search(figure["body"])
figures[name["name"] if name else None] = (figure["align"], depth)
Comment on lines +37 to +38
return figures


@pytest.mark.sphinx("latex", testroot="latexfigure")
def test_latex_figure_in_admonition(app):
"""Figures inside prf directive boxes must not float (#169).

A floating figure environment inside the sphinxadmonition box stops
LaTeX with "Not in outer par mode".
"""
app.build()
tex = (app.outdir / "sphinx-prooftest.tex").read_text(encoding="utf8")

assert latex_figures(tex) == {
"fig-numbered-theorem": ("H", 1),
"fig-unnumbered-lemma": ("H", 1),
# prf:proof and note boxes come from Sphinx's own admonition visitors
"fig-proof": ("H", 1),
"fig-note": ("H", 1),
# Figures after a nested box closes are still inside the outer box
"fig-nested-lemma": ("H", 2),
"fig-after-nested-lemma": ("H", 1),
"fig-definition-in-note": ("H", 2),
# Figures after the boxes float again, so the counter was restored
"fig-outside": ("htbp", 0),
}
Loading