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,428 @@
1
+ """
2
+ CAS Popup Directive
3
+ ===================
4
+
5
+ A Sphinx directive that creates a button which opens a GeoGebra CAS (Computer Algebra System)
6
+ window in a popup dialog. The dialog can be resized, dragged, and positioned anywhere on the page.
7
+
8
+ Features:
9
+ - Customizable button text and dialog title
10
+ - Configurable width and height
11
+ - Sidebar layout option for floating the button to the side
12
+ - Scroll-locking when interacting with GeoGebra
13
+ - Responsive resizing
14
+ - Theme-aware styling (light/dark mode)
15
+
16
+ Dependencies:
17
+ - jQuery UI (for dialog functionality)
18
+ - GeoGebra API (loaded via geogebra-setup.js)
19
+
20
+ Usage Examples:
21
+ --------------
22
+
23
+ Basic usage (default 350x500 CAS window):
24
+ ```
25
+ .. cas-popup::
26
+ ```
27
+
28
+ Custom size with 600x700 window:
29
+ ```
30
+ .. cas-popup:: 600 700
31
+ ```
32
+
33
+ Custom button text and dialog title:
34
+ ```
35
+ .. cas-popup:: 400 600 "Open Calculator" "My CAS Window"
36
+ ```
37
+
38
+ Sidebar layout (floating button):
39
+ ```
40
+ .. cas-popup:: 350 500 "CAS" "Calculator"
41
+ :layout: sidebar
42
+ ```
43
+
44
+ Author: Original implementation from matematikk_r1
45
+ """
46
+
47
+ from docutils import nodes
48
+ from sphinx.util.docutils import SphinxDirective
49
+ import uuid
50
+
51
+
52
+ class CASPopUpDirective(SphinxDirective):
53
+ """
54
+ Directive to create a popup GeoGebra CAS window.
55
+
56
+ Arguments:
57
+ 1. width (optional): Width of the CAS window in pixels (default: 350)
58
+ 2. height (optional): Height of the CAS window in pixels (default: 500)
59
+ 3. button_text (optional): Text displayed on the button (default: "Åpne CAS‑vindu")
60
+ 4. dialog_title (optional): Title of the popup dialog (default: "CAS‑vindu")
61
+
62
+ Options:
63
+ :layout: Set to "sidebar" to float the button to the right side of the page
64
+ """
65
+
66
+ required_arguments = 0
67
+ optional_arguments = 4 # width, height, button text, dialog title
68
+ final_argument_whitespace = True
69
+ has_content = False
70
+ option_spec = {
71
+ "layout": lambda arg: arg, # e.g., "sidebar"
72
+ }
73
+
74
+ def run(self):
75
+ # 1 » parse args -----------------------------------------------------
76
+ width = int(self.arguments[0]) if len(self.arguments) > 0 else 350
77
+ height = int(self.arguments[1]) if len(self.arguments) > 1 else 500
78
+
79
+ button_text = self.arguments[2] if len(self.arguments) > 2 else "Åpne CAS‑vindu"
80
+ dialog_title = self.arguments[3] if len(self.arguments) > 3 else "CAS‑vindu"
81
+
82
+ # 2 » unique IDs -----------------------------------------------------
83
+ cid = f"ggb-cas-{uuid.uuid4().hex[:8]}"
84
+ dialog_id = f"dialog-{cid}"
85
+ button_id = f"button-{cid}"
86
+
87
+ # 3 » layout option --------------------------------------------------
88
+ layout = self.options.get("layout", "").strip().lower()
89
+ use_sidebar = layout == "sidebar"
90
+
91
+ wrapper_start = '<div class="sidebar-cas">' if use_sidebar else ""
92
+ wrapper_end = "</div>" if use_sidebar else ""
93
+
94
+ # 4 » HTML content ---------------------------------------------------
95
+ html = f"""
96
+ {wrapper_start}
97
+ <meta name="viewport" content="width=device-width, initial-scale=1">
98
+
99
+ <button id="{button_id}" class="ggb-cas-button">{button_text}</button>
100
+ <div id="{dialog_id}" title="{dialog_title}" style="display:none;">
101
+ <div id="{cid}" class="ggb-window"></div>
102
+ </div>
103
+
104
+ <style>
105
+ .ui-resizable-handle {{ min-width:16px;min-height:16px; }}
106
+ .ui-dialog-content{{padding:0!important;}}
107
+ .ggb-window {{width:100%!important;height:100%!important;box-sizing:border-box;}}
108
+ .ggb-cas-button {{margin-top: 1em; margin-bottom: 1em;}}
109
+ /* Ensure CAS dialog appears above jeopardy modal (which has z-index: 9999) */
110
+ .ui-dialog {{
111
+ z-index: 10000 !important;
112
+ }}
113
+ .ui-widget-overlay {{
114
+ z-index: 9999 !important;
115
+ }}
116
+ .ggb-reset-btn {{
117
+ position: absolute;
118
+ right: 2.5em;
119
+ top: 50%;
120
+ transform: translateY(-50%);
121
+ width: 2em;
122
+ height: 2em;
123
+ padding: 0.25em;
124
+ margin: 0;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ border: none;
129
+ border-radius: 4px;
130
+ background: transparent;
131
+ cursor: pointer;
132
+ transition: background-color 0.2s, opacity 0.2s;
133
+ opacity: 0.7;
134
+ }}
135
+ .ggb-reset-btn:hover {{
136
+ opacity: 1;
137
+ background-color: rgba(0, 0, 0, 0.1);
138
+ }}
139
+ [data-theme="dark"] .ggb-reset-btn:hover,
140
+ html[data-theme="dark"] .ggb-reset-btn:hover {{
141
+ background-color: rgba(255, 255, 255, 0.1);
142
+ }}
143
+ .ggb-reset-btn svg {{
144
+ color: inherit;
145
+ }}
146
+ </style>
147
+
148
+ <script>
149
+ (function() {{
150
+ $(function() {{
151
+ let ggbReady = false;
152
+ const storageKey = 'ggb-cas-state-{cid}';
153
+
154
+ function applySize() {{
155
+ if (!ggbReady) return;
156
+ const w = $("#{cid}").width(),
157
+ h = $("#{cid}").height();
158
+ window.ggbApplet.setSize(Math.round(w), Math.round(h));
159
+ }}
160
+
161
+ function saveState() {{
162
+ if (!ggbReady || !window.ggbApplet) return;
163
+ try {{
164
+ const state = window.ggbApplet.getBase64();
165
+ localStorage.setItem(storageKey, state);
166
+ // Update timestamp for this state
167
+ localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
168
+ }} catch (e) {{
169
+ // If quota exceeded, try cleaning up old states and retry
170
+ if (e.name === 'QuotaExceededError') {{
171
+ cleanupOldStates();
172
+ try {{
173
+ const state = window.ggbApplet.getBase64();
174
+ localStorage.setItem(storageKey, state);
175
+ localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
176
+ }} catch (retryError) {{
177
+ // Still failed after cleanup - silently give up
178
+ }}
179
+ }}
180
+ }}
181
+ }}
182
+
183
+ function cleanupOldStates() {{
184
+ try {{
185
+ // Find all GeoGebra CAS states with timestamps
186
+ const casStates = [];
187
+ for (let i = 0; i < localStorage.length; i++) {{
188
+ const key = localStorage.key(i);
189
+ if (key && key.startsWith('ggb-cas-state-') && key.endsWith('-timestamp')) {{
190
+ const stateKey = key.replace('-timestamp', '');
191
+ const timestamp = parseInt(localStorage.getItem(key) || '0', 10);
192
+ casStates.push({{ key: stateKey, timestamp: timestamp }});
193
+ }}
194
+ }}
195
+
196
+ // Sort by timestamp (oldest first)
197
+ casStates.sort((a, b) => a.timestamp - b.timestamp);
198
+
199
+ // Delete oldest 25% of states (minimum 1, maximum 10)
200
+ const numToDelete = Math.max(1, Math.min(10, Math.floor(casStates.length * 0.25)));
201
+ for (let i = 0; i < numToDelete && i < casStates.length; i++) {{
202
+ localStorage.removeItem(casStates[i].key);
203
+ localStorage.removeItem(casStates[i].key + '-timestamp');
204
+ }}
205
+ }} catch (e) {{
206
+ // Cleanup failed - silently continue
207
+ }}
208
+ }}
209
+
210
+ function restoreState() {{
211
+ if (!ggbReady || !window.ggbApplet) return;
212
+ try {{
213
+ const savedState = localStorage.getItem(storageKey);
214
+ if (savedState) {{
215
+ window.ggbApplet.setBase64(savedState);
216
+ // Re-apply custom toolbar after restoring state to prevent it from being overwritten
217
+ setTimeout(() => {{
218
+ if (window.ggbApplet && window.ggbApplet.setCustomToolBar) {{
219
+ window.ggbApplet.setCustomToolBar("1001 | 1002 | 1007 | 1010 | 6");
220
+ }}
221
+ }}, 50);
222
+ }}
223
+ }} catch (e) {{
224
+ // Silently fail if restore fails
225
+ }}
226
+ }}
227
+
228
+ const $dlg = $("#{dialog_id}").dialog({{
229
+ autoOpen: false,
230
+ width: {width+40}, height: {height+80},
231
+ resizable: true, draggable: true,
232
+ position: {{ my: "center", at: "center", of: window }},
233
+ zIndex: 10000,
234
+ resize: () => window.requestAnimationFrame(applySize),
235
+ open: function() {{
236
+ if (!ggbReady) {{
237
+ new GGBApplet({{
238
+ appName: "classic", id: "{cid}",
239
+ width: {width}, height: {height},
240
+ perspective: "C", language: "nb",
241
+ showToolBar: true, showAlgebraInput: false,
242
+ borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
243
+ customToolBar: "1001 | 1002 | 1007 | 1010 | 6",
244
+ appletOnLoad: () => {{
245
+ ggbReady = true;
246
+ applySize();
247
+ // Restore state after a short delay to ensure GeoGebra is fully initialized
248
+ setTimeout(restoreState, 100);
249
+ }}
250
+ }}, true).inject("{cid}");
251
+ }} else {{
252
+ applySize();
253
+ }}
254
+ }},
255
+ close: function() {{
256
+ // Save state when dialog is closed
257
+ saveState();
258
+ }}
259
+ }});
260
+
261
+ // Add refresh button to title bar
262
+ const titleBar = $dlg.parent().find('.ui-dialog-titlebar');
263
+ const refreshBtn = $('<button type="button" class="ggb-reset-btn" title="Start på nytt (slett lagret innhold)"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.65 2.35C12.2 0.9 10.21 0 8 0 3.58 0 0.01 3.58 0.01 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L9 7h7V0l-2.35 2.35z" fill="currentColor"/></svg></button>');
264
+ refreshBtn.on('click', function() {{
265
+ if (confirm('Er du sikker på at du vil slette lagret innhold og starte på nytt?')) {{
266
+ try {{
267
+ localStorage.removeItem(storageKey);
268
+ localStorage.removeItem(storageKey + '-timestamp');
269
+ if (ggbReady && window.ggbApplet) {{
270
+ // Clear the container
271
+ $("#{cid}").empty();
272
+ // Reset the flag
273
+ ggbReady = false;
274
+ // Recreate the applet from scratch
275
+ new GGBApplet({{
276
+ appName: "classic", id: "{cid}",
277
+ width: {width}, height: {height},
278
+ perspective: "C", language: "nb",
279
+ showToolBar: true, showAlgebraInput: false,
280
+ borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
281
+ customToolBar: "1001 | 1002 | 1007 | 1010 | 6",
282
+ appletOnLoad: () => {{
283
+ ggbReady = true;
284
+ applySize();
285
+ }}
286
+ }}, true).inject("{cid}");
287
+ }}
288
+ }} catch (e) {{
289
+ console.error('Failed to reset:', e);
290
+ }}
291
+ }}
292
+ }});
293
+ titleBar.append(refreshBtn);
294
+
295
+ // Save state when page is unloaded (refresh, navigate away, close tab)
296
+ $(window).on('beforeunload', function() {{
297
+ if ($dlg.dialog('isOpen')) {{
298
+ saveState();
299
+ }}
300
+ }});
301
+
302
+ // Prevent background scroll when GeoGebra applet is active
303
+ let scrollLocked = false;
304
+ let lockedScrollTop = 0;
305
+ let keyHandler = null;
306
+ let wheelHandler = null;
307
+ let scrollHandler = null;
308
+
309
+ function lockScroll() {{
310
+ if (scrollLocked) return;
311
+ scrollLocked = true;
312
+ lockedScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
313
+
314
+ // Instead of preventing keys, monitor and restore scroll position
315
+ scrollHandler = () => {{
316
+ if (scrollLocked) {{
317
+ const currentScroll = window.pageYOffset || document.documentElement.scrollTop || 0;
318
+ if (currentScroll !== lockedScrollTop) {{
319
+ window.scrollTo(0, lockedScrollTop);
320
+ }}
321
+ }}
322
+ }};
323
+
324
+ // Prevent wheel scrolling on the page background
325
+ wheelHandler = (e) => {{
326
+ const dialogElement = e.target.closest('.ui-dialog');
327
+ if (dialogElement) {{
328
+ // Allow scrolling within dialog elements that can scroll
329
+ let target = e.target;
330
+ while (target && target !== dialogElement) {{
331
+ const overflow = window.getComputedStyle(target).overflow;
332
+ if (overflow === 'auto' || overflow === 'scroll') {{
333
+ return; // Allow internal scrolling
334
+ }}
335
+ target = target.parentElement;
336
+ }}
337
+ // Prevent page scroll but don't stop the event from reaching GeoGebra
338
+ if (!e.target.closest('#{cid}')) {{
339
+ e.preventDefault();
340
+ e.stopPropagation();
341
+ }}
342
+ }}
343
+ }};
344
+
345
+ // Monitor scroll events to maintain position
346
+ window.addEventListener('scroll', scrollHandler, {{ passive: true }});
347
+ document.addEventListener('wheel', wheelHandler, {{ capture: true, passive: false }});
348
+ }}
349
+
350
+ function unlockScroll() {{
351
+ if (!scrollLocked) return;
352
+ scrollLocked = false;
353
+
354
+ if (scrollHandler) {{
355
+ window.removeEventListener('scroll', scrollHandler);
356
+ scrollHandler = null;
357
+ }}
358
+ if (wheelHandler) {{
359
+ document.removeEventListener('wheel', wheelHandler, {{ capture: true }});
360
+ wheelHandler = null;
361
+ }}
362
+ }}
363
+
364
+ // Monitor GeoGebra applet focus/interaction to control scroll lock
365
+ function bindScrollLock() {{
366
+ const ggbContainer = document.getElementById('{cid}');
367
+ if (!ggbContainer) {{
368
+ setTimeout(bindScrollLock, 300);
369
+ return;
370
+ }}
371
+
372
+ // Look for the actual GeoGebra canvas/applet elements
373
+ const ggbApplet = ggbContainer.querySelector('canvas') ||
374
+ ggbContainer.querySelector('.ggbApplet') ||
375
+ ggbContainer.querySelector('[id^="ggbApplet"]') ||
376
+ ggbContainer;
377
+
378
+ if (ggbApplet) {{
379
+ // Lock scroll when interacting with GeoGebra
380
+ ggbApplet.addEventListener('mousedown', lockScroll);
381
+ ggbApplet.addEventListener('focus', lockScroll);
382
+ ggbApplet.addEventListener('click', lockScroll);
383
+
384
+ // Also monitor the container for any focus events
385
+ ggbContainer.addEventListener('focusin', lockScroll);
386
+ ggbContainer.addEventListener('mousedown', lockScroll);
387
+
388
+ // Unlock when clicking outside or losing focus
389
+ document.addEventListener('click', (e) => {{
390
+ if (!ggbContainer.contains(e.target) && !e.target.closest('.ui-dialog')) {{
391
+ unlockScroll();
392
+ }}
393
+ }});
394
+
395
+ ggbContainer.addEventListener('focusout', (e) => {{
396
+ // Small delay to check if focus moved outside the container
397
+ setTimeout(() => {{
398
+ if (!ggbContainer.contains(document.activeElement)) {{
399
+ unlockScroll();
400
+ }}
401
+ }}, 100);
402
+ }});
403
+ }}
404
+ }}
405
+
406
+ // Start monitoring after GeoGebra is loaded
407
+ setTimeout(bindScrollLock, 1000);
408
+
409
+ // Cleanup on dialog close
410
+ $dlg.on('dialogclose', unlockScroll);
411
+
412
+ $("#{button_id}").button()
413
+ .on("click touchend pointerup", e => {{
414
+ e.preventDefault();
415
+ $("#{dialog_id}").dialog("open");
416
+ }});
417
+ }});
418
+ }})();
419
+ </script>
420
+ {wrapper_end}
421
+ """
422
+ return [nodes.raw("", html, format="html")]
423
+
424
+
425
+ def setup(app):
426
+ """Register the directive with Sphinx."""
427
+ app.add_directive("cas-popup", CASPopUpDirective)
428
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
@@ -0,0 +1,103 @@
1
+ """
2
+ Clear Directive for Munchboka Edutools
3
+ ======================================
4
+
5
+ This directive inserts a CSS clear:both element to clear floating elements.
6
+ Useful after floated images or other content to ensure proper layout flow.
7
+
8
+ Usage in MyST Markdown:
9
+ ```{clear}
10
+ ```
11
+
12
+ Purpose:
13
+ The clear directive is commonly used in web layouts to clear CSS floats.
14
+ When you have floated elements (like images with float:left or float:right),
15
+ subsequent content can wrap around them. The clear directive ensures that
16
+ content after it starts on a new line below all floated elements.
17
+
18
+ Common Use Cases:
19
+ - After floating images to prevent text wrapping
20
+ - Between sections to ensure proper spacing
21
+ - To reset layout flow after floated elements
22
+
23
+ Features:
24
+ - No arguments or options needed
25
+ - Simply inserts a clearing div
26
+ - Works with all CSS float properties
27
+
28
+ Example:
29
+ ![Floating Image](image.png){.float-left}
30
+
31
+ Some text that wraps around the image...
32
+
33
+ ```{clear}
34
+ ```
35
+
36
+ This text starts below the image, not wrapped around it.
37
+
38
+ Author: René Aasen (ported from matematikk_r1)
39
+ Date: November 2025
40
+ """
41
+
42
+ from docutils import nodes
43
+ from docutils.parsers.rst import Directive
44
+
45
+
46
+ class ClearDirective(Directive):
47
+ """
48
+ Sphinx directive for clearing CSS floats.
49
+
50
+ This directive inserts a div with `clear: both` CSS property,
51
+ which forces content to start below any floated elements.
52
+
53
+ Arguments:
54
+ None
55
+
56
+ Options:
57
+ None
58
+
59
+ Example:
60
+ After a floated image:
61
+ ```{clear}
62
+ ```
63
+
64
+ Or between sections:
65
+ ```{clear}
66
+ ```
67
+ """
68
+
69
+ has_content = False
70
+ required_arguments = 0
71
+ optional_arguments = 0
72
+
73
+ def run(self):
74
+ """
75
+ Generate the HTML for the clear element.
76
+
77
+ Returns:
78
+ list: List of docutils nodes (raw HTML node with clear div)
79
+ """
80
+ html = '<div style="clear: both;"></div>'
81
+ return [nodes.raw("", html, format="html")]
82
+
83
+
84
+ def setup(app):
85
+ """
86
+ Setup function to register the directive with Sphinx.
87
+
88
+ This function is called automatically by Sphinx when the extension is loaded.
89
+ It registers the 'clear' directive for use in documentation.
90
+
91
+ Args:
92
+ app: The Sphinx application instance
93
+
94
+ Returns:
95
+ dict: Extension metadata including version and parallel processing flags
96
+ """
97
+ app.add_directive("clear", ClearDirective)
98
+
99
+ return {
100
+ "version": "0.1",
101
+ "parallel_read_safe": True,
102
+ "parallel_write_safe": True,
103
+ }
@@ -0,0 +1,137 @@
1
+ """
2
+ Dialogue directive for creating conversation-style content.
3
+
4
+ This directive creates a dialogue between two speakers with chat-bubble styling.
5
+ Each speaker's messages are aligned left or right and styled with different colors.
6
+
7
+ Usage:
8
+ ```{dialogue}
9
+ :name1: Alice
10
+ :name2: Bob
11
+ :speaker1: left
12
+ :speaker2: right
13
+
14
+ Alice: Hello! How are you?
15
+ Bob: I'm great, thanks for asking!
16
+ Alice: That's wonderful to hear.
17
+ ```
18
+
19
+ Options:
20
+ - name1 (required): Name of the first speaker
21
+ - name2 (required): Name of the second speaker
22
+ - speaker1 (required): Alignment for first speaker ("left" or "right")
23
+ - speaker2 (required): Alignment for second speaker ("left" or "right")
24
+
25
+ Content:
26
+ Each line should be in the format: "SpeakerName: Message text"
27
+ Lines not matching this format are ignored.
28
+ Messages can contain math notation and will be parsed as normal content.
29
+ """
30
+
31
+ from docutils import nodes
32
+ from docutils.parsers.rst import Directive, directives
33
+ from sphinx.directives import optional_int
34
+
35
+
36
+ class DialogueWrapperNode(nodes.General, nodes.Element):
37
+ """Container node for the entire dialogue."""
38
+
39
+ pass
40
+
41
+
42
+ class DialogueEntryNode(nodes.Admonition, nodes.Element):
43
+ """Node for a single dialogue entry (one message from one speaker)."""
44
+
45
+ pass
46
+
47
+
48
+ def visit_dialogue_wrapper_node_html(self, node):
49
+ """Open the dialogue container div."""
50
+ self.body.append('<div class="dialogue">')
51
+
52
+
53
+ def depart_dialogue_wrapper_node_html(self, node):
54
+ """Close the dialogue container div."""
55
+ self.body.append("</div>")
56
+
57
+
58
+ def visit_dialogue_entry_node_html(self, node):
59
+ """Open a dialogue entry with speaker name."""
60
+ self.body.append(f'<div class="dialogue-entry {node["css_class"]}">')
61
+ self.body.append(f'<div class="speaker-name">{node["speaker_name"]}</div>')
62
+
63
+
64
+ def depart_dialogue_entry_node_html(self, node):
65
+ """Close the dialogue entry div."""
66
+ self.body.append("</div>")
67
+
68
+
69
+ class DialogueDirective(Directive):
70
+ """
71
+ Directive for creating dialogue between two speakers.
72
+
73
+ Creates chat-bubble styled conversations with left/right alignment.
74
+ """
75
+
76
+ has_content = True
77
+ required_arguments = 0
78
+ option_spec = {
79
+ "name1": directives.unchanged_required,
80
+ "name2": directives.unchanged_required,
81
+ "speaker1": lambda s: directives.choice(s, ("left", "right")),
82
+ "speaker2": lambda s: directives.choice(s, ("left", "right")),
83
+ }
84
+
85
+ def run(self):
86
+ """Parse dialogue content and create dialogue nodes."""
87
+ name1 = self.options.get("name1")
88
+ name2 = self.options.get("name2")
89
+ class1 = "speaker1" if self.options.get("speaker1") == "left" else "speaker2"
90
+ class2 = "speaker1" if self.options.get("speaker2") == "left" else "speaker2"
91
+
92
+ wrapper_node = DialogueWrapperNode()
93
+
94
+ for line in self.content:
95
+ line = line.strip()
96
+ if not line or ":" not in line:
97
+ continue
98
+
99
+ speaker_name, message = line.split(":", 1)
100
+ speaker_name = speaker_name.strip()
101
+ message = message.strip()
102
+
103
+ if speaker_name == name1:
104
+ css_class = class1
105
+ elif speaker_name == name2:
106
+ css_class = class2
107
+ else:
108
+ continue
109
+
110
+ entry_node = DialogueEntryNode()
111
+ entry_node["css_class"] = css_class
112
+ entry_node["speaker_name"] = speaker_name
113
+
114
+ # Parse the message line like normal content so math works
115
+ self.state.nested_parse([message], self.content_offset, entry_node)
116
+ wrapper_node += entry_node
117
+
118
+ return [wrapper_node]
119
+
120
+
121
+ def setup(app):
122
+ """Register the dialogue directive and nodes with Sphinx."""
123
+ app.add_node(
124
+ DialogueWrapperNode,
125
+ html=(visit_dialogue_wrapper_node_html, depart_dialogue_wrapper_node_html),
126
+ )
127
+ app.add_node(
128
+ DialogueEntryNode,
129
+ html=(visit_dialogue_entry_node_html, depart_dialogue_entry_node_html),
130
+ )
131
+ app.add_directive("dialogue", DialogueDirective)
132
+
133
+ return {
134
+ "version": "0.1.0",
135
+ "parallel_read_safe": True,
136
+ "parallel_write_safe": True,
137
+ }