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.
- munchboka_edutools/__init__.py +184 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +389 -0
- munchboka_edutools/directives/cas_popup.py +428 -0
- munchboka_edutools/directives/clear.py +103 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/escape_room2.py +318 -0
- munchboka_edutools/directives/factor_tree.py +552 -0
- munchboka_edutools/directives/flashcards.py +233 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +308 -0
- munchboka_edutools/directives/horner.py +326 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/jeopardy2.py +636 -0
- munchboka_edutools/directives/multi_plot.py +2524 -0
- munchboka_edutools/directives/multi_plot2.py +252 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3758 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +346 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/quiz2.py +453 -0
- munchboka_edutools/directives/signchart.py +519 -0
- munchboka_edutools/directives/signchart2.py +1545 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +321 -0
- munchboka_edutools/static/css/flashcards.css +219 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +147 -0
- munchboka_edutools/static/css/github-light.css +155 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +553 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/popup.css +115 -0
- munchboka_edutools/static/css/quiz.css +377 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/flashcards.js +199 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +648 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +441 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/jeopardy.js +560 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/popup.js +85 -0
- munchboka_edutools/static/js/quiz.js +566 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.2.3.dist-info/METADATA +109 -0
- munchboka_edutools-0.2.3.dist-info/RECORD +157 -0
- munchboka_edutools-0.2.3.dist-info/WHEEL +4 -0
- 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
|
+
}
|