munchboka-edutools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. munchboka_edutools/__init__.py +182 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +356 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/dialogue.py +137 -0
  8. munchboka_edutools/directives/escape_room.py +296 -0
  9. munchboka_edutools/directives/factor_tree.py +549 -0
  10. munchboka_edutools/directives/ggb.py +209 -0
  11. munchboka_edutools/directives/ggb_icon.py +62 -0
  12. munchboka_edutools/directives/ggb_popup.py +165 -0
  13. munchboka_edutools/directives/horner.py +324 -0
  14. munchboka_edutools/directives/interactive_code.py +75 -0
  15. munchboka_edutools/directives/jeopardy.py +252 -0
  16. munchboka_edutools/directives/multi_plot.py +1126 -0
  17. munchboka_edutools/directives/pair_puzzle.py +191 -0
  18. munchboka_edutools/directives/parsons.py +109 -0
  19. munchboka_edutools/directives/plot.py +3012 -0
  20. munchboka_edutools/directives/poly_icon.py +91 -0
  21. munchboka_edutools/directives/polydiv.py +344 -0
  22. munchboka_edutools/directives/quiz.py +291 -0
  23. munchboka_edutools/directives/signchart.py +474 -0
  24. munchboka_edutools/directives/timed_quiz.py +436 -0
  25. munchboka_edutools/directives/turtle.py +157 -0
  26. munchboka_edutools/static/css/admonitions.css +2012 -0
  27. munchboka_edutools/static/css/cas_popup.css +242 -0
  28. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  29. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  33. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  34. munchboka_edutools/static/css/dialogue.css +92 -0
  35. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  36. munchboka_edutools/static/css/figures.css +274 -0
  37. munchboka_edutools/static/css/general_style.css +74 -0
  38. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  39. munchboka_edutools/static/css/github-dark.css +112 -0
  40. munchboka_edutools/static/css/github-light.css +120 -0
  41. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  42. munchboka_edutools/static/css/interactive_code.css +582 -0
  43. munchboka_edutools/static/css/jeopardy.css +476 -0
  44. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  45. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  46. munchboka_edutools/static/css/quiz.css +312 -0
  47. munchboka_edutools/static/css/timedQuiz.css +375 -0
  48. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  49. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  50. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  51. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  55. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  56. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  57. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  58. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  74. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  75. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  76. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  81. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  82. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  84. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  85. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  88. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  91. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  92. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  93. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  94. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  96. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  97. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  98. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  108. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  109. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  110. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  112. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  113. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  115. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  116. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  118. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  119. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  120. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  121. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  122. munchboka_edutools/static/js/casThemeManager.js +99 -0
  123. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  124. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  125. munchboka_edutools/static/js/highlight-init.js +6 -0
  126. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  127. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  128. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  129. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  130. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  131. munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
  132. munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
  133. munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
  134. munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
  135. munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
  136. munchboka_edutools/static/js/jeopardy.js +457 -0
  137. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  138. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  139. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  140. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  141. munchboka_edutools/static/js/quiz.js +422 -0
  142. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  143. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  144. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  145. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  146. munchboka_edutools/static/js/utils.js +3 -0
  147. munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
  148. munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
  149. munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
  150. munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,182 @@
