munchboka-edutools 0.1.0__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.
Files changed (150) hide show
  1. munchboka_edutools/__init__.py +182 -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 +356 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/dialogue.py +137 -0
  8. munchboka_edutools/directives/escape_room.py +296 -0
  9. munchboka_edutools/directives/factor_tree.py +549 -0
  10. munchboka_edutools/directives/ggb.py +209 -0
  11. munchboka_edutools/directives/ggb_icon.py +62 -0
  12. munchboka_edutools/directives/ggb_popup.py +165 -0
  13. munchboka_edutools/directives/horner.py +324 -0
  14. munchboka_edutools/directives/interactive_code.py +75 -0
  15. munchboka_edutools/directives/jeopardy.py +252 -0
  16. munchboka_edutools/directives/multi_plot.py +1126 -0
  17. munchboka_edutools/directives/pair_puzzle.py +191 -0
  18. munchboka_edutools/directives/parsons.py +109 -0
  19. munchboka_edutools/directives/plot.py +3012 -0
  20. munchboka_edutools/directives/poly_icon.py +91 -0
  21. munchboka_edutools/directives/polydiv.py +344 -0
  22. munchboka_edutools/directives/quiz.py +291 -0
  23. munchboka_edutools/directives/signchart.py +474 -0
  24. munchboka_edutools/directives/timed_quiz.py +436 -0
  25. munchboka_edutools/directives/turtle.py +157 -0
  26. munchboka_edutools/static/css/admonitions.css +2012 -0
  27. munchboka_edutools/static/css/cas_popup.css +242 -0
  28. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  29. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  33. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  34. munchboka_edutools/static/css/dialogue.css +92 -0
  35. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  36. munchboka_edutools/static/css/figures.css +274 -0
  37. munchboka_edutools/static/css/general_style.css +74 -0
  38. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  39. munchboka_edutools/static/css/github-dark.css +112 -0
  40. munchboka_edutools/static/css/github-light.css +120 -0
  41. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  42. munchboka_edutools/static/css/interactive_code.css +582 -0
  43. munchboka_edutools/static/css/jeopardy.css +476 -0
  44. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  45. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  46. munchboka_edutools/static/css/quiz.css +312 -0
  47. munchboka_edutools/static/css/timedQuiz.css +375 -0
  48. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  49. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  50. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  51. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  55. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  56. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  57. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  58. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  74. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  75. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  76. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  81. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  82. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  88. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  91. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  92. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  93. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  94. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  96. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  97. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  98. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  108. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  109. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  110. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  112. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  113. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  115. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  116. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  118. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  119. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  120. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  121. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  122. munchboka_edutools/static/js/casThemeManager.js +99 -0
  123. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  124. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  125. munchboka_edutools/static/js/highlight-init.js +6 -0
  126. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  127. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  128. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  129. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  130. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  131. munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
  132. munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
  133. munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
  134. munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
  135. munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
  136. munchboka_edutools/static/js/jeopardy.js +457 -0
  137. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  138. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  139. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  140. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  141. munchboka_edutools/static/js/quiz.js +422 -0
  142. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  143. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  144. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  145. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  146. munchboka_edutools/static/js/utils.js +3 -0
  147. munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
  148. munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
  149. munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
  150. munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,291 @@
