Skip to content

Commit f17d812

Browse files
authored
Merge pull request #58 from executablebooks/latex-features
ENH: Add latex and support for non-title nodes
2 parents 1014f8f + c64732b commit f17d812

19 files changed

+266
-68
lines changed

docs/source/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"sphinx_togglebutton",
1919
]
2020

21+
bibtex_bibfiles = ["references.bib"]
22+
2123
# Add any paths that contain templates here, relative to this directory.
2224
templates_path = ["_templates"]
2325

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"pytest-regressions",
2828
"beautifulsoup4",
2929
"myst-parser",
30+
"texsoup",
3031
],
3132
"rtd": [
3233
"sphinx>=3.0",

sphinx_proof/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,21 @@ def setup(app: Sphinx) -> Dict[str, Any]:
7676
proof_node,
7777
singlehtml=(visit_proof_node, depart_proof_node),
7878
html=(visit_proof_node, depart_proof_node),
79+
latex=(visit_proof_node, depart_proof_node),
7980
)
8081
app.add_node(
8182
unenumerable_node,
8283
singlehtml=(visit_unenumerable_node, depart_unenumerable_node),
8384
html=(visit_unenumerable_node, depart_unenumerable_node),
85+
latex=(visit_unenumerable_node, depart_unenumerable_node),
8486
)
8587
app.add_enumerable_node(
8688
enumerable_node,
8789
"proof",
8890
None,
8991
singlehtml=(visit_enumerable_node, depart_enumerable_node),
9092
html=(visit_enumerable_node, depart_enumerable_node),
93+
latex=(visit_enumerable_node, depart_enumerable_node),
9194
)
9295

