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,233 @@
1
+ from __future__ import annotations
2
+
3
+ import html as _html
4
+ import json
5
+ import os
6
+ import re
7
+ import uuid
8
+ from typing import Any, Dict, List
9
+
10
+ from docutils import nodes
11
+ from docutils.parsers.rst import directives
12
+ from sphinx.util.docutils import SphinxDirective
13
+
14
+
15
+ class FlashcardsDirective(SphinxDirective):
16
+ has_content = True
17
+ required_arguments = 0
18
+ option_spec = {
19
+ "shuffle": directives.flag,
20
+ "show_progress": directives.flag,
21
+ "start_index": directives.nonnegative_int,
22
+ }
23
+
24
+ def run(self):
25
+ deck_id = uuid.uuid4().hex
26
+ container_id = f"flashcards-{deck_id}"
27
+
28
+ data = self._parse_deck()
29
+
30
+ source_file = self.state.document["source"]
31
+ source_dir = os.path.dirname(source_file)
32
+ app_src_dir = self.env.srcdir
33
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
34
+ rel_prefix = "../" * (depth + 1)
35
+
36
+ cfg: Dict[str, Any] = {
37
+ "cards": data["cards"],
38
+ "options": {
39
+ "shuffle": "shuffle" in self.options,
40
+ "show_progress": "show_progress" in self.options,
41
+ "start_index": int(self.options.get("start_index", 0) or 0),
42
+ "staticPrefix": rel_prefix,
43
+ },
44
+ }
45
+
46
+ cfg_str_attr = _html.escape(json.dumps(cfg, ensure_ascii=False), quote=True)
47
+ json_str = json.dumps(cfg, ensure_ascii=False)
48
+
49
+ html = f"""
50
+ <div id="{container_id}" class=\"flashcards-container\" data-config=\"{cfg_str_attr}\">
51
+ <script type=\"application/json\" class=\"flashcards-data\">{json_str}</script>
52
+ </div>
53
+ <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css\">
54
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js\"></script>
55
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js\"></script>
56
+ """
57
+ return [nodes.raw("", html, format="html")]
58
+
59
+ def _parse_deck(self) -> Dict[str, Any]:
60
+ cards: List[Dict[str, Any]] = []
61
+ current: Dict[str, Any] | None = None
62
+ section: str | None = None
63
+
64
+ def flush():
65
+ nonlocal current
66
+ if current is not None:
67
+ for k in ("front", "back"):
68
+ if current.get(k) is None:
69
+ current[k] = ""
70
+ cards.append(current)
71
+ current = None
72
+
73
+ for raw in self.content:
74
+ line = self._process_figures(raw)
75
+ if line is None:
76
+ line = raw
77
+ s = line.rstrip("\n")
78
+
79
+ m = re.match(r"^\s*Q\s*:\s*(.*)$", s, flags=re.IGNORECASE)
80
+ if m:
81
+ flush()
82
+ current = {"front": m.group(1), "back": ""}
83
+ section = "front"
84
+ continue
85
+
86
+ m = re.match(r"^\s*A\s*:\s*(.*)$", s, flags=re.IGNORECASE)
87
+ if m and current is not None:
88
+ current["back"] = (current.get("back") or "") + m.group(1)
89
+ section = "back"
90
+ continue
91
+
92
+ if section == "front" and current is not None:
93
+ current["front"] = (current.get("front") or "") + "\n" + s
94
+ continue
95
+ if section == "back" and current is not None:
96
+ current["back"] = (current.get("back") or "") + "\n" + s
97
+ continue
98
+
99
+ flush()
100
+
101
+ for c in cards:
102
+ for key in ("front", "back"):
103
+ val = c.get(key) or ""
104
+ c[key] = self._process_code_blocks(val)
105
+
106
+ return {"cards": cards}
107
+
108
+ def _process_code_blocks(self, text: str) -> str:
109
+ def repl(match):
110
+ code = match.group(2).replace("\\n", "\n")
111
+ lang = match.group(1)
112
+ return f'<pre><code class="{lang}">{code}</code></pre>'
113
+
114
+ pattern = r'<pre><code class="([\\w-]+)">(.*?)</code></pre>'
115
+ return re.sub(pattern, repl, text, flags=re.DOTALL)
116
+
117
+ def _process_figures(self, text: str):
118
+ import shutil
119
+ import json as _json
120
+
121
+ if not hasattr(self, "_image_counter"):
122
+ self._image_counter = 0
123
+
124
+ def _parse_figure_options(alt_text: str) -> Dict[str, Any]:
125
+ opts: Dict[str, Any] = {}
126
+ s = (alt_text or "").strip()
127
+
128
+ def parse_pairs(content: str):
129
+ for m in re.finditer(
130
+ r'(\\w+)\\s*=\\s*(?:"([^"]*)"|\'([^\']*)\'|([^\\s}]+))', content
131
+ ):
132
+ val = m.group(2) or m.group(3) or m.group(4) or ""
133
+ opts[m.group(1)] = val
134
+
135
+ if s.startswith("{") and s.endswith("}"):
136
+ inner = s[1:-1].strip()
137
+ ok = False
138
+ try:
139
+ js = s
140
+ js = re.sub(r"(\\w+)\\s*:", r'"\\1":', js)
141
+ js = re.sub(r':\\s*([^",}]+)', r': "\\1"', js)
142
+ opts.update(_json.loads(js))
143
+ ok = True
144
+ except Exception:
145
+ ok = False
146
+ if not ok:
147
+ parse_pairs(inner)
148
+ else:
149
+ parse_pairs(s)
150
+ if not opts and s:
151
+ opts["alt"] = s
152
+ return opts
153
+
154
+ def _build_figure_html(html_img_path: str, options: Dict[str, Any]) -> str:
155
+ user_opts = dict(options or {})
156
+ user_class = user_opts.pop("class", "").strip()
157
+ classes = "flashcard-image adaptive-figure" + (f" {user_class}" if user_class else "")
158
+
159
+ alt_text = user_opts.pop("alt", "Figure")
160
+ title = user_opts.pop("title", None)
161
+ width = user_opts.pop("width", None)
162
+ height = user_opts.pop("height", None)
163
+ extra_style = user_opts.pop("style", None)
164
+
165
+ def _normalize_wh(val: Any) -> str:
166
+ s = str(val).strip()
167
+ if re.fullmatch(r"\\d+(?:\\.\\d+)?", s):
168
+ return f"{s}px"
169
+ s = re.sub(r"\\s+(?=(px|%|em|rem|vh|vw)$)", "", s)
170
+ return s
171
+
172
+ styles: List[str] = []
173
+ if width is not None:
174
+ styles.append(f"width: {_normalize_wh(width)};")
175
+ if height is not None:
176
+ styles.append(f"height: {_normalize_wh(height)};")
177
+ if extra_style:
178
+ styles.append(str(extra_style))
179
+
180
+ attrs = [f'src="{html_img_path}"', f'class="{classes}"', f'alt="{alt_text}"']
181
+ if title:
182
+ attrs.append(f'title="{title}"')
183
+ if styles:
184
+ style_str = " ".join(styles)
185
+ attrs.append(f'style="{style_str}"')
186
+ for k, v in user_opts.items():
187
+ if k not in {"src", "class", "alt", "title", "width", "height", "style"}:
188
+ attrs.append(f'{k}="{v}"')
189
+ img = f"<img {' '.join(attrs)} >"
190
+ return f'<div class="flashcard-image-container">{img}</div>'
191
+
192
+ def replace(m):
193
+ alt_or_opts = m.group(1).strip()
194
+ raw_src = m.group(2)
195
+ self._image_counter += 1
196
+ options = _parse_figure_options(alt_or_opts)
197
+
198
+ source_file = self.state.document["source"]
199
+ source_dir = os.path.dirname(source_file)
200
+ app_src_dir = self.env.srcdir
201
+
202
+ abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
203
+ if not os.path.exists(abs_fig_src):
204
+ return f'<img src="{raw_src}" class="flashcard-image adaptive-figure" alt="Figure (missing)">'
205
+
206
+ relative_doc_path = os.path.relpath(source_dir, app_src_dir)
207
+ figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
208
+ os.makedirs(figure_dest_dir, exist_ok=True)
209
+
210
+ rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
211
+ safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
212
+ base, ext = os.path.splitext(safe_path)
213
+ fig_filename = f"flash_{self._image_counter}_{base}{ext}"
214
+ fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
215
+ shutil.copy2(abs_fig_src, fig_dest_path)
216
+
217
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
218
+ rel_prefix = "../" * (depth + 1)
219
+ html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
220
+ return _build_figure_html(html_img_path, options)
221
+
222
+ return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
223
+
224
+
225
+ def setup(app):
226
+ app.add_directive("flashcards", FlashcardsDirective)
227
+ try:
228
+ app.add_css_file("munchboka/css/flashcards.css")
229
+ app.add_js_file("munchboka/js/flashcards.js")
230
+ app.add_css_file("munchboka/css/general_style.css")
231
+ except Exception:
232
+ pass
233
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
@@ -0,0 +1,209 @@
1
+ """
2
+ GeoGebra Directive for Munchboka Edutools
3
+ =========================================
4
+
5
+ This directive embeds interactive GeoGebra applets in the documentation.
6
+ It uses the GeoGebra API to create fully functional mathematics tools
7
+ directly in the browser.
8
+
9
+ Usage in MyST Markdown:
10
+ ```{ggb} 720 600
11
+ :material_id: abcdef123
12
+ :toolbar: true
13
+ :menubar: true
14
+ :algebra: true
15
+ ```
16
+
17
+ Or with defaults (empty applet):
18
+ ```{ggb} 800 600
19
+ ```
20
+
21
+ Or with perspective:
22
+ ```{ggb} 720 600
23
+ :material_id: xyz789
24
+ :perspective: graphing
25
+ ```
26
+
27
+ Features:
28
+ - Embed existing GeoGebra materials by ID
29
+ - Configure toolbar, menubar, and algebra view visibility
30
+ - Set custom dimensions (width × height)
31
+ - Multiple perspective options (graphing, geometry, 3d, etc.)
32
+ - Automatic fullscreen and reset buttons
33
+ - Norwegian language interface
34
+
35
+ Arguments:
36
+ width (optional): Width in pixels (default: 720)
37
+ height (optional): Height in pixels (default: 600)
38
+
39
+ Options:
40
+ material_id (optional): GeoGebra material ID to load
41
+ toolbar (optional): Show toolbar ("true"/"false", default: "false")
42
+ menubar (optional): Show menubar ("true"/"false", default: "false")
43
+ algebra (optional): Show algebra view ("true"/"false", default: "false")
44
+ perspective (optional): Perspective to use (e.g., "graphing", "geometry", "3d")
45
+
46
+ Dependencies:
47
+ - GeoGebra API: Loaded via deployggb.js (already registered in __init__.py)
48
+ - geogebra-setup.js: Theme management and initialization
49
+
50
+ Author: René Aasen (ported from matematikk_r1)
51
+ Date: November 2025
52
+ """
53
+
54
+ from docutils import nodes
55
+ from sphinx.util.docutils import SphinxDirective
56
+ from docutils.parsers.rst import directives
57
+ import uuid
58
+
59
+
60
+ class GGBDirective(SphinxDirective):
61
+ """
62
+ Sphinx directive for embedding interactive GeoGebra applets.
63
+
64
+ This directive creates a container div and uses the GeoGebra API
65
+ (deployggb.js) to inject an interactive mathematics applet.
66
+
67
+ Arguments:
68
+ width (optional): Width in pixels (default: 720)
69
+ height (optional): Height in pixels (default: 600)
70
+
71
+ Options:
72
+ material_id: GeoGebra material ID (e.g., from geogebra.org/m/xyz)
73
+ toolbar: Show toolbar ("true"/"false")
74
+ menubar: Show menubar ("true"/"false")
75
+ algebra: Show algebra view ("true"/"false")
76
+ perspective: Perspective to use ("graphing", "geometry", "3d", etc.)
77
+
78
+ Examples:
79
+ Embed a specific GeoGebra material:
80
+ ```{ggb} 720 600
81
+ :material_id: abcdef123
82
+ :toolbar: true
83
+ :menubar: true
84
+ :algebra: true
85
+ ```
86
+
87
+ Create an empty applet:
88
+ ```{ggb} 800 600
89
+ ```
90
+
91
+ Use a specific perspective:
92
+ ```{ggb} 720 600
93
+ :material_id: xyz789
94
+ :perspective: graphing
95
+ ```
96
+ """
97
+
98
+ required_arguments = 0
99
+ optional_arguments = 2 # width and height
100
+ has_content = False
101
+
102
+ option_spec = {
103
+ "material_id": directives.unchanged,
104
+ "toolbar": directives.unchanged,
105
+ "menubar": directives.unchanged,
106
+ "algebra": directives.unchanged,
107
+ "perspective": directives.unchanged,
108
+ }
109
+
110
+ def run(self):
111
+ """
112
+ Generate the HTML for the GeoGebra applet.
113
+
114
+ Returns:
115
+ list: List of docutils nodes (raw HTML node)
116
+ """
117
+ # Convert arguments to integers (with defaults if conversion fails)
118
+ try:
119
+ width = int(self.arguments[0])
120
+ except (IndexError, ValueError):
121
+ width = 720
122
+ try:
123
+ height = int(self.arguments[1])
124
+ except (IndexError, ValueError):
125
+ height = 600
126
+
127
+ # Get options
128
+ material_id = self.options.get("material_id", None)
129
+ toolbar = self.options.get("toolbar", "false")
130
+ menubar = self.options.get("menubar", "false")
131
+ algebra = self.options.get("algebra", "false")
132
+ perspective = self.options.get("perspective", None)
133
+
134
+ # Format perspective option
135
+ if perspective:
136
+ perspective_option = f"perspective: '{perspective}'"
137
+ else:
138
+ perspective_option = "'': ''"
139
+
140
+ # Format material_id option
141
+ if material_id:
142
+ material_id_option = f"material_id: '{material_id}'"
143
+ else:
144
+ # If no material_id is provided, enable all controls by default
145
+ # This creates an empty applet with full functionality
146
+ material_id_option = "'': ''"
147
+ toolbar = "true"
148
+ menubar = "true"
149
+ algebra = "true"
150
+
151
+ # Generate a unique container ID using a short UUID
152
+ container_id = f"ggb-cas-{uuid.uuid4().hex[:8]}"
153
+
154
+ # Create the raw HTML
155
+ # Note: GeoGebra API (deployggb.js) is already loaded via __init__.py
156
+ html = f"""
157
+ <div id="{container_id}" style="width: {width}px; height: {height}px;" class="ggb-window"></div>
158
+ <script>
159
+ document.addEventListener("DOMContentLoaded", function() {{
160
+ var options = {{
161
+ appName: 'classic',
162
+ width: {width},
163
+ height: {height},
164
+ showToolBar: {toolbar},
165
+ showAlgebraInput: {algebra},
166
+ showMenuBar: {menubar},
167
+ language: 'nb',
168
+ borderRadius: 8,
169
+ borderColor: '#000000',
170
+ showFullscreenButton: true,
171
+ showResetIcon: true,
172
+ scale: 1,
173
+ rounding: 2,
174
+ showKeyboardOnFocus: false,
175
+ preventFocus: true,
176
+ id: '{container_id}',
177
+ {material_id_option},
178
+ {perspective_option},
179
+ }};
180
+
181
+ var applet = new GGBApplet(options, true);
182
+ applet.inject('{container_id}');
183
+ }});
184
+ </script>
185
+ """
186
+
187
+ return [nodes.raw("", html, format="html")]
188
+
189
+
190
+ def setup(app):
191
+ """
192
+ Setup function to register the directive with Sphinx.
193
+
194
+ This function is called automatically by Sphinx when the extension is loaded.
195
+ It registers the 'ggb' directive for use in documentation.
196
+
197
+ Args:
198
+ app: The Sphinx application instance
199
+
200
+ Returns:
201
+ dict: Extension metadata including version and parallel processing flags
202
+ """
203
+ app.add_directive("ggb", GGBDirective)
204
+
205
+ return {
206
+ "version": "0.1",
207
+ "parallel_read_safe": True,
208
+ "parallel_write_safe": True,
209
+ }
@@ -0,0 +1,105 @@
1
+ """
2
+ GeoGebra icon role for inline SVG icons.
3
+
4
+ This role allows you to insert GeoGebra tool icons inline in text.
5
+ The icons are SVG files that represent various GeoGebra tools and modes.
6
+
7
+ Usage:
8
+ This is the {ggb-icon}`mode_intersect` tool in GeoGebra.
9
+
10
+ Click the {ggb-icon}`mode_solve` icon to solve equations.
11
+
12
+ Available icons:
13
+ - mode_evaluate: Evaluate/compute icon
14
+ - mode_extremum: Find extremum (min/max) icon
15
+ - mode_intersect: Find intersection point icon
16
+ - mode_nsolve: Numeric solve icon
17
+ - mode_numeric: Numeric computation icon
18
+ - mode_point: Point tool icon
19
+ - mode_solve: Symbolic solve icon
20
+
21
+ The icons are rendered as inline images with appropriate alt text.
22
+ """
23
+
24
+ from docutils import nodes
25
+ from docutils.parsers.rst import roles
26
+ from sphinx.util.osutil import relative_uri
27
+
28
+
29
+ # Custom node for GeoGebra icons
30
+ class ggb_icon_node(nodes.Inline, nodes.Element):
31
+ """Custom node for GeoGebra icons."""
32
+
33
+ pass
34
+
35
+
36
+ def ggb_icon_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
37
+ """
38
+ Custom role for GeoGebra icons.
39
+
40
+ Usage: {ggb-icon}`mode_intersect`
41
+
42
+ This generates a custom node that will be rendered with relative paths.
43
+ """
44
+ # Clean up the icon name (remove any extra whitespace)
45
+ icon_name = text.strip()
46
+
47
+ # Create our custom node
48
+ node = ggb_icon_node()
49
+ node["icon_name"] = icon_name
50
+ node["classes"] = ["inline-image"]
51
+
52
+ return [node], []
53
+
54
+
55
+ def visit_ggb_icon_html(self, node):
56
+ """HTML visitor for ggb_icon_node - generates relative path."""
57
+ icon_name = node["icon_name"]
58
+
59
+ # Get the relative path from current document to _static directory
60
+ # self.builder.current_docname is like "examples/ggb_icon"
61
+ # We want path from there to ../static/munchboka/icons/ggb/
62
+ from os.path import relpath, dirname, join
63
+
64
+ # Current document directory depth
65
+ doc_dir = dirname(self.builder.current_docname) if "/" in self.builder.current_docname else ""
66
+
67
+ # Calculate relative path
68
+ if doc_dir:
69
+ # For documents in subdirectories like "examples/ggb_icon"
70
+ depth = doc_dir.count("/") + 1
71
+ rel_prefix = "../" * depth
72
+ else:
73
+ # For top-level documents
74
+ rel_prefix = ""
75
+
76
+ img_path = f"{rel_prefix}_static/munchboka/icons/ggb/{icon_name}.svg"
77
+
78
+ # Generate the HTML with relative path
79
+ html = f'<img src="{img_path}" alt="GeoGebra {icon_name} icon" class="inline-image" />'
80
+ self.body.append(html)
81
+ raise nodes.SkipNode
82
+
83
+
84
+ def depart_ggb_icon_html(self, node):
85
+ """Depart function (not needed as we raise SkipNode)."""
86
+ pass
87
+
88
+
89
+ def setup(app):
90
+ """Setup function to register the role with Sphinx."""
91
+ # Register the custom node
92
+ app.add_node(
93
+ ggb_icon_node,
94
+ html=(visit_ggb_icon_html, depart_ggb_icon_html),
95
+ )
96
+
97
+ # Register the role with both hyphenated and unhyphenated names
98
+ roles.register_local_role("ggb-icon", ggb_icon_role)
99
+ roles.register_local_role("ggbicon", ggb_icon_role)
100
+
101
+ return {
102
+ "version": "0.1.0",
103
+ "parallel_read_safe": True,
104
+ "parallel_write_safe": True,
105
+ }