munchboka-edutools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. munchboka_edutools/__init__.py +182 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +356 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/dialogue.py +137 -0
  8. munchboka_edutools/directives/escape_room.py +296 -0
  9. munchboka_edutools/directives/factor_tree.py +549 -0
  10. munchboka_edutools/directives/ggb.py +209 -0
  11. munchboka_edutools/directives/ggb_icon.py +62 -0
  12. munchboka_edutools/directives/ggb_popup.py +165 -0
  13. munchboka_edutools/directives/horner.py +324 -0
  14. munchboka_edutools/directives/interactive_code.py +75 -0
  15. munchboka_edutools/directives/jeopardy.py +252 -0
  16. munchboka_edutools/directives/multi_plot.py +1126 -0
  17. munchboka_edutools/directives/pair_puzzle.py +191 -0
  18. munchboka_edutools/directives/parsons.py +109 -0
  19. munchboka_edutools/directives/plot.py +3012 -0
  20. munchboka_edutools/directives/poly_icon.py +91 -0
  21. munchboka_edutools/directives/polydiv.py +344 -0
  22. munchboka_edutools/directives/quiz.py +291 -0
  23. munchboka_edutools/directives/signchart.py +474 -0
  24. munchboka_edutools/directives/timed_quiz.py +436 -0
  25. munchboka_edutools/directives/turtle.py +157 -0
  26. munchboka_edutools/static/css/admonitions.css +2012 -0
  27. munchboka_edutools/static/css/cas_popup.css +242 -0
  28. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  29. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  33. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  34. munchboka_edutools/static/css/dialogue.css +92 -0
  35. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  36. munchboka_edutools/static/css/figures.css +274 -0
  37. munchboka_edutools/static/css/general_style.css +74 -0
  38. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  39. munchboka_edutools/static/css/github-dark.css +112 -0
  40. munchboka_edutools/static/css/github-light.css +120 -0
  41. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  42. munchboka_edutools/static/css/interactive_code.css +582 -0
  43. munchboka_edutools/static/css/jeopardy.css +476 -0
  44. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  45. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  46. munchboka_edutools/static/css/quiz.css +312 -0
  47. munchboka_edutools/static/css/timedQuiz.css +375 -0
  48. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  49. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  50. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  51. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  55. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  56. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  57. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  58. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  74. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  75. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  76. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  81. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  82. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  88. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  91. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  92. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  93. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  94. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  96. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  97. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  98. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  108. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  109. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  110. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  112. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  113. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  115. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  116. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  118. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  119. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  120. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  121. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  122. munchboka_edutools/static/js/casThemeManager.js +99 -0
  123. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  124. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  125. munchboka_edutools/static/js/highlight-init.js +6 -0
  126. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  127. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  128. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  129. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  130. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  131. munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
  132. munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
  133. munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
  134. munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
  135. munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
  136. munchboka_edutools/static/js/jeopardy.js +457 -0
  137. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  138. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  139. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  140. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  141. munchboka_edutools/static/js/quiz.js +422 -0
  142. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  143. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  144. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  145. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  146. munchboka_edutools/static/js/utils.js +3 -0
  147. munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
  148. munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
  149. munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
  150. munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,272 @@