1
+ from docutils import nodes
2
+ from docutils.parsers.rst import Directive, directives
3
+ from sphinx.util.docutils import SphinxDirective
4
+ import json
5
+ import uuid
6
+ import re
7
+ import os
8
+
9
+
10
+ class QuizDirective(SphinxDirective):
11
+ """Directive for embedding interactive quizzes."""
12
+
13
+ has_content = True
14
+ required_arguments = 0
15
+ optional_arguments = 0
16
+ final_argument_whitespace = True
17
+ option_spec = {
18
+ "shuffle": directives.flag,
19
+ }
20
+
21
+ def run(self):
22
+
23
+ # Parse the content
24
+ # self.state.nested_parse(self.content, self.content_offset, admonition_node)
25
+ self.quiz_id = uuid.uuid4().hex
26
+ container_id = f"quiz-container-{self.quiz_id}"
27
+
28
+ # Parse quiz content
29
+ quiz_data = self._parse_quiz_content()
30
+
31
+ # Create the HTML output
32
+ html = f"""
33
+ <!-- Container for the quiz -->
34
+ <div id="{container_id}" class="quiz-main-container"></div>
35
+ <!-- Include KaTeX for LaTeX rendering -->
36
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css">
37
+ <script defer src="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js"></script>
38
+ <script defer src="https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js"></script>
39
+
40
+ <script type="text/javascript">
41
+ document.addEventListener("DOMContentLoaded", () => {{
42
+ // Define your questions and answers
43
+ const questionsData = {json.dumps(quiz_data, ensure_ascii=False)};
44
+
45
+ // Initialize the multiple-choice quiz
46
+ const quiz = new SequentialMultipleChoiceQuiz('{container_id}', questionsData);
47
+ }});
48
+ </script>
49
+ """
50
+
51
+ raw_node = nodes.raw("", html, format="html")
52
+
53
+ return [raw_node]
54
+
55
+ def _parse_quiz_content(self):
56
+ """Parse the directive content into quiz questions data."""
57
+ questions = []
58
+ current_question = None
59
+ current_answers = []
60
+
61
+ for line in self.content:
62
+ line = self._process_figures(line)
63
+ line = line.strip()
64
+
65
+ # Skip empty lines
66
+ if not line:
67
+ continue
68
+
69
+ # New question starts with Q:
70
+ if line.startswith("Q:"):
71
+ # Save previous question if exists
72
+ if current_question:
73
+ questions.append({"content": current_question, "answers": current_answers})
74
+
75
+ # Start new question and process newlines
76
+ question_text = line[2:].strip()
77
+ # Replace \\n with actual newlines for code blocks
78
+ question_text = self._process_code_blocks(question_text)
79
+ current_question = question_text
80
+ current_answers = []
81
+
82
+ # Correct answer starts with +
83
+ elif line.startswith("+"):
84
+ answer_text = line[1:].strip()
85
+ # Process code blocks in answers too
86
+ answer_text = self._process_code_blocks(answer_text)
87
+ current_answers.append({"content": answer_text, "isCorrect": True})
88
+
89
+ # Incorrect answer starts with -
90
+ elif line.startswith("-"):
91
+ answer_text = line[1:].strip()
92
+ # Process code blocks in answers too
93
+ answer_text = self._process_code_blocks(answer_text)
94
+ current_answers.append({"content": answer_text, "isCorrect": False})
95
+
96
+ # Add the last question
97
+ if current_question:
98
+ questions.append({"content": current_question, "answers": current_answers})
99
+
100
+ return questions
101
+
102
+ def _process_code_blocks(self, text):
103
+ """Process code blocks to handle newlines properly."""
104
+
105
+ # Helper function to process code blocks by type
106
+ def replace_newlines(match):
107
+ code = match.group(2) # The actual code content
108
+ lang = match.group(1) # The language class (python or console)
109
+
110
+ # Replace escaped newlines with actual newlines
111
+ code = code.replace("\\n", "\n")
112
+ return f'<pre><code class="{lang}">{code}</code></pre>'
113
+
114
+ # Find all code blocks with any class and process them
115
+ pattern = r'<pre><code class="([\w-]+)">(.*?)</code></pre>'
116
+ text = re.sub(pattern, replace_newlines, text, flags=re.DOTALL)
117
+
118
+ return text
119
+
120
+ def _process_figures(self, text):
121
+ """Replace Markdown images with HTML <img> tags, copy figures, and fix path."""
122
+ import shutil
123
+ import re
124
+ import json
125
+
126
+ # Add a counter to track images within this quiz
127
+ if not hasattr(self, "_image_counter"):
128
+ self._image_counter = 0
129
+
130
+ def replace(match):
131
+ alt_or_options = match.group(1).strip() # Alt text or options
132
+ raw_src = match.group(2) # Image source path
133
+
134
+ # Increment image counter for unique naming
135
+ self._image_counter += 1
136
+
137
+ # Parse options from alt text
138
+ options = self._parse_figure_options(alt_or_options)
139
+
140
+ # Path of the source .md/.rst file
141
+ source_file = self.state.document["source"]
142
+ source_dir = os.path.dirname(source_file)
143
+ app_src_dir = self.env.srcdir # Root of source directory
144
+
145
+ # Absolute source path of the image file
146
+ abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
147
+
148
+ if not os.path.exists(abs_fig_src):
149
+ print(f"⚠️ QuizDirective: Figure not found: {abs_fig_src}")
150
+ return f'<img src="{raw_src}" class="quiz-image adaptive-figure" alt="Quiz figure (missing)">'
151
+
152
+ # Determine quiz-local static path: _static/figurer/<path to .md>/<filename>
153
+ relative_doc_path = os.path.relpath(source_dir, app_src_dir)
154
+ figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
155
+ os.makedirs(figure_dest_dir, exist_ok=True)
156
+
157
+ # Create unique filename using the full relative path to avoid conflicts
158
+ # Convert the relative path from source to a safe filename part
159
+ rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
160
+ safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
161
+ base_name, ext = os.path.splitext(safe_path)
162
+
163
+ # Use quiz_id, image counter, and the safe path for uniqueness
164
+ fig_filename = f"{self.quiz_id}_img{self._image_counter}_{base_name}{ext}"
165
+ fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
166
+
167
+ # Copy image
168
+ shutil.copy2(abs_fig_src, fig_dest_path)
169
+
170
+ # Now calculate relative path from output HTML to _static
171
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
172
+ rel_prefix = "../" * (depth + 1)
173
+
174
+ html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
175
+
176
+ # Build HTML with options
177
+ html_img = self._build_figure_html(html_img_path, options)
178
+
179
+ return html_img
180
+
181
+ # Updated regex to capture both alt text and source
182
+ return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
183
+
184
+ def _parse_figure_options(self, alt_text):
185
+ """Parse figure options from alt text."""
186
+ options = {}
187
+
188
+ # Method 1: JSON-like syntax {width: 60%, class: adaptive-figure}
189
+ if alt_text.startswith("{") and alt_text.endswith("}"):
190
+ try:
191
+ # Clean up the syntax for JSON parsing
192
+ json_str = alt_text
193
+ # Add quotes to keys: width: -> "width":
194
+ json_str = re.sub(r"(\w+):", r'"\1":', json_str)
195
+ # Add quotes to unquoted values
196
+ json_str = re.sub(r':\s*([^",}]+)', r': "\1"', json_str)
197
+ options = json.loads(json_str)
198
+ except:
199
+ # Fallback to simple parsing
200
+ options = self._parse_simple_options(alt_text[1:-1])
201
+
202
+ # Method 2: HTML-style attributes: width="60%" class="adaptive-figure"
203
+ elif "=" in alt_text:
204
+ options = self._parse_html_style_options(alt_text)
205
+
206
+ # Method 3: Plain alt text (traditional)
207
+ else:
208
+ if alt_text:
209
+ options["alt"] = alt_text
210
+
211
+ return options
212
+
213
+ def _parse_html_style_options(self, alt_text):
214
+ """Parse HTML-style attributes: width="60%" class="adaptive-figure" """
215
+ options = {}
216
+ # Match attribute="value" or attribute='value'
217
+ pattern = r'(\w+)=(["\'])([^"\']*)\2'
218
+
219
+ for match in re.finditer(pattern, alt_text):
220
+ key = match.group(1)
221
+ value = match.group(3)
222
+ options[key] = value
223
+
224
+ return options
225
+
226
+ def _parse_simple_options(self, options_str):
227
+ """Parse simple key: value syntax"""
228
+ options = {}
229
+ pairs = options_str.split(",")
230
+
231
+ for pair in pairs:
232
+ if ":" in pair:
233
+ key, value = pair.split(":", 1)
234
+ key = key.strip()
235
+ value = value.strip()
236
+ # Remove quotes if present
237
+ if (value.startswith('"') and value.endswith('"')) or (
238
+ value.startswith("'") and value.endswith("'")
239
+ ):
240
+ value = value[1:-1]
241
+ options[key] = value
242
+
243
+ return options
244
+
245
+ def _build_figure_html(self, html_img_path, options):
246
+ """Build the HTML for the figure with options."""
247
+
248
+ # Default options
249
+ default_options = {"class": "quiz-image adaptive-figure", "alt": "Quiz figure"}
250
+
251
+ # Merge with user options (user options override defaults)
252
+ final_options = {**default_options, **options}
253
+
254
+ # Build img attributes
255
+ img_attrs = []
256
+ img_attrs.append(f'src="{html_img_path}"')
257
+
258
+ for key, value in final_options.items():
259
+ if key == "width":
260
+ img_attrs.append(f'style="width: {value};"')
261
+ elif key == "height":
262
+ img_attrs.append(f'style="height: {value};"')
263
+ elif key in ["class", "alt", "title"]:
264
+ img_attrs.append(f'{key}="{value}"')
265
+
266
+ img_tag = f'<img {" ".join(img_attrs)}>'
267
+
268
+ # Wrap in container
269
+ html_img = f"""<div class="quiz-image-container">
270
+ {img_tag}
271
+ </div>
272
+ """
273
+
274
+ return html_img
275
+
276
+
277
+ def setup(app):
278
+ app.add_directive("quiz", QuizDirective)
279
+ # Also link shared figure styles in case only this submodule is loaded
280
+ try:
281
+ app.add_css_file("munchboka/css/general_style.css")
282
+ app.add_css_file("munchboka/css/quiz.css")
283
+ app.add_js_file("munchboka/js/quiz.js")
284
+ except Exception:
285
+ pass
286
+
287
+ return {
288
+ "version": "0.1",
289
+ "parallel_read_safe": True,
290
+ "parallel_write_safe": True,
291
+ }