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,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}
|