1
+ """
2
+ CAS Popup Directive
3
+ ===================
4
+
5
+ A Sphinx directive that creates a button which opens a GeoGebra CAS (Computer Algebra System)
6
+ window in a popup dialog. The dialog can be resized, dragged, and positioned anywhere on the page.
7
+
8
+ Features:
9
+ - Customizable button text and dialog title
10
+ - Configurable width and height
11
+ - Sidebar layout option for floating the button to the side
12
+ - Scroll-locking when interacting with GeoGebra
13
+ - Responsive resizing
14
+ - Theme-aware styling (light/dark mode)
15
+
16
+ Dependencies:
17
+ - jQuery UI (for dialog functionality)
18
+ - GeoGebra API (loaded via geogebra-setup.js)
19
+
20
+ Usage Examples:
21
+ --------------
22
+
23
+ Basic usage (default 350x500 CAS window):
24
+ ```
25
+ .. cas-popup::
26
+ ```
27
+
28
+ Custom size with 600x700 window:
29
+ ```
30
+ .. cas-popup:: 600 700
31
+ ```
32
+
33
+ Custom button text and dialog title:
34
+ ```
35
+ .. cas-popup:: 400 600 "Open Calculator" "My CAS Window"
36
+ ```
37
+
38
+ Sidebar layout (floating button):
39
+ ```
40
+ .. cas-popup:: 350 500 "CAS" "Calculator"
41
+ :layout: sidebar
42
+ ```
43
+
44
+ Author: Original implementation from matematikk_r1
45
+ """
46
+
47
+ from docutils import nodes
48
+ from sphinx.util.docutils import SphinxDirective
49
+ import uuid
50
+
51
+
52
+ class CASPopUpDirective(SphinxDirective):
53
+ """
54
+ Directive to create a popup GeoGebra CAS window.
55
+
56
+ Arguments:
57
+ 1. width (optional): Width of the CAS window in pixels (default: 350)
58
+ 2. height (optional): Height of the CAS window in pixels (default: 500)
59
+ 3. button_text (optional): Text displayed on the button (default: "Åpne CAS‑vindu")
60
+ 4. dialog_title (optional): Title of the popup dialog (default: "CAS‑vindu")
61
+
62
+ Options:
63
+ :layout: Set to "sidebar" to float the button to the right side of the page
64
+ """
65
+
66
+ required_arguments = 0
67
+ optional_arguments = 4 # width, height, button text, dialog title
68
+ final_argument_whitespace = True
69
+ has_content = False
70
+ option_spec = {
71
+ "layout": lambda arg: arg, # e.g., "sidebar"
72
+ }
73
+
74
+ def run(self):
75
+ # 1 » parse args -----------------------------------------------------
76
+ width = int(self.arguments[0]) if len(self.arguments) > 0 else 350
77
+ height = int(self.arguments[1]) if len(self.arguments) > 1 else 500
78
+
79
+ button_text = self.arguments[2] if len(self.arguments) > 2 else "Åpne CAS‑vindu"
80
+ dialog_title = self.arguments[3] if len(self.arguments) > 3 else "CAS‑vindu"
81
+
82
+ # 2 » unique IDs -----------------------------------------------------
83
+ cid = f"ggb-cas-{uuid.uuid4().hex[:8]}"
84
+ dialog_id = f"dialog-{cid}"
85
+ button_id = f"button-{cid}"
86
+
87
+ # 3 » layout option --------------------------------------------------
88
+ layout = self.options.get("layout", "").strip().lower()
89
+ use_sidebar = layout == "sidebar"
90
+
91
+ wrapper_start = '<div class="sidebar-cas">' if use_sidebar else ""
92
+ wrapper_end = "</div>" if use_sidebar else ""
93
+
94
+ # 4 » HTML content ---------------------------------------------------
95
+ html = f"""
96
+ {wrapper_start}
97
+ <meta name="viewport" content="width=device-width, initial-scale=1">
98
+
99
+ <button id="{button_id}" class="ggb-cas-button">{button_text}</button>
100
+ <div id="{dialog_id}" title="{dialog_title}" style="display:none;">
101
+ <div id="{cid}" class="ggb-window"></div>
102
+ </div>
103
+
104
+ <style>
105
+ .ui-resizable-handle {{ min-width:16px;min-height:16px; }}
106
+ .ui-dialog-content{{padding:0!important;}}
107
+ .ggb-window {{width:100%!important;height:100%!important;box-sizing:border-box;}}
108
+ .ggb-cas-button {{margin-top: 1em; margin-bottom: 1em;}}
109
+ </style>
110
+
111
+ <script>
112
+ (function() {{
113
+ $(function() {{
114
+ let ggbReady = false;
115
+
116
+ function applySize() {{
117
+ if (!ggbReady) return;
118
+ const w = $("#{cid}").width(),
119
+ h = $("#{cid}").height();
120
+ window.ggbApplet.setSize(Math.round(w), Math.round(h));
121
+ }}
122
+
123
+ const $dlg = $("#{dialog_id}").dialog({{
124
+ autoOpen: false,
125
+ width: {width+40}, height: {height+80},
126
+ resizable: true, draggable: true,
127
+ position: {{ my: "center", at: "center", of: window }},
128
+ resize: () => window.requestAnimationFrame(applySize),
129
+ open: function() {{
130
+ if (!ggbReady) {{
131
+ new GGBApplet({{
132
+ appName: "classic", id: "{cid}",
133
+ width: {width}, height: {height},
134
+ perspective: "C", language: "nb",
135
+ showToolBar: true, showAlgebraInput: true,
136
+ borderRadius: 8, enableRightClick: true, showKeyboardOnFocus: false,
137
+ customToolBar: "1001 | 1002 | 1007 | 1010 | 6",
138
+ appletOnLoad: () => {{ ggbReady = true; applySize(); }}
139
+ }}, true).inject("{cid}");
140
+ }} else {{
141
+ applySize();
142
+ }}
143
+ }}
144
+ }});
145
+
146
+ // Prevent background scroll when GeoGebra applet is active
147
+ let scrollLocked = false;
148
+ let lockedScrollTop = 0;
149
+ let keyHandler = null;
150
+ let wheelHandler = null;
151
+ let scrollHandler = null;
152
+
153
+ function lockScroll() {{
154
+ if (scrollLocked) return;
155
+ scrollLocked = true;
156
+ lockedScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
157
+
158
+ // Instead of preventing keys, monitor and restore scroll position
159
+ scrollHandler = () => {{
160
+ if (scrollLocked) {{
161
+ const currentScroll = window.pageYOffset || document.documentElement.scrollTop || 0;
162
+ if (currentScroll !== lockedScrollTop) {{
163
+ window.scrollTo(0, lockedScrollTop);
164
+ }}
165
+ }}
166
+ }};
167
+
168
+ // Prevent wheel scrolling on the page background
169
+ wheelHandler = (e) => {{
170
+ const dialogElement = e.target.closest('.ui-dialog');
171
+ if (dialogElement) {{
172
+ // Allow scrolling within dialog elements that can scroll
173
+ let target = e.target;
174
+ while (target && target !== dialogElement) {{
175
+ const overflow = window.getComputedStyle(target).overflow;
176
+ if (overflow === 'auto' || overflow === 'scroll') {{
177
+ return; // Allow internal scrolling
178
+ }}
179
+ target = target.parentElement;
180
+ }}
181
+ // Prevent page scroll but don't stop the event from reaching GeoGebra
182
+ if (!e.target.closest('#{cid}')) {{
183
+ e.preventDefault();
184
+ e.stopPropagation();
185
+ }}
186
+ }}
187
+ }};
188
+
189
+ // Monitor scroll events to maintain position
190
+ window.addEventListener('scroll', scrollHandler, {{ passive: true }});
191
+ document.addEventListener('wheel', wheelHandler, {{ capture: true, passive: false }});
192
+ }}
193
+
194
+ function unlockScroll() {{
195
+ if (!scrollLocked) return;
196
+ scrollLocked = false;
197
+
198
+ if (scrollHandler) {{
199
+ window.removeEventListener('scroll', scrollHandler);
200
+ scrollHandler = null;
201
+ }}
202
+ if (wheelHandler) {{
203
+ document.removeEventListener('wheel', wheelHandler, {{ capture: true }});
204
+ wheelHandler = null;
205
+ }}
206
+ }}
207
+
208
+ // Monitor GeoGebra applet focus/interaction to control scroll lock
209
+ function bindScrollLock() {{
210
+ const ggbContainer = document.getElementById('{cid}');
211
+ if (!ggbContainer) {{
212
+ setTimeout(bindScrollLock, 300);
213
+ return;
214
+ }}
215
+
216
+ // Look for the actual GeoGebra canvas/applet elements
217
+ const ggbApplet = ggbContainer.querySelector('canvas') ||
218
+ ggbContainer.querySelector('.ggbApplet') ||
219
+ ggbContainer.querySelector('[id^="ggbApplet"]') ||
220
+ ggbContainer;
221
+
222
+ if (ggbApplet) {{
223
+ // Lock scroll when interacting with GeoGebra
224
+ ggbApplet.addEventListener('mousedown', lockScroll);
225
+ ggbApplet.addEventListener('focus', lockScroll);
226
+ ggbApplet.addEventListener('click', lockScroll);
227
+
228
+ // Also monitor the container for any focus events
229
+ ggbContainer.addEventListener('focusin', lockScroll);
230
+ ggbContainer.addEventListener('mousedown', lockScroll);
231
+
232
+ // Unlock when clicking outside or losing focus
233
+ document.addEventListener('click', (e) => {{
234
+ if (!ggbContainer.contains(e.target) && !e.target.closest('.ui-dialog')) {{
235
+ unlockScroll();
236
+ }}
237
+ }});
238
+
239
+ ggbContainer.addEventListener('focusout', (e) => {{
240
+ // Small delay to check if focus moved outside the container
241
+ setTimeout(() => {{
242
+ if (!ggbContainer.contains(document.activeElement)) {{
243
+ unlockScroll();
244
+ }}
245
+ }}, 100);
246
+ }});
247
+ }}
248
+ }}
249
+
250
+ // Start monitoring after GeoGebra is loaded
251
+ setTimeout(bindScrollLock, 1000);
252
+
253
+ // Cleanup on dialog close
254
+ $dlg.on('dialogclose', unlockScroll);
255
+
256
+ $("#{button_id}").button()
257
+ .on("click touchend pointerup", e => {{
258
+ e.preventDefault();
259
+ $("#{dialog_id}").dialog("open");
260
+ }});
261
+ }});
262
+ }})();
263
+ </script>
264
+ {wrapper_end}
265
+ """
266
+ return [nodes.raw("", html, format="html")]
267
+
268
+
269
+ def setup(app):
270
+ """Register the directive with Sphinx."""
271
+ app.add_directive("cas-popup", CASPopUpDirective)
272
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
@@ -0,0 +1,137 @@
1
+ """
2
+ Dialogue directive for creating conversation-style content.
3
+
4
+ This directive creates a dialogue between two speakers with chat-bubble styling.
5
+ Each speaker's messages are aligned left or right and styled with different colors.
6
+
7
+ Usage:
8
+ ```{dialogue}
9
+ :name1: Alice
10
+ :name2: Bob
11
+ :speaker1: left
12
+ :speaker2: right
13
+
14
+ Alice: Hello! How are you?
15
+ Bob: I'm great, thanks for asking!
16
+ Alice: That's wonderful to hear.
17
+ ```
18
+
19
+ Options:
20
+ - name1 (required): Name of the first speaker
21
+ - name2 (required): Name of the second speaker
22
+ - speaker1 (required): Alignment for first speaker ("left" or "right")
23
+ - speaker2 (required): Alignment for second speaker ("left" or "right")
24
+
25
+ Content:
26
+ Each line should be in the format: "SpeakerName: Message text"
27
+ Lines not matching this format are ignored.
28
+ Messages can contain math notation and will be parsed as normal content.
29
+ """
30
+
31
+ from docutils import nodes
32
+ from docutils.parsers.rst import Directive, directives
33
+ from sphinx.directives import optional_int
34
+
35
+
36
+ class DialogueWrapperNode(nodes.General, nodes.Element):
37
+ """Container node for the entire dialogue."""
38
+
39
+ pass
40
+
41
+
42
+ class DialogueEntryNode(nodes.Admonition, nodes.Element):
43
+ """Node for a single dialogue entry (one message from one speaker)."""
44
+
45
+ pass
46
+
47
+
48
+ def visit_dialogue_wrapper_node_html(self, node):
49
+ """Open the dialogue container div."""
50
+ self.body.append('<div class="dialogue">')
51
+
52
+
53
+ def depart_dialogue_wrapper_node_html(self, node):
54
+ """Close the dialogue container div."""
55
+ self.body.append("</div>")
56
+
57
+
58
+ def visit_dialogue_entry_node_html(self, node):
59
+ """Open a dialogue entry with speaker name."""
60
+ self.body.append(f'<div class="dialogue-entry {node["css_class"]}">')
61
+ self.body.append(f'<div class="speaker-name">{node["speaker_name"]}</div>')
62
+
63
+
64
+ def depart_dialogue_entry_node_html(self, node):
65
+ """Close the dialogue entry div."""
66
+ self.body.append("</div>")
67
+
68
+
69
+ class DialogueDirective(Directive):
70
+ """
71
+ Directive for creating dialogue between two speakers.
72
+
73
+ Creates chat-bubble styled conversations with left/right alignment.
74
+ """
75
+
76
+ has_content = True
77
+ required_arguments = 0
78
+ option_spec = {
79
+ "name1": directives.unchanged_required,
80
+ "name2": directives.unchanged_required,
81
+ "speaker1": lambda s: directives.choice(s, ("left", "right")),
82
+ "speaker2": lambda s: directives.choice(s, ("left", "right")),
83
+ }
84
+
85
+ def run(self):
86
+ """Parse dialogue content and create dialogue nodes."""
87
+ name1 = self.options.get("name1")
88
+ name2 = self.options.get("name2")
89
+ class1 = "speaker1" if self.options.get("speaker1") == "left" else "speaker2"
90
+ class2 = "speaker1" if self.options.get("speaker2") == "left" else "speaker2"
91
+
92
+ wrapper_node = DialogueWrapperNode()
93
+
94
+ for line in self.content:
95
+ line = line.strip()
96
+ if not line or ":" not in line:
97
+ continue
98
+
99
+ speaker_name, message = line.split(":", 1)
100
+ speaker_name = speaker_name.strip()
101
+ message = message.strip()
102
+
103
+ if speaker_name == name1:
104
+ css_class = class1
105
+ elif speaker_name == name2:
106
+ css_class = class2
107
+ else:
108
+ continue
109
+
110
+ entry_node = DialogueEntryNode()
111
+ entry_node["css_class"] = css_class
112
+ entry_node["speaker_name"] = speaker_name
113
+
114
+ # Parse the message line like normal content so math works
115
+ self.state.nested_parse([message], self.content_offset, entry_node)
116
+ wrapper_node += entry_node
117
+
118
+ return [wrapper_node]
119
+
120
+
121
+ def setup(app):
122
+ """Register the dialogue directive and nodes with Sphinx."""
123
+ app.add_node(
124
+ DialogueWrapperNode,
125
+ html=(visit_dialogue_wrapper_node_html, depart_dialogue_wrapper_node_html),
126
+ )
127
+ app.add_node(
128
+ DialogueEntryNode,
129
+ html=(visit_dialogue_entry_node_html, depart_dialogue_entry_node_html),
130
+ )
131
+ app.add_directive("dialogue", DialogueDirective)
132
+
133
+ return {
134
+ "version": "0.1.0",
135
+ "parallel_read_safe": True,
136
+ "parallel_write_safe": True,
137
+ }