munchboka-edutools 0.1.13__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 (149) 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 +272 -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/factor_tree.py +549 -0
  11. munchboka_edutools/directives/ggb.py +209 -0
  12. munchboka_edutools/directives/ggb_icon.py +105 -0
  13. munchboka_edutools/directives/ggb_popup.py +165 -0
  14. munchboka_edutools/directives/horner.py +324 -0
  15. munchboka_edutools/directives/interactive_code.py +75 -0
  16. munchboka_edutools/directives/jeopardy.py +252 -0
  17. munchboka_edutools/directives/multi_plot.py +1126 -0
  18. munchboka_edutools/directives/pair_puzzle.py +191 -0
  19. munchboka_edutools/directives/parsons.py +109 -0
  20. munchboka_edutools/directives/plot.py +3105 -0
  21. munchboka_edutools/directives/poly_icon.py +111 -0
  22. munchboka_edutools/directives/polydiv.py +344 -0
  23. munchboka_edutools/directives/popup.py +245 -0
  24. munchboka_edutools/directives/quiz.py +291 -0
  25. munchboka_edutools/directives/signchart.py +516 -0
  26. munchboka_edutools/directives/timed_quiz.py +436 -0
  27. munchboka_edutools/directives/turtle.py +157 -0
  28. munchboka_edutools/static/css/admonitions.css +2012 -0
  29. munchboka_edutools/static/css/cas_popup.css +242 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  33. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  34. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  35. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  36. munchboka_edutools/static/css/dialogue.css +92 -0
  37. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  38. munchboka_edutools/static/css/figures.css +274 -0
  39. munchboka_edutools/static/css/general_style.css +74 -0
  40. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  41. munchboka_edutools/static/css/github-dark.css +112 -0
  42. munchboka_edutools/static/css/github-light.css +120 -0
  43. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  44. munchboka_edutools/static/css/interactive_code.css +582 -0
  45. munchboka_edutools/static/css/jeopardy.css +529 -0
  46. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  47. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  48. munchboka_edutools/static/css/popup.css +115 -0
  49. munchboka_edutools/static/css/quiz.css +312 -0
  50. munchboka_edutools/static/css/timedQuiz.css +375 -0
  51. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  55. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  56. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  57. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  58. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  81. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  82. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  84. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  85. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  92. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  96. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  97. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  98. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  112. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  113. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  115. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  116. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  119. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  121. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  123. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  124. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  125. munchboka_edutools/static/js/casThemeManager.js +99 -0
  126. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  127. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  128. munchboka_edutools/static/js/highlight-init.js +6 -0
  129. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  130. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  131. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  132. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  133. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  134. munchboka_edutools/static/js/jeopardy.js +523 -0
  135. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  136. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  137. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  138. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  139. munchboka_edutools/static/js/popup.js +85 -0
  140. munchboka_edutools/static/js/quiz.js +422 -0
  141. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  142. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  143. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  144. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  145. munchboka_edutools/static/js/utils.js +3 -0
  146. munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
  147. munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
  148. munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
  149. munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,296 @@
