diff --git a/CHANGELOG.md b/CHANGELOG.md index f6cabd9..2e5b929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + ## v0.4.0 (2026-01-27) ### Added diff --git a/sphinx_proof/nodes.py b/sphinx_proof/nodes.py index d77b257..756a044 100644 --- a/sphinx_proof/nodes.py +++ b/sphinx_proof/nodes.py @@ -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")) @@ -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) @@ -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")) @@ -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'
'
diff --git a/tests/books/test-latexfigure/conf.py b/tests/books/test-latexfigure/conf.py
new file mode 100644
index 0000000..d73e591
--- /dev/null
+++ b/tests/books/test-latexfigure/conf.py
@@ -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"]
diff --git a/tests/books/test-latexfigure/figures.rst b/tests/books/test-latexfigure/figures.rst
new file mode 100644
index 0000000..489bc2b
--- /dev/null
+++ b/tests/books/test-latexfigure/figures.rst
@@ -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
diff --git a/tests/books/test-latexfigure/index.rst b/tests/books/test-latexfigure/index.rst
new file mode 100644
index 0000000..cd2016f
--- /dev/null
+++ b/tests/books/test-latexfigure/index.rst
@@ -0,0 +1,6 @@
+Figures in proof boxes
+======================
+
+.. toctree::
+
+ figures
diff --git a/tests/books/test-latexfigure/plot.png b/tests/books/test-latexfigure/plot.png
new file mode 100644
index 0000000..6904e5e
Binary files /dev/null and b/tests/books/test-latexfigure/plot.png differ
diff --git a/tests/test_latex.py b/tests/test_latex.py
index 1820cb2..cabc1f2 100644
--- a/tests/test_latex.py
+++ b/tests/test_latex.py
@@ -1,3 +1,5 @@
+import re
+
from TexSoup import TexSoup
import pytest
@@ -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