munchboka-edutools 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of munchboka-edutools might be problematic. Click here for more details.
- munchboka_edutools/__init__.py +184 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +389 -0
- munchboka_edutools/directives/cas_popup.py +428 -0
- munchboka_edutools/directives/clear.py +103 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/escape_room2.py +318 -0
- munchboka_edutools/directives/factor_tree.py +552 -0
- munchboka_edutools/directives/flashcards.py +233 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +308 -0
- munchboka_edutools/directives/horner.py +326 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/jeopardy2.py +636 -0
- munchboka_edutools/directives/multi_plot.py +2524 -0
- munchboka_edutools/directives/multi_plot2.py +252 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3758 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +346 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/quiz2.py +453 -0
- munchboka_edutools/directives/signchart.py +519 -0
- munchboka_edutools/directives/signchart2.py +1545 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +321 -0
- munchboka_edutools/static/css/flashcards.css +219 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +147 -0
- munchboka_edutools/static/css/github-light.css +155 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +553 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/popup.css +115 -0
- munchboka_edutools/static/css/quiz.css +377 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/flashcards.js +199 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +648 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +441 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/jeopardy.js +560 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/popup.js +85 -0
- munchboka_edutools/static/js/quiz.js +566 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.2.3.dist-info/METADATA +109 -0
- munchboka_edutools-0.2.3.dist-info/RECORD +157 -0
- munchboka_edutools-0.2.3.dist-info/WHEEL +4 -0
- munchboka_edutools-0.2.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
multi-plot2 directive: A container directive that arranges multiple plot directives in a grid.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
::::{multi-plot2}
|
|
6
|
+
---
|
|
7
|
+
rows: 2
|
|
8
|
+
cols: 2
|
|
9
|
+
width: 100%
|
|
10
|
+
align: center
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
:::{plot}
|
|
14
|
+
function: x**2, f
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
:::{plot}
|
|
18
|
+
function: x**3, g
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
::::
|
|
22
|
+
|
|
23
|
+
The directive creates a grid layout and renders each nested plot directive in order (row-major).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any, Dict, List
|
|
28
|
+
|
|
29
|
+
from docutils import nodes
|
|
30
|
+
from docutils.parsers.rst import directives
|
|
31
|
+
from sphinx.application import Sphinx
|
|
32
|
+
from sphinx.util.docutils import SphinxDirective
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MultiPlot2Directive(SphinxDirective):
|
|
36
|
+
"""Container directive that arranges multiple plot directives in a grid layout."""
|
|
37
|
+
|
|
38
|
+
has_content = True
|
|
39
|
+
required_arguments = 0
|
|
40
|
+
option_spec = {
|
|
41
|
+
# Grid layout options
|
|
42
|
+
"rows": directives.positive_int,
|
|
43
|
+
"cols": directives.positive_int,
|
|
44
|
+
"width": directives.length_or_percentage_or_unitless,
|
|
45
|
+
"align": lambda a: directives.choice(a, ["left", "center", "right"]),
|
|
46
|
+
"class": directives.class_option,
|
|
47
|
+
"name": directives.unchanged,
|
|
48
|
+
# Plot directive options that cascade to children
|
|
49
|
+
"xmin": directives.unchanged,
|
|
50
|
+
"xmax": directives.unchanged,
|
|
51
|
+
"ymin": directives.unchanged,
|
|
52
|
+
"ymax": directives.unchanged,
|
|
53
|
+
"xstep": directives.unchanged,
|
|
54
|
+
"ystep": directives.unchanged,
|
|
55
|
+
"fontsize": directives.unchanged,
|
|
56
|
+
"ticks": directives.unchanged,
|
|
57
|
+
"grid": directives.unchanged,
|
|
58
|
+
"xticks": directives.unchanged,
|
|
59
|
+
"yticks": directives.unchanged,
|
|
60
|
+
"lw": directives.unchanged,
|
|
61
|
+
"alpha": directives.unchanged,
|
|
62
|
+
"figsize": directives.unchanged,
|
|
63
|
+
"xlabel": directives.unchanged,
|
|
64
|
+
"ylabel": directives.unchanged,
|
|
65
|
+
"usetex": directives.unchanged,
|
|
66
|
+
"axis": directives.unchanged,
|
|
67
|
+
"alt": directives.unchanged,
|
|
68
|
+
"nocache": directives.flag,
|
|
69
|
+
"debug": directives.flag,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def run(self) -> List[nodes.Node]:
|
|
73
|
+
"""Process the multi-plot2 directive."""
|
|
74
|
+
|
|
75
|
+
# Grid layout options
|
|
76
|
+
grid_options = {"rows", "cols", "width", "align", "class", "name"}
|
|
77
|
+
|
|
78
|
+
# Get options with defaults
|
|
79
|
+
rows = self.options.get("rows", 1)
|
|
80
|
+
cols = self.options.get("cols", 1)
|
|
81
|
+
width = self.options.get("width", "100%")
|
|
82
|
+
align = self.options.get("align", "center")
|
|
83
|
+
|
|
84
|
+
# Expected number of plot directives
|
|
85
|
+
expected_plots = rows * cols
|
|
86
|
+
|
|
87
|
+
# Collect plot options to cascade to children (exclude grid layout options)
|
|
88
|
+
plot_defaults = {k: v for k, v in self.options.items() if k not in grid_options}
|
|
89
|
+
|
|
90
|
+
# Inject default options into child plot directives
|
|
91
|
+
modified_content = self._inject_plot_defaults(self.content, plot_defaults)
|
|
92
|
+
|
|
93
|
+
# Simply let Sphinx parse the nested content normally
|
|
94
|
+
# This will create all the plot directive nodes
|
|
95
|
+
container = nodes.container()
|
|
96
|
+
container["classes"].append("multi-plot2-container")
|
|
97
|
+
|
|
98
|
+
# Add custom CSS class if provided
|
|
99
|
+
if "class" in self.options:
|
|
100
|
+
container["classes"].extend(self.options["class"])
|
|
101
|
+
|
|
102
|
+
# Parse all nested content with injected defaults
|
|
103
|
+
self.state.nested_parse(modified_content, self.content_offset, container)
|
|
104
|
+
|
|
105
|
+
# Count how many plot figures we got
|
|
106
|
+
plot_count = sum(1 for child in container.traverse(nodes.figure))
|
|
107
|
+
|
|
108
|
+
# Warn if count doesn't match
|
|
109
|
+
if plot_count != expected_plots:
|
|
110
|
+
warning = self.state_machine.reporter.warning(
|
|
111
|
+
f"multi-plot2: Expected {expected_plots} plot directives "
|
|
112
|
+
f"({rows}×{cols}), but found {plot_count}",
|
|
113
|
+
line=self.lineno,
|
|
114
|
+
)
|
|
115
|
+
return [warning, container]
|
|
116
|
+
|
|
117
|
+
# Wrap the container in a div with CSS grid styling
|
|
118
|
+
grid_style = (
|
|
119
|
+
f"display: grid; "
|
|
120
|
+
f"grid-template-columns: repeat({cols}, 1fr); "
|
|
121
|
+
f"grid-template-rows: repeat({rows}, auto); "
|
|
122
|
+
f"gap: 0; "
|
|
123
|
+
f"width: {width}; "
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if align == "center":
|
|
127
|
+
grid_style += "margin-left: auto; margin-right: auto; "
|
|
128
|
+
elif align == "left":
|
|
129
|
+
grid_style += "margin-right: auto; "
|
|
130
|
+
elif align == "right":
|
|
131
|
+
grid_style += "margin-left: auto; "
|
|
132
|
+
|
|
133
|
+
# Create wrapper with inline styles
|
|
134
|
+
wrapper = nodes.container()
|
|
135
|
+
wrapper["classes"].append("multi-plot2-grid")
|
|
136
|
+
|
|
137
|
+
# Add CSS to constrain child figures/SVGs to grid cells
|
|
138
|
+
# This ensures plots without explicit width fit properly
|
|
139
|
+
css_node = nodes.raw(
|
|
140
|
+
"",
|
|
141
|
+
"""<style>
|
|
142
|
+
.multi-plot2-grid { }
|
|
143
|
+
.multi-plot2-grid > * { max-width: 100%; }
|
|
144
|
+
.multi-plot2-grid figure { margin: 0; max-width: 100%; }
|
|
145
|
+
.multi-plot2-grid svg { max-width: 100%; height: auto; }
|
|
146
|
+
</style>""",
|
|
147
|
+
format="html",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Add raw HTML opening
|
|
151
|
+
raw_open = nodes.raw(
|
|
152
|
+
"", f'<div class="multi-plot2-grid" style="{grid_style}">', format="html"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Add raw HTML closing
|
|
156
|
+
raw_close = nodes.raw("", "</div>", format="html")
|
|
157
|
+
|
|
158
|
+
# Build result: open tag, content, close tag
|
|
159
|
+
result_container = nodes.container()
|
|
160
|
+
result_container.append(css_node)
|
|
161
|
+
result_container.append(raw_open)
|
|
162
|
+
result_container.extend(container.children)
|
|
163
|
+
result_container.append(raw_close)
|
|
164
|
+
|
|
165
|
+
return [result_container]
|
|
166
|
+
|
|
167
|
+
def _inject_plot_defaults(self, content, plot_defaults):
|
|
168
|
+
"""
|
|
169
|
+
Inject default plot options into child plot directives.
|
|
170
|
+
|
|
171
|
+
This method parses the content to find plot directives and adds
|
|
172
|
+
default options to them, but only if those options are not already
|
|
173
|
+
specified in the individual plot directive.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
content: StringList of directive content
|
|
177
|
+
plot_defaults: Dict of default options to inject
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Modified StringList with injected defaults
|
|
181
|
+
"""
|
|
182
|
+
from docutils.statemachine import StringList
|
|
183
|
+
|
|
184
|
+
if not plot_defaults:
|
|
185
|
+
return content
|
|
186
|
+
|
|
187
|
+
new_lines = []
|
|
188
|
+
i = 0
|
|
189
|
+
while i < len(content):
|
|
190
|
+
line = content[i]
|
|
191
|
+
|
|
192
|
+
# Check if this line starts a plot directive
|
|
193
|
+
if re.match(r"^\s*:::\{plot\}\s*$", line):
|
|
194
|
+
new_lines.append(line)
|
|
195
|
+
i += 1
|
|
196
|
+
|
|
197
|
+
# Collect existing options in this plot directive
|
|
198
|
+
existing_options = set()
|
|
199
|
+
plot_content_start = i
|
|
200
|
+
|
|
201
|
+
# Scan ahead to find what options are already defined
|
|
202
|
+
while i < len(content):
|
|
203
|
+
next_line = content[i]
|
|
204
|
+
# Stop if we hit the closing ::: or another directive
|
|
205
|
+
if re.match(r"^\s*:::\s*$", next_line) or re.match(r"^\s*:::\{", next_line):
|
|
206
|
+
break
|
|
207
|
+
# Extract option name from lines like "xmin: -4" or "function: x**2"
|
|
208
|
+
option_match = re.match(r"^\s*(\w+):\s*", next_line)
|
|
209
|
+
if option_match:
|
|
210
|
+
existing_options.add(option_match.group(1))
|
|
211
|
+
i += 1
|
|
212
|
+
|
|
213
|
+
# Now inject defaults that aren't already present
|
|
214
|
+
# We need to insert them right after the directive opening
|
|
215
|
+
defaults_to_inject = []
|
|
216
|
+
for key, value in plot_defaults.items():
|
|
217
|
+
if key not in existing_options:
|
|
218
|
+
# Format the option line based on value type
|
|
219
|
+
if isinstance(value, bool):
|
|
220
|
+
# For flags like nocache, debug
|
|
221
|
+
continue # Flags are handled differently, skip for now
|
|
222
|
+
else:
|
|
223
|
+
defaults_to_inject.append(f"{key}: {value}")
|
|
224
|
+
|
|
225
|
+
# Insert the defaults at the beginning of plot content
|
|
226
|
+
for default_line in defaults_to_inject:
|
|
227
|
+
new_lines.append(default_line)
|
|
228
|
+
|
|
229
|
+
# Now add the original plot content
|
|
230
|
+
for j in range(plot_content_start, i):
|
|
231
|
+
new_lines.append(content[j])
|
|
232
|
+
else:
|
|
233
|
+
new_lines.append(line)
|
|
234
|
+
i += 1
|
|
235
|
+
|
|
236
|
+
# Convert back to StringList with proper source tracking
|
|
237
|
+
result = StringList()
|
|
238
|
+
for line in new_lines:
|
|
239
|
+
result.append(line, source="<multi-plot2>")
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def setup(app: Sphinx) -> Dict[str, Any]:
|
|
245
|
+
"""Register the multi-plot2 directive."""
|
|
246
|
+
app.add_directive("multi-plot2", MultiPlot2Directive)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"version": "0.1",
|
|
250
|
+
"parallel_read_safe": True,
|
|
251
|
+
"parallel_write_safe": True,
|
|
252
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pair Puzzle Directive
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
A Sphinx directive for creating interactive pairing puzzles where users drag and drop
|
|
6
|
+
items to match pairs. Uses KaTeX for math rendering.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
.. pair-puzzle::
|
|
10
|
+
:class: optional-css-class
|
|
11
|
+
|
|
12
|
+
Left item 1 : Right item 1
|
|
13
|
+
Left item 2 : Right item 2
|
|
14
|
+
Left item 3 : Right item 3
|
|
15
|
+
|
|
16
|
+
The directive accepts content as pairs separated by ':'. Each line represents a pair
|
|
17
|
+
that the user must match. Items can contain HTML, LaTeX math (using $ or $$), or code blocks.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
.. pair-puzzle::
|
|
21
|
+
|
|
22
|
+
$x^2 + 2x + 1$ : $(x+1)^2$
|
|
23
|
+
$\\sin^2(x) + \\cos^2(x)$ : $1$
|
|
24
|
+
<code>print("hello")</code> : Python output function
|
|
25
|
+
|
|
26
|
+
MyST Syntax (colon-fence):
|
|
27
|
+
:::{pairpuzzle}
|
|
28
|
+
:class: optional-css-class
|
|
29
|
+
|
|
30
|
+
Left item 1 : Right item 1
|
|
31
|
+
Left item 2 : Right item 2
|
|
32
|
+
:::
|
|
33
|
+
|
|
34
|
+
Note: Due to MyST limitations with hyphens in directive names when using colon-fence
|
|
35
|
+
syntax (:::), the directive is also registered as "pairpuzzle" (no hyphen).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from docutils import nodes
|
|
39
|
+
from docutils.parsers.rst import Directive
|
|
40
|
+
from docutils.parsers.rst import directives
|
|
41
|
+
from sphinx.application import Sphinx
|
|
42
|
+
import uuid
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PairPuzzleNode(nodes.General, nodes.Element):
|
|
46
|
+
"""Custom docutils node for pair puzzles."""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PairPuzzleDirective(Directive):
|
|
52
|
+
"""
|
|
53
|
+
Directive for creating interactive pairing puzzles.
|
|
54
|
+
|
|
55
|
+
The directive parses content as colon-separated pairs and generates
|
|
56
|
+
HTML/JavaScript for an interactive drag-and-drop game.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
has_content = True
|
|
60
|
+
option_spec = {
|
|
61
|
+
"class": directives.class_option,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def run(self):
|
|
65
|
+
"""Parse the directive content and create the puzzle node."""
|
|
66
|
+
# Generate unique container ID
|
|
67
|
+
container_id = f"pair-puzzle-{uuid.uuid4()}"
|
|
68
|
+
|
|
69
|
+
# Parse pairs from content
|
|
70
|
+
pairs = self._parse_pairs()
|
|
71
|
+
|
|
72
|
+
if not pairs:
|
|
73
|
+
error = self.state_machine.reporter.error(
|
|
74
|
+
'pair-puzzle directive requires at least one pair (format: "left : right")',
|
|
75
|
+
nodes.literal_block(self.block_text, self.block_text),
|
|
76
|
+
line=self.lineno,
|
|
77
|
+
)
|
|
78
|
+
return [error]
|
|
79
|
+
|
|
80
|
+
# Create the custom node
|
|
81
|
+
node = PairPuzzleNode()
|
|
82
|
+
node["container_id"] = container_id
|
|
83
|
+
node["pairs"] = pairs
|
|
84
|
+
node["classes"] = self.options.get("class", [])
|
|
85
|
+
|
|
86
|
+
return [node]
|
|
87
|
+
|
|
88
|
+
def _parse_pairs(self):
|
|
89
|
+
"""
|
|
90
|
+
Parse content into pairs.
|
|
91
|
+
|
|
92
|
+
Expected format:
|
|
93
|
+
Left item 1 : Right item 1
|
|
94
|
+
Left item 2 : Right item 2
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of tuples [(left1, right1), (left2, right2), ...]
|
|
98
|
+
"""
|
|
99
|
+
pairs = []
|
|
100
|
+
for line in self.content:
|
|
101
|
+
line = line.strip()
|
|
102
|
+
if not line:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Split on first ':' only
|
|
106
|
+
if ":" in line:
|
|
107
|
+
left, right = line.split(":", 1)
|
|
108
|
+
pairs.append((left.strip(), right.strip()))
|
|
109
|
+
else:
|
|
110
|
+
# Skip lines without ':' separator
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
return pairs
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def visit_pair_puzzle_html(self, node):
|
|
117
|
+
"""Generate HTML for the pair puzzle."""
|
|
118
|
+
container_id = node["container_id"]
|
|
119
|
+
pairs = node["pairs"]
|
|
120
|
+
extra_classes = " ".join(node["classes"])
|
|
121
|
+
|
|
122
|
+
# Build JavaScript pairs array
|
|
123
|
+
js_pairs = []
|
|
124
|
+
for left, right in pairs:
|
|
125
|
+
js_pairs.append(f'["{left}", "{right}"]')
|
|
126
|
+
|
|
127
|
+
pairs_js = ",\n ".join(js_pairs)
|
|
128
|
+
|
|
129
|
+
# Generate HTML container
|
|
130
|
+
html = f"""
|
|
131
|
+
<div id="{container_id}" class="pairing-puzzle-container {extra_classes}">
|
|
132
|
+
<!-- Content will be generated by JavaScript -->
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<script>
|
|
136
|
+
// Initialize the pairing puzzle when DOM is ready
|
|
137
|
+
(function() {{
|
|
138
|
+
function initPuzzle() {{
|
|
139
|
+
const pairs = [
|
|
140
|
+
{pairs_js}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Check if initGame is available
|
|
144
|
+
if (typeof initGame === 'function') {{
|
|
145
|
+
initGame('{container_id}', pairs);
|
|
146
|
+
}} else {{
|
|
147
|
+
console.error('initGame function not found. Make sure game.js is loaded.');
|
|
148
|
+
}}
|
|
149
|
+
}}
|
|
150
|
+
|
|
151
|
+
// Wait for DOM and required dependencies
|
|
152
|
+
if (document.readyState === 'loading') {{
|
|
153
|
+
document.addEventListener('DOMContentLoaded', initPuzzle);
|
|
154
|
+
}} else {{
|
|
155
|
+
initPuzzle();
|
|
156
|
+
}}
|
|
157
|
+
}})();
|
|
158
|
+
</script>
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
self.body.append(html)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def depart_pair_puzzle_html(self, node):
|
|
165
|
+
"""No closing tags needed."""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def setup(app: Sphinx):
|
|
170
|
+
"""
|
|
171
|
+
Setup the pair-puzzle directive.
|
|
172
|
+
|
|
173
|
+
Registers the directive under two names:
|
|
174
|
+
- "pair-puzzle" for RST compatibility
|
|
175
|
+
- "pairpuzzle" for MyST colon-fence compatibility (no hyphens allowed)
|
|
176
|
+
"""
|
|
177
|
+
# Register the custom node
|
|
178
|
+
app.add_node(PairPuzzleNode, html=(visit_pair_puzzle_html, depart_pair_puzzle_html))
|
|
179
|
+
|
|
180
|
+
# Register directive with both names for compatibility
|
|
181
|
+
app.add_directive("pair-puzzle", PairPuzzleDirective)
|
|
182
|
+
app.add_directive("pairpuzzle", PairPuzzleDirective) # MyST compatibility
|
|
183
|
+
|
|
184
|
+
# Note: CSS and JS files are registered in __init__.py with the munchboka/ prefix
|
|
185
|
+
# No need to register them here to avoid duplicate/incorrect paths
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"version": "0.1",
|
|
189
|
+
"parallel_read_safe": True,
|
|
190
|
+
"parallel_write_safe": True,
|
|
191
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parsons Puzzle directive for creating code-reordering exercises.
|
|
3
|
+
|
|
4
|
+
This directive creates interactive puzzles where students must arrange shuffled
|
|
5
|
+
lines of code into the correct order through drag-and-drop.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
```{parsons-puzzle} puzzle-id
|
|
9
|
+
:lang: python
|
|
10
|
+
|
|
11
|
+
def fibonacci(n):
|
|
12
|
+
if n <= 1:
|
|
13
|
+
return n
|
|
14
|
+
else:
|
|
15
|
+
return fibonacci(n-1) + fibonacci(n-2)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
- lang (optional): Programming language for syntax highlighting (default: python)
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
- Puzzle identifier (optional): If provided, creates a unique ID for the puzzle.
|
|
23
|
+
Otherwise, a random ID is generated.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Drag-and-drop code lines into correct order
|
|
27
|
+
- Syntax highlighting with highlight.js
|
|
28
|
+
- Check solution button with visual feedback (toast notifications)
|
|
29
|
+
- Reset button to reshuffle and try again
|
|
30
|
+
- Modal popup showing complete code when solved
|
|
31
|
+
- Copy to clipboard functionality
|
|
32
|
+
- Theme-aware styling (light/dark mode)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from docutils import nodes
|
|
36
|
+
from docutils.parsers.rst import Directive, directives
|
|
37
|
+
import uuid
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ParsonsPuzzleDirective(Directive):
|
|
41
|
+
"""
|
|
42
|
+
Directive for creating Parsons puzzles (code reordering exercises).
|
|
43
|
+
|
|
44
|
+
Students must drag and drop shuffled code lines into the correct order.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
has_content = True
|
|
48
|
+
required_arguments = 0
|
|
49
|
+
optional_arguments = 1
|
|
50
|
+
final_argument_whitespace = True
|
|
51
|
+
option_spec = {
|
|
52
|
+
"lang": directives.unchanged,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def run(self):
|
|
56
|
+
"""Generate HTML for the Parsons puzzle."""
|
|
57
|
+
# Generate a unique identifier or use the provided one
|
|
58
|
+
if self.arguments:
|
|
59
|
+
identifier = self.arguments[0]
|
|
60
|
+
else:
|
|
61
|
+
identifier = f"puzzle-{uuid.uuid4().hex[:8]}"
|
|
62
|
+
|
|
63
|
+
puzzle_container_id = f"container-parsons-puzzle-{identifier}"
|
|
64
|
+
editor_container_id = f"container-kode-{identifier}"
|
|
65
|
+
|
|
66
|
+
# Get code content from the directive content
|
|
67
|
+
code_content = "\n".join(self.content)
|
|
68
|
+
|
|
69
|
+
# Escape code for JavaScript
|
|
70
|
+
escaped_code = code_content.replace("`", "\\`").replace("$", "\\$")
|
|
71
|
+
|
|
72
|
+
# Create the HTML with the template
|
|
73
|
+
html = f"""
|
|
74
|
+
<div id="{puzzle_container_id}" class="puzzle-container"></div>
|
|
75
|
+
<div id="{editor_container_id}" style="display: none"></div>
|
|
76
|
+
|
|
77
|
+
<script type="text/javascript">
|
|
78
|
+
document.addEventListener("DOMContentLoaded", () => {{
|
|
79
|
+
const code =
|
|
80
|
+
`{escaped_code}`;
|
|
81
|
+
|
|
82
|
+
const puzzleContainerId = '{puzzle_container_id}';
|
|
83
|
+
const editorId = '{editor_container_id}';
|
|
84
|
+
|
|
85
|
+
const switchToCodeEditor = makeCallbackFunction(puzzleContainerId, editorId);
|
|
86
|
+
const puzzle = new ParsonsPuzzle(
|
|
87
|
+
puzzleContainerId,
|
|
88
|
+
code,
|
|
89
|
+
switchToCodeEditor,
|
|
90
|
+
);
|
|
91
|
+
}});
|
|
92
|
+
</script>
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
raw_node = nodes.raw("", html, format="html")
|
|
96
|
+
return [raw_node]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def setup(app):
|
|
100
|
+
"""Register the parsons-puzzle directive with Sphinx."""
|
|
101
|
+
app.add_directive("parsons-puzzle", ParsonsPuzzleDirective)
|
|
102
|
+
# Also register without hyphen for MyST compatibility
|
|
103
|
+
app.add_directive("parsonspuzzle", ParsonsPuzzleDirective)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"version": "0.1.0",
|
|
107
|
+
"parallel_read_safe": True,
|
|
108
|
+
"parallel_write_safe": True,
|
|
109
|
+
}
|