1
+ from ._version import __version__
2
+
3
+ import importlib
4
+ import pkgutil
5
+ import os
6
+ from importlib.resources import files
7
+
8
+
9
+ def _copy_static(app):
10
+ """Copy packaged static assets to the builder's _static directory and register defaults."""
11
+ try:
12
+ static_root = files(__name__) / "static"
13
+ if not static_root.exists():
14
+ return
15
+
16
+ out_static_root = os.path.join(app.outdir, "_static", "munchboka")
17
+ os.makedirs(out_static_root, exist_ok=True)
18
+
19
+ # Recursively copy packaged static files
20
+ for path in static_root.rglob("*"):
21
+ if path.is_file():
22
+ rel = path.relative_to(static_root)
23
+ dest = os.path.join(out_static_root, str(rel))
24
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
25
+ with path.open("rb") as src, open(dest, "wb") as dst:
26
+ dst.write(src.read())
27
+
28
+ # Register a couple of common defaults if present
29
+ def _register(relpath: str, priority: int = 500):
30
+ full = os.path.join(out_static_root, relpath)
31
+ if os.path.exists(full):
32
+ if relpath.endswith(".css"):
33
+ app.add_css_file(f"munchboka/{relpath}", priority=priority)
34
+ elif relpath.endswith(".js"):
35
+ app.add_js_file(f"munchboka/{relpath}", priority=priority)
36
+
37
+ # Register CSS files (including CodeMirror CSS for interactive code)
38
+ app.add_css_file(
39
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"
40
+ )
41
+ # jQuery UI CSS for dialog/popup functionality
42
+ app.add_css_file("https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css")
43
+ # KaTeX CSS for math rendering (required by pair-puzzle directive)
44
+ app.add_css_file("https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css")
45
+ _register("css/admonitions.css")
46
+ _register("css/dialogue.css")
47
+ _register("css/figures.css")
48
+ _register("css/interactive_code.css")
49
+ _register("css/jeopardy.css")
50
+ _register("css/general_style.css")
51
+ _register("css/quiz.css")
52
+ _register("css/timedQuiz.css")
53
+ _register("css/cas_popup.css")
54
+ _register("css/github-light.css")
55
+ _register("css/github-dark.css")
56
+ _register("css/github-dark-high-contrast.css")
57
+ _register("css/pairPuzzle/style.css")
58
+ _register("css/escapeRoom/escape-room.css")
59
+ _register("css/parsons/parsonsPuzzle.css")
60
+
61
+ # Register JS files with explicit priorities to match matematikk_r1 loading order:
62
+ # 1. utils.js (early - priority 450)
63
+ # 2. KaTeX (priority 490-495 - before pair-puzzle)
64
+ # 3. quiz.js, jeopardy.js (priority 500 - default)
65
+ # 4. interactive code scripts (priority 500 - default)
66
+ # 5. pair-puzzle scripts (priority 700-720)
67
+ # 6. jQuery, jQuery UI, and GeoGebra (priority 800-900)
68
+ # 7. CodeMirror (last - priority 900)
69
+ _register("js/utils.js", priority=450)
70
+ _register("js/casThemeManager.js", priority=480) # Before jQuery UI for theme handling
71
+
72
+ # Add KaTeX before pair-puzzle scripts (which depend on it)
73
+ app.add_js_file(
74
+ "https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js",
75
+ priority=490,
76
+ )
77
+ app.add_js_file(
78
+ "https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js",
79
+ priority=495,
80
+ )
81
+
82
+ _register("js/jeopardy.js")
83
+ _register("js/quiz.js")
84
+ _register("js/timedQuiz/utils.js")
85
+ _register("js/timedQuiz/multipleChoiceQuestion.js")
86
+ _register("js/timedQuiz/timedMultipleChoiceQuiz.js")
87
+ _register("js/interactiveCode/pythonRunner.js")
88
+ _register("js/interactiveCode/codeEditor.js")
89
+ _register("js/interactiveCode/interactiveCodeSetup.js")
90
+ _register("js/interactiveCode/workerManager.js")
91
+ # Skulpt must be loaded before turtleCode.js (lower priority = loaded first)
92
+ _register("js/skulpt/skulpt.js", priority=490)
93
+ _register("js/interactiveCode/turtleCode.js")
94
+ _register("js/pairPuzzle/draggableItem.js", priority=700)
95
+ _register("js/pairPuzzle/dropZone.js", priority=710)
96
+ _register("js/pairPuzzle/game.js", priority=720)
97
+ _register("js/escapeRoom/escape-room.js", priority=730)
98
+ _register("js/parsons/parsonsPuzzle.js", priority=740)
99
+
100
+ # Add jQuery and jQuery UI for dialog functionality
101
+ app.add_js_file(
102
+ "https://code.jquery.com/jquery-3.6.0.min.js",
103
+ priority=800,
104
+ )
105
+ app.add_js_file(
106
+ "https://code.jquery.com/ui/1.13.2/jquery-ui.min.js",
107
+ priority=850,
108
+ )
109
+
110
+ # Add highlight.js for syntax highlighting in Parsons puzzles and other directives
111
+ app.add_css_file(
112
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/github.min.css",
113
+ id="highlight-theme-light",
114
+ )
115
+ app.add_css_file(
116
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/github-dark.min.css",
117
+ id="highlight-theme-dark",
118
+ )
119
+ app.add_js_file(
120
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js",
121
+ priority=855,
122
+ )
123
+ _register("js/highlight-init.js", priority=856)
124
+
125
+ # Add GeoGebra API
126
+ app.add_js_file(
127
+ "https://www.geogebra.org/apps/deployggb.js",
128
+ priority=860,
129
+ )
130
+ _register("js/geogebra-setup.js", priority=870)
131
+
132
+ # Add CodeMirror LAST with high priority so it loads at the end
133
+ app.add_js_file(
134
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js",
135
+ priority=900,
136
+ )
137
+ app.add_js_file(
138
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js",
139
+ priority=900,
140
+ )
141
+ except Exception as exc: # pragma: no cover - non-fatal
142
+ try:
143
+ from sphinx.util import logging
144
+
145
+ logging.getLogger(__name__).warning(
146
+ "munchboka_edutools: failed to copy static assets: %s", exc
147
+ )
148
+ except Exception:
149
+ pass
150
+
151
+
152
+ def setup(app):
153
+ """
154
+ Sphinx entry point for the extension.
155
+
156
+ - Copies packaged static assets on builder init
157
+ - Auto-loads all modules under munchboka_edutools.directives that expose setup(app)
158
+ """
159
+ # Ensure packaged static assets are available for HTML builder
160
+ app.connect("builder-inited", _copy_static)
161
+
162
+ # Auto discover and load directive modules
163
+ pkg_prefix = __name__ + ".directives"
164
+ try:
165
+ pkg = importlib.import_module(pkg_prefix)
166
+ for modinfo in pkgutil.iter_modules(pkg.__path__, pkg_prefix + "."):
167
+ app.setup_extension(modinfo.name)
168
+ except Exception as exc: # pragma: no cover - non-fatal
169
+ try:
170
+ from sphinx.util import logging
171
+
172
+ logging.getLogger(__name__).warning(
173
+ "munchboka_edutools: failed to auto-load directives: %s", exc
174
+ )
175
+ except Exception:
176
+ pass
177
+
178
+ return {
179
+ "version": __version__,
180
+ "parallel_read_safe": True,
181
+ "parallel_write_safe": True,
182
+ }
@@ -0,0 +1,126 @@
1
+ """Minimal plotmath shim for development and tests.
2
+
3
+ This is NOT a full replacement for the real plotmath package. It implements
4
+ just enough for the plot directive tests to run:
5
+ - plot(): create fig/ax and set up ticks/grid/limits.
6
+ - annotate(): arrow annotation on current axes.
7
+ - polygon(): draw edges and/or filled polygon.
8
+ - make_bar(): draw a simple double-headed measurement bar.
9
+ - COLORS: a small named color palette.
10
+
11
+ If the real plotmath is installed, the directive will use that instead.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Iterable, Tuple, List, Optional
17
+
18
+ import matplotlib
19
+
20
+ matplotlib.use("Agg")
21
+ import matplotlib.pyplot as plt
22
+
23
+ COLORS = {
24
+ "red": "#d62728",
25
+ "blue": "#1f77b4",
26
+ "green": "#2ca02c",
27
+ "orange": "#ff7f0e",
28
+ "purple": "#9467bd",
29
+ "teal": "#17becf",
30
+ "black": "#000000",
31
+ }
32
+
33
+
34
+ def plot(
35
+ functions: List = None,
36
+ fn_labels: bool = False,
37
+ xmin: float = -6,
38
+ xmax: float = 6,
39
+ ymin: float = -6,
40
+ ymax: float = 6,
41
+ xstep: float = 1,
42
+ ystep: float = 1,
43
+ ticks: bool = True,
44
+ grid: bool = True,
45
+ lw: float = 2.5,
46
+ alpha: float | None = None,
47
+ fontsize: int = 20,
48
+ ):
49
+ fig, ax = plt.subplots()
50
+ ax.set_xlim(xmin, xmax)
51
+ ax.set_ylim(ymin, ymax)
52
+ if ticks:
53
+ try:
54
+ ax.set_xticks([x for x in frange(xmin, xmax, xstep)])
55
+ ax.set_yticks([y for y in frange(ymin, ymax, ystep)])
56
+ except Exception:
57
+ pass
58
+ else:
59
+ ax.set_xticks([])
60
+ ax.set_yticks([])
61
+ if grid:
62
+ ax.grid(True, which="both", alpha=0.25)
63
+ for spine in ax.spines.values():
64
+ spine.set_linewidth(1.0)
65
+ return fig, ax
66
+
67
+
68
+ def annotate(xy, xytext, s: str, arc: float = 0.3, fontsize: int = 14):
69
+ ax = plt.gca()
70
+ ax.annotate(
71
+ s,
72
+ xy=xy,
73
+ xytext=xytext,
74
+ arrowprops=dict(arrowstyle="->", connectionstyle=f"arc3,rad={arc}"),
75
+ fontsize=fontsize,
76
+ )
77
+
78
+
79
+ def polygon(
80
+ *pts: Tuple[float, float],
81
+ edges: bool = True,
82
+ color=None,
83
+ facecolor=None,
84
+ alpha=None,
85
+ show_vertices: bool = False,
86
+ ):
87
+ ax = plt.gca()
88
+ xs = [p[0] for p in pts] + [pts[0][0]]
89
+ ys = [p[1] for p in pts] + [pts[0][1]]
90
+ if facecolor or (color and not edges):
91
+ ax.fill(xs, ys, color=(facecolor or color), alpha=alpha or 0.1)
92
+ if edges:
93
+ ax.plot(xs, ys, color=(color or COLORS.get("black", "black")))
94
+ if show_vertices:
95
+ ax.plot(
96
+ [p[0] for p in pts],
97
+ [p[1] for p in pts],
98
+ "o",
99
+ color=(color or COLORS.get("black", "black")),
100
+ )
101
+
102
+
103
+ def make_bar(xy: Tuple[float, float], length: float, orientation: str = "horizontal"):
104
+ x, y = xy
105
+ ax = plt.gca()
106
+ if orientation.lower().startswith("h"):
107
+ ax.annotate("", xy=(x, y), xytext=(x + length, y), arrowprops=dict(arrowstyle="|-|"))
108
+ else:
109
+ ax.annotate("", xy=(x, y), xytext=(x, y + length), arrowprops=dict(arrowstyle="|-|"))
110
+
111
+
112
+ def frange(start: float, stop: float, step: float):
113
+ vals = []
114
+ if step == 0:
115
+ return vals
116
+ x = start
117
+ # ensure inclusion of stop within floating tolerance
118
+ if start <= stop:
119
+ while x <= stop + 1e-9:
120
+ vals.append(x)
121
+ x += step
122
+ else:
123
+ while x >= stop - 1e-9:
124
+ vals.append(x)
125
+ x -= abs(step)
126
+ return vals
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ # Namespace package for directive modules. Each module should expose `setup(app)`.
@@ -0,0 +1,356 @@
1
+ """
2
+ Custom Admonition Directives
3
+ =============================
4
+
5
+ A collection of custom styled admonition directives for educational content.
6
+
7
+ Directives included:
8
+ - answer: For showing answers to problems (with dropdown option)
9
+ - example: For worked examples
10
+ - exercise: For exercise problems
11
+ - explore: For exploratory activities
12
+ - hints: For providing hints (with dropdown option)
13
+ - solution: For full solutions (with dropdown option, default on)
14
+ - summary: For chapter/section summaries
15
+ - theory: For theoretical content
16
+
17
+ All directives support MyST colon-fence syntax (:::).
18
+ """
19
+
20
+ from docutils import nodes
21
+ from docutils.parsers.rst import Directive, directives
22
+ from sphinx.util.docutils import SphinxDirective
23
+ from sphinx.util import logging
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class AnswerDirective(SphinxDirective):
29
+ """
30
+ Answer directive with optional dropdown.
31
+
32
+ Usage:
33
+ .. answer::
34
+ .. answer:: Custom Title
35
+ .. answer::
36
+ :dropdown: 0
37
+ """
38
+
39
+ has_content = True
40
+ required_arguments = 0
41
+ optional_arguments = 1
42
+ final_argument_whitespace = True
43
+
44
+ option_spec = {
45
+ "dropdown": directives.unchanged,
46
+ }
47
+
48
+ def run(self):
49
+ if len(self.arguments) > 0:
50
+ title = self.arguments[0]
51
+ else:
52
+ title = "Fasit"
53
+
54
+ # Create the admonition node
55
+ admonition_node = nodes.admonition()
56
+ admonition_node["classes"] = ["admonition", "answer"]
57
+
58
+ if "dropdown" in self.options:
59
+ dropdown_val = int(self.options["dropdown"])
60
+ else:
61
+ dropdown_val = 1
62
+
63
+ if dropdown_val == 1:
64
+ admonition_node["classes"].append("dropdown")
65
+
66
+ # Create the title node
67
+ title_node = nodes.title(text=title)
68
+ admonition_node += title_node
69
+
70
+ # Parse the content
71
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
72
+
73
+ return [admonition_node]
74
+
75
+
76
+ class ExampleDirective(SphinxDirective):
77
+ """
78
+ Example directive for worked examples.
79
+
80
+ Usage:
81
+ .. example:: Example Title
82
+ """
83
+
84
+ has_content = True
85
+ required_arguments = 1
86
+ optional_arguments = 0
87
+ final_argument_whitespace = True
88
+
89
+ def run(self):
90
+ title = self.arguments[0]
91
+
92
+ # Create the admonition node
93
+ admonition_node = nodes.admonition()
94
+ admonition_node["classes"] = ["admonition", "example"]
95
+
96
+ # Create the title node
97
+ title_node = nodes.title()
98
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
99
+ title_node += parsed_title
100
+ admonition_node += title_node
101
+
102
+ # Parse the content
103
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
104
+
105
+ return [admonition_node]
106
+
107
+
108
+ class ExerciseDirective(SphinxDirective):
109
+ """
110
+ Exercise directive for problems.
111
+
112
+ Usage:
113
+ .. exercise:: Exercise Title
114
+ .. exercise:: Exercise Title
115
+ :level: 2
116
+ """
117
+
118
+ has_content = True
119
+ required_arguments = 1
120
+ optional_arguments = 0
121
+ final_argument_whitespace = True
122
+ option_spec = {
123
+ "level": directives.unchanged,
124
+ }
125
+
126
+ def run(self):
127
+ title = self.arguments[0]
128
+ # Create the admonition node
129
+ admonition_node = nodes.admonition()
130
+ admonition_node["classes"] = ["admonition", "exercise"]
131
+
132
+ # Create the title node
133
+ title_node = nodes.title()
134
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
135
+ title_node += parsed_title
136
+ admonition_node += title_node
137
+
138
+ # Parse the content
139
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
140
+
141
+ return [admonition_node]
142
+
143
+
144
+ class ExploreDirective(SphinxDirective):
145
+ """
146
+ Explore directive for exploratory activities.
147
+
148
+ Usage:
149
+ .. explore:: Activity Title
150
+ """
151
+
152
+ has_content = True
153
+ required_arguments = 1
154
+ optional_arguments = 0
155
+ final_argument_whitespace = True
156
+
157
+ def run(self):
158
+ title = self.arguments[0]
159
+
160
+ # Create the admonition node
161
+ admonition_node = nodes.admonition()
162
+ admonition_node["classes"] = ["admonition", "explore"]
163
+
164
+ # Create the title node
165
+ title_node = nodes.title()
166
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
167
+ title_node += parsed_title
168
+ admonition_node += title_node
169
+
170
+ # Parse the content
171
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
172
+
173
+ return [admonition_node]
174
+
175
+
176
+ class HintsDirective(SphinxDirective):
177
+ """
178
+ Hints directive with optional dropdown.
179
+
180
+ Usage:
181
+ .. hints::
182
+ .. hints:: Custom Title
183
+ .. hints::
184
+ :dropdown: 0
185
+ """
186
+
187
+ has_content = True
188
+ required_arguments = 0
189
+ optional_arguments = 1
190
+ final_argument_whitespace = True
191
+
192
+ option_spec = {
193
+ "dropdown": directives.unchanged,
194
+ }
195
+
196
+ def run(self):
197
+ if len(self.arguments) > 0:
198
+ title = self.arguments[0]
199
+ else:
200
+ title = "Hint"
201
+
202
+ # Create the admonition node
203
+ admonition_node = nodes.admonition()
204
+ admonition_node["classes"] = ["admonition", "hints"]
205
+
206
+ if "dropdown" in self.options:
207
+ dropdown_val = int(self.options["dropdown"])
208
+ else:
209
+ dropdown_val = 1
210
+
211
+ if dropdown_val == 1:
212
+ admonition_node["classes"].append("dropdown")
213
+
214
+ # Create the title node
215
+ title_node = nodes.title(text=title)
216
+ admonition_node += title_node
217
+
218
+ # Parse the content
219
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
220
+
221
+ return [admonition_node]
222
+
223
+
224
+ class SolutionDirective(SphinxDirective):
225
+ """
226
+ Solution directive with dropdown (default on).
227
+
228
+ Usage:
229
+ .. solution::
230
+ .. solution:: Custom Title
231
+ .. solution::
232
+ :dropdown: 0
233
+ """
234
+
235
+ has_content = True
236
+ required_arguments = 0
237
+ optional_arguments = 1
238
+ final_argument_whitespace = True
239
+
240
+ option_spec = {
241
+ "dropdown": directives.unchanged,
242
+ }
243
+
244
+ def run(self):
245
+ if len(self.arguments) > 0:
246
+ title = self.arguments[0]
247
+ else:
248
+ title = "Løsning"
249
+
250
+ # Create the admonition node
251
+ admonition_node = nodes.admonition()
252
+ admonition_node["classes"] = ["admonition", "solution"]
253
+
254
+ if self.options.get("dropdown"):
255
+ dropdown = self.options.get("dropdown")
256
+ if dropdown == "1":
257
+ admonition_node["classes"].append("dropdown")
258
+ elif dropdown == "0":
259
+ pass
260
+ else:
261
+ admonition_node["classes"].append("dropdown")
262
+
263
+ # Create the title node
264
+ title_node = nodes.title(text=title)
265
+ admonition_node += title_node
266
+
267
+ # Parse the content
268
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
269
+
270
+ return [admonition_node]
271
+
272
+
273
+ class SummaryDirective(SphinxDirective):
274
+ """
275
+ Summary directive for chapter/section summaries.
276
+
277
+ Usage:
278
+ .. summary:: Summary Title
279
+ """
280
+
281
+ has_content = True
282
+ required_arguments = 1
283
+ optional_arguments = 0
284
+ final_argument_whitespace = True
285
+
286
+ def run(self):
287
+ title = self.arguments[0]
288
+
289
+ # Create the admonition node
290
+ admonition_node = nodes.admonition()
291
+ admonition_node["classes"] = ["admonition", "summary"]
292
+
293
+ title_node = nodes.title()
294
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
295
+ title_node += parsed_title
296
+ admonition_node += title_node
297
+
298
+ # Parse the content
299
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
300
+
301
+ return [admonition_node]
302
+
303
+
304
+ class TheoryDirective(SphinxDirective):
305
+ """
306
+ Theory directive for theoretical content.
307
+
308
+ Usage:
309
+ .. theory:: Theory Title
310
+ """
311
+
312
+ has_content = True
313
+ required_arguments = 1
314
+ optional_arguments = 0
315
+ final_argument_whitespace = True
316
+
317
+ def run(self):
318
+ title = self.arguments[0]
319
+
320
+ # Create the admonition node
321
+ admonition_node = nodes.admonition()
322
+ admonition_node["classes"] = ["admonition", "theory"]
323
+
324
+ # Create the title node
325
+ title_node = nodes.title()
326
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
327
+ title_node += parsed_title
328
+ admonition_node += title_node
329
+
330
+ # Parse the content
331
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
332
+
333
+ return [admonition_node]
334
+
335
+
336
+ def setup(app):
337
+ """
338
+ Setup all custom admonition directives.
339
+
340
+ Note: CSS files are registered in __init__.py with the munchboka/ prefix.
341
+ The admonitions.css file contains all styling for these directives.
342
+ """
343
+ app.add_directive("answer", AnswerDirective)
344
+ app.add_directive("example", ExampleDirective)
345
+ app.add_directive("exercise", ExerciseDirective)
346
+ app.add_directive("explore", ExploreDirective)
347
+ app.add_directive("hints", HintsDirective)
348
+ app.add_directive("solution", SolutionDirective)
349
+ app.add_directive("summary", SummaryDirective)
350
+ app.add_directive("theory", TheoryDirective)
351
+
352
+ return {
353
+ "version": "0.1",
354
+ "parallel_read_safe": True,
355
+ "parallel_write_safe": True,
356
+ }