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,308 @@
1
+ """
2
+ GeoGebra popup directive for creating interactive GeoGebra applets in dialog windows.
3
+
4
+ This directive creates a button that opens a GeoGebra Classic applet in a
5
+ draggable, resizable dialog window using jQuery UI.
6
+
7
+ Usage:
8
+ Basic usage with defaults (700x500 window):
9
+ ```{ggb-popup}
10
+ ```
11
+
12
+ Custom size and text:
13
+ ```{ggb-popup} 800 600 "Open Calculator" "GeoGebra Calculator"
14
+ ```
15
+
16
+ With options:
17
+ ```{ggb-popup} 900 700 "Open Geometry" "Geometry Window"
18
+ :perspective: G
19
+ :menubar: true
20
+ :layout: sidebar
21
+ ```
22
+
23
+ Arguments (all optional):
24
+ 1. Width (default: 700) - Width of the GeoGebra applet in pixels
25
+ 2. Height (default: 500) - Height of the GeoGebra applet in pixels
26
+ 3. Button text (default: "Åpne Geogebra‑vindu") - Text shown on the button
27
+ 4. Dialog title (default: "Geogebra‑vindu") - Title of the dialog window
28
+
29
+ Options:
30
+ - perspective: GeoGebra perspective/view (default: "AG")
31
+ Common values: "AG" (Algebra & Graphics), "G" (Geometry), "GS" (Graphing), "CAS"
32
+ - menubar: Show menu bar (default: "false") - "true" or "false"
33
+ - layout: Layout style (default: none) - Use "sidebar" to wrap in sidebar-cas div
34
+
35
+ Features:
36
+ - Draggable and resizable dialog window
37
+ - GeoGebra Classic applet with full features
38
+ - Responsive sizing when dialog is resized
39
+ - Centered on screen when opened
40
+ - jQuery UI styling
41
+ """
42
+
43
+ from docutils import nodes
44
+ from sphinx.util.docutils import SphinxDirective
45
+ import uuid
46
+
47
+
48
+ class GGBPopUpDirective(SphinxDirective):
49
+ """
50
+ Directive for creating GeoGebra popup windows.
51
+
52
+ Creates a button that opens a GeoGebra Classic applet in a jQuery UI dialog.
53
+ """
54
+
55
+ required_arguments = 0
56
+ optional_arguments = 4 # width, height, button text, dialog title
57
+ final_argument_whitespace = True
58
+ has_content = False
59
+ option_spec = {
60
+ "layout": lambda arg: arg, # e.g., "sidebar"
61
+ "menubar": lambda arg: arg, # e.g., "true" or "false"
62
+ "perspective": lambda arg: arg, # e.g., "GS"
63
+ }
64
+
65
+ def run(self):
66
+ """Generate HTML for the GeoGebra popup."""
67
+ # 1 » Parse arguments
68
+ width = int(self.arguments[0]) if len(self.arguments) > 0 else 700
69
+ height = int(self.arguments[1]) if len(self.arguments) > 1 else 500
70
+
71
+ button_text = self.arguments[2] if len(self.arguments) > 2 else "Åpne Geogebra‑vindu"
72
+ dialog_title = self.arguments[3] if len(self.arguments) > 3 else "Geogebra‑vindu"
73
+
74
+ menubar = self.options.get("menubar", "false")
75
+
76
+ perspective = self.options.get("perspective", "AG").strip()
77
+
78
+ # 2 » Generate unique IDs
79
+ cid = f"ggb-geogebra-{uuid.uuid4().hex[:8]}"
80
+ dialog_id = f"dialog-{cid}"
81
+ button_id = f"button-{cid}"
82
+
83
+ # 3 » Handle layout option
84
+ layout = self.options.get("layout", "").strip().lower()
85
+ use_sidebar = layout == "sidebar"
86
+
87
+ wrapper_start = '<div class="sidebar-cas">' if use_sidebar else ""
88
+ wrapper_end = "</div>" if use_sidebar else ""
89
+
90
+ # 4 » Generate HTML content
91
+ html = f"""
92
+ {wrapper_start}
93
+ <meta name="viewport" content="width=device-width, initial-scale=1">
94
+
95
+ <button id="{button_id}" class="ggb-cas-button">{button_text}</button>
96
+ <div id="{dialog_id}" title="{dialog_title}" style="display:none;">
97
+ <div id="{cid}" class="ggb-window"></div>
98
+ </div>
99
+
100
+ <style>
101
+ .ui-resizable-handle {{ min-width:16px;min-height:16px; }}
102
+ .ui-dialog-content{{padding:0!important;}}
103
+ .ggb-window {{width:100%!important;height:100%!important;box-sizing:border-box;}}
104
+ .ggb-popup-button {{margin-top: 1em; margin-bottom: 1em;}}
105
+ .ggb-reset-btn {{
106
+ position: absolute;
107
+ right: 2.5em;
108
+ top: 50%;
109
+ transform: translateY(-50%);
110
+ width: 2em;
111
+ height: 2em;
112
+ padding: 0.25em;
113
+ margin: 0;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ border: none;
118
+ border-radius: 4px;
119
+ background: transparent;
120
+ cursor: pointer;
121
+ transition: background-color 0.2s, opacity 0.2s;
122
+ opacity: 0.7;
123
+ }}
124
+ .ggb-reset-btn:hover {{
125
+ opacity: 1;
126
+ background-color: rgba(0, 0, 0, 0.1);
127
+ }}
128
+ [data-theme="dark"] .ggb-reset-btn:hover,
129
+ html[data-theme="dark"] .ggb-reset-btn:hover {{
130
+ background-color: rgba(255, 255, 255, 0.1);
131
+ }}
132
+ .ggb-reset-btn svg {{
133
+ color: inherit;
134
+ }}
135
+ </style>
136
+
137
+ <script>
138
+ (function() {{
139
+ $(function() {{
140
+ let ggbReady = false;
141
+ const storageKey = 'ggb-geogebra-state-{cid}';
142
+
143
+ function applySize() {{
144
+ if (!ggbReady) return;
145
+ const w = $("#{cid}").width(),
146
+ h = $("#{cid}").height();
147
+ window.ggbApplet.setSize(Math.round(w), Math.round(h));
148
+ }}
149
+
150
+ function saveState() {{
151
+ if (!ggbReady || !window.ggbApplet) return;
152
+ try {{
153
+ const state = window.ggbApplet.getBase64();
154
+ localStorage.setItem(storageKey, state);
155
+ // Update timestamp for this state
156
+ localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
157
+ }} catch (e) {{
158
+ // If quota exceeded, try cleaning up old states and retry
159
+ if (e.name === 'QuotaExceededError') {{
160
+ cleanupOldStates();
161
+ try {{
162
+ const state = window.ggbApplet.getBase64();
163
+ localStorage.setItem(storageKey, state);
164
+ localStorage.setItem(storageKey + '-timestamp', Date.now().toString());
165
+ }} catch (retryError) {{
166
+ // Still failed after cleanup - silently give up
167
+ }}
168
+ }}
169
+ }}
170
+ }}
171
+
172
+ function cleanupOldStates() {{
173
+ try {{
174
+ // Find all GeoGebra states with timestamps (both CAS and regular)
175
+ const ggbStates = [];
176
+ for (let i = 0; i < localStorage.length; i++) {{
177
+ const key = localStorage.key(i);
178
+ if (key && (key.startsWith('ggb-geogebra-state-') || key.startsWith('ggb-cas-state-')) && key.endsWith('-timestamp')) {{
179
+ const stateKey = key.replace('-timestamp', '');
180
+ const timestamp = parseInt(localStorage.getItem(key) || '0', 10);
181
+ ggbStates.push({{ key: stateKey, timestamp: timestamp }});
182
+ }}
183
+ }}
184
+
185
+ // Sort by timestamp (oldest first)
186
+ ggbStates.sort((a, b) => a.timestamp - b.timestamp);
187
+
188
+ // Delete oldest 25% of states (minimum 1, maximum 10)
189
+ const numToDelete = Math.max(1, Math.min(10, Math.floor(ggbStates.length * 0.25)));
190
+ for (let i = 0; i < numToDelete && i < ggbStates.length; i++) {{
191
+ localStorage.removeItem(ggbStates[i].key);
192
+ localStorage.removeItem(ggbStates[i].key + '-timestamp');
193
+ }}
194
+ }} catch (e) {{
195
+ // Cleanup failed - silently continue
196
+ }}
197
+ }}
198
+
199
+ function restoreState() {{
200
+ if (!ggbReady || !window.ggbApplet) return;
201
+ try {{
202
+ const savedState = localStorage.getItem(storageKey);
203
+ if (savedState) {{
204
+ window.ggbApplet.setBase64(savedState);
205
+ }}
206
+ }} catch (e) {{
207
+ // Silently fail if restore fails
208
+ }}
209
+ }}
210
+
211
+ $("#{dialog_id}").dialog({{
212
+ autoOpen: false,
213
+ width: {width+40}, height: {height+80},
214
+ resizable: true, draggable: true,
215
+ position: {{ my: "center", at: "center", of: window }},
216
+ resize: () => window.requestAnimationFrame(applySize),
217
+ open: function() {{
218
+ if (!ggbReady) {{
219
+ new GGBApplet({{
220
+ appName: "classic", id: "{cid}",
221
+ width: {width}, height: {height},
222
+ perspective: "{perspective}", language: "nb",
223
+ showToolBar: true, showAlgebraInput: true,
224
+ borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
225
+ showMenuBar: {menubar},
226
+ appletOnLoad: () => {{
227
+ ggbReady = true;
228
+ applySize();
229
+ // Restore state after a short delay to ensure GeoGebra is fully initialized
230
+ setTimeout(restoreState, 100);
231
+ }}
232
+ }}, true).inject("{cid}");
233
+ }} else {{
234
+ applySize();
235
+ }}
236
+ }},
237
+ close: function() {{
238
+ // Save state when dialog is closed
239
+ saveState();
240
+ }}
241
+ }});
242
+
243
+ // Add refresh button to title bar
244
+ const $dlg = $("#{dialog_id}");
245
+ const titleBar = $dlg.parent().find('.ui-dialog-titlebar');
246
+ 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>');
247
+ refreshBtn.on('click', function() {{
248
+ if (confirm('Er du sikker på at du vil slette lagret innhold og starte på nytt?')) {{
249
+ try {{
250
+ localStorage.removeItem(storageKey);
251
+ localStorage.removeItem(storageKey + '-timestamp');
252
+ if (ggbReady && window.ggbApplet) {{
253
+ // Clear the container
254
+ $("#{cid}").empty();
255
+ // Reset the flag
256
+ ggbReady = false;
257
+ // Recreate the applet from scratch
258
+ new GGBApplet({{
259
+ appName: "classic", id: "{cid}",
260
+ width: {width}, height: {height},
261
+ perspective: "{perspective}", language: "nb",
262
+ showToolBar: true, showAlgebraInput: true,
263
+ borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
264
+ showMenuBar: {menubar},
265
+ appletOnLoad: () => {{
266
+ ggbReady = true;
267
+ applySize();
268
+ }}
269
+ }}, true).inject("{cid}");
270
+ }}
271
+ }} catch (e) {{
272
+ console.error('Failed to reset:', e);
273
+ }}
274
+ }}
275
+ }});
276
+ titleBar.append(refreshBtn);
277
+
278
+ // Save state when page is unloaded (refresh, navigate away, close tab)
279
+ $(window).on('beforeunload', function() {{
280
+ if ($dlg.dialog('isOpen')) {{
281
+ saveState();
282
+ }}
283
+ }});
284
+
285
+ $("#{button_id}").button()
286
+ .on("click touchend pointerup", e => {{
287
+ e.preventDefault();
288
+ $("#{dialog_id}").dialog("open");
289
+ }});
290
+ }});
291
+ }})();
292
+ </script>
293
+ {wrapper_end}
294
+ """
295
+ return [nodes.raw("", html, format="html")]
296
+
297
+
298
+ def setup(app):
299
+ """Register the ggb-popup directive with Sphinx."""
300
+ app.add_directive("ggb-popup", GGBPopUpDirective)
301
+ # Also register without hyphen for MyST compatibility
302
+ app.add_directive("ggbpopup", GGBPopUpDirective)
303
+
304
+ return {
305
+ "version": "0.1.0",
306
+ "parallel_read_safe": True,
307
+ "parallel_write_safe": True,
308
+ }
@@ -0,0 +1,326 @@
1
+ """
2
+ Horner scheme (synthetic division) directive for Sphinx/Jupyter Book.
3
+
4
+ Generates SVG visualizations of Horner's method/synthetic division using LaTeX.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ import hashlib
10
+ import re
11
+ import uuid
12
+ from docutils import nodes
13
+ from docutils.parsers.rst import directives
14
+ from sphinx.util.docutils import SphinxDirective
15
+
16
+
17
+ def _hash_key(*parts):
18
+ """Generate a hash key from multiple parts for caching."""
19
+ h = hashlib.sha1()
20
+ for p in parts:
21
+ if p is None:
22
+ p = "__NONE__"
23
+ h.update(str(p).encode("utf-8"))
24
+ h.update(b"||")
25
+ return h.hexdigest()[:12]
26
+
27
+
28
+ def synthetic_div(
29
+ fname: str,
30
+ p: str,
31
+ x: float,
32
+ stage: int = 12,
33
+ svg: bool = True,
34
+ tutor: bool = False,
35
+ ):
36
+ """
37
+ Generate Horner scheme (synthetic division) figure using LaTeX.
38
+
39
+ Args:
40
+ fname: Base filename (without extension)
41
+ p: Polynomial as string (e.g., "x^3 + 2x^2 - 3x - 6")
42
+ x: Value to evaluate at
43
+ stage: Stage number for step-by-step display (default: 12 = complete)
44
+ svg: If True, convert to SVG; otherwise keep as PDF
45
+ tutor: If True, enable tutor mode with step-by-step guidance
46
+ """
47
+ if not tutor:
48
+ div_cmd = r"\polyhornerscheme[x={x}, resultstyle=\color{red}, showvar=true]{{p}}"
49
+ div_cmd = div_cmd.replace("{p}", p).replace("{x}", str(x))
50
+ else:
51
+ div_cmd = r"\polyhornerscheme[x={x}, stage={stage}, tutor=true, tutorlimit=12, resultstyle=\color{red}, showvar=true]{{p}}"
52
+ div_cmd = div_cmd.replace("{p}", p).replace("{x}", str(x)).replace("{stage}", str(stage))
53
+
54
+ s = f"""\\documentclass{{standalone}}
55
+ \\usepackage{{polynom}}
56
+ \\usepackage{{xcolor}}
57
+ \\begin{{document}}
58
+ {div_cmd}
59
+ \\end{{document}}
60
+ """
61
+
62
+ with open("tmp.tex", "w") as f:
63
+ f.write(s)
64
+
65
+ os.system("pdflatex tmp.tex")
66
+ if fname.endswith(".svg"):
67
+ fname = fname.strip(".svg")
68
+
69
+ if svg:
70
+ os.system(f"pdf2svg tmp.pdf {fname}.svg")
71
+ else:
72
+ os.system(f"mv tmp.pdf {fname}.pdf")
73
+
74
+ os.system("rm tmp.*")
75
+
76
+
77
+ class HornerDirective(SphinxDirective):
78
+ """
79
+ Generate (and cache) a Horner (synthetic division) scheme as inline SVG.
80
+
81
+ Usage (MyST):
82
+
83
+ ```
84
+ ::::{horner}
85
+ :p: x^3 + 2x^2 - 3x - 6
86
+ :x: 1
87
+ :stage: 2 # optional
88
+ :width: 60% # optional
89
+
90
+ Optional caption here.
91
+ ::::
92
+ ```
93
+
94
+ Or classic reStructuredText:
95
+
96
+ ```
97
+ .. horner::
98
+ :p: x^3 + 2x^2 - 3x - 6
99
+ :x: 1
100
+ :stage: 2
101
+
102
+ Optional caption here.
103
+ ```
104
+ """
105
+
106
+ has_content = True
107
+ required_arguments = 0
108
+ optional_arguments = 0
109
+ final_argument_whitespace = False
110
+ option_spec = {
111
+ "p": directives.unchanged_required,
112
+ "x": directives.unchanged_required,
113
+ "stage": directives.nonnegative_int,
114
+ "tutor": directives.flag,
115
+ "align": lambda a: directives.choice(a, ["left", "center", "right"]),
116
+ "class": directives.class_option,
117
+ "name": directives.unchanged,
118
+ "nocache": directives.flag,
119
+ "alt": directives.unchanged,
120
+ "width": directives.length_or_percentage_or_unitless,
121
+ }
122
+
123
+ def run(self):
124
+ env = self.state.document.settings.env
125
+ app = env.app
126
+
127
+ # Required options
128
+ p = self.options.get("p")
129
+ x_val = self.options.get("x")
130
+ if p is None or x_val is None:
131
+ return [
132
+ self.state_machine.reporter.error(
133
+ "Directive 'horner' requires both :p: and :x: options.",
134
+ line=self.lineno,
135
+ )
136
+ ]
137
+
138
+ # Optional options
139
+ stage = self.options.get("stage", 12)
140
+ tutor_mode = "tutor" in self.options
141
+ explicit_name = self.options.get("name")
142
+
143
+ # Cache key must include tutor mode so variants don't collide
144
+ content_hash = _hash_key(p, x_val, stage, int(tutor_mode))
145
+ base_name = explicit_name or f"horner_{content_hash}"
146
+
147
+ # Paths / caching
148
+ src_dir = app.srcdir
149
+ rel_dir = os.path.join("_static", "horner")
150
+ abs_dir = os.path.join(src_dir, rel_dir)
151
+ os.makedirs(abs_dir, exist_ok=True)
152
+ svg_filename = f"{base_name}.svg"
153
+ abs_svg_path = os.path.join(abs_dir, svg_filename)
154
+
155
+ regenerate = "nocache" in self.options or not os.path.exists(abs_svg_path)
156
+ if regenerate:
157
+ cwd = os.getcwd()
158
+ try:
159
+ os.chdir(abs_dir)
160
+ synthetic_div(
161
+ fname=base_name,
162
+ p=p,
163
+ x=x_val,
164
+ stage=stage,
165
+ svg=True,
166
+ tutor=tutor_mode,
167
+ )
168
+ except Exception as e:
169
+ return [
170
+ self.state_machine.reporter.error(
171
+ f"Error generating Horner scheme: {e}",
172
+ line=self.lineno,
173
+ )
174
+ ]
175
+ finally:
176
+ os.chdir(cwd)
177
+
178
+ # Post-process: strip explicit width/height if viewBox present for responsiveness
179
+ try:
180
+ if os.path.exists(abs_svg_path):
181
+ with open(abs_svg_path, "r", encoding="utf-8") as f_svg:
182
+ raw_tmp = f_svg.read()
183
+ if "viewBox" in raw_tmp:
184
+ cleaned = re.sub(r'\swidth="[^"]+"', "", raw_tmp)
185
+ cleaned = re.sub(r'\sheight="[^"]+"', "", cleaned)
186
+ if cleaned != raw_tmp:
187
+ with open(abs_svg_path, "w", encoding="utf-8") as f_out:
188
+ f_out.write(cleaned)
189
+ except Exception:
190
+ pass
191
+
192
+ if not os.path.exists(abs_svg_path):
193
+ return [
194
+ self.state_machine.reporter.error(
195
+ (
196
+ f"horner: failed to generate SVG '{svg_filename}'. "
197
+ "Check that 'pdflatex' and 'pdf2svg' are installed."
198
+ ),
199
+ line=self.lineno,
200
+ )
201
+ ]
202
+
203
+ # Track dependency & copy to output for HTML builder
204
+ env.note_dependency(abs_svg_path)
205
+ try:
206
+ out_static = os.path.join(app.outdir, "_static", "horner")
207
+ os.makedirs(out_static, exist_ok=True)
208
+ shutil.copy2(abs_svg_path, os.path.join(out_static, svg_filename))
209
+ except Exception:
210
+ pass
211
+
212
+ # Alt text construction
213
+ if stage is not None and stage != 12:
214
+ default_alt = f"Horner's scheme for ({p}) at x={x_val} – stage {stage}"
215
+ else:
216
+ default_alt = f"Horner's scheme for ({p}) at x={x_val}"
217
+ if tutor_mode:
218
+ default_alt += " (tutor mode)"
219
+ alt = self.options.get("alt", default_alt)
220
+
221
+ width_opt = self.options.get("width")
222
+ percentage_width = isinstance(width_opt, str) and width_opt.strip().endswith("%")
223
+
224
+ # Read generated SVG
225
+ try:
226
+ with open(abs_svg_path, "r", encoding="utf-8") as f_svg:
227
+ raw_svg = f_svg.read()
228
+ except Exception as e:
229
+ return [
230
+ self.state_machine.reporter.error(
231
+ f"horner inline: could not read SVG: {e}",
232
+ line=self.lineno,
233
+ )
234
+ ]
235
+
236
+ # Ensure unique IDs per embedding
237
+ def _uniquify_ids(svg_text: str, prefix: str) -> str:
238
+ ids = set(re.findall(r'\bid="([^"]+)"', svg_text))
239
+ if not ids:
240
+ return svg_text
241
+ mapping = {old: f"{prefix}{old}" for old in ids}
242
+ for old, new in mapping.items():
243
+ svg_text = re.sub(rf'\bid="{re.escape(old)}"', f'id="{new}"', svg_text)
244
+ for old, new in mapping.items():
245
+ svg_text = re.sub(
246
+ rf'(?:xlink:)?href="#?{re.escape(old)}"', f'href="#{new}"', svg_text
247
+ )
248
+ svg_text = re.sub(
249
+ rf'xlink:href="#?{re.escape(old)}"',
250
+ f'xlink:href="#{new}"',
251
+ svg_text,
252
+ )
253
+ for old, new in mapping.items():
254
+ svg_text = re.sub(rf"url\(#\s*{re.escape(old)}\s*\)", f"url(#{new})", svg_text)
255
+ for old, new in mapping.items():
256
+ svg_text = re.sub(rf"#({re.escape(old)})\b", f"#{new}", svg_text)
257
+ return svg_text
258
+
259
+ unique_prefix = f"hnr_{_hash_key(p, x_val, stage, int(tutor_mode))}_{uuid.uuid4().hex[:6]}_"
260
+ raw_svg = _uniquify_ids(raw_svg, unique_prefix)
261
+
262
+ # Augment root <svg> (unified width handling)
263
+ def _augment(match):
264
+ tag = match.group(0)
265
+ if "class=" not in tag:
266
+ tag = tag[:-1] + ' class="horner-inline-svg"' + ">"
267
+ else:
268
+ tag = tag.replace('class="', 'class="horner-inline-svg ')
269
+ if alt and "aria-label=" not in tag:
270
+ tag = tag[:-1] + f' role="img" aria-label="{alt}"' + ">"
271
+ if width_opt:
272
+ w_raw = width_opt.strip()
273
+ if percentage_width:
274
+ w_css = w_raw
275
+ margin = "margin:0 auto;" if "margin:" not in tag else ""
276
+ style_frag = f"width:{w_css}; height:auto; display:block; {margin}".strip()
277
+ else:
278
+ w_css = (w_raw + "px") if w_raw.isdigit() else w_raw
279
+ style_frag = f"width:{w_css}; height:auto; display:block;"
280
+ if "style=" in tag:
281
+ tag = re.sub(
282
+ r'style="([^"]*)"',
283
+ lambda m: f'style="{m.group(1)}; {style_frag}"',
284
+ tag,
285
+ count=1,
286
+ )
287
+ else:
288
+ tag = tag[:-1] + f' style="{style_frag}"' + ">"
289
+ return tag
290
+
291
+ raw_svg = re.sub(r"<svg\b[^>]*>", _augment, raw_svg, count=1)
292
+
293
+ # Build docutils figure
294
+ figure = nodes.figure()
295
+ figure.setdefault("classes", []).extend(["adaptive-figure", "horner-figure", "no-click"])
296
+
297
+ raw_node = nodes.raw("", raw_svg, format="html")
298
+ raw_node.setdefault("classes", []).extend(["horner-image", "no-click", "no-scaled-link"])
299
+ figure += raw_node
300
+
301
+ extra_classes = self.options.get("class")
302
+ if extra_classes:
303
+ figure["classes"].extend(extra_classes)
304
+ figure["align"] = self.options.get("align", "center")
305
+
306
+ if self.content:
307
+ caption_node = nodes.caption()
308
+ caption_text = "\n".join(self.content)
309
+ # Parse as inline text to support math while avoiding extra paragraph nodes
310
+ parsed_nodes, messages = self.state.inline_text(caption_text, self.lineno)
311
+ caption_node.extend(parsed_nodes)
312
+ figure += caption_node
313
+
314
+ if explicit_name:
315
+ self.add_name(figure)
316
+
317
+ return [figure]
318
+
319
+
320
+ def setup(app):
321
+ app.add_directive("horner", HornerDirective)
322
+ return {
323
+ "version": "0.1",
324
+ "parallel_read_safe": True,
325
+ "parallel_write_safe": True,
326
+ }