Skip to content

Commit c583ccf

Browse files
authored
👌 IMPROVE: Add comments, bit refactoring, docs, test for latex (#34)
* adding comments and creating common functions * added a simple test for latex * removing unnecessary test latex files
1 parent ee0f065 commit c583ccf

File tree

14 files changed

+382
-64
lines changed

14 files changed

+382
-64
lines changed

docs/source/index.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ testing
1616
**An exercise extension for Sphinx**.
1717

1818
This package contains a [Sphinx](http://www.sphinx-doc.org/en/master/) extension
19-
for producing exercise and solution directives.
20-
21-
```{warning}
22-
sphinx-exercise `0.1.1` is in a development stage and may change rapidly.
23-
```
19+
for producing exercise and solution directives, for html and pdf outputs.
2420

2521
**Features**:
2622

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"pytest-regressions",
2727
"beautifulsoup4",
2828
"myst-nb",
29+
"texsoup",
2930
],
3031
"rtd": [
3132
"sphinx>=3.0",

sphinx_exercise/__init__.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
from docutils import nodes as docutil_nodes
1919
from sphinx.util import logging
2020
from sphinx.util.fileutil import copy_asset
21+
from sphinx.builders.latex import LaTeXBuilder
22+
from sphinx.transforms.post_transforms import SphinxPostTransform
23+
2124
from .directive import ExerciseDirective, SolutionDirective
2225
from .nodes import (
2326
exercise_node,
@@ -31,14 +34,15 @@
3134
depart_solution_node,
3235
is_solution_node,
3336
is_exercise_node,
34-
is_unenumerable_node,
37+
is_exercise_unenumerable_node,
3538
is_extension_node,
3639
NODE_TYPES,
3740
)
38-
from sphinx.transforms.post_transforms import SphinxPostTransform
39-
from .utils import get_node_number, get_refuri, has_math_child
41+
from .utils import get_node_number, get_refuri, has_math_child, find_parent
4042

4143
logger = logging.getLogger(__name__)
44+
45+
# Variables
4246
SOLUTION_PLACEHOLDER = "Solution to "
4347
MATH_PLACEHOLDER = ":math:"
4448

@@ -118,6 +122,9 @@ def doctree_read(app: Sphinx, document: Node) -> None:
118122

119123

120124
def update_title(title):
125+
"""
126+
Does necessary formatting to the title node, and wraps it with an inline node.
127+
"""
121128
inline = docutil_nodes.inline()
122129

123130
if len(title) == 1 and isinstance(title[0], docutil_nodes.Text):
@@ -142,15 +149,21 @@ def update_title(title):
142149

143150

144151
def process_math_placeholder(node, update_title, source_node):
152+
"""Convert the placeholder math text to a math node."""
145153
if MATH_PLACEHOLDER in node.astext():
146154
title = update_title(source_node[0])
147155
return node.replace(node[0], title)
148156

149157

150158
def process_reference(self, node, default_title=""):
159+
"""
160+
Processing reference nodes in the document to facilitate the design and the
161+
functionality requirements.
162+
"""
151163
label = get_refuri(node)
152164
if label in self.env.exercise_list:
153165
source_node = self.env.exercise_list[label].get("node")
166+
# if reference source is a solution node
154167
if is_solution_node(source_node):
155168
target_label = source_node.attributes.get("target_label", "")
156169
if node.astext().strip() == "Solution to":
@@ -159,15 +172,16 @@ def process_reference(self, node, default_title=""):
159172
target_label = source_node.attributes.get("label", "")
160173
target_attr = self.env.exercise_list[target_label]
161174
target_node = target_attr.get("node", Node)
175+
# if reference target is exercise node
162176
if is_exercise_node(target_node):
163177
if default_title:
164178
number = get_node_number(self.app, target_node, "exercise")
165179
node.insert(len(node[0]), docutil_nodes.Text(" Exercise " + number))
166180
return
167181
else:
168182
node = process_math_placeholder(node, update_title, source_node)
169-
170-
if is_unenumerable_node(target_node):
183+
# if reference target is an exercise unenumerable node
184+
if is_exercise_unenumerable_node(target_node):
171185
if default_title:
172186
if target_attr.get("title"):
173187
if has_math_child(target_node[0]):
@@ -186,7 +200,7 @@ def process_reference(self, node, default_title=""):
186200

187201

188202
class ReferenceTransform(SphinxPostTransform):
189-
default_priority = 998
203+
default_priority = 998 # should be processed before processing solution nodes
190204

191205
def run(self):
192206

@@ -195,7 +209,7 @@ def run(self):
195209

196210

197211
class SolutionTransorm(SphinxPostTransform):
198-
default_priority = 999
212+
default_priority = 999 # should be after processing reference nodes
199213

200214
def run(self):
201215

@@ -205,7 +219,11 @@ def run(self):
205219
target_attr = self.env.exercise_list[target_labelid]
206220
except Exception:
207221
# target_labelid not found
208-
docpath = self.env.doc2path(self.app.builder.current_docname)
222+
if isinstance(self.app.builder, LaTeXBuilder):
223+
docname = find_parent(self.app.builder.env, node, "section")
224+
else:
225+
docname = self.app.builder.current_docname
226+
docpath = self.env.doc2path(docname)
209227
path = docpath[: docpath.rfind(".")]
210228
msg = f"undefined label: {target_labelid}"
211229
logger.warning(msg, location=path, color="red")
@@ -250,6 +268,7 @@ def run(self):
250268
source_node = source_attr.get("node", Node)
251269
node_title = node.get("title", "")
252270

271+
# processing for nodes which have
253272
if "{name}" in node_title and has_math_child(source_node[0]):
254273
newtitle = docutil_nodes.inline()
255274
for item in node_title.split():
@@ -278,11 +297,11 @@ def setup(app: Sphinx) -> Dict[str, Any]:
278297
app.add_config_value("hide_solutions", False, "env")
279298

280299
app.add_css_file("exercise.css")
281-
app.connect("config-inited", init_numfig) # 1
282-
app.connect("env-purge-doc", purge_exercises) # 5 per file
283-
app.connect("doctree-read", doctree_read) # 8
284-
app.connect("env-merge-info", merge_exercises) # 9
285-
app.connect("build-finished", copy_asset_files) # 16
300+
app.connect("config-inited", init_numfig) # event order - 1
301+
app.connect("env-purge-doc", purge_exercises) # event order - 5 per file
302+
app.connect("doctree-read", doctree_read) # event order - 8
303+
app.connect("env-merge-info", merge_exercises) # event order - 9
304+
app.connect("build-finished", copy_asset_files) # event order - 16
286305

287306
app.add_enumerable_node(
288307
exercise_node,

sphinx_exercise/directive.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def run(self) -> List[Node]:
3939
if class_name:
4040
classes.extend(class_name)
4141

42-
title_text = ""
42+
# Have a dummy title text if no title specified, as 'std' domain needs
43+
# a title to process it as enumerable node.
4344
if typ == "exercise":
4445
title_text = f"{self.name.title()} "
4546

@@ -49,11 +50,7 @@ def run(self) -> List[Node]:
4950
title_text = f"{self.name.title()} to "
5051
target_label = self.arguments[0]
5152

52-
textnodes, messages = self.state.inline_text(title_text, self.lineno)
53-
54-
section = nodes.section(ids=[f"{typ}-content"])
55-
self.state.nested_parse(self.content, self.content_offset, section)
56-
53+
# selecting the type of node
5754
if typ == "exercise":
5855
if "nonumber" in self.options:
5956
node = exercise_unenumerable_node()
@@ -62,6 +59,11 @@ def run(self) -> List[Node]:
6259
else:
6360
node = solution_node()
6461

62+
# state parsing
63+
section = nodes.section(ids=[f"{typ}-content"])
64+
textnodes, messages = self.state.inline_text(title_text, self.lineno)
65+
self.state.nested_parse(self.content, self.content_offset, section)
66+
6567
node += nodes.title(title_text, "", *textnodes)
6668
node += section
6769

sphinx_exercise/nodes.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sphinx.util import logging
1212
from docutils import nodes as docutil_nodes
1313
from sphinx.writers.latex import LaTeXTranslator
14-
from .utils import get_node_number, find_parent
14+
from .utils import get_node_number, find_parent, list_rindex
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -32,51 +32,67 @@ class exercise_unenumerable_node(docutil_nodes.Admonition, docutil_nodes.Element
3232
pass
3333

3434

35+
def _visit_nodes_latex(self, node, find_parent):
36+
""" Function to handle visit_node for latex. """
37+
docname = find_parent(self.builder.env, node, "section")
38+
self.body.append(
39+
"\\phantomsection \\label{" + f"{docname}:{node.attributes['label']}" + "}"
40+
)
41+
self.body.append(latex_admonition_start)
42+
43+
44+
def _depart_nodes_latex(self, node, title, pop_index=False):
45+
""" Function to handle depart_node for latex. """
46+
idx = list_rindex(self.body, latex_admonition_start) + 2
47+
if pop_index:
48+
self.body.pop(idx)
49+
self.body.insert(idx, title)
50+
self.body.append(latex_admonition_end)
51+
52+
53+
def _remove_placeholder_title_exercise(typ, node):
54+
""" Removing the exercise placeholder we put in title earlier."""
55+
for title in node.traverse(docutil_nodes.title):
56+
if typ.title() in title.astext():
57+
title[0] = docutil_nodes.Text("")
58+
59+
3560
def visit_enumerable_node(self, node: Node) -> None:
61+
typ = node.attributes.get("type", "")
3662
if isinstance(self, LaTeXTranslator):
37-
docname = find_parent(self.builder.env, node, "section")
38-
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
39-
self.body.append(latex_admonition_start)
63+
_remove_placeholder_title_exercise(typ, node)
64+
_visit_nodes_latex(self, node, find_parent)
4065
else:
41-
for title in node.traverse(docutil_nodes.title):
42-
if "Exercise" in title.astext():
43-
title[0] = docutil_nodes.Text("")
66+
_remove_placeholder_title_exercise(typ, node)
4467
self.body.append(self.starttag(node, "div", CLASS="admonition"))
4568

4669

4770
def depart_enumerable_node(self, node: Node) -> None:
4871
typ = node.attributes.get("type", "")
4972
if isinstance(self, LaTeXTranslator):
5073
number = get_node_number(self, node, typ)
51-
idx = list_rindex(self.body, latex_admonition_start) + 2
52-
self.body.insert(idx, f"{typ.title()} {number} ")
53-
self.body.append(latex_admonition_end)
74+
_depart_nodes_latex(self, node, f"{typ.title()} {number} ")
5475
else:
5576
number = get_node_number(self, node, typ)
56-
if number:
57-
idx = self.body.index(f"{typ.title()} {number} ")
58-
self.body[idx] = f"{typ.title()} {number} "
77+
idx = list_rindex(self.body, f"{typ.title()} {number} ")
78+
self.body[idx] = f"{typ.title()} {number} "
5979
self.body.append("</div>")
6080

6181

6282
def visit_exercise_unenumerable_node(self, node: Node) -> None:
83+
typ = node.attributes.get("type", "")
6384
if isinstance(self, LaTeXTranslator):
64-
docname = find_parent(self.builder.env, node, "section")
65-
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
66-
self.body.append(latex_admonition_start)
85+
_remove_placeholder_title_exercise(typ, node)
86+
_visit_nodes_latex(self, node, find_parent)
6787
else:
68-
for title in node.traverse(docutil_nodes.title):
69-
if "Exercise" in title.astext():
70-
title[0] = docutil_nodes.Text("")
88+
_remove_placeholder_title_exercise(typ, node)
7189
self.body.append(self.starttag(node, "div", CLASS="admonition"))
7290

7391

7492
def depart_exercise_unenumerable_node(self, node: Node) -> None:
7593
typ = node.attributes.get("type", "")
7694
if isinstance(self, LaTeXTranslator):
77-
idx = list_rindex(self.body, latex_admonition_start) + 2
78-
self.body.insert(idx, f"{typ.title()} ")
79-
self.body.append(latex_admonition_end)
95+
_depart_nodes_latex(self, node, f"{typ.title()} ")
8096
else:
8197
idx = list_rindex(self.body, '<p class="admonition-title">') + 1
8298
element = f"<span>{typ.title()} </span>"
@@ -86,23 +102,18 @@ def depart_exercise_unenumerable_node(self, node: Node) -> None:
86102

87103
def visit_solution_node(self, node: Node) -> None:
88104
if isinstance(self, LaTeXTranslator):
89-
docname = find_parent(self.builder.env, node, "section")
90-
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
91-
self.body.append(latex_admonition_start)
105+
_visit_nodes_latex(self, node, find_parent)
92106
else:
93107
self.body.append(self.starttag(node, "div", CLASS="admonition"))
94108

95109

96110
def depart_solution_node(self, node: Node) -> None:
97111
typ = node.attributes.get("type", "")
98112
if isinstance(self, LaTeXTranslator):
99-
idx = list_rindex(self.body, latex_admonition_start) + 2
100-
self.body.pop(idx)
101-
self.body.insert(idx, f"{typ.title()} ")
102-
self.body.append(latex_admonition_end)
113+
_depart_nodes_latex(self, node, f"{typ.title()} to ", True)
103114
else:
104115
number = get_node_number(self, node, typ)
105-
idx = self.body.index(f"{typ.title()} {number} ")
116+
idx = list_rindex(self.body, f"{typ.title()} {number} ")
106117
self.body.pop(idx)
107118
self.body.append("</div>")
108119

@@ -111,7 +122,7 @@ def is_exercise_node(node):
111122
return isinstance(node, exercise_node)
112123

113124

114-
def is_unenumerable_node(node):
125+
def is_exercise_unenumerable_node(node):
115126
return isinstance(node, exercise_unenumerable_node)
116127

117128

@@ -121,7 +132,9 @@ def is_solution_node(node):
121132

122133
def is_extension_node(node):
123134
return (
124-
is_exercise_node(node) or is_unenumerable_node(node) or is_solution_node(node)
135+
is_exercise_node(node)
136+
or is_exercise_unenumerable_node(node)
137+
or is_solution_node(node)
125138
)
126139

127140

@@ -131,14 +144,6 @@ def rreplace(s, old, new, occurrence):
131144
return new.join(li)
132145

133146

134-
def list_rindex(li, x) -> int:
135-
"""Getting the last occurence of an item in a list."""
136-
for i in reversed(range(len(li))):
137-
if li[i] == x:
138-
return i
139-
raise ValueError("{} is not in list".format(x))
140-
141-
142147
NODE_TYPES = {
143148
"exercise": {"node": exercise_node, "type": "exercise"},
144149
"solution": {"node": solution_node, "type": "solution"},

sphinx_exercise/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Utility functions
12
from sphinx.writers.latex import LaTeXTranslator
23
from docutils import nodes as docutil_nodes
34

@@ -53,3 +54,11 @@ def get_refuri(node):
5354
id_ = node.get("refid", "")
5455

5556
return id_.split("#")[-1]
57+
58+
59+
def list_rindex(li, x) -> int:
60+
"""Getting the last occurence of an item in a list."""
61+
for i in reversed(range(len(li))):
62+
if li[i] == x:
63+
return i
64+
raise ValueError("{} is not in list".format(x))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal makefile for Sphinx documentation
2+
#
3+
4+
# You can set these variables from the command line, and also
5+
# from the environment for the first two.
6+
SPHINXOPTS ?=
7+
SPHINXBUILD ?= sphinx-build
8+
SOURCEDIR = .
9+
BUILDDIR = build
10+
11+
# Put it first so that "make" without argument is like "make help".
12+
help:
13+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14+
15+
.PHONY: help Makefile
16+
17+
# Catch-all target: route all unknown targets to Sphinx using the new
18+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19+
%: Makefile
20+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

0 commit comments

Comments
 (0)