munchboka-edutools 0.1.13__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 +272 -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/factor_tree.py +549 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +165 -0
- munchboka_edutools/directives/horner.py +324 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/multi_plot.py +1126 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3105 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +516 -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 +274 -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 +112 -0
- munchboka_edutools/static/css/github-light.css +120 -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 +529 -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 +312 -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/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -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 +523 -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 +422 -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.1.13.dist-info/METADATA +108 -0
- munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
- munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Escape Room Directive
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
A Sphinx directive for creating interactive escape room puzzles where students must
|
|
6
|
+
solve problems sequentially by entering codes to unlock the next room.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
.. escape-room::
|
|
10
|
+
:case_insensitive:
|
|
11
|
+
|
|
12
|
+
Puzzle: Step 1 title
|
|
13
|
+
Code: ABC123
|
|
14
|
+
Q: Question text can include markdown images and code
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
Puzzle: Step 2 title
|
|
18
|
+
Code: 42, forty-two, XLII
|
|
19
|
+
Q: More content ...
|
|
20
|
+
|
|
21
|
+
Recognized headers (case-insensitive):
|
|
22
|
+
- Puzzle: or Step: - Starts a new puzzle/room
|
|
23
|
+
- Code: - Code(s) to unlock this room (comma-separated for multiple)
|
|
24
|
+
- Q: - Question/content for this room
|
|
25
|
+
|
|
26
|
+
MyST Syntax (colon-fence):
|
|
27
|
+
:::{escaperoom}
|
|
28
|
+
:case_insensitive:
|
|
29
|
+
|
|
30
|
+
Puzzle: Step 1 title
|
|
31
|
+
Code: ABC123
|
|
32
|
+
Q: Question text
|
|
33
|
+
:::
|
|
34
|
+
|
|
35
|
+
Note: Due to MyST limitations with hyphens in directive names when using colon-fence
|
|
36
|
+
syntax (:::), the directive is also registered as "escaperoom" (no hyphen).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import html as _html
|
|
43
|
+
import os
|
|
44
|
+
import re
|
|
45
|
+
import uuid
|
|
46
|
+
from typing import Any, Dict, List
|
|
47
|
+
|
|
48
|
+
from docutils import nodes
|
|
49
|
+
from docutils.parsers.rst import directives
|
|
50
|
+
from sphinx.util.docutils import SphinxDirective
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EscapeRoomDirective(SphinxDirective):
|
|
54
|
+
"""Directive that renders an Escape-Room style sequence of puzzles.
|
|
55
|
+
|
|
56
|
+
Only one question is shown at a time; students must type a code to unlock
|
|
57
|
+
the next. Supports figures and code blocks like quiz/jeopardy.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
has_content = True
|
|
61
|
+
required_arguments = 0
|
|
62
|
+
option_spec = {
|
|
63
|
+
# Placeholder for future options (e.g., case-insensitive codes)
|
|
64
|
+
"case_insensitive": directives.flag,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def run(self):
|
|
68
|
+
self.board_id = uuid.uuid4().hex
|
|
69
|
+
container_id = f"escape-room-{self.board_id}"
|
|
70
|
+
|
|
71
|
+
data = self._parse_content()
|
|
72
|
+
|
|
73
|
+
# Compute relative prefix to _static (same approach as jeopardy)
|
|
74
|
+
source_file = self.state.document["source"]
|
|
75
|
+
source_dir = os.path.dirname(source_file)
|
|
76
|
+
app_src_dir = self.env.srcdir
|
|
77
|
+
depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
|
|
78
|
+
rel_prefix = "../" * (depth + 1)
|
|
79
|
+
|
|
80
|
+
json_str = json.dumps(data, ensure_ascii=False)
|
|
81
|
+
# Escape JSON for safe embedding in a double-quoted attribute
|
|
82
|
+
attr_json = _html.escape(json_str, quote=True)
|
|
83
|
+
|
|
84
|
+
# Note: CSS and JS are registered in __init__.py with munchboka/ prefix
|
|
85
|
+
# KaTeX is also loaded globally, no need to load it per-directive
|
|
86
|
+
html = f"""
|
|
87
|
+
<div id="{container_id}" class="escape-room-container" data-config="{attr_json}">
|
|
88
|
+
<script type="application/json" class="escape-room-data">{json_str}</script>
|
|
89
|
+
</div>
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
return [nodes.raw("", html, format="html")]
|
|
93
|
+
|
|
94
|
+
# -------------------- parsing helpers --------------------
|
|
95
|
+
def _parse_content(self) -> Dict[str, Any]:
|
|
96
|
+
steps: List[Dict[str, Any]] = []
|
|
97
|
+
current: Dict[str, Any] | None = None
|
|
98
|
+
current_section: str | None = None # 'Q'
|
|
99
|
+
|
|
100
|
+
def flush():
|
|
101
|
+
nonlocal current
|
|
102
|
+
if current is not None:
|
|
103
|
+
# normalize
|
|
104
|
+
current.setdefault("title", "")
|
|
105
|
+
codes = current.get("codes") or []
|
|
106
|
+
current["codes"] = [str(c).strip() for c in codes if str(c).strip()]
|
|
107
|
+
current.setdefault("question", "")
|
|
108
|
+
# post-process code blocks
|
|
109
|
+
current["question"] = self._process_code_blocks(current["question"])
|
|
110
|
+
steps.append(current)
|
|
111
|
+
current = None
|
|
112
|
+
|
|
113
|
+
for raw in self.content:
|
|
114
|
+
line = self._process_figures(raw) or raw
|
|
115
|
+
s = line.rstrip("\n")
|
|
116
|
+
|
|
117
|
+
# Puzzle/Step header
|
|
118
|
+
m = re.match(r"^\s*(?:Puzzle|Step)\s*:\s*(.+?)\s*$", s, flags=re.IGNORECASE)
|
|
119
|
+
if m:
|
|
120
|
+
flush()
|
|
121
|
+
current = {"title": m.group(1).strip(), "codes": [], "question": ""}
|
|
122
|
+
current_section = None
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Code line (allow comma-separated)
|
|
126
|
+
m = re.match(r"^\s*Code\s*:\s*(.+?)\s*$", s, flags=re.IGNORECASE)
|
|
127
|
+
if m and current is not None:
|
|
128
|
+
codes_str = m.group(1).strip()
|
|
129
|
+
codes = [c.strip() for c in re.split(r",|;", codes_str)] if codes_str else []
|
|
130
|
+
current["codes"] = codes
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Q: starts question block
|
|
134
|
+
m = re.match(r"^\s*Q\s*:\s*(.*)$", s, flags=re.IGNORECASE)
|
|
135
|
+
if m and current is not None:
|
|
136
|
+
current_section = "Q"
|
|
137
|
+
current["question"] = (current.get("question") or "") + m.group(1)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Continuation
|
|
141
|
+
if current_section == "Q" and current is not None:
|
|
142
|
+
current["question"] = (current.get("question") or "") + "\n" + s
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
flush()
|
|
146
|
+
|
|
147
|
+
case_insensitive = "case_insensitive" in self.options
|
|
148
|
+
return {"steps": steps, "caseInsensitive": bool(case_insensitive)}
|
|
149
|
+
|
|
150
|
+
def _process_figures(self, text):
|
|
151
|
+
"""Copy images to _static/figurer/... and rewrite to HTML, like quiz/jeopardy."""
|
|
152
|
+
import shutil
|
|
153
|
+
import json as _json
|
|
154
|
+
|
|
155
|
+
if not hasattr(self, "_image_counter"):
|
|
156
|
+
self._image_counter = 0
|
|
157
|
+
|
|
158
|
+
def _parse_figure_options(alt_text):
|
|
159
|
+
# Support JSON-like {width: 60%, class: adaptive-figure} and key=value forms
|
|
160
|
+
opts = {}
|
|
161
|
+
s = (alt_text or "").strip()
|
|
162
|
+
|
|
163
|
+
def parse_pairs(text: str):
|
|
164
|
+
for m in re.finditer(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s}]+))', text):
|
|
165
|
+
val = m.group(2) or m.group(3) or m.group(4) or ""
|
|
166
|
+
opts[m.group(1)] = val
|
|
167
|
+
|
|
168
|
+
if s.startswith("{") and s.endswith("}"):
|
|
169
|
+
inner = s[1:-1].strip()
|
|
170
|
+
ok = False
|
|
171
|
+
try:
|
|
172
|
+
js = s
|
|
173
|
+
js = re.sub(r"(\w+)\s*:", r'"\1":', js)
|
|
174
|
+
js = re.sub(r':\s*([^",}]+)', r': "\1"', js)
|
|
175
|
+
opts.update(_json.loads(js))
|
|
176
|
+
ok = True
|
|
177
|
+
except Exception:
|
|
178
|
+
ok = False
|
|
179
|
+
if not ok:
|
|
180
|
+
parse_pairs(inner)
|
|
181
|
+
else:
|
|
182
|
+
parse_pairs(s)
|
|
183
|
+
if not opts and s:
|
|
184
|
+
opts["alt"] = s
|
|
185
|
+
return opts
|
|
186
|
+
|
|
187
|
+
def _normalize_wh(val: Any) -> str:
|
|
188
|
+
s = str(val).strip()
|
|
189
|
+
if re.fullmatch(r"\d+(?:\.\d+)?", s):
|
|
190
|
+
return f"{s}px"
|
|
191
|
+
s = re.sub(r"\s+(?=(px|%|em|rem|vh|vw)$)", "", s)
|
|
192
|
+
return s
|
|
193
|
+
|
|
194
|
+
def _build_figure_html(html_img_path, options):
|
|
195
|
+
user_opts = dict(options or {})
|
|
196
|
+
user_class = user_opts.pop("class", "").strip()
|
|
197
|
+
classes = "escape-room-image adaptive-figure" + (f" {user_class}" if user_class else "")
|
|
198
|
+
alt_text = user_opts.pop("alt", "Figure")
|
|
199
|
+
title = user_opts.pop("title", None)
|
|
200
|
+
width = user_opts.pop("width", None)
|
|
201
|
+
height = user_opts.pop("height", None)
|
|
202
|
+
extra_style = user_opts.pop("style", None)
|
|
203
|
+
|
|
204
|
+
styles: List[str] = []
|
|
205
|
+
if width is not None:
|
|
206
|
+
styles.append(f"width: {_normalize_wh(width)};")
|
|
207
|
+
if height is not None:
|
|
208
|
+
styles.append(f"height: {_normalize_wh(height)};")
|
|
209
|
+
if extra_style:
|
|
210
|
+
styles.append(str(extra_style))
|
|
211
|
+
|
|
212
|
+
attrs = [
|
|
213
|
+
f'src="{html_img_path}"',
|
|
214
|
+
f'class="{classes}"',
|
|
215
|
+
f'alt="{alt_text}"',
|
|
216
|
+
]
|
|
217
|
+
if title:
|
|
218
|
+
attrs.append(f'title="{title}"')
|
|
219
|
+
if styles:
|
|
220
|
+
attrs.append(f'style="{' '.join(styles)}"')
|
|
221
|
+
for k, v in user_opts.items():
|
|
222
|
+
if k not in {
|
|
223
|
+
"src",
|
|
224
|
+
"class",
|
|
225
|
+
"alt",
|
|
226
|
+
"title",
|
|
227
|
+
"width",
|
|
228
|
+
"height",
|
|
229
|
+
"style",
|
|
230
|
+
}:
|
|
231
|
+
attrs.append(f'{k}="{v}"')
|
|
232
|
+
|
|
233
|
+
img = f"<img {' '.join(attrs)}>"
|
|
234
|
+
return f'<div class="escape-room-image-container">{img}</div>'
|
|
235
|
+
|
|
236
|
+
def replace(m):
|
|
237
|
+
alt_or_opts = m.group(1).strip()
|
|
238
|
+
raw_src = m.group(2)
|
|
239
|
+
|
|
240
|
+
self._image_counter += 1
|
|
241
|
+
|
|
242
|
+
options = _parse_figure_options(alt_or_opts)
|
|
243
|
+
|
|
244
|
+
source_file = self.state.document["source"]
|
|
245
|
+
source_dir = os.path.dirname(source_file)
|
|
246
|
+
app_src_dir = self.env.srcdir
|
|
247
|
+
|
|
248
|
+
abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
|
|
249
|
+
if not os.path.exists(abs_fig_src):
|
|
250
|
+
return f'<img src="{raw_src}" class="escape-room-image adaptive-figure" alt="Figure (missing)">' # noqa: E501
|
|
251
|
+
|
|
252
|
+
relative_doc_path = os.path.relpath(source_dir, app_src_dir)
|
|
253
|
+
figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
|
|
254
|
+
os.makedirs(figure_dest_dir, exist_ok=True)
|
|
255
|
+
|
|
256
|
+
rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
|
|
257
|
+
safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
|
|
258
|
+
base, ext = os.path.splitext(safe_path)
|
|
259
|
+
fig_filename = f"{self.board_id}_img{self._image_counter}_{base}{ext}"
|
|
260
|
+
fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
|
|
261
|
+
shutil.copy2(abs_fig_src, fig_dest_path)
|
|
262
|
+
|
|
263
|
+
depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
|
|
264
|
+
rel_prefix = "../" * (depth + 1)
|
|
265
|
+
html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
|
|
266
|
+
return _build_figure_html(html_img_path, options)
|
|
267
|
+
|
|
268
|
+
return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
|
|
269
|
+
|
|
270
|
+
def _process_code_blocks(self, text: str) -> str:
|
|
271
|
+
"""Process HTML code blocks to handle escaped newlines (like quiz)."""
|
|
272
|
+
|
|
273
|
+
def replace_newlines(match):
|
|
274
|
+
lang = match.group(1)
|
|
275
|
+
code = match.group(2)
|
|
276
|
+
code = code.replace("\\n", "\n")
|
|
277
|
+
return f'<pre><code class="{lang}">{code}</code></pre>'
|
|
278
|
+
|
|
279
|
+
pattern = r'<pre><code class="([\w-]+)">(.*?)</code></pre>'
|
|
280
|
+
return re.sub(pattern, replace_newlines, text, flags=re.DOTALL)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def setup(app):
|
|
284
|
+
"""
|
|
285
|
+
Setup the escape-room directive.
|
|
286
|
+
|
|
287
|
+
Registers the directive under two names:
|
|
288
|
+
- "escape-room" for RST compatibility
|
|
289
|
+
- "escaperoom" for MyST colon-fence compatibility (no hyphens allowed)
|
|
290
|
+
|
|
291
|
+
Note: CSS and JS files are registered in __init__.py with the munchboka/ prefix
|
|
292
|
+
"""
|
|
293
|
+
app.add_directive("escape-room", EscapeRoomDirective)
|
|
294
|
+
app.add_directive("escaperoom", EscapeRoomDirective) # MyST compatibility
|
|
295
|
+
|
|
296
|
+
return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
|