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,75 @@
1
+ from docutils import nodes
2
+ from docutils.parsers.rst import Directive, directives
3
+ import re
4
+ import uuid
5
+
6
+
7
+ class InteractiveCodeDirective(Directive):
8
+ has_content = True
9
+ required_arguments = 0 # The unique identifier
10
+ optional_arguments = 1
11
+ final_argument_whitespace = True
12
+ option_spec = {
13
+ "lang": directives.unchanged,
14
+ "predict": directives.flag,
15
+ }
16
+
17
+ def run(self):
18
+ # Get the unique identifier from arguments
19
+ # Generate a unique identifier or use the provided one
20
+ if self.arguments:
21
+ identifier = self.arguments[0]
22
+ else:
23
+ identifier = f"code-{uuid.uuid4().hex[:8]}"
24
+
25
+ container_id = f"container-{identifier}"
26
+
27
+ # Get code content from the directive content
28
+ code_content = "\n".join(self.content)
29
+
30
+ # Escape code for JavaScript
31
+ escaped_code = code_content.replace("`", "\\`").replace("$", "\\$")
32
+
33
+ is_prediction = "predict" in self.options
34
+ # Choose the appropriate function based on the predict flag
35
+ function_name = "makePredictionInteractiveCode" if is_prediction else "makeInteractiveCode"
36
+
37
+ # Create the HTML with the template
38
+ html = f"""
39
+ <div id="{container_id}"></div>
40
+ <script type="text/javascript">
41
+ document.addEventListener("DOMContentLoaded", () => {{
42
+ const code =
43
+ `{escaped_code}`;
44
+
45
+ {function_name}(
46
+ "{container_id}",
47
+ code,
48
+ );
49
+ }});
50
+ </script>
51
+ """
52
+
53
+ raw_node = nodes.raw("", html, format="html")
54
+ return [raw_node]
55
+
56
+
57
+ def setup(app):
58
+ app.add_directive("interactive-code", InteractiveCodeDirective)
59
+
60
+ # Ensure assets loaded even if submodule loaded directly
61
+ try:
62
+ app.add_css_file("munchboka/css/interactive_code.css")
63
+ app.add_js_file("munchboka/js/interactiveCode/interactiveCodeSetup.js")
64
+ app.add_js_file("munchboka/js/interactiveCode/codeEditor.js")
65
+ app.add_js_file("munchboka/js/interactiveCode/pythonRunner.js")
66
+ app.add_js_file("munchboka/js/interactiveCode/workerManager.js")
67
+ app.add_js_file("munchboka/js/interactiveCode/turtleCode.js")
68
+ except Exception:
69
+ pass
70
+
71
+ return {
72
+ "version": "0.1",
73
+ "parallel_read_safe": True,
74
+ "parallel_write_safe": True,
75
+ }
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import html as _html
4
+ import json
5
+ import os
6
+ import re
7
+ import uuid
8
+ from typing import Any, Dict, List
9
+
10
+ from docutils import nodes
11
+ from docutils.parsers.rst import directives
12
+ from sphinx.util.docutils import SphinxDirective
13
+
14
+
15
+ class JeopardyDirective(SphinxDirective):
16
+ has_content = True
17
+ required_arguments = 0
18
+ option_spec = {
19
+ "teams": directives.unchanged,
20
+ }
21
+
22
+ def run(self):
23
+ self.board_id = uuid.uuid4().hex
24
+ container_id = f"jeopardy-{self.board_id}"
25
+
26
+ data = self._parse_board()
27
+
28
+ # Compute relative prefix to _static (if needed later)
29
+ source_file = self.state.document["source"]
30
+ source_dir = os.path.dirname(source_file)
31
+ app_src_dir = self.env.srcdir
32
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
33
+ rel_prefix = "../" * (depth + 1)
34
+
35
+ # Prepare config both as data-config (HTML-escaped) and inline JSON script inside the container
36
+ cfg_str_attr = _html.escape(json.dumps(data, ensure_ascii=False), quote=True)
37
+ json_str = json.dumps(data, ensure_ascii=False)
38
+
39
+ # Include KaTeX like the quiz/legacy extension to ensure math renders
40
+ html = f"""
41
+ <div id="{container_id}" class=\"jeopardy-container\" lang=\"no\" data-config=\"{cfg_str_attr}\">
42
+ <script type=\"application/json\" class=\"jeopardy-data\">{json_str}</script>
43
+ </div>
44
+ <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css\">
45
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js\"></script>
46
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js\"></script>
47
+ """
48
+ return [nodes.raw("", html, format="html")]
49
+
50
+ def _parse_board(self) -> Dict[str, Any]:
51
+ teams_opt = self.options.get("teams")
52
+ try:
53
+ teams = max(1, int(str(teams_opt).strip())) if teams_opt is not None else 2
54
+ except Exception:
55
+ teams = 2
56
+
57
+ categories: List[Dict[str, Any]] = []
58
+ current_cat: Dict[str, Any] | None = None
59
+ current_tile: Dict[str, Any] | None = None
60
+ current_section: str | None = None
61
+ values_set = set()
62
+
63
+ def flush_tile():
64
+ nonlocal current_tile
65
+ if current_cat is not None and current_tile is not None:
66
+ for k in ("question", "answer"):
67
+ if current_tile.get(k) is None:
68
+ current_tile[k] = ""
69
+ (current_cat.setdefault("tiles", [])).append(current_tile)
70
+ current_tile = None
71
+
72
+ for raw in self.content:
73
+ line = self._process_figures(raw)
74
+ if line is None:
75
+ line = raw
76
+ s = line.rstrip("\n")
77
+
78
+ m = re.match(r"^\s*Category\s*:\s*(.+?)\s*$", s, flags=re.IGNORECASE)
79
+ if m:
80
+ flush_tile()
81
+ current_cat = {"name": m.group(1).strip(), "tiles": []}
82
+ categories.append(current_cat)
83
+ current_section = None
84
+ continue
85
+
86
+ m = re.match(r"^\s*(\d+)\s*:\s*$", s)
87
+ if m and current_cat is not None:
88
+ flush_tile()
89
+ v = int(m.group(1))
90
+ values_set.add(v)
91
+ current_tile = {"value": v, "question": "", "answer": ""}
92
+ current_section = None
93
+ continue
94
+
95
+ m = re.match(r"^\s*Q\s*:\s*(.*)$", s, flags=re.IGNORECASE)
96
+ if m and current_tile is not None:
97
+ current_section = "Q"
98
+ current_tile["question"] = (current_tile.get("question") or "") + m.group(1)
99
+ continue
100
+ m = re.match(r"^\s*A\s*:\s*(.*)$", s, flags=re.IGNORECASE)
101
+ if m and current_tile is not None:
102
+ current_section = "A"
103
+ current_tile["answer"] = (current_tile.get("answer") or "") + m.group(1)
104
+ continue
105
+
106
+ if current_section == "Q" and current_tile is not None:
107
+ current_tile["question"] = (current_tile.get("question") or "") + "\n" + s
108
+ continue
109
+ if current_section == "A" and current_tile is not None:
110
+ current_tile["answer"] = (current_tile.get("answer") or "") + "\n" + s
111
+ continue
112
+
113
+ flush_tile()
114
+
115
+ values = sorted(values_set)
116
+ for cat in categories:
117
+ by_val = {t.get("value"): t for t in (cat.get("tiles") or [])}
118
+ cat["tiles"] = [by_val.get(v) for v in values if v in by_val]
119
+
120
+ for cat in categories:
121
+ for t in cat.get("tiles") or []:
122
+ for key in ("question", "answer"):
123
+ s = t.get(key) or ""
124
+ t[key] = self._process_code_blocks(s)
125
+
126
+ return {"teams": teams, "categories": categories, "values": values}
127
+
128
+ def _process_figures(self, text):
129
+ import shutil
130
+ import json as _json
131
+
132
+ if not hasattr(self, "_image_counter"):
133
+ self._image_counter = 0
134
+
135
+ def _parse_figure_options(alt_text):
136
+ opts: Dict[str, Any] = {}
137
+ s = (alt_text or "").strip()
138
+
139
+ def parse_pairs(text: str):
140
+ for m in re.finditer(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s}]+))', text):
141
+ val = m.group(2) or m.group(3) or m.group(4) or ""
142
+ opts[m.group(1)] = val
143
+
144
+ if s.startswith("{") and s.endswith("}"):
145
+ inner = s[1:-1].strip()
146
+ ok = False
147
+ try:
148
+ js = s
149
+ js = re.sub(r"(\w+)\s*:", r'"\\1":', js)
150
+ js = re.sub(r':\s*([^",}]+)', r': "\\1"', js)
151
+ opts.update(_json.loads(js))
152
+ ok = True
153
+ except Exception:
154
+ ok = False
155
+ if not ok:
156
+ parse_pairs(inner)
157
+ else:
158
+ parse_pairs(s)
159
+ if not opts and s:
160
+ opts["alt"] = s
161
+ return opts
162
+
163
+ def _build_figure_html(html_img_path, options):
164
+ user_opts = dict(options or {})
165
+ user_class = user_opts.pop("class", "").strip()
166
+ classes = "jeopardy-image adaptive-figure" + (f" {user_class}" if user_class else "")
167
+
168
+ alt_text = user_opts.pop("alt", "Figure")
169
+ title = user_opts.pop("title", None)
170
+ width = user_opts.pop("width", None)
171
+ height = user_opts.pop("height", None)
172
+ extra_style = user_opts.pop("style", None)
173
+
174
+ def _normalize_wh(val: Any) -> str:
175
+ s = str(val).strip()
176
+ if re.fullmatch(r"\d+(?:\.\d+)?", s):
177
+ return f"{s}px"
178
+ s = re.sub(r"\s+(?=(px|%|em|rem|vh|vw)$)", "", s)
179
+ return s
180
+
181
+ styles: List[str] = []
182
+ if width is not None:
183
+ styles.append(f"width: {_normalize_wh(width)};")
184
+ if height is not None:
185
+ styles.append(f"height: {_normalize_wh(height)};")
186
+ if extra_style:
187
+ styles.append(str(extra_style))
188
+
189
+ attrs = [f'src="{html_img_path}"', f'class="{classes}"', f'alt="{alt_text}"']
190
+ if title:
191
+ attrs.append(f'title="{title}"')
192
+ if styles:
193
+ style_str = " ".join(styles)
194
+ attrs.append(f'style="{style_str}"')
195
+ for k, v in user_opts.items():
196
+ if k not in {"src", "class", "alt", "title", "width", "height", "style"}:
197
+ attrs.append(f'{k}="{v}"')
198
+ img = f"<img {' '.join(attrs)} >"
199
+ return f'<div class="jeopardy-image-container">{img}</div>'
200
+
201
+ def replace(m):
202
+ alt_or_opts = m.group(1).strip()
203
+ raw_src = m.group(2)
204
+ self._image_counter += 1
205
+ options = _parse_figure_options(alt_or_opts)
206
+
207
+ source_file = self.state.document["source"]
208
+ source_dir = os.path.dirname(source_file)
209
+ app_src_dir = self.env.srcdir
210
+
211
+ abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
212
+ if not os.path.exists(abs_fig_src):
213
+ return f'<img src="{raw_src}" class="jeopardy-image adaptive-figure" alt="Figure (missing)">'
214
+
215
+ relative_doc_path = os.path.relpath(source_dir, app_src_dir)
216
+ figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
217
+ os.makedirs(figure_dest_dir, exist_ok=True)
218
+
219
+ rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
220
+ safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
221
+ base, ext = os.path.splitext(safe_path)
222
+ fig_filename = f"{self.board_id}_img{self._image_counter}_{base}{ext}"
223
+ fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
224
+ shutil.copy2(abs_fig_src, fig_dest_path)
225
+
226
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
227
+ rel_prefix = "../" * (depth + 1)
228
+ html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
229
+ return _build_figure_html(html_img_path, options)
230
+
231
+ return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
232
+
233
+ def _process_code_blocks(self, text: str) -> str:
234
+ def replace_newlines(match):
235
+ code = match.group(2).replace("\\n", "\n")
236
+ lang = match.group(1)
237
+ return f'<pre><code class="{lang}">{code}</code></pre>'
238
+
239
+ pattern = r'<pre><code class="([\w-]+)">(.*?)</code></pre>'
240
+ return re.sub(pattern, replace_newlines, text, flags=re.DOTALL)
241
+
242
+
243
+ def setup(app):
244
+ app.add_directive("jeopardy", JeopardyDirective)
245
+ # Ensure assets loaded even if submodule loaded directly
246
+ try:
247
+ app.add_css_file("munchboka/css/jeopardy.css")
248
+ app.add_js_file("munchboka/js/jeopardy.js")
249
+ app.add_css_file("munchboka/css/general_style.css")
250
+ except Exception:
251
+ pass
252
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}