Skip to content

Commit 78bf69b

Browse files
authored
Merge pull request #27 from executablebooks/add-hide-option
✨ NEW: Add hide option and configuration key
2 parents f37722f + 6a8c8a9 commit 78bf69b

File tree

16 files changed

+325
-96
lines changed

16 files changed

+325
-96
lines changed

docs/source/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ sphinx-exercise `0.1.1` is in a development stage and may change rapidly.
2727
The **exercise** directive is
2828

2929
1. automatically numbered
30-
2. supports options such as `class`, `label`, and `nonumber`
30+
2. supports options such as `class`, `label`, `nonumber`, and `hidden`
3131
3. can easily be referenced through `ref` and `numref` roles
3232

3333
The **solution** directive
3434

35-
1. supports options such as `class` and `label`
35+
1. supports options such as `class`, `label`, and `hidden`
3636
2. can be referenced through `ref` role
3737

3838
(getting-started)=

docs/source/syntax.md

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ An exercise directive can be included using the `exercise` pattern. The directiv
1818
* `nonumber` : flag (empty)
1919

2020
Turns off exercise auto numbering.
21+
* `hidden` : flag (empty)
22+
23+
Removes the directive from the final output.
2124

2225
**Example**
2326

@@ -76,6 +79,9 @@ A solution directive can be included using the `solution` pattern. It takes in t
7679
* `class` : text
7780

7881
Value of the solution’s class attribute which can be used to add custom CSS or JavaScript.
82+
* `hidden` : flag (empty)
83+
84+
Removes the directive from the final output.
7985