9396
return {

sphinx_proof/domain.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
:copyright: Copyright 2020 by the QuantEcon team, see AUTHORS
88
:licences: see LICENSE for details
99
"""
10-
from typing import Any, Dict, Tuple, List
10+
from typing import Any, Dict, Tuple, List, Callable
1111
from docutils.nodes import Element, Node, document, system_message
1212
from sphinx.environment import BuildEnvironment
1313
from sphinx.addnodes import pending_xref
@@ -21,6 +21,7 @@
2121
from docutils import nodes
2222
from .directive import ProofDirective
2323
from .proof_type import PROOF_TYPES
24+
from copy import copy
2425

2526
logger = logging.getLogger(__name__)
2627

@@ -38,6 +39,7 @@ def generate(self, docnames=None) -> Tuple[Dict[str, Any], bool]:
3839
return content, True
3940

4041
proofs = self.domain.env.proof_list
42+
# {'theorem-0': {'docname': 'start/overview', 'type': 'theorem', 'ids': ['theorem-0'], 'label': 'theorem-0', 'prio': 0, 'nonumber': False}} # noqa: E501
4143

4244
# name, subtype, docname, typ, anchor, extra, qualifier, description
4345
for anchor, values in proofs.items():
@@ -71,11 +73,23 @@ class ProofDomain(Domain):
7173
name = "prf"
7274
label = "Proof Domain"
7375

74-
roles = {"ref": ProofXRefRole()}
76+
roles = {"ref": ProofXRefRole()} # role name -> role callable
7577

76-
indices = {ProofIndex}
78+
indices = {ProofIndex} # a list of index subclasses
7779

78-
directives = {**{"proof": ProofDirective}, **PROOF_TYPES}
80+
directives = {**{"proof": ProofDirective}, **PROOF_TYPES} # list of directives
81+
82+
enumerable_nodes = {} # type: Dict[[Node], Tuple[str, Callable]]
83+
84+
def __init__(self, env: "BuildEnvironment") -> None:
85+
super().__init__(env)
86+
87+
# set up enumerable nodes
88+
self.enumerable_nodes = copy(
89+
self.enumerable_nodes
90+
) # create a copy for this instance
91+
for node, settings in env.app.registry.enumerable_nodes.items():
92+
self.enumerable_nodes[node] = settings
7993

8094
def resolve_xref(
8195
self,
@@ -87,7 +101,14 @@ def resolve_xref(
87101
node: pending_xref,
88102
contnode: Element,
89103
) -> Element:
90-
104+
"""
105+
Resolve the pending_xref node with the given typ and target. This method should
106+
return a new node, to replace the xref node, containing the contnode which is
107+
the markup content of the cross-reference. If no resolution can be found, None
108+
can be returned; the xref node will then given to the missing-reference event,
109+
and if that yields no resolution, replaced by contnode.The method can also raise
110+
sphinx.environment.NoUri to suppress the missing-reference event being emitted.
111+
"""
91112
try:
92113
match = env.proof_list[target]
93114
except Exception:
@@ -98,7 +119,6 @@ def resolve_xref(
98119

99120
todocname = match["docname"]
100121
title = contnode[0]
101-
102122
if target in contnode[0]:
103123
number = ""
104124
if not env.proof_list[target]["nonumber"]:

sphinx_proof/nodes.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
:copyright: Copyright 2020 by the QuantEcon team, see AUTHORS
88
:licences: see LICENSE for details
99
"""
10-
from sphinx.builders.html import HTMLTranslator
1110
from docutils import nodes
1211
from docutils.nodes import Node
12+
from sphinx.writers.latex import LaTeXTranslator
13+
14+
CR = "\n"
15+
latex_admonition_start = CR + "\\begin{sphinxadmonition}{note}"
16+
latex_admonition_end = "\\end{sphinxadmonition}" + CR
1317

1418

1519
class proof_node(nodes.Admonition, nodes.Element):
@@ -25,35 +29,53 @@ class unenumerable_node(nodes.Admonition, nodes.Element):
2529

2630

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

3039

3140
def depart_enumerable_node(self, node: Node) -> None:
3241
typ = node.attributes.get("type", "")
33-
34-
# Find index in list of 'Proof #'
35-
number = get_node_number(self, node)
36-
idx = self.body.index(f"Proof {number} ")
37-
self.body[idx] = f"{typ.title()} {number} "
38-
self.body.append("</div>")
42+
if isinstance(self, LaTeXTranslator):
43+
number = get_node_number(self, node)
44+
idx = list_rindex(self.body, latex_admonition_start) + 2
45+
self.body.insert(idx, f"{typ.title()} {number}")
46+
self.body.append(latex_admonition_end)
47+
else:
48+
# Find index in list of 'Proof #'
49+
number = get_node_number(self, node)
50+
idx = self.body.index(f"Proof {number} ")
51+
self.body[idx] = f"{typ.title()} {number} "
52+
self.body.append("</div>")
3953

4054

4155
def visit_unenumerable_node(self, node: Node) -> None:
42-
self.body.append(self.starttag(node, "div", CLASS="admonition"))
56+
if isinstance(self, LaTeXTranslator):
57+
docname = find_parent(self.builder.env, node, "section")
58+
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
59+
self.body.append(latex_admonition_start)
60+
else:
61+
self.body.append(self.starttag(node, "div", CLASS="admonition"))
4362

4463

4564
def depart_unenumerable_node(self, node: Node) -> None:
4665
typ = node.attributes.get("type", "")
4766
title = node.attributes.get("title", "")
48-
49-
if title == "":
50-
idx = len(self.body) - self.body[-1::-1].index('<p class="admonition-title">')
67+
if isinstance(self, LaTeXTranslator):
68+
idx = list_rindex(self.body, latex_admonition_start) + 2
69+
self.body.insert(idx, f"{typ.title()}")
70+
self.body.append(latex_admonition_end)
5171
else:
52-
idx = self.body.index(title)
53-
54-
element = f"<span>{typ.title()} </span>"
55-
self.body.insert(idx, element)
56-
self.body.append("</div>")
72+
if title == "":
73+
idx = list_rindex(self.body, '<p class="admonition-title">') + 1
74+
else:
75+
idx = list_rindex(self.body, title)
76+
element = f"<span>{typ.title()} </span>"
77+
self.body.insert(idx, element)
78+
self.body.append("</div>")
5779

5880

5981
def visit_proof_node(self, node: Node) -> None:
@@ -64,8 +86,44 @@ def depart_proof_node(self, node: Node) -> None:
6486
pass
6587

6688

67-
def get_node_number(self: HTMLTranslator, node: Node) -> str:
89+
def get_node_number(self, node: Node) -> str:
90+
"""Get the number for the directive node for HTML."""
6891
key = "proof"
6992
ids = node.attributes.get("ids", [])[0]
70-
number = self.builder.fignumbers.get(key, {}).get(ids, ())
93+
if isinstance(self, LaTeXTranslator):
94+
docname = find_parent(self.builder.env, node, "section")
95+
fignumbers = self.builder.env.toc_fignumbers.get(
96+
docname, {}
97+
) # Latex does not have builder.fignumbers
98+
else:
99+
fignumbers = self.builder.fignumbers
100+
number = fignumbers.get(key, {}).get(ids, ())
71101
return ".".join(map(str, number))
102+
103+
104+
def find_parent(env, node, parent_tag):
105+
"""Find the nearest parent node with the given tagname."""
106+
while True:
107+
node = node.parent
108+
if node is None:
109+
return None
110+
# parent should be a document in toc
111+
if (
112+
"docname" in node.attributes
113+
and env.titles[node.attributes["docname"]].astext().lower()
114+
in node.attributes["names"]
115+
):
116+
return node.attributes["docname"]
117+
118+
if node.tagname == parent_tag:
119+
return node.attributes["docname"]
120+
121+
return None
122+
123+
124+
def list_rindex(li, x) -> int:
125+
"""Getting the last occurence of an item in a list."""
126+
for i in reversed(range(len(li))):
127+
if li[i] == x:
128+
return i
129+
raise ValueError("{} is not in list".format(x))

tests/test_build.py

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
import pytest
33

44

5+
@pytest.mark.sphinx("html", testroot="missingref")
6+
def test_missing_ref(app, warnings):
7+
# Clean build
8+
app.build()
9+
assert "WARNING: label 'foo' not found" in warnings(app)
10+
11+
12+
# Tests for algorithms
513
@pytest.mark.sphinx("html", testroot="mybook")
614
@pytest.mark.parametrize(
715
"idir", ["_algo_labeled_titled_with_classname.html", "_algo_nonumber.html"]
@@ -38,3 +46,28 @@ def test_reference(app, idir, file_regression):
3846
def test_duplicate_label(app, warnings):
3947
app.build()
4048
assert "WARNING: duplicate algorithm label" in warnings(app)
49+
50+
51+
# Tests for Proofs
52+
@pytest.mark.sphinx("html", testroot="mybook")
53+
@pytest.mark.parametrize(
54+
"idir",
55+
[
56+
"_proof_with_classname.html",
57+
"_proof_no_classname.html",
58+
"_proof_with_argument_content.html",
59+
"_proof_with_labeled_math.html",
60+
"_proof_with_unlabeled_math.html",
61+
],
62+
)
63+
def test_proof(app, idir, file_regression):
64+
"""Test proof directive markup."""
65+
app.build()
66+
path_proof_directive = app.outdir / "proof" / idir
67+
assert path_proof_directive.exists()
68+
69+
# get content markup
70+
soup = BeautifulSoup(path_proof_directive.read_text(encoding="utf8"), "html.parser")
71+
72+
proof = soup.select("div.proof")[0]
73+
file_regression.check(str(proof), basename=idir.split(".")[0], extension=".html")

tests/test_algorithm/_algo_labeled_titled_with_classname.html renamed to tests/test_html/_algo_labeled_titled_with_classname.html

File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)