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,453 @@
1
+ """
2
+ Quiz 2.0 Directive System
3
+ =========================
4
+
5
+ A modular quiz system using nested directives:
6
+ - quiz-2: Main container
7
+ - quiz-question: Individual question with nested content support
8
+ - quiz-answer: Answer option with correct/incorrect flag
9
+
10
+ Features:
11
+ - Supports nested directives (plot, code, etc.) in questions
12
+ - Markdown/MyST formatting support
13
+ - Answer shuffling
14
+ - Math rendering with KaTeX
15
+ - Preserves existing quiz JavaScript functionality
16
+
17
+ Usage:
18
+ -----
19
+ :::::{quiz-2}
20
+
21
+ ::::{quiz-question}
22
+ What is 2 + 2?
23
+
24
+ :::{quiz-answer}
25
+ ---
26
+ correct: false
27
+ ---
28
+ 3
29
+ :::
30
+
31
+ :::{quiz-answer}
32
+ ---
33
+ correct: true
34
+ ---
35
+ 4
36
+ :::
37
+ ::::
38
+
39
+ :::::
40
+ """
41
+
42
+ from docutils import nodes
43
+ from docutils.statemachine import StringList
44
+ from sphinx.util.docutils import SphinxDirective
45
+ from docutils.parsers.rst import directives
46
+ import json
47
+ import uuid
48
+ import html as _html
49
+
50
+
51
+ class Quiz2Directive(SphinxDirective):
52
+ """Main container directive for Quiz 2.0.
53
+
54
+ Collects nested quiz-question directives and generates the quiz HTML.
55
+ """
56
+
57
+ has_content = True
58
+ required_arguments = 0
59
+ option_spec = {
60
+ "shuffle": directives.flag,
61
+ }
62
+
63
+ def run(self):
64
+ # Generate unique ID for this quiz
65
+ self.quiz_id = f"quiz-{uuid.uuid4().hex[:8]}"
66
+
67
+ # Store quiz ID in environment for nested directives
68
+ if not hasattr(self.env, "temp"):
69
+ self.env.temp = {}
70
+
71
+ self.env.temp["current_quiz2_id"] = self.quiz_id
72
+
73
+ # Parse nested content (quiz-question directives)
74
+ container = nodes.container()
75
+ self.state.nested_parse(self.content, self.content_offset, container)
76
+
77
+ # Get questions from environment
78
+ questions_key = f"quiz2_questions_{self.quiz_id}"
79
+ questions = self.env.temp.get(questions_key, [])
80
+
81
+ # Generate HTML output
82
+ container_id = f"quiz-container-{self.quiz_id}"
83
+ html = self._generate_quiz_html(container_id, questions)
84
+
85
+ # Clean up environment
86
+ self.env.temp.pop(questions_key, None)
87
+ if "current_quiz2_id" in self.env.temp:
88
+ self.env.temp.pop("current_quiz2_id")
89
+
90
+ return [nodes.raw("", html, format="html")]
91
+
92
+ def _generate_quiz_html(self, container_id: str, questions: list) -> str:
93
+ """Generate the HTML for the quiz."""
94
+ # Escape </script> and </style> tags to prevent breaking the JSON script tag
95
+ json_str = json.dumps(questions, ensure_ascii=False)
96
+ json_str = json_str.replace("</script>", "<\\/script>").replace("</style>", "<\\/style>")
97
+
98
+ html = f"""
99
+ <!-- Container for the quiz -->
100
+ <div id="{container_id}" class="quiz-main-container"></div>
101
+ <!-- Include KaTeX for LaTeX rendering -->
102
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css">
103
+ <script defer src="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js"></script>
104
+ <script defer src="https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js"></script>
105
+
106
+ <script type="text/javascript">
107
+ document.addEventListener("DOMContentLoaded", () => {{
108
+ // Define your questions and answers
109
+ const questionsData = {json_str};
110
+
111
+ // Initialize the multiple-choice quiz
112
+ const quiz = new SequentialMultipleChoiceQuiz('{container_id}', questionsData);
113
+ }});
114
+ </script>
115
+ """
116
+
117
+ return html
118
+
119
+
120
+ class QuizQuestionDirective(SphinxDirective):
121
+ """Individual question directive for Quiz 2.0.
122
+
123
+ Accepts content that can include any other directives (plot, code, etc.).
124
+ Collects nested quiz-answer directives.
125
+ """
126
+
127
+ has_content = True
128
+ required_arguments = 0
129
+ option_spec = {}
130
+
131
+ def run(self):
132
+ # Get the current quiz ID from the environment
133
+ if not hasattr(self.env, "temp"):
134
+ self.env.temp = {}
135
+
136
+ quiz_id = self.env.temp.get("current_quiz2_id")
137
+ if quiz_id is None:
138
+ error_msg = self.state_machine.reporter.error(
139
+ "quiz-question directive must be used inside a quiz-2 directive",
140
+ nodes.literal_block(self.block_text, self.block_text),
141
+ line=self.lineno,
142
+ )
143
+ return [error_msg]
144
+
145
+ # Generate unique ID for this question
146
+ question_id = f"question-{uuid.uuid4().hex[:8]}"
147
+ self.env.temp["current_quiz_question_id"] = question_id
148
+
149
+ # Parse and render question content
150
+ question_html = self._render_to_html(self.content)
151
+
152
+ # Get answers from environment
153
+ answers_key = f"quiz2_answers_{question_id}"
154
+ answers = self.env.temp.get(answers_key, [])
155
+
156
+ # Store question in environment
157
+ questions_key = f"quiz2_questions_{quiz_id}"
158
+ if questions_key not in self.env.temp:
159
+ self.env.temp[questions_key] = []
160
+
161
+ self.env.temp[questions_key].append({"content": question_html, "answers": answers})
162
+
163
+ # Clean up
164
+ self.env.temp.pop(answers_key, None)
165
+ self.env.temp.pop("current_quiz_question_id", None)
166
+
167
+ # Return empty list - the parent quiz-2 directive will render everything
168
+ return []
169
+
170
+ def _render_to_html(self, content_lines: StringList) -> str:
171
+ """Render content lines to HTML, processing any nested directives."""
172
+ if not content_lines:
173
+ return ""
174
+
175
+ # Create a container node for the content
176
+ container = nodes.container()
177
+
178
+ # Parse the content, which will process nested directives
179
+ self.state.nested_parse(content_lines, self.content_offset, container)
180
+
181
+ # Convert to HTML
182
+ html_parts = []
183
+ for node in container.children:
184
+ html_parts.append(self._node_to_html(node))
185
+
186
+ return "\n".join(html_parts)
187
+
188
+ def _node_to_html(self, node) -> str:
189
+ """Convert a docutils node to HTML string."""
190
+ if isinstance(node, nodes.paragraph):
191
+ content = "".join(self._node_to_html(child) for child in node.children)
192
+ return f"<p>{content}</p>"
193
+
194
+ elif isinstance(node, nodes.Text):
195
+ return _html.escape(str(node))
196
+
197
+ elif isinstance(node, nodes.raw):
198
+ if node.get("format") == "html":
199
+ return node.astext()
200
+ return ""
201
+
202
+ elif isinstance(node, nodes.math):
203
+ # Render inline math using KaTeX-compatible format
204
+ math_text = node.astext()
205
+ return f"${math_text}$"
206
+
207
+ elif isinstance(node, nodes.math_block):
208
+ # Render display math ($$...$$) using KaTeX-compatible format
209
+ math_text = node.astext()
210
+ return f"$${math_text}$$"
211
+
212
+ elif isinstance(node, nodes.image):
213
+ uri = node.get("uri", "")
214
+ alt = node.get("alt", "")
215
+ return f'<img src="{uri}" alt="{alt}" />'
216
+
217
+ elif isinstance(node, nodes.literal_block):
218
+ content = _html.escape(node.astext())
219
+ language = node.get("language", "")
220
+ return f'<pre><code class="{language}">{content}</code></pre>'
221
+
222
+ elif isinstance(node, nodes.figure):
223
+ # Handle figure nodes (e.g., from plot directive)
224
+ content = "".join(self._node_to_html(child) for child in node.children)
225
+ classes = " ".join(node.get("classes", []))
226
+ align = node.get("align", "center")
227
+
228
+ attrs = []
229
+ if classes:
230
+ attrs.append(f'class="{classes}"')
231
+ if align:
232
+ attrs.append(f'align="{align}"')
233
+
234
+ attrs_str = " ".join(attrs) if attrs else ""
235
+ return f"<figure {attrs_str}>{content}</figure>"
236
+
237
+ elif isinstance(node, nodes.caption):
238
+ content = "".join(self._node_to_html(child) for child in node.children)
239
+ return f"<figcaption>{content}</figcaption>"
240
+
241
+ elif isinstance(node, nodes.bullet_list):
242
+ content = "".join(self._node_to_html(child) for child in node.children)
243
+ return f"<ul>{content}</ul>"
244
+
245
+ elif isinstance(node, nodes.enumerated_list):
246
+ content = "".join(self._node_to_html(child) for child in node.children)
247
+ return f"<ol>{content}</ol>"
248
+
249
+ elif isinstance(node, nodes.list_item):
250
+ content = "".join(self._node_to_html(child) for child in node.children)
251
+ return f"<li>{content}</li>"
252
+
253
+ elif isinstance(node, nodes.container):
254
+ content = "".join(self._node_to_html(child) for child in node.children)
255
+ classes = " ".join(node.get("classes", []))
256
+ if classes:
257
+ return f'<div class="{classes}">{content}</div>'
258
+ return content
259
+
260
+ elif hasattr(node, "children"):
261
+ return "".join(self._node_to_html(child) for child in node.children)
262
+
263
+ else:
264
+ return ""
265
+
266
+
267
+ class QuizAnswerDirective(SphinxDirective):
268
+ """Individual answer directive for Quiz 2.0.
269
+
270
+ Accepts front matter (correct: true/false) and content.
271
+ """
272
+
273
+ has_content = True
274
+ required_arguments = 0
275
+ option_spec = {
276
+ "correct": directives.flag,
277
+ }
278
+
279
+ def run(self):
280
+ # Get the current question ID from the environment
281
+ if not hasattr(self.env, "temp"):
282
+ self.env.temp = {}
283
+
284
+ question_id = self.env.temp.get("current_quiz_question_id")
285
+ if question_id is None:
286
+ error_msg = self.state_machine.reporter.error(
287
+ "quiz-answer directive must be used inside a quiz-question directive",
288
+ nodes.literal_block(self.block_text, self.block_text),
289
+ line=self.lineno,
290
+ )
291
+ return [error_msg]
292
+
293
+ # Parse front matter and content
294
+ is_correct, content_lines = self._parse_content()
295
+
296
+ # Use option if provided, otherwise use front matter
297
+ if "correct" in self.options:
298
+ is_correct = True
299
+
300
+ # Render content to HTML
301
+ answer_html = self._render_to_html(content_lines)
302
+
303
+ # Store answer in environment
304
+ answers_key = f"quiz2_answers_{question_id}"
305
+ if answers_key not in self.env.temp:
306
+ self.env.temp[answers_key] = []
307
+
308
+ self.env.temp[answers_key].append({"content": answer_html, "isCorrect": is_correct})
309
+
310
+ # Return empty list
311
+ return []
312
+
313
+ def _parse_content(self):
314
+ """Parse front matter and content from the directive body."""
315
+ is_correct = False
316
+ content_start = 0
317
+
318
+ # Check for YAML front matter (---)
319
+ if len(self.content) > 0 and self.content[0].strip() == "---":
320
+ # Find the closing ---
321
+ end_idx = None
322
+ for i in range(1, len(self.content)):
323
+ if self.content[i].strip() == "---":
324
+ end_idx = i
325
+ break
326
+
327
+ if end_idx is not None:
328
+ # Parse front matter
329
+ for i in range(1, end_idx):
330
+ line = self.content[i].strip()
331
+ if ":" in line:
332
+ key, value = line.split(":", 1)
333
+ key = key.strip().lower()
334
+ value = value.strip().lower()
335
+
336
+ if key == "correct":
337
+ is_correct = value in ["true", "yes", "1"]
338
+
339
+ content_start = end_idx + 1
340
+
341
+ # Get content lines after front matter
342
+ content_lines = self.content[content_start:] if content_start < len(self.content) else []
343
+
344
+ return is_correct, content_lines
345
+
346
+ def _render_to_html(self, content_lines: StringList) -> str:
347
+ """Render content lines to HTML, processing any nested directives."""
348
+ if not content_lines:
349
+ return ""
350
+
351
+ # Create a container node for the content
352
+ container = nodes.container()
353
+
354
+ # Parse the content, which will process nested directives
355
+ self.state.nested_parse(content_lines, self.content_offset, container)
356
+
357
+ # Convert to HTML
358
+ html_parts = []
359
+ for node in container.children:
360
+ html_parts.append(self._node_to_html(node))
361
+
362
+ return "\n".join(html_parts)
363
+
364
+ def _node_to_html(self, node) -> str:
365
+ """Convert a docutils node to HTML string."""
366
+ if isinstance(node, nodes.paragraph):
367
+ content = "".join(self._node_to_html(child) for child in node.children)
368
+ return f"<p>{content}</p>"
369
+
370
+ elif isinstance(node, nodes.Text):
371
+ return _html.escape(str(node))
372
+
373
+ elif isinstance(node, nodes.raw):
374
+ if node.get("format") == "html":
375
+ return node.astext()
376
+ return ""
377
+
378
+ elif isinstance(node, nodes.math):
379
+ math_text = node.astext()
380
+ return f"${math_text}$"
381
+
382
+ elif isinstance(node, nodes.math_block):
383
+ math_text = node.astext()
384
+ return f"$${math_text}$$"
385
+
386
+ elif isinstance(node, nodes.image):
387
+ uri = node.get("uri", "")
388
+ alt = node.get("alt", "")
389
+ return f'<img src="{uri}" alt="{alt}" />'
390
+
391
+ elif isinstance(node, nodes.literal_block):
392
+ content = _html.escape(node.astext())
393
+ language = node.get("language", "")
394
+ return f'<pre><code class="{language}">{content}</code></pre>'
395
+
396
+ elif isinstance(node, nodes.figure):
397
+ content = "".join(self._node_to_html(child) for child in node.children)
398
+ classes = " ".join(node.get("classes", []))
399
+ align = node.get("align", "center")
400
+
401
+ attrs = []
402
+ if classes:
403
+ attrs.append(f'class="{classes}"')
404
+ if align:
405
+ attrs.append(f'align="{align}"')
406
+
407
+ attrs_str = " ".join(attrs) if attrs else ""
408
+ return f"<figure {attrs_str}>{content}</figure>"
409
+
410
+ elif isinstance(node, nodes.caption):
411
+ content = "".join(self._node_to_html(child) for child in node.children)
412
+ return f"<figcaption>{content}</figcaption>"
413
+
414
+ elif isinstance(node, nodes.bullet_list):
415
+ content = "".join(self._node_to_html(child) for child in node.children)
416
+ return f"<ul>{content}</ul>"
417
+
418
+ elif isinstance(node, nodes.enumerated_list):
419
+ content = "".join(self._node_to_html(child) for child in node.children)
420
+ return f"<ol>{content}</ol>"
421
+
422
+ elif isinstance(node, nodes.list_item):
423
+ content = "".join(self._node_to_html(child) for child in node.children)
424
+ return f"<li>{content}</li>"
425
+
426
+ elif isinstance(node, nodes.container):
427
+ content = "".join(self._node_to_html(child) for child in node.children)
428
+ classes = " ".join(node.get("classes", []))
429
+ if classes:
430
+ return f'<div class="{classes}">{content}</div>'
431
+ return content
432
+
433
+ elif hasattr(node, "children"):
434
+ return "".join(self._node_to_html(child) for child in node.children)
435
+
436
+ else:
437
+ return ""
438
+
439
+
440
+ def setup(app):
441
+ """Register the directives with Sphinx."""
442
+ app.add_directive("quiz-2", Quiz2Directive)
443
+ app.add_directive("quiz-question", QuizQuestionDirective)
444
+ app.add_directive("quiz-answer", QuizAnswerDirective)
445
+
446
+ # Reuse the same CSS/JS as original quiz
447
+ try:
448
+ app.add_css_file("munchboka/css/quiz.css")
449
+ app.add_js_file("munchboka/js/quiz.js")
450
+ except Exception:
451
+ pass
452
+
453
+ return {"version": "0.2", "parallel_read_safe": True, "parallel_write_safe": True}