8086
```{note}
8187
The title of the solution directive links directly to the referred directive.
@@ -184,20 +190,31 @@ static int factorial(int n){
184190
````
185191

186192

187-
## How to Hide Directives
193+
## Hide or Remove Directives
188194

189-
Directives can be hidden using the `dropdown` class which is available through [Sphinx Book Theme](https://sphinx-book-theme.readthedocs.io/en/latest/index.html). For Sphinx projects, add `"sphinx_book_theme"` to your `html_theme` in the `conf.py` to activate the theme in your Sphinx configuration
195+
### Hide Content
190196

191-
```md
192-
...
193-
html_theme = "sphinx_book_theme"
194-
...
197+
The content of directives can be hidden using the `dropdown` class which is available through [sphinx-togglebutton](https://sphinx-togglebutton.readthedocs.io/en/latest/). For Sphinx projects, add `"sphinx_togglebutton"` to your `extensions` list in `conf.py` to activate the extension
198+
199+
```python
200+
extensions = [
201+
...
202+
"sphinx_togglebutton"
203+
...
204+
]
195205
```
196206

197-
Jupyter Book's default theme is Sphinx Book Theme; therefore, Jupyter Book projects can utilize `dropdown` without having to activate the theme in your Sphinx configuration.
207+
For Jupyter Book projects, add `sphinx_togglebutton` under `extra_extensions`
198208

209+
```yaml
210+
sphinx:
211+
extra_extensions:
212+
- sphinx_togglebutton
213+
```
199214
200-
To hide the directive, simply add `:class: dropdown` as a directive option.
215+
To hide the content, simply add `:class: dropdown` as a directive option.
216+
217+
For more use cases see [sphinx-togglebutton](https://sphinx-togglebutton.readthedocs.io/en/latest/#usage).
201218

202219
**Example**
203220

@@ -231,6 +248,36 @@ for any positive integer $n$.
231248
```
232249
````
233250

251+
### Remove Directives
252+
253+
Any specific directive can be hidden by introducing the `:hidden:` option. For example, the following example will not be displayed
254+
255+
````md
256+
```{exercise}
257+
:hidden:
258+
259+
This is a hidden exercise directive.
260+
```
261+
````
262+
263+
```{exercise}
264+
:hidden:
265+
266+
This is a hidden exercise directive.
267+
```
268+
269+
### Remove All Solutions
270+
271+
All solution directives can be removed from the final output by setting `hide_solutions` to `True`. For Sphinx projects, add the configuration key in the `conf.py` file. Jupyter Book projects, should set the configuration key in `_config.yml` as follows
272+
273+
```yaml
274+
...
275+
sphinx:
276+
config:
277+
hide_solutions: True
278+
...
279+
```
280+
234281
## Custom CSS or JavaScript
235282

236283
Custom JavaScript scripts and CSS rules will allow you to add additional functionality or customize how elements are displayed. If you'd like to include custom CSS or JavaScript scripts in Jupyter Book, simply add any files ending in `.css` or `.js` under a `_static` folder. Any files under this folder will be automatically copied into the built book.

sphinx_exercise/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ def process(self, doctree: nodes.document, docname: str) -> None:
340340

341341
def setup(app: Sphinx) -> Dict[str, Any]:
342342

343+
app.add_config_value("hide_solutions", False, "env")
344+
343345
app.add_css_file("exercise.css")
344346
app.connect("build-finished", copy_asset_files)
345347
app.connect("config-inited", init_numfig)

sphinx_exercise/directive.py

Lines changed: 51 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,48 @@
1919
logger = logging.getLogger(__name__)
2020

2121

22-
class ExerciseDirective(SphinxDirective):
23-
""" A custom Sphinx Directive """
22+
class CustomDirective(SphinxDirective):
23+
""" A custom Sphinx directive """
2424

25-
name = "exercise"
26-
has_content = True
27-
required_arguments = 0
28-
optional_arguments = 1
29-
final_argument_whitespace = True
30-
option_spec = {
31-
"label": directives.unchanged_required,
32-
"class": directives.class_option,
33-
"nonumber": directives.flag,
34-
}
25+
name = ""
3526

3627
def run(self) -> List[Node]:
28+
if self.name == "solution" and self.env.app.config.hide_solutions:
29+
return []
30+
3731
serial_no = self.env.new_serialno()
3832

3933
if not hasattr(self.env, "exercise_list"):
4034
self.env.exercise_list = {}
4135

42-
# Take care of class option
4336
classes, class_name = [self.name], self.options.get("class", "")
4437
if class_name:
4538
classes.extend(class_name)
4639

47-
title_text, _ = "", ""
48-
if "nonumber" in self.options:
49-
title_text = f"{self.name.title()} "
40+
title_text, title = "", ""
41+
if self.name == "exercise":
42+
if "nonumber" in self.options:
43+
title_text = f"{self.name.title()} "
5044

51-
if self.arguments != []:
52-
title_text += f"({self.arguments[0]})"
53-
_ += self.arguments[0]
45+
if self.arguments != []:
46+
title_text += f"({self.arguments[0]})"
47+
title += self.arguments[0]
48+
else:
49+
title_text = f"{self.name.title()} to "
50+
target_label = self.arguments[0]
5451

5552
textnodes, messages = self.state.inline_text(title_text, self.lineno)
5653

5754
section = nodes.section(ids=[f"{self.name}-content"])
5855
self.state.nested_parse(self.content, self.content_offset, section)
5956

60-
if "nonumber" in self.options:
61-
node = unenumerable_node()
57+
if self.name == "exercise":
58+
if "nonumber" in self.options:
59+
node = unenumerable_node()
60+
else:
61+
node = enumerable_node()
6262
else:
63-
node = enumerable_node()
63+
node = linked_node()
6464

6565
node += nodes.title(title_text, "", *textnodes)
6666
node += section
@@ -88,21 +88,46 @@ def run(self) -> List[Node]:
8888
node["ids"].append(label)
8989
node["label"] = label
9090
node["docname"] = self.env.docname
91+
node["hidden"] = True if "hidden" in self.options else False
9192
node.document = self.state.document
9293

94+
if self.name == "solution":
95+
node["target_label"] = target_label
96+
9397
self.add_name(node)
9498

9599
self.env.exercise_list[label] = {
96100
"type": self.name,
97101
"docname": self.env.docname,
98102
"node": node,
99-
"title": _,
103+
"title": title,
104+
"hidden": node.get("hidden", bool),
100105
}
106+
107+
if node.get("hidden", bool):
108+
return []
109+
101110
return [node]
102111

103112

104-
class SolutionDirective(SphinxDirective):
105-
""" A custom Sphinx Directive """
113+
class ExerciseDirective(CustomDirective):
114+
""" A custom exercise directive """
115+
116+
name = "exercise"
117+
has_content = True
118+
required_arguments = 0
119+
optional_arguments = 1
120+
final_argument_whitespace = True
121+
option_spec = {
122+
"label": directives.unchanged_required,
123+
"class": directives.class_option,
124+
"nonumber": directives.flag,
125+
"hidden": directives.flag,
126+
}
127+
128+
129+
class SolutionDirective(CustomDirective):
130+
""" A custom solution directive """
106131

107132
name = "solution"
108133
has_content = True
@@ -112,65 +137,5 @@ class SolutionDirective(SphinxDirective):
112137
option_spec = {
113138
"label": directives.unchanged_required,
114139
"class": directives.class_option,
140+
"hidden": directives.flag,
115141
}
116-
title_text = f"{name.title()} to "
117-
118-
def run(self):
119-
serial_no = self.env.new_serialno()
120-
121-
if not hasattr(self.env, "exercise_list"):
122-
self.env.exercise_list = {}
123-
124-
# Take care of class option
125-
classes, class_name = [self.name], self.options.get("class", [])
126-
if class_name:
127-
classes.extend(class_name)
128-
129-
target_label = self.arguments[0]
130-
131-
textnodes, messages = self.state.inline_text(self.title_text, self.lineno)
132-
133-
section = nodes.section(ids=[f"{self.name}-content"])
134-
self.state.nested_parse(self.content, self.content_offset, section)
135-
136-
node = linked_node()
137-
node.document = self.state.document
138-
node += nodes.title(self.title_text, "", *textnodes)
139-
node += section
140-
141-
label = self.options.get("label", "")
142-
if label:
143-
self.options["noindex"] = False
144-
else:
145-
self.options["noindex"] = True
146-
label = f"{self.env.docname}-{self.name}-{serial_no}"
147-
148-
# Duplicate label warning
149-
if not label == "" and label in self.env.exercise_list.keys():
150-
docpath = self.env.doc2path(self.env.docname)
151-
path = docpath[: docpath.rfind(".")]
152-
other_path = self.env.doc2path(self.env.exercise_list[label]["docname"])
153-
msg = f"duplicate label: {label}; other instance in {other_path}"
154-
logger.warning(msg, location=path, color="red")
155-
return []
156-
157-
self.options["name"] = label
158-
159-
# Set node attributes
160-
node["classes"].extend(classes)
161-
node["ids"].append(label)
162-
node["label"] = label
163-
node["docname"] = self.env.docname
164-
node["target_label"] = target_label
165-
node.document = self.state.document
166-
167-
self.add_name(node)
168-
169-
self.env.exercise_list[label] = {
170-
"type": self.name,
171-
"docname": self.env.docname,
172-
"node": node,
173-
"title": "",
174-
}
175-
176-
return [node]
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)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
_enum_duplicatelabel_hidden
2+
===========================
3+
4+
.. exercise:: duplicate directive 1
5+
:label: ex-hidden-number
6+
:hidden:
7+
8+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
_enum_hidden
2+
============
3+
4+
.. exercise:: duplicate directive 1
5+
:label: ex-hidden-number
6+
:hidden:
7+
8+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
_linked_enum_hidden
2+
===================
3+
4+
5+
.. solution:: ex-hidden-number
6+
:label: solution-hidden-label
7+
:hidden:
8+
9+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
_linked_unenum_hidden
2+
=====================
3+
4+
5+
.. solution:: unenum-hidden
6+
:label: solution-unenum-label
7+
:hidden:
8+
9+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
_unenum_hidden
2+
==============
3+
4+
.. exercise:: Excepteur sint occaecat
5+
:label: unenum-hidden
6+
:nonumber:
7+
:hidden:
8+
9+
10+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

0 commit comments

Comments
 (0)