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.

Files changed (157) hide show
  1. munchboka_edutools/__init__.py +184 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +389 -0
  6. munchboka_edutools/directives/cas_popup.py +428 -0
  7. munchboka_edutools/directives/clear.py +103 -0
  8. munchboka_edutools/directives/dialogue.py +137 -0
  9. munchboka_edutools/directives/escape_room.py +296 -0
  10. munchboka_edutools/directives/escape_room2.py +318 -0
  11. munchboka_edutools/directives/factor_tree.py +552 -0
  12. munchboka_edutools/directives/flashcards.py +233 -0
  13. munchboka_edutools/directives/ggb.py +209 -0
  14. munchboka_edutools/directives/ggb_icon.py +105 -0
  15. munchboka_edutools/directives/ggb_popup.py +308 -0
  16. munchboka_edutools/directives/horner.py +326 -0
  17. munchboka_edutools/directives/interactive_code.py +75 -0
  18. munchboka_edutools/directives/jeopardy.py +252 -0
  19. munchboka_edutools/directives/jeopardy2.py +636 -0
  20. munchboka_edutools/directives/multi_plot.py +2524 -0
  21. munchboka_edutools/directives/multi_plot2.py +252 -0
  22. munchboka_edutools/directives/pair_puzzle.py +191 -0
  23. munchboka_edutools/directives/parsons.py +109 -0
  24. munchboka_edutools/directives/plot.py +3758 -0
  25. munchboka_edutools/directives/poly_icon.py +111 -0
  26. munchboka_edutools/directives/polydiv.py +346 -0
  27. munchboka_edutools/directives/popup.py +245 -0
  28. munchboka_edutools/directives/quiz.py +291 -0
  29. munchboka_edutools/directives/quiz2.py +453 -0
  30. munchboka_edutools/directives/signchart.py +519 -0
  31. munchboka_edutools/directives/signchart2.py +1545 -0
  32. munchboka_edutools/directives/timed_quiz.py +436 -0
  33. munchboka_edutools/directives/turtle.py +157 -0
  34. munchboka_edutools/static/css/admonitions.css +2012 -0
  35. munchboka_edutools/static/css/cas_popup.css +242 -0
  36. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  37. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  38. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  39. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  40. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  41. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  42. munchboka_edutools/static/css/dialogue.css +92 -0
  43. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  44. munchboka_edutools/static/css/figures.css +321 -0
  45. munchboka_edutools/static/css/flashcards.css +219 -0
  46. munchboka_edutools/static/css/general_style.css +74 -0
  47. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  48. munchboka_edutools/static/css/github-dark.css +147 -0
  49. munchboka_edutools/static/css/github-light.css +155 -0
  50. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  51. munchboka_edutools/static/css/interactive_code.css +582 -0
  52. munchboka_edutools/static/css/jeopardy.css +553 -0
  53. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  54. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  55. munchboka_edutools/static/css/popup.css +115 -0
  56. munchboka_edutools/static/css/quiz.css +377 -0
  57. munchboka_edutools/static/css/timedQuiz.css +375 -0
  58. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  59. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  60. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  61. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  62. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  63. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  64. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  65. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  77. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  78. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  79. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  80. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  81. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  82. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  83. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  92. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  95. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  96. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  97. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  98. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  99. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  100. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  101. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  102. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  103. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  104. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  105. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  111. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  112. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  113. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  114. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  115. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  116. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  117. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  119. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  121. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  123. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  124. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  125. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  126. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  127. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  128. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  129. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  130. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  131. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  132. munchboka_edutools/static/js/casThemeManager.js +99 -0
  133. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  134. munchboka_edutools/static/js/flashcards.js +199 -0
  135. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  136. munchboka_edutools/static/js/highlight-init.js +6 -0
  137. munchboka_edutools/static/js/interactiveCode/codeEditor.js +648 -0
  138. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +441 -0
  139. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  140. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  141. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  142. munchboka_edutools/static/js/jeopardy.js +560 -0
  143. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  144. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  145. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  146. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  147. munchboka_edutools/static/js/popup.js +85 -0
  148. munchboka_edutools/static/js/quiz.js +566 -0
  149. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  150. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  151. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  152. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  153. munchboka_edutools/static/js/utils.js +3 -0
  154. munchboka_edutools-0.2.3.dist-info/METADATA +109 -0
  155. munchboka_edutools-0.2.3.dist-info/RECORD +157 -0
  156. munchboka_edutools-0.2.3.dist-info/WHEEL +4 -0
  157. 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
+ }