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,184 @@
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/popup.css")
52
+ _register("css/quiz.css")
53
+ _register("css/timedQuiz.css")
54
+ _register("css/cas_popup.css")
55
+ _register("css/github-light.css")
56
+ _register("css/github-dark.css")
57
+ _register("css/github-dark-high-contrast.css")
58
+ _register("css/pairPuzzle/style.css")
59
+ _register("css/escapeRoom/escape-room.css")
60
+ _register("css/parsons/parsonsPuzzle.css")
61
+
62
+ # Register JS files with explicit priorities to match matematikk_r1 loading order:
63
+ # 1. utils.js (early - priority 450)
64
+ # 2. KaTeX (priority 490-495 - before pair-puzzle)
65
+ # 3. quiz.js, jeopardy.js (priority 500 - default)
66
+ # 4. interactive code scripts (priority 500 - default)
67
+ # 5. pair-puzzle scripts (priority 700-720)
68
+ # 6. jQuery, jQuery UI, and GeoGebra (priority 800-900)
69
+ # 7. CodeMirror (last - priority 900)
70
+ _register("js/utils.js", priority=450)
71
+ _register("js/casThemeManager.js", priority=480) # Before jQuery UI for theme handling
72
+
73
+ # Add KaTeX before pair-puzzle scripts (which depend on it)
74
+ app.add_js_file(
75
+ "https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js",
76
+ priority=490,
77
+ )
78
+ app.add_js_file(
79
+ "https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js",
80
+ priority=495,
81
+ )
82
+
83
+ _register("js/jeopardy.js")
84
+ _register("js/popup.js")
85
+ _register("js/quiz.js")
86
+ _register("js/timedQuiz/utils.js")
87
+ _register("js/timedQuiz/multipleChoiceQuestion.js")
88
+ _register("js/timedQuiz/timedMultipleChoiceQuiz.js")
89
+ _register("js/interactiveCode/pythonRunner.js")
90
+ _register("js/interactiveCode/codeEditor.js")
91
+ _register("js/interactiveCode/interactiveCodeSetup.js")
92
+ _register("js/interactiveCode/workerManager.js")
93
+ # Skulpt must be loaded before turtleCode.js (lower priority = loaded first)
94
+ _register("js/skulpt/skulpt.js", priority=490)
95
+ _register("js/interactiveCode/turtleCode.js")
96
+ _register("js/pairPuzzle/draggableItem.js", priority=700)
97
+ _register("js/pairPuzzle/dropZone.js", priority=710)
98
+ _register("js/pairPuzzle/game.js", priority=720)
99
+ _register("js/escapeRoom/escape-room.js", priority=730)
100
+ _register("js/parsons/parsonsPuzzle.js", priority=740)
101
+
102
+ # Add jQuery and jQuery UI for dialog functionality
103
+ app.add_js_file(
104
+ "https://code.jquery.com/jquery-3.6.0.min.js",
105
+ priority=800,
106
+ )
107
+ app.add_js_file(
108
+ "https://code.jquery.com/ui/1.13.2/jquery-ui.min.js",
109
+ priority=850,
110
+ )
111
+
112
+ # Add highlight.js for syntax highlighting in Parsons puzzles and other directives
113
+ app.add_css_file(
114
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/github.min.css",
115
+ id="highlight-theme-light",
116
+ )
117
+ app.add_css_file(
118
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/github-dark.min.css",
119
+ id="highlight-theme-dark",
120
+ )
121
+ app.add_js_file(
122
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js",
123
+ priority=855,
124
+ )
125
+ _register("js/highlight-init.js", priority=856)
126
+
127
+ # Add GeoGebra API
128
+ app.add_js_file(
129
+ "https://www.geogebra.org/apps/deployggb.js",
130
+ priority=860,
131
+ )
132
+ _register("js/geogebra-setup.js", priority=870)
133
+
134
+ # Add CodeMirror LAST with high priority so it loads at the end
135
+ app.add_js_file(
136
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js",
137
+ priority=900,
138
+ )
139
+ app.add_js_file(
140
+ "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js",
141
+ priority=900,
142
+ )
143
+ except Exception as exc: # pragma: no cover - non-fatal
144
+ try:
145
+ from sphinx.util import logging
146
+
147
+ logging.getLogger(__name__).warning(
148
+ "munchboka_edutools: failed to copy static assets: %s", exc
149
+ )
150
+ except Exception:
151
+ pass
152
+
153
+
154
+ def setup(app):
155
+ """
156
+ Sphinx entry point for the extension.
157
+
158
+ - Copies packaged static assets on builder init
159
+ - Auto-loads all modules under munchboka_edutools.directives that expose setup(app)
160
+ """
161
+ # Ensure packaged static assets are available for HTML builder
162
+ app.connect("builder-inited", _copy_static)
163
+
164
+ # Auto discover and load directive modules
165
+ pkg_prefix = __name__ + ".directives"
166
+ try:
167
+ pkg = importlib.import_module(pkg_prefix)
168
+ for modinfo in pkgutil.iter_modules(pkg.__path__, pkg_prefix + "."):
169
+ app.setup_extension(modinfo.name)
170
+ except Exception as exc: # pragma: no cover - non-fatal
171
+ try:
172
+ from sphinx.util import logging
173
+
174
+ logging.getLogger(__name__).warning(
175
+ "munchboka_edutools: failed to auto-load directives: %s", exc
176
+ )
177
+ except Exception:
178
+ pass
179
+
180
+ return {
181
+ "version": __version__,
182
+ "parallel_read_safe": True,
183
+ "parallel_write_safe": True,
184
+ }
@@ -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.2.3"
@@ -0,0 +1 @@
1
+ # Namespace package for directive modules. Each module should expose `setup(app)`.
@@ -0,0 +1,389 @@
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
+ - goals: For learning objectives/goals
13
+ - hints: For providing hints (with dropdown option)
14
+ - solution: For full solutions (with dropdown option, default on)
15
+ - summary: For chapter/section summaries
16
+ - theory: For theoretical content
17
+
18
+ All directives support MyST colon-fence syntax (:::).
19
+ """
20
+
21
+ from docutils import nodes
22
+ from docutils.parsers.rst import Directive, directives
23
+ from sphinx.util.docutils import SphinxDirective
24
+ from sphinx.util import logging
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class AnswerDirective(SphinxDirective):
30
+ """
31
+ Answer directive with optional dropdown.
32
+
33
+ Usage:
34
+ .. answer::
35
+ .. answer:: Custom Title
36
+ .. answer::
37
+ :dropdown: 0
38
+ """
39
+
40
+ has_content = True
41
+ required_arguments = 0
42
+ optional_arguments = 1
43
+ final_argument_whitespace = True
44
+
45
+ option_spec = {
46
+ "dropdown": directives.unchanged,
47
+ }
48
+
49
+ def run(self):
50
+ if len(self.arguments) > 0:
51
+ title = self.arguments[0]
52
+ else:
53
+ title = "Fasit"
54
+
55
+ # Create the admonition node
56
+ admonition_node = nodes.admonition()
57
+ admonition_node["classes"] = ["admonition", "answer"]
58
+
59
+ if "dropdown" in self.options:
60
+ dropdown_val = int(self.options["dropdown"])
61
+ else:
62
+ dropdown_val = 1
63
+
64
+ if dropdown_val == 1:
65
+ admonition_node["classes"].append("dropdown")
66
+
67
+ # Create the title node
68
+ title_node = nodes.title(text=title)
69
+ admonition_node += title_node
70
+
71
+ # Parse the content
72
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
73
+
74
+ return [admonition_node]
75
+
76
+
77
+ class ExampleDirective(SphinxDirective):
78
+ """
79
+ Example directive for worked examples.
80
+
81
+ Usage:
82
+ .. example:: Example Title
83
+ """
84
+
85
+ has_content = True
86
+ required_arguments = 1
87
+ optional_arguments = 0
88
+ final_argument_whitespace = True
89
+
90
+ def run(self):
91
+ title = self.arguments[0]
92
+
93
+ # Create the admonition node
94
+ admonition_node = nodes.admonition()
95
+ admonition_node["classes"] = ["admonition", "example"]
96
+
97
+ # Create the title node
98
+ title_node = nodes.title()
99
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
100
+ title_node += parsed_title
101
+ admonition_node += title_node
102
+
103
+ # Parse the content
104
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
105
+
106
+ return [admonition_node]
107
+
108
+
109
+ class ExerciseDirective(SphinxDirective):
110
+ """
111
+ Exercise directive for problems.
112
+
113
+ Usage:
114
+ .. exercise:: Exercise Title
115
+ .. exercise:: Exercise Title
116
+ :level: 2
117
+ """
118
+
119
+ has_content = True
120
+ required_arguments = 1
121
+ optional_arguments = 0
122
+ final_argument_whitespace = True
123
+ option_spec = {
124
+ "level": directives.unchanged,
125
+ }
126
+
127
+ def run(self):
128
+ title = self.arguments[0]
129
+ # Create the admonition node
130
+ admonition_node = nodes.admonition()
131
+ admonition_node["classes"] = ["admonition", "exercise"]
132
+
133
+ # Create the title node
134
+ title_node = nodes.title()
135
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
136
+ title_node += parsed_title
137
+ admonition_node += title_node
138
+
139
+ # Parse the content
140
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
141
+
142
+ return [admonition_node]
143
+
144
+
145
+ class ExploreDirective(SphinxDirective):
146
+ """
147
+ Explore directive for exploratory activities.
148
+
149
+ Usage:
150
+ .. explore:: Activity Title
151
+ """
152
+
153
+ has_content = True
154
+ required_arguments = 1
155
+ optional_arguments = 0
156
+ final_argument_whitespace = True
157
+
158
+ def run(self):
159
+ title = self.arguments[0]
160
+
161
+ # Create the admonition node
162
+ admonition_node = nodes.admonition()
163
+ admonition_node["classes"] = ["admonition", "explore"]
164
+
165
+ # Create the title node
166
+ title_node = nodes.title()
167
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
168
+ title_node += parsed_title
169
+ admonition_node += title_node
170
+
171
+ # Parse the content
172
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
173
+
174
+ return [admonition_node]
175
+
176
+
177
+ class GoalsDirective(SphinxDirective):
178
+ """
179
+ Goals directive for learning objectives.
180
+
181
+ Usage:
182
+ .. goals:: Learning Goals
183
+ """
184
+
185
+ has_content = True
186
+ required_arguments = 1
187
+ optional_arguments = 0
188
+ final_argument_whitespace = True
189
+
190
+ def run(self):
191
+ title = self.arguments[0]
192
+
193
+ # Create the admonition node
194
+ admonition_node = nodes.admonition()
195
+ admonition_node["classes"] = ["admonition", "tip"]
196
+
197
+ title_node = nodes.title()
198
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
199
+ title_node += parsed_title
200
+ admonition_node += title_node
201
+
202
+ # Parse the content
203
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
204
+
205
+ return [admonition_node]
206
+
207
+
208
+ class HintsDirective(SphinxDirective):
209
+ """
210
+ Hints directive with optional dropdown.
211
+
212
+ Usage:
213
+ .. hints::
214
+ .. hints:: Custom Title
215
+ .. hints::
216
+ :dropdown: 0
217
+ """
218
+
219
+ has_content = True
220
+ required_arguments = 0
221
+ optional_arguments = 1
222
+ final_argument_whitespace = True
223
+
224
+ option_spec = {
225
+ "dropdown": directives.unchanged,
226
+ }
227
+
228
+ def run(self):
229
+ if len(self.arguments) > 0:
230
+ title = self.arguments[0]
231
+ else:
232
+ title = "Hint"
233
+
234
+ # Create the admonition node
235
+ admonition_node = nodes.admonition()
236
+ admonition_node["classes"] = ["admonition", "hints"]
237
+
238
+ if "dropdown" in self.options:
239
+ dropdown_val = int(self.options["dropdown"])
240
+ else:
241
+ dropdown_val = 1
242
+
243
+ if dropdown_val == 1:
244
+ admonition_node["classes"].append("dropdown")
245
+
246
+ # Create the title node
247
+ title_node = nodes.title(text=title)
248
+ admonition_node += title_node
249
+
250
+ # Parse the content
251
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
252
+
253
+ return [admonition_node]
254
+
255
+
256
+ class SolutionDirective(SphinxDirective):
257
+ """
258
+ Solution directive with dropdown (default on).
259
+
260
+ Usage:
261
+ .. solution::
262
+ .. solution:: Custom Title
263
+ .. solution::
264
+ :dropdown: 0
265
+ """
266
+
267
+ has_content = True
268
+ required_arguments = 0
269
+ optional_arguments = 1
270
+ final_argument_whitespace = True
271
+
272
+ option_spec = {
273
+ "dropdown": directives.unchanged,
274
+ }
275
+
276
+ def run(self):
277
+ if len(self.arguments) > 0:
278
+ title = self.arguments[0]
279
+ else:
280
+ title = "Løsning"
281
+
282
+ # Create the admonition node
283
+ admonition_node = nodes.admonition()
284
+ admonition_node["classes"] = ["admonition", "solution"]
285
+
286
+ if self.options.get("dropdown"):
287
+ dropdown = self.options.get("dropdown")
288
+ if dropdown == "1":
289
+ admonition_node["classes"].append("dropdown")
290
+ elif dropdown == "0":
291
+ pass
292
+ else:
293
+ admonition_node["classes"].append("dropdown")
294
+
295
+ # Create the title node
296
+ title_node = nodes.title(text=title)
297
+ admonition_node += title_node
298
+
299
+ # Parse the content
300
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
301
+
302
+ return [admonition_node]
303
+
304
+
305
+ class SummaryDirective(SphinxDirective):
306
+ """
307
+ Summary directive for chapter/section summaries.
308
+
309
+ Usage:
310
+ .. summary:: Summary Title
311
+ """
312
+
313
+ has_content = True
314
+ required_arguments = 1
315
+ optional_arguments = 0
316
+ final_argument_whitespace = True
317
+
318
+ def run(self):
319
+ title = self.arguments[0]
320
+
321
+ # Create the admonition node
322
+ admonition_node = nodes.admonition()
323
+ admonition_node["classes"] = ["admonition", "summary"]
324
+
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
+ class TheoryDirective(SphinxDirective):
337
+ """
338
+ Theory directive for theoretical content.
339
+
340
+ Usage:
341
+ .. theory:: Theory Title
342
+ """
343
+
344
+ has_content = True
345
+ required_arguments = 1
346
+ optional_arguments = 0
347
+ final_argument_whitespace = True
348
+
349
+ def run(self):
350
+ title = self.arguments[0]
351
+
352
+ # Create the admonition node
353
+ admonition_node = nodes.admonition()
354
+ admonition_node["classes"] = ["admonition", "theory"]
355
+
356
+ # Create the title node
357
+ title_node = nodes.title()
358
+ parsed_title, _ = self.state.inline_text(title, self.lineno)
359
+ title_node += parsed_title
360
+ admonition_node += title_node
361
+
362
+ # Parse the content
363
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
364
+
365
+ return [admonition_node]
366
+
367
+
368
+ def setup(app):
369
+ """
370
+ Setup all custom admonition directives.
371
+
372
+ Note: CSS files are registered in __init__.py with the munchboka/ prefix.
373
+ The admonitions.css file contains all styling for these directives.
374
+ """
375
+ app.add_directive("answer", AnswerDirective)
376
+ app.add_directive("example", ExampleDirective)
377
+ app.add_directive("exercise", ExerciseDirective)
378
+ app.add_directive("explore", ExploreDirective)
379
+ app.add_directive("goals", GoalsDirective)
380
+ app.add_directive("hints", HintsDirective)
381
+ app.add_directive("solution", SolutionDirective)
382
+ app.add_directive("summary", SummaryDirective)
383
+ app.add_directive("theory", TheoryDirective)
384
+
385
+ return {
386
+ "version": "0.1",
387
+ "parallel_read_safe": True,
388
+ "parallel_write_safe": True,
389
+ }