1
+ """
2
+ Escape Room Directive
3
+ =====================
4
+
5
+ A Sphinx directive for creating interactive escape room puzzles where students must
6
+ solve problems sequentially by entering codes to unlock the next room.
7
+
8
+ Usage:
9
+ .. escape-room::
10
+ :case_insensitive:
11
+
12
+ Puzzle: Step 1 title
13
+ Code: ABC123
14
+ Q: Question text can include markdown images and code
15
+ ![{width="60%" class="adaptive-figure"}](path/to/img.png)
16
+
17
+ Puzzle: Step 2 title
18
+ Code: 42, forty-two, XLII
19
+ Q: More content ...
20
+
21
+ Recognized headers (case-insensitive):
22
+ - Puzzle: or Step: - Starts a new puzzle/room
23
+ - Code: - Code(s) to unlock this room (comma-separated for multiple)
24
+ - Q: - Question/content for this room
25
+
26
+ MyST Syntax (colon-fence):
27
+ :::{escaperoom}
28
+ :case_insensitive:
29
+
30
+ Puzzle: Step 1 title
31
+ Code: ABC123
32
+ Q: Question text
33
+ :::
34
+
35
+ Note: Due to MyST limitations with hyphens in directive names when using colon-fence
36
+ syntax (:::), the directive is also registered as "escaperoom" (no hyphen).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import html as _html
43
+ import os
44
+ import re
45
+ import uuid
46
+ from typing import Any, Dict, List
47
+
48
+ from docutils import nodes
49
+ from docutils.parsers.rst import directives
50
+ from sphinx.util.docutils import SphinxDirective
51
+
52
+
53
+ class EscapeRoomDirective(SphinxDirective):
54
+ """Directive that renders an Escape-Room style sequence of puzzles.
55
+
56
+ Only one question is shown at a time; students must type a code to unlock
57
+ the next. Supports figures and code blocks like quiz/jeopardy.
58
+ """
59
+
60
+ has_content = True
61
+ required_arguments = 0
62
+ option_spec = {
63
+ # Placeholder for future options (e.g., case-insensitive codes)
64
+ "case_insensitive": directives.flag,
65
+ }
66
+
67
+ def run(self):
68
+ self.board_id = uuid.uuid4().hex
69
+ container_id = f"escape-room-{self.board_id}"
70
+
71
+ data = self._parse_content()
72
+
73
+ # Compute relative prefix to _static (same approach as jeopardy)
74
+ source_file = self.state.document["source"]
75
+ source_dir = os.path.dirname(source_file)
76
+ app_src_dir = self.env.srcdir
77
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
78
+ rel_prefix = "../" * (depth + 1)
79
+
80
+ json_str = json.dumps(data, ensure_ascii=False)
81
+ # Escape JSON for safe embedding in a double-quoted attribute
82
+ attr_json = _html.escape(json_str, quote=True)
83
+
84
+ # Note: CSS and JS are registered in __init__.py with munchboka/ prefix
85
+ # KaTeX is also loaded globally, no need to load it per-directive
86
+ html = f"""
87
+ <div id="{container_id}" class="escape-room-container" data-config="{attr_json}">
88
+ <script type="application/json" class="escape-room-data">{json_str}</script>
89
+ </div>
90
+ """
91
+
92
+ return [nodes.raw("", html, format="html")]
93
+
94
+ # -------------------- parsing helpers --------------------
95
+ def _parse_content(self) -> Dict[str, Any]:
96
+ steps: List[Dict[str, Any]] = []
97
+ current: Dict[str, Any] | None = None
98
+ current_section: str | None = None # 'Q'
99
+
100
+ def flush():
101
+ nonlocal current
102
+ if current is not None:
103
+ # normalize
104
+ current.setdefault("title", "")
105
+ codes = current.get("codes") or []
106
+ current["codes"] = [str(c).strip() for c in codes if str(c).strip()]
107
+ current.setdefault("question", "")
108
+ # post-process code blocks
109
+ current["question"] = self._process_code_blocks(current["question"])
110
+ steps.append(current)
111
+ current = None
112
+
113
+ for raw in self.content:
114
+ line = self._process_figures(raw) or raw
115
+ s = line.rstrip("\n")
116
+
117
+ # Puzzle/Step header
118
+ m = re.match(r"^\s*(?:Puzzle|Step)\s*:\s*(.+?)\s*$", s, flags=re.IGNORECASE)
119
+ if m:
120
+ flush()
121
+ current = {"title": m.group(1).strip(), "codes": [], "question": ""}
122
+ current_section = None
123
+ continue
124
+
125
+ # Code line (allow comma-separated)
126
+ m = re.match(r"^\s*Code\s*:\s*(.+?)\s*$", s, flags=re.IGNORECASE)
127
+ if m and current is not None:
128
+ codes_str = m.group(1).strip()
129
+ codes = [c.strip() for c in re.split(r",|;", codes_str)] if codes_str else []
130
+ current["codes"] = codes
131
+ continue
132
+
133
+ # Q: starts question block
134
+ m = re.match(r"^\s*Q\s*:\s*(.*)$", s, flags=re.IGNORECASE)
135
+ if m and current is not None:
136
+ current_section = "Q"
137
+ current["question"] = (current.get("question") or "") + m.group(1)
138
+ continue
139
+
140
+ # Continuation
141
+ if current_section == "Q" and current is not None:
142
+ current["question"] = (current.get("question") or "") + "\n" + s
143
+ continue
144
+
145
+ flush()
146
+
147
+ case_insensitive = "case_insensitive" in self.options
148
+ return {"steps": steps, "caseInsensitive": bool(case_insensitive)}
149
+
150
+ def _process_figures(self, text):
151
+ """Copy images to _static/figurer/... and rewrite to HTML, like quiz/jeopardy."""
152
+ import shutil
153
+ import json as _json
154
+
155
+ if not hasattr(self, "_image_counter"):
156
+ self._image_counter = 0
157
+
158
+ def _parse_figure_options(alt_text):
159
+ # Support JSON-like {width: 60%, class: adaptive-figure} and key=value forms
160
+ opts = {}
161
+ s = (alt_text or "").strip()
162
+
163
+ def parse_pairs(text: str):
164
+ for m in re.finditer(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s}]+))', text):
165
+ val = m.group(2) or m.group(3) or m.group(4) or ""
166
+ opts[m.group(1)] = val
167
+
168
+ if s.startswith("{") and s.endswith("}"):
169
+ inner = s[1:-1].strip()
170
+ ok = False
171
+ try:
172
+ js = s
173
+ js = re.sub(r"(\w+)\s*:", r'"\1":', js)
174
+ js = re.sub(r':\s*([^",}]+)', r': "\1"', js)
175
+ opts.update(_json.loads(js))
176
+ ok = True
177
+ except Exception:
178
+ ok = False
179
+ if not ok:
180
+ parse_pairs(inner)
181
+ else:
182
+ parse_pairs(s)
183
+ if not opts and s:
184
+ opts["alt"] = s
185
+ return opts
186
+
187
+ def _normalize_wh(val: Any) -> str:
188
+ s = str(val).strip()
189
+ if re.fullmatch(r"\d+(?:\.\d+)?", s):
190
+ return f"{s}px"
191
+ s = re.sub(r"\s+(?=(px|%|em|rem|vh|vw)$)", "", s)
192
+ return s
193
+
194
+ def _build_figure_html(html_img_path, options):
195
+ user_opts = dict(options or {})
196
+ user_class = user_opts.pop("class", "").strip()
197
+ classes = "escape-room-image adaptive-figure" + (f" {user_class}" if user_class else "")
198
+ alt_text = user_opts.pop("alt", "Figure")
199
+ title = user_opts.pop("title", None)
200
+ width = user_opts.pop("width", None)
201
+ height = user_opts.pop("height", None)
202
+ extra_style = user_opts.pop("style", None)
203
+
204
+ styles: List[str] = []
205
+ if width is not None:
206
+ styles.append(f"width: {_normalize_wh(width)};")
207
+ if height is not None:
208
+ styles.append(f"height: {_normalize_wh(height)};")
209
+ if extra_style:
210
+ styles.append(str(extra_style))
211
+
212
+ attrs = [
213
+ f'src="{html_img_path}"',
214
+ f'class="{classes}"',
215
+ f'alt="{alt_text}"',
216
+ ]
217
+ if title:
218
+ attrs.append(f'title="{title}"')
219
+ if styles:
220
+ attrs.append(f'style="{' '.join(styles)}"')
221
+ for k, v in user_opts.items():
222
+ if k not in {
223
+ "src",
224
+ "class",
225
+ "alt",
226
+ "title",
227
+ "width",
228
+ "height",
229
+ "style",
230
+ }:
231
+ attrs.append(f'{k}="{v}"')
232
+
233
+ img = f"<img {' '.join(attrs)}>"
234
+ return f'<div class="escape-room-image-container">{img}</div>'
235
+
236
+ def replace(m):
237
+ alt_or_opts = m.group(1).strip()
238
+ raw_src = m.group(2)
239
+
240
+ self._image_counter += 1
241
+
242
+ options = _parse_figure_options(alt_or_opts)
243
+
244
+ source_file = self.state.document["source"]
245
+ source_dir = os.path.dirname(source_file)
246
+ app_src_dir = self.env.srcdir
247
+
248
+ abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
249
+ if not os.path.exists(abs_fig_src):
250
+ return f'<img src="{raw_src}" class="escape-room-image adaptive-figure" alt="Figure (missing)">' # noqa: E501
251
+
252
+ relative_doc_path = os.path.relpath(source_dir, app_src_dir)
253
+ figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
254
+ os.makedirs(figure_dest_dir, exist_ok=True)
255
+
256
+ rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
257
+ safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
258
+ base, ext = os.path.splitext(safe_path)
259
+ fig_filename = f"{self.board_id}_img{self._image_counter}_{base}{ext}"
260
+ fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
261
+ shutil.copy2(abs_fig_src, fig_dest_path)
262
+
263
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
264
+ rel_prefix = "../" * (depth + 1)
265
+ html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
266
+ return _build_figure_html(html_img_path, options)
267
+
268
+ return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
269
+
270
+ def _process_code_blocks(self, text: str) -> str:
271
+ """Process HTML code blocks to handle escaped newlines (like quiz)."""
272
+
273
+ def replace_newlines(match):
274
+ lang = match.group(1)
275
+ code = match.group(2)
276
+ code = code.replace("\\n", "\n")
277
+ return f'<pre><code class="{lang}">{code}</code></pre>'
278
+
279
+ pattern = r'<pre><code class="([\w-]+)">(.*?)</code></pre>'
280
+ return re.sub(pattern, replace_newlines, text, flags=re.DOTALL)
281
+
282
+
283
+ def setup(app):
284
+ """
285
+ Setup the escape-room directive.
286
+
287
+ Registers the directive under two names:
288
+ - "escape-room" for RST compatibility
289
+ - "escaperoom" for MyST colon-fence compatibility (no hyphens allowed)
290
+
291
+ Note: CSS and JS files are registered in __init__.py with the munchboka/ prefix
292
+ """
293
+ app.add_directive("escape-room", EscapeRoomDirective)
294
+ app.add_directive("escaperoom", EscapeRoomDirective) # MyST compatibility
295
+
296
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}