munchboka-edutools 0.1.14__py3-none-any.whl → 0.1.19__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.

@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.14"
2
+ __version__ = "0.1.19"
@@ -0,0 +1,231 @@
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 FlashcardsDirective(SphinxDirective):
16
+ has_content = True
17
+ required_arguments = 0
18
+ option_spec = {
19
+ "shuffle": directives.flag,
20
+ "show_progress": directives.flag,
21
+ "start_index": directives.nonnegative_int,
22
+ }
23
+
24
+ def run(self):
25
+ deck_id = uuid.uuid4().hex
26
+ container_id = f"flashcards-{deck_id}"
27
+
28
+ data = self._parse_deck()
29
+
30
+ source_file = self.state.document["source"]
31
+ source_dir = os.path.dirname(source_file)
32
+ app_src_dir = self.env.srcdir
33
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
34
+ rel_prefix = "../" * (depth + 1)
35
+
36
+ cfg: Dict[str, Any] = {
37
+ "cards": data["cards"],
38
+ "options": {
39
+ "shuffle": "shuffle" in self.options,
40
+ "show_progress": "show_progress" in self.options,
41
+ "start_index": int(self.options.get("start_index", 0) or 0),
42
+ "staticPrefix": rel_prefix,
43
+ },
44
+ }
45
+
46
+ cfg_str_attr = _html.escape(json.dumps(cfg, ensure_ascii=False), quote=True)
47
+ json_str = json.dumps(cfg, ensure_ascii=False)
48
+
49
+ html = f"""
50
+ <div id="{container_id}" class=\"flashcards-container\" data-config=\"{cfg_str_attr}\">
51
+ <script type=\"application/json\" class=\"flashcards-data\">{json_str}</script>
52
+ </div>
53
+ <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css\">
54
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js\"></script>
55
+ <script defer src=\"https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js\"></script>
56
+ """
57
+ return [nodes.raw("", html, format="html")]
58
+
59
+ def _parse_deck(self) -> Dict[str, Any]:
60
+ cards: List[Dict[str, Any]] = []
61
+ current: Dict[str, Any] | None = None
62
+ section: str | None = None
63
+
64
+ def flush():
65
+ nonlocal current
66
+ if current is not None:
67
+ for k in ("front", "back"):
68
+ if current.get(k) is None:
69
+ current[k] = ""
70
+ cards.append(current)
71
+ current = None
72
+
73
+ for raw in self.content:
74
+ line = self._process_figures(raw)
75
+ if line is None:
76
+ line = raw
77
+ s = line.rstrip("\n")
78
+
79
+ m = re.match(r"^\s*Q\s*:\s*(.*)$", s, flags=re.IGNORECASE)
80
+ if m:
81
+ flush()
82
+ current = {"front": m.group(1), "back": ""}
83
+ section = "front"
84
+ continue
85
+
86
+ m = re.match(r"^\s*A\s*:\s*(.*)$", s, flags=re.IGNORECASE)
87
+ if m and current is not None:
88
+ current["back"] = (current.get("back") or "") + m.group(1)
89
+ section = "back"
90
+ continue
91
+
92
+ if section == "front" and current is not None:
93
+ current["front"] = (current.get("front") or "") + "\n" + s
94
+ continue
95
+ if section == "back" and current is not None:
96
+ current["back"] = (current.get("back") or "") + "\n" + s
97
+ continue
98
+
99
+ flush()
100
+
101
+ for c in cards:
102
+ for key in ("front", "back"):
103
+ val = c.get(key) or ""
104
+ c[key] = self._process_code_blocks(val)
105
+
106
+ return {"cards": cards}
107
+
108
+ def _process_code_blocks(self, text: str) -> str:
109
+ def repl(match):
110
+ code = match.group(2).replace("\\n", "\n")
111
+ lang = match.group(1)
112
+ return f'<pre><code class="{lang}">{code}</code></pre>'
113
+
114
+ pattern = r'<pre><code class="([\\w-]+)">(.*?)</code></pre>'
115
+ return re.sub(pattern, repl, text, flags=re.DOTALL)
116
+
117
+ def _process_figures(self, text: str):
118
+ import shutil
119
+ import json as _json
120
+
121
+ if not hasattr(self, "_image_counter"):
122
+ self._image_counter = 0
123
+
124
+ def _parse_figure_options(alt_text: str) -> Dict[str, Any]:
125
+ opts: Dict[str, Any] = {}
126
+ s = (alt_text or "").strip()
127
+
128
+ def parse_pairs(content: str):
129
+ for m in re.finditer(r'(\\w+)\\s*=\\s*(?:"([^"]*)"|\'([^\']*)\'|([^\\s}]+))', content):
130
+ val = m.group(2) or m.group(3) or m.group(4) or ""
131
+ opts[m.group(1)] = val
132
+
133
+ if s.startswith("{") and s.endswith("}"):
134
+ inner = s[1:-1].strip()
135
+ ok = False
136
+ try:
137
+ js = s
138
+ js = re.sub(r"(\\w+)\\s*:", r'"\\1":', js)
139
+ js = re.sub(r':\\s*([^",}]+)', r': "\\1"', js)
140
+ opts.update(_json.loads(js))
141
+ ok = True
142
+ except Exception:
143
+ ok = False
144
+ if not ok:
145
+ parse_pairs(inner)
146
+ else:
147
+ parse_pairs(s)
148
+ if not opts and s:
149
+ opts["alt"] = s
150
+ return opts
151
+
152
+ def _build_figure_html(html_img_path: str, options: Dict[str, Any]) -> str:
153
+ user_opts = dict(options or {})
154
+ user_class = user_opts.pop("class", "").strip()
155
+ classes = "flashcard-image adaptive-figure" + (f" {user_class}" if user_class else "")
156
+
157
+ alt_text = user_opts.pop("alt", "Figure")
158
+ title = user_opts.pop("title", None)
159
+ width = user_opts.pop("width", None)
160
+ height = user_opts.pop("height", None)
161
+ extra_style = user_opts.pop("style", None)
162
+
163
+ def _normalize_wh(val: Any) -> str:
164
+ s = str(val).strip()
165
+ if re.fullmatch(r"\\d+(?:\\.\\d+)?", s):
166
+ return f"{s}px"
167
+ s = re.sub(r"\\s+(?=(px|%|em|rem|vh|vw)$)", "", s)
168
+ return s
169
+
170
+ styles: List[str] = []
171
+ if width is not None:
172
+ styles.append(f"width: {_normalize_wh(width)};")
173
+ if height is not None:
174
+ styles.append(f"height: {_normalize_wh(height)};")
175
+ if extra_style:
176
+ styles.append(str(extra_style))
177
+
178
+ attrs = [f'src="{html_img_path}"', f'class="{classes}"', f'alt="{alt_text}"']
179
+ if title:
180
+ attrs.append(f'title="{title}"')
181
+ if styles:
182
+ style_str = " ".join(styles)
183
+ attrs.append(f'style="{style_str}"')
184
+ for k, v in user_opts.items():
185
+ if k not in {"src", "class", "alt", "title", "width", "height", "style"}:
186
+ attrs.append(f'{k}="{v}"')
187
+ img = f"<img {' '.join(attrs)} >"
188
+ return f'<div class="flashcard-image-container">{img}</div>'
189
+
190
+ def replace(m):
191
+ alt_or_opts = m.group(1).strip()
192
+ raw_src = m.group(2)
193
+ self._image_counter += 1
194
+ options = _parse_figure_options(alt_or_opts)
195
+
196
+ source_file = self.state.document["source"]
197
+ source_dir = os.path.dirname(source_file)
198
+ app_src_dir = self.env.srcdir
199
+
200
+ abs_fig_src = os.path.normpath(os.path.join(source_dir, raw_src))
201
+ if not os.path.exists(abs_fig_src):
202
+ return f'<img src="{raw_src}" class="flashcard-image adaptive-figure" alt="Figure (missing)">'
203
+
204
+ relative_doc_path = os.path.relpath(source_dir, app_src_dir)
205
+ figure_dest_dir = os.path.join(app_src_dir, "_static", "figurer", relative_doc_path)
206
+ os.makedirs(figure_dest_dir, exist_ok=True)
207
+
208
+ rel_path_from_source = os.path.relpath(abs_fig_src, source_dir)
209
+ safe_path = rel_path_from_source.replace(os.sep, "_").replace("/", "_")
210
+ base, ext = os.path.splitext(safe_path)
211
+ fig_filename = f"flash_{self._image_counter}_{base}{ext}"
212
+ fig_dest_path = os.path.join(figure_dest_dir, fig_filename)
213
+ shutil.copy2(abs_fig_src, fig_dest_path)
214
+
215
+ depth = os.path.relpath(source_dir, app_src_dir).count(os.sep)
216
+ rel_prefix = "../" * (depth + 1)
217
+ html_img_path = f"{rel_prefix}_static/figurer/{relative_doc_path}/{fig_filename}"
218
+ return _build_figure_html(html_img_path, options)
219
+
220
+ return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace, text)
221
+
222
+
223
+ def setup(app):
224
+ app.add_directive("flashcards", FlashcardsDirective)
225
+ try:
226
+ app.add_css_file("munchboka/css/flashcards.css")
227
+ app.add_js_file("munchboka/js/flashcards.js")
228
+ app.add_css_file("munchboka/css/general_style.css")
229
+ except Exception:
230
+ pass
231
+ return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True}
@@ -823,30 +823,48 @@ class PlotDirective(SphinxDirective):
823
823
  if s0 in _num_cache:
824
824
  return _num_cache[s0]
825
825
  s = s0
826
- # Replace user function label calls iteratively.
827
- # Allow nested parentheses now by a balanced scan: we match label( ... ) at top-level of that paren group.
828
- pat = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\(")
829
- # Simpler: fallback to previous non-nested approach but broaden attempt; for safety keep prior pattern.
830
- pat_simple = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\(([^()]+)\)")
826
+ # Replace user function label calls iteratively, allowing
827
+ # general expressions as arguments, e.g. f(2 - sqrt(2)).
831
828
  for _ in range(50):
832
- m = pat_simple.search(s)
829
+ m = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\(", s)
833
830
  if not m:
834
831
  break
835
- lbl, arg_expr = m.group(1), m.group(2)
836
- if lbl in fn_labels_list:
837
- try:
838
- arg_val = _eval_expr(arg_expr)
839
- idx = fn_labels_list.index(lbl)
840
- f = functions[idx]
841
- yv = float(f([arg_val])[0])
842
- s = s[: m.start()] + f"{yv}" + s[m.end() :]
843
- continue
844
- except Exception:
845
- # leave unresolved for sympy
846
- pass
847
- # If not user function, just proceed to next occurrence
848
- # Remove nothing to avoid infinite loop: break
849
- break
832
+ lbl = m.group(1)
833
+ if lbl not in fn_labels_list:
834
+ # Skip this label, look for the next one
835
+ start_next = m.start() + 1
836
+ n = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\(", s[start_next:])
837
+ if not n:
838
+ break
839
+ m = n
840
+ lbl = m.group(1)
841
+ if lbl not in fn_labels_list:
842
+ break
843
+ # Find matching closing parenthesis for this call
844
+ start = m.end() - 1 # position of '('
845
+ depth = 0
846
+ end = None
847
+ for i in range(start, len(s)):
848
+ if s[i] == "(":
849
+ depth += 1
850
+ elif s[i] == ")":
851
+ depth -= 1
852
+ if depth == 0:
853
+ end = i
854
+ break
855
+ if end is None:
856
+ break
857
+ arg_expr = s[start + 1 : end]
858
+ try:
859
+ arg_val = _eval_expr(arg_expr)
860
+ idx = fn_labels_list.index(lbl)
861
+ f = functions[idx]
862
+ yv = float(f([arg_val])[0])
863
+ s = s[: m.start()] + f"{yv}" + s[end + 1 :]
864
+ continue
865
+ except Exception:
866
+ # leave unresolved for sympy if evaluation fails
867
+ break
850
868
  allowed = {
851
869
  k: getattr(sympy, k)
852
870
  for k in [
@@ -862,6 +880,7 @@ class PlotDirective(SphinxDirective):
862
880
  "acos",
863
881
  "atan",
864
882
  "Rational",
883
+ "erf",
865
884
  ]
866
885
  if hasattr(sympy, k)
867
886
  }
@@ -941,8 +960,10 @@ class PlotDirective(SphinxDirective):
941
960
  except Exception:
942
961
  pass
943
962
 
944
- # Tangents: entries like "(x0, f(x0))" or
945
- # "(x0, f(x0)), dashed, red" -> draw tangent to f at x0
963
+ # Tangents: can be specified either as
964
+ # - "(x0, f(x0))" or "(x0, f(x0)), dashed, red"
965
+ # - "x0, f [, style] [, color]" (preferred, simpler form)
966
+ # In all cases, we draw the tangent to the function with label f at x0.
946
967
  tangent_vals: List[Tuple[float, float, float, str | None, str | None]] = (
947
968
  []
948
969
  ) # (a, b, x0, style, color)
@@ -972,7 +993,52 @@ class PlotDirective(SphinxDirective):
972
993
  if not parts_t:
973
994
  continue
974
995
 
975
- # First part must be the point pair (x0, f(x0))
996
+ # Helper: parse optional style/color tokens
997
+ def _parse_tangent_style(tokens: List[str]) -> Tuple[str | None, str | None]:
998
+ style_t: str | None = None
999
+ color_t: str | None = None
1000
+ _allowed_styles_t = {"solid", "dotted", "dashed", "dashdot"}
1001
+ for extra in tokens:
1002
+ tok = extra.strip().strip("'\"")
1003
+ low = tok.lower()
1004
+ if low in _allowed_styles_t and style_t is None:
1005
+ style_t = low
1006
+ elif color_t is None:
1007
+ color_t = tok
1008
+ return style_t, color_t
1009
+
1010
+ # First try the simple form: x0, f [, style] [, color]
1011
+ simple_ok = False
1012
+ if len(parts_t) >= 2:
1013
+ x0_raw = parts_t[0].strip()
1014
+ f_lbl_raw = parts_t[1].strip()
1015
+ # f_lbl_raw should look like a bare label, e.g. "f"
1016
+ if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", f_lbl_raw):
1017
+ lbl = f_lbl_raw
1018
+ if lbl in fn_labels_list:
1019
+ try:
1020
+ x0 = _eval_expr(x0_raw)
1021
+ style_t, color_t = _parse_tangent_style(parts_t[2:])
1022
+ idx = fn_labels_list.index(lbl)
1023
+ f = functions[idx]
1024
+ import numpy as _np_t
1025
+
1026
+ # Finite-difference derivative around x0
1027
+ h = max(1e-5, 1e-5 * (1.0 + abs(x0)))
1028
+ y_plus = float(f(_np_t.array([x0 + h], dtype=float))[0])
1029
+ y_minus = float(f(_np_t.array([x0 - h], dtype=float))[0])
1030
+ a_t = (y_plus - y_minus) / (2 * h)
1031
+ y0 = float(f(_np_t.array([x0], dtype=float))[0])
1032
+ b_t = y0 - a_t * x0
1033
+ tangent_vals.append((a_t, b_t, x0, style_t, color_t))
1034
+ simple_ok = True
1035
+ except Exception:
1036
+ pass
1037
+
1038
+ if simple_ok:
1039
+ continue
1040
+
1041
+ # Fallback: old form where first part is the point pair (x0, f(x0))
976
1042
  m_pair = re.match(r"^\(\s*([^,]+?)\s*,\s*([^,]+?)\s*\)$", parts_t[0])
977
1043
  if not m_pair:
978
1044
  continue
@@ -991,20 +1057,10 @@ class PlotDirective(SphinxDirective):
991
1057
  continue
992
1058
  try:
993
1059
  x0 = _eval_expr(x_raw)
994
- arg_val = _eval_expr(arg_expr)
1060
+ _ = _eval_expr(arg_expr) # just ensure it's valid
995
1061
  except Exception:
996
1062
  continue
997
- # Remaining parts: optional linestyle/color in any order (like vline/hline/line)
998
- style_t: str | None = None
999
- color_t: str | None = None
1000
- _allowed_styles_t = {"solid", "dotted", "dashed", "dashdot"}
1001
- for extra in parts_t[1:]:
1002
- tok = extra.strip().strip("'\"")
1003
- low = tok.lower()
1004
- if low in _allowed_styles_t and style_t is None:
1005
- style_t = low
1006
- elif color_t is None:
1007
- color_t = tok
1063
+ style_t, color_t = _parse_tangent_style(parts_t[1:])
1008
1064
  try:
1009
1065
  idx = fn_labels_list.index(lbl)
1010
1066
  f = functions[idx]
@@ -2441,10 +2497,17 @@ class PlotDirective(SphinxDirective):
2441
2497
  for a_l, b_l, st_l, col_l in line_vals:
2442
2498
  _draw_line(a_l, b_l, st_l, col_l)
2443
2499
 
2444
- # Tangents: allow optional style/color, with dashed orange default
2500
+ # Tangents: allow optional style/color, with dashed red default
2445
2501
  for a_t, b_t, _x0, st_t, col_t in tangent_vals:
2446
2502
  style_use = st_t or "dashed"
2447
- color_use = col_t or "orange"
2503
+ # Prefer the plotmath color palette for the default
2504
+ if col_t:
2505
+ color_use = col_t
2506
+ else:
2507
+ _mapped_red = (
2508
+ plotmath.COLORS.get("red") if hasattr(plotmath, "COLORS") else None
2509
+ )
2510
+ color_use = _mapped_red or "red"
2448
2511
  _draw_line(a_t, b_t, style_use, color_use)
2449
2512
 
2450
2513
  # Bars
@@ -0,0 +1,219 @@
1
+ :root {
2
+ --flashcard-bg: #ffffff;
3
+ --flashcard-border: #d1d5db;
4
+ --flashcard-shadow: rgba(15, 23, 42, 0.12);
5
+ --flashcard-text: #111827;
6
+ --flashcard-muted: #6b7280;
7
+ --flashcard-accent: #0ea5e9;
8
+ --flashcard-accent-soft: rgba(14, 165, 233, 0.08);
9
+ --flashcard-control-bg: #f9fafb;
10
+ --flashcard-control-border: #d1d5db;
11
+ }
12
+
13
+ [data-mode="dark"] {
14
+ --flashcard-bg: #020617;
15
+ --flashcard-border: #1f2937;
16
+ --flashcard-shadow: rgba(15, 23, 42, 0.7);
17
+ --flashcard-text: #e5e7eb;
18
+ --flashcard-muted: #9ca3af;
19
+ --flashcard-accent: #38bdf8;
20
+ --flashcard-accent-soft: rgba(56, 189, 248, 0.15);
21
+ --flashcard-control-bg: #020617;
22
+ --flashcard-control-border: #374151;
23
+ }
24
+
25
+ [data-mode="auto"] {}
26
+
27
+ @media (prefers-color-scheme: dark) {
28
+ [data-mode="auto"] {
29
+ --flashcard-bg: #020617;
30
+ --flashcard-border: #1f2937;
31
+ --flashcard-shadow: rgba(15, 23, 42, 0.7);
32
+ --flashcard-text: #e5e7eb;
33
+ --flashcard-muted: #9ca3af;
34
+ --flashcard-accent: #38bdf8;
35
+ --flashcard-accent-soft: rgba(56, 189, 248, 0.15);
36
+ --flashcard-control-bg: #020617;
37
+ --flashcard-control-border: #374151;
38
+ }
39
+ }
40
+
41
+ .flashcards-root {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 0.75rem;
45
+ margin: 1.5rem 0;
46
+ }
47
+
48
+ .flashcards-card-shell {
49
+ display: flex;
50
+ justify-content: center;
51
+ }
52
+
53
+ .flashcards-card {
54
+ position: relative;
55
+ width: min(640px, 100%);
56
+ min-height: 160px;
57
+ padding: 1.5rem 1.75rem;
58
+ border-radius: 0.9rem;
59
+ border: 1px solid var(--flashcard-border);
60
+ background: radial-gradient(circle at top left, var(--flashcard-accent-soft), transparent 55%), var(--flashcard-bg);
61
+ box-shadow: 0 18px 35px -22px var(--flashcard-shadow);
62
+ color: var(--flashcard-text);
63
+ overflow: hidden;
64
+ }
65
+
66
+ .flashcards-root[data-side="back"] .flashcards-card {
67
+ background: radial-gradient(circle at top left, rgba(34, 197, 94, 0.12), transparent 55%), var(--flashcard-bg);
68
+ }
69
+
70
+ .flashcards-card-flip {
71
+ animation: flashcards-flip 0.22s ease-out;
72
+ }
73
+
74
+ @keyframes flashcards-flip {
75
+ 0% {
76
+ transform: translateY(0) rotateY(0deg);
77
+ opacity: 1;
78
+ }
79
+ 50% {
80
+ transform: translateY(-4px) rotateY(7deg);
81
+ opacity: 0.85;
82
+ }
83
+ 100% {
84
+ transform: translateY(0) rotateY(0deg);
85
+ opacity: 1;
86
+ }
87
+ }
88
+
89
+ .flashcards-face {
90
+ transition: opacity 0.24s ease, transform 0.24s ease;
91
+ }
92
+
93
+ .flashcards-face.back {
94
+ display: none;
95
+ }
96
+
97
+ .flashcards-root[data-side="back"] .flashcards-face.front {
98
+ display: none;
99
+ }
100
+
101
+ .flashcards-root[data-side="back"] .flashcards-face.back {
102
+ display: block;
103
+ }
104
+
105
+ .flashcards-header {
106
+ display: flex;
107
+ justify-content: space-between;
108
+ align-items: center;
109
+ margin-bottom: 0.75rem;
110
+ font-size: 0.8rem;
111
+ text-transform: uppercase;
112
+ letter-spacing: 0.08em;
113
+ color: var(--flashcard-muted);
114
+ }
115
+
116
+ .flashcards-progress {
117
+ font-weight: 600;
118
+ }
119
+
120
+ .flashcards-pill {
121
+ padding: 0.15rem 0.6rem;
122
+ border-radius: 999px;
123
+ border: 1px solid var(--flashcard-border);
124
+ background-color: rgba(15, 23, 42, 0.02);
125
+ }
126
+
127
+ .flashcards-body {
128
+ font-size: 0.98rem;
129
+ line-height: 1.6;
130
+ }
131
+
132
+ .flashcards-body pre {
133
+ margin: 0.5rem 0 0;
134
+ }
135
+
136
+ .flashcards-body img.flashcard-image {
137
+ max-width: 100%;
138
+ height: auto;
139
+ }
140
+
141
+ .flashcards-footer {
142
+ display: flex;
143
+ justify-content: space-between;
144
+ align-items: center;
145
+ margin-top: 0.75rem;
146
+ font-size: 0.8rem;
147
+ color: var(--flashcard-muted);
148
+ }
149
+
150
+ .flashcards-controls {
151
+ display: flex;
152
+ gap: 0.5rem;
153
+ }
154
+
155
+ .flashcards-btn {
156
+ border-radius: 999px;
157
+ border: 1px solid var(--flashcard-control-border);
158
+ background-color: var(--flashcard-control-bg);
159
+ color: var(--flashcard-text);
160
+ font-size: 0.8rem;
161
+ padding: 0.4rem 0.75rem;
162
+ display: inline-flex;
163
+ align-items: center;
164
+ gap: 0.25rem;
165
+ cursor: pointer;
166
+ transition: background-color 0.17s ease, transform 0.1s ease, box-shadow 0.1s ease, border-color 0.17s ease;
167
+ }
168
+
169
+ .flashcards-btn-primary {
170
+ background-color: var(--flashcard-accent-soft);
171
+ border-color: rgba(56, 189, 248, 0.4);
172
+ }
173
+
174
+ .flashcards-btn:hover {
175
+ background-color: rgba(15, 23, 42, 0.03);
176
+ transform: translateY(-1px);
177
+ box-shadow: 0 8px 18px -12px var(--flashcard-shadow);
178
+ }
179
+
180
+ .flashcards-btn:active {
181
+ transform: translateY(0);
182
+ box-shadow: none;
183
+ }
184
+
185
+ .flashcards-btn:disabled {
186
+ opacity: 0.55;
187
+ cursor: default;
188
+ box-shadow: none;
189
+ }
190
+
191
+ .flashcards-hotkeys {
192
+ display: none;
193
+ }
194
+
195
+ @media (min-width: 640px) {
196
+ .flashcards-hotkeys {
197
+ display: inline-flex;
198
+ gap: 0.4rem;
199
+ align-items: center;
200
+ }
201
+
202
+ .flashcards-hotkey-pill {
203
+ padding: 0.1rem 0.45rem;
204
+ border-radius: 999px;
205
+ border: 1px solid rgba(148, 163, 184, 0.8);
206
+ font-size: 0.7rem;
207
+ }
208
+ }
209
+
210
+ @media (max-width: 640px) {
211
+ .flashcards-card {
212
+ padding: 1.15rem 1.25rem;
213
+ }
214
+
215
+ .flashcards-controls {
216
+ flex-wrap: wrap;
217
+ justify-content: flex-end;
218
+ }
219
+ }
@@ -1,44 +1,79 @@
1
- /* github-dark theme for CodeMirror */
2
- .cm-s-github-dark.CodeMirror {
3
- background-color: #0d1117;
4
- color: #e6edf3;
5
- }
6
-
7
- .cm-s-github-dark .CodeMirror-gutters {
8
- background-color: #0d1117;
9
- border-right: 1px solid #30363d;
10
- }
11
-
12
- .cm-s-github-dark .CodeMirror-linenumber {
13
- color: #6e7681;
14
- }
15
-
16
- .cm-s-github-dark .CodeMirror-cursor {
17
- border-left: 1px solid #2f81f7;
18
- }
19
-
20
- .cm-s-github-dark .CodeMirror-selected {
21
- background: #6e768166;
22
- }
23
-
24
- /* Token Colors */
25
- .cm-s-github-dark .cm-comment {
26
- color: #8b949e;
27
- /* font-style: italic; */
28
- }
29
-
30
- .cm-s-github-dark .cm-error {
31
- color: #f85149;
32
- }
33
-
34
- .cm-s-github-dark .cm-keyword {
35
- color: #ff7b72;
36
- }
37
-
38
- .cm-s-github-dark .cm-operator {
39
- color: #ff7b72;
40
- font-weight: bold;
41
- }
1
+ /* GitHub-like DARK theme for rendered code blocks.
2
+ Scoped to dark-mode attributes so it can coexist with a light theme.
3
+ */
4
+
5
+ html.dark-mode,
6
+ body.dark-mode,
7
+ html[data-theme='dark'],
8
+ body[data-theme='dark'],
9
+ [data-mode='dark'] {
10
+ }
11
+
12
+ html.dark-mode pre, html.dark-mode code,
13
+ body.dark-mode pre, body.dark-mode code,
14
+ html[data-theme='dark'] pre, html[data-theme='dark'] code,
15
+ body[data-theme='dark'] pre, body[data-theme='dark'] code,
16
+ [data-mode='dark'] pre, [data-mode='dark'] code {
17
+ background-color: #0d1117;
18
+ color: #e6edf3;
19
+ }
20
+
21
+ html.dark-mode pre, body.dark-mode pre,
22
+ html[data-theme='dark'] pre, body[data-theme='dark'] pre,
23
+ [data-mode='dark'] pre {
24
+ border: 1px solid #30363d;
25
+ border-radius: 6px;
26
+ }
27
+
28
+ /* Basic token colors assuming highlight.js / pygments style classes */
29
+ html.dark-mode .hljs-comment,
30
+ body.dark-mode .hljs-comment,
31
+ html[data-theme='dark'] .hljs-comment,
32
+ body[data-theme='dark'] .hljs-comment,
33
+ [data-mode='dark'] .hljs-comment {
34
+ color: #8b949e;
35
+ }
36
+
37
+ html.dark-mode .hljs-keyword,
38
+ body.dark-mode .hljs-keyword,
39
+ html[data-theme='dark'] .hljs-keyword,
40
+ body[data-theme='dark'] .hljs-keyword,
41
+ [data-mode='dark'] .hljs-keyword {
42
+ color: #ff7b72;
43
+ }
44
+
45
+ html.dark-mode .hljs-string,
46
+ body.dark-mode .hljs-string,
47
+ html[data-theme='dark'] .hljs-string,
48
+ body[data-theme='dark'] .hljs-string,
49
+ [data-mode='dark'] .hljs-string {
50
+ color: #7ee787;
51
+ }
52
+
53
+ html.dark-mode .hljs-number,
54
+ body.dark-mode .hljs-number,
55
+ html[data-theme='dark'] .hljs-number,
56
+ body[data-theme='dark'] .hljs-number,
57
+ [data-mode='dark'] .hljs-number {
58
+ color: #79c0ff;
59
+ }
60
+
61
+ html.dark-mode .hljs-title,
62
+ body.dark-mode .hljs-title,
63
+ html[data-theme='dark'] .hljs-title,
64
+ body[data-theme='dark'] .hljs-title,
65
+ [data-mode='dark'] .hljs-title {
66
+ color: #d2a8ff;
67
+ }
68
+
69
+ /* Fallback for generic code tokens used by sphinx/highlight.js */
70
+ html.dark-mode .nb-output pre,
71
+ body.dark-mode .nb-output pre,
72
+ html[data-theme='dark'] .nb-output pre,
73
+ body[data-theme='dark'] .nb-output pre,
74
+ [data-mode='dark'] .nb-output pre {
75
+ background-color: #161b22;
76
+ }
42
77
 
43
78
  .cm-s-github-dark .cm-literal {
44
79
  color: #a5d6ff;
@@ -1,43 +1,78 @@
1
- /* github-light theme for CodeMirror */
2
- .cm-s-github-light.CodeMirror {
1
+ /* GitHub-like LIGHT theme for rendered code blocks.
2
+ Scoped to light-mode attributes so it can coexist with a dark theme.
3
+ */
4
+
5
+ html.light-mode,
6
+ body.light-mode,
7
+ html[data-theme='light'],
8
+ body[data-theme='light'],
9
+ [data-mode='light'] {
10
+ }
11
+
12
+ html.light-mode pre, html.light-mode code,
13
+ body.light-mode pre, body.light-mode code,
14
+ html[data-theme='light'] pre, html[data-theme='light'] code,
15
+ body[data-theme='light'] pre, body[data-theme='light'] code,
16
+ [data-mode='light'] pre, [data-mode='light'] code {
3
17
  background-color: #ffffff;
4
18
  color: #24292f;
5
19
  }
6
20
 
7
- .cm-s-github-light .CodeMirror-gutters {
8
- background-color: #ffffff;
9
- border-right: 1px solid #d1d5da;
21
+ html.light-mode pre, body.light-mode pre,
22
+ html[data-theme='light'] pre, body[data-theme='light'] pre,
23
+ [data-mode='light'] pre {
24
+ border: 1px solid #d1d5da;
25
+ border-radius: 6px;
10
26
  }
11
27
 
12
- .cm-s-github-light .CodeMirror-linenumber {
28
+ /* Basic token colors assuming highlight.js / pygments style classes */
29
+ html.light-mode .hljs-comment,
30
+ body.light-mode .hljs-comment,
31
+ html[data-theme='light'] .hljs-comment,
32
+ body[data-theme='light'] .hljs-comment,
33
+ [data-mode='light'] .hljs-comment {
13
34
  color: #6e7781;
14
35
  }
15
36
 
16
- .cm-s-github-light .CodeMirror-cursor {
17
- border-left: 1px solid #24292f;
18
- }
19
-
20
- .cm-s-github-light .CodeMirror-selected {
21
- background: #0969da4a;
37
+ html.light-mode .hljs-keyword,
38
+ body.light-mode .hljs-keyword,
39
+ html[data-theme='light'] .hljs-keyword,
40
+ body[data-theme='light'] .hljs-keyword,
41
+ [data-mode='light'] .hljs-keyword {
42
+ color: #cf222e;
22
43
  }
23
44
 
24
- /* Token Colors */
25
- .cm-s-github-light .cm-comment {
26
- color: #6e7781;
27
- /* font-style: italic; */
45
+ html.light-mode .hljs-string,
46
+ body.light-mode .hljs-string,
47
+ html[data-theme='light'] .hljs-string,
48
+ body[data-theme='light'] .hljs-string,
49
+ [data-mode='light'] .hljs-string {
50
+ color: #116329;
28
51
  }
29
52
 
30
- .cm-s-github-light .cm-error {
31
- color: #cf222e;
53
+ html.light-mode .hljs-number,
54
+ body.light-mode .hljs-number,
55
+ html[data-theme='light'] .hljs-number,
56
+ body[data-theme='light'] .hljs-number,
57
+ [data-mode='light'] .hljs-number {
58
+ color: #0550ae;
32
59
  }
33
60
 
34
- .cm-s-github-light .cm-keyword {
35
- color: #cf222e;
61
+ html.light-mode .hljs-title,
62
+ body.light-mode .hljs-title,
63
+ html[data-theme='light'] .hljs-title,
64
+ body[data-theme='light'] .hljs-title,
65
+ [data-mode='light'] .hljs-title {
66
+ color: #0550ae;
36
67
  }
37
68
 
38
- .cm-s-github-light .cm-operator {
39
- color: #116329;
40
- font-weight: bold;
69
+ /* Fallback for generic code tokens used by sphinx/highlight.js */
70
+ html.light-mode .nb-output pre,
71
+ body.light-mode .nb-output pre,
72
+ html[data-theme='light'] .nb-output pre,
73
+ body[data-theme='light'] .nb-output pre,
74
+ [data-mode='light'] .nb-output pre {
75
+ background-color: #f6f8fa;
41
76
  }
42
77
 
43
78
  .cm-s-github-light .cm-literal {
@@ -307,29 +307,31 @@ html[data-theme='dark'] .jeopardy-modal-body,
307
307
  body[data-theme='dark'] .jeopardy-modal-body,
308
308
  [data-mode='dark'] .jeopardy-modal-body { background: var(--jp-modal-bg) !important; color: var(--jp-modal-fg) !important; }
309
309
 
310
- /* Code inside Jeopardy content should follow the page's syntax theme.
311
- Do NOT override background in light mode; only tweak spacing and borders. */
310
+ /* Code inside Jeopardy content should fully follow the site's
311
+ syntax-highlighting theme. Do not touch colors, only allow
312
+ small layout tweaks if needed. */
312
313
  .jeopardy-modal-body pre,
313
314
  .jeopardy-modal-body code {
314
- background: inherit;
315
+ /* no color overrides here; let global syntax theme win */
316
+ margin-top: 0.5rem;
315
317
  }
316
318
 
319
+ /* Light theme: rely entirely on the site's light code theme. */
317
320
  html.light-mode .jeopardy-modal-body pre,
318
321
  body.light-mode .jeopardy-modal-body pre,
319
322
  html[data-theme='light'] .jeopardy-modal-body pre,
320
323
  body[data-theme='light'] .jeopardy-modal-body pre,
321
324
  [data-mode='light'] .jeopardy-modal-body pre {
322
- background-color: transparent;
325
+ /* intentionally empty */
323
326
  }
324
327
 
325
- /* In dark mode, still avoid forcing a separate dark block if the
326
- global theme already supplies one; inherit instead. */
328
+ /* Dark theme: rely entirely on the site's dark code theme. */
327
329
  html.dark-mode .jeopardy-modal-body pre,
328
330
  body.dark-mode .jeopardy-modal-body pre,
329
331
  html[data-theme='dark'] .jeopardy-modal-body pre,
330
332
  body[data-theme='dark'] .jeopardy-modal-body pre,
331
333
  [data-mode='dark'] .jeopardy-modal-body pre {
332
- background-color: inherit;
334
+ /* intentionally empty */
333
335
  }
334
336
 
335
337
  .jeopardy-q { font-size: 1.05rem; margin-bottom: 1rem; background: inherit !important; }
@@ -365,7 +367,23 @@ body[data-theme='dark'] .jeopardy-modal-body pre,
365
367
  .j-btn.accent:hover { filter: brightness(1.05); }
366
368
  .j-btn:focus { outline: 2px solid var(--jp-outline); outline-offset: 2px; }
367
369
 
368
- .jeopardy-team-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
370
+ .jeopardy-team-actions {
371
+ display: flex;
372
+ flex-wrap: wrap;
373
+ gap: 0.75rem;
374
+ margin-top: 0.75rem;
375
+ }
376
+ .jeopardy-team-actions .jeopardy-team-column {
377
+ display: flex;
378
+ flex-direction: column;
379
+ align-items: stretch;
380
+ gap: 0.35rem;
381
+ min-width: 7rem;
382
+ }
383
+ .jeopardy-team-actions .jeopardy-team-label {
384
+ font-weight: 600;
385
+ font-size: 0.9rem;
386
+ }
369
387
  .jeopardy-team-actions .j-btn { font-size: 0.9rem; }
370
388
 
371
389
  /* Per-team scoring buttons in the modal: make choices visually distinct
@@ -0,0 +1,199 @@
1
+ (function () {
2
+ function renderMathIn(element) {
3
+ if (typeof window !== 'undefined' && typeof window.renderMathInElement === 'function') {
4
+ window.renderMathInElement(element, {
5
+ delimiters: [
6
+ { left: '$$', right: '$$', display: true },
7
+ { left: '$', right: '$', display: false },
8
+ { left: '\\[', right: '\\]', display: true },
9
+ { left: '\\(', right: '\\)', display: false },
10
+ ],
11
+ });
12
+ }
13
+ }
14
+
15
+ function highlightCode(element) {
16
+ if (typeof window !== 'undefined' && window.hljs && typeof window.hljs.highlightElement === 'function') {
17
+ const blocks = element.querySelectorAll('code');
18
+ blocks.forEach((b) => window.hljs.highlightElement(b));
19
+ }
20
+ }
21
+
22
+ function createFlashcards(root, cfg) {
23
+ const cards = Array.isArray(cfg.cards) ? cfg.cards.slice() : [];
24
+ const opts = cfg.options || {};
25
+ const showProgress = !!opts.show_progress;
26
+ let index = Number.isFinite(opts.start_index) ? Math.max(0, Math.min(cards.length - 1, opts.start_index)) : 0;
27
+ let side = 'front';
28
+
29
+ if (opts.shuffle && cards.length > 1) {
30
+ for (let i = cards.length - 1; i > 0; i -= 1) {
31
+ const j = Math.floor(Math.random() * (i + 1));
32
+ [cards[i], cards[j]] = [cards[j], cards[i]];
33
+ }
34
+ }
35
+
36
+ root.innerHTML = '';
37
+ root.classList.add('flashcards-root');
38
+
39
+ const cardShell = document.createElement('div');
40
+ cardShell.className = 'flashcards-card-shell';
41
+
42
+ const card = document.createElement('div');
43
+ card.className = 'flashcards-card';
44
+
45
+ const header = document.createElement('div');
46
+ header.className = 'flashcards-header';
47
+
48
+ const sideLabel = document.createElement('div');
49
+ sideLabel.className = 'flashcards-pill';
50
+
51
+ const progress = document.createElement('div');
52
+ progress.className = 'flashcards-progress';
53
+
54
+ header.appendChild(sideLabel);
55
+ header.appendChild(progress);
56
+
57
+ const body = document.createElement('div');
58
+ body.className = 'flashcards-body';
59
+
60
+ const frontDiv = document.createElement('div');
61
+ frontDiv.className = 'flashcards-face front';
62
+
63
+ const backDiv = document.createElement('div');
64
+ backDiv.className = 'flashcards-face back';
65
+
66
+ body.appendChild(frontDiv);
67
+ body.appendChild(backDiv);
68
+
69
+ const footer = document.createElement('div');
70
+ footer.className = 'flashcards-footer';
71
+
72
+ const hint = document.createElement('div');
73
+ hint.textContent = 'Tips: bruk Space for å snu kortet';
74
+
75
+ const controls = document.createElement('div');
76
+ controls.className = 'flashcards-controls';
77
+
78
+ const prevBtn = document.createElement('button');
79
+ prevBtn.type = 'button';
80
+ prevBtn.className = 'flashcards-btn';
81
+ prevBtn.textContent = '← Forrige';
82
+
83
+ const flipBtn = document.createElement('button');
84
+ flipBtn.type = 'button';
85
+ flipBtn.className = 'flashcards-btn flashcards-btn-primary';
86
+ flipBtn.textContent = 'Snu kortet';
87
+
88
+ const nextBtn = document.createElement('button');
89
+ nextBtn.type = 'button';
90
+ nextBtn.className = 'flashcards-btn';
91
+ nextBtn.textContent = 'Neste →';
92
+
93
+ controls.appendChild(prevBtn);
94
+ controls.appendChild(flipBtn);
95
+ controls.appendChild(nextBtn);
96
+
97
+ footer.appendChild(hint);
98
+ footer.appendChild(controls);
99
+
100
+ card.appendChild(header);
101
+ card.appendChild(body);
102
+ card.appendChild(footer);
103
+ cardShell.appendChild(card);
104
+ root.appendChild(cardShell);
105
+
106
+ function update() {
107
+ if (!cards.length) {
108
+ frontDiv.innerHTML = '<em>Ingen kort.</em>';
109
+ backDiv.innerHTML = '';
110
+ sideLabel.textContent = 'Kort';
111
+ progress.textContent = '';
112
+ prevBtn.disabled = true;
113
+ nextBtn.disabled = true;
114
+ flipBtn.disabled = true;
115
+ return;
116
+ }
117
+
118
+ const current = cards[index] || { front: '', back: '' };
119
+ frontDiv.innerHTML = current.front || '';
120
+ backDiv.innerHTML = current.back || '';
121
+ root.dataset.side = side;
122
+
123
+ sideLabel.textContent = side === 'front' ? 'Forside' : 'Bakside';
124
+ progress.textContent = showProgress ? `Kort ${index + 1} / ${cards.length}` : '';
125
+
126
+ prevBtn.disabled = index === 0;
127
+ nextBtn.disabled = index === cards.length - 1;
128
+
129
+ [frontDiv, backDiv].forEach((el) => {
130
+ renderMathIn(el);
131
+ highlightCode(el);
132
+ });
133
+ }
134
+
135
+ function flip() {
136
+ side = side === 'front' ? 'back' : 'front';
137
+ card.classList.remove('flashcards-card-flip');
138
+ // Force reflow so the animation can restart
139
+ // eslint-disable-next-line no-unused-expressions
140
+ void card.offsetWidth;
141
+ card.classList.add('flashcards-card-flip');
142
+ update();
143
+ }
144
+
145
+ function prev() {
146
+ if (index > 0) {
147
+ index -= 1;
148
+ side = 'front';
149
+ update();
150
+ }
151
+ }
152
+
153
+ function next() {
154
+ if (index < cards.length - 1) {
155
+ index += 1;
156
+ side = 'front';
157
+ update();
158
+ }
159
+ }
160
+
161
+ flipBtn.addEventListener('click', flip);
162
+ prevBtn.addEventListener('click', prev);
163
+ nextBtn.addEventListener('click', next);
164
+
165
+ root.tabIndex = 0;
166
+ root.addEventListener('keydown', (ev) => {
167
+ if (ev.key === ' ' || ev.code === 'Space') {
168
+ ev.preventDefault();
169
+ flip();
170
+ } else if (ev.key === 'ArrowLeft') {
171
+ prev();
172
+ } else if (ev.key === 'ArrowRight') {
173
+ next();
174
+ }
175
+ });
176
+
177
+ update();
178
+ }
179
+
180
+ function init() {
181
+ const containers = document.querySelectorAll('.flashcards-container');
182
+ containers.forEach((c) => {
183
+ try {
184
+ const script = c.querySelector('.flashcards-data');
185
+ const raw = script && script.textContent ? script.textContent : c.getAttribute('data-config') || '{}';
186
+ const cfg = JSON.parse(raw);
187
+ createFlashcards(c, cfg);
188
+ } catch (e) {
189
+ console.error('flashcards: init failed', e);
190
+ }
191
+ });
192
+ }
193
+
194
+ if (document.readyState === 'loading') {
195
+ document.addEventListener('DOMContentLoaded', init);
196
+ } else {
197
+ init();
198
+ }
199
+ })();
@@ -361,18 +361,22 @@
361
361
  teams.forEach((t, i)=>{
362
362
  // In turn-based mode, only the active team can be scored on
363
363
  if (gameMode==='turn' && i!==currentTurn) return;
364
+ // Create a per-team column with label and buttons
365
+ const teamCol = document.createElement('div');
366
+ teamCol.className = 'jeopardy-team-column';
367
+ const teamLabel = document.createElement('div');
368
+ teamLabel.className = 'jeopardy-team-label';
369
+ teamLabel.textContent = t.name;
370
+ teamCol.appendChild(teamLabel);
364
371
 
365
372
  const add = document.createElement('button');
366
373
  add.className='j-btn primary';
367
- add.textContent = `+${value} ${t.name}`;
374
+ add.textContent = `+${value} poeng`;
368
375
 
369
- // In duel mode, we need an explicit "0 points" option per team
370
- let zeroBtn = null;
371
- if (gameMode === 'duel') {
372
- zeroBtn = document.createElement('button');
373
- zeroBtn.className = 'j-btn secondary';
374
- zeroBtn.textContent = `0 ${t.name}`;
375
- }
376
+ // Explicit 0-points option per team in both modes
377
+ let zeroBtn = document.createElement('button');
378
+ zeroBtn.className = 'j-btn secondary';
379
+ zeroBtn.textContent = '0 poeng';
376
380
 
377
381
  // Register this team as pending in duel mode
378
382
  if (gameMode === 'duel') {
@@ -414,6 +418,7 @@
414
418
  registerScore(value);
415
419
  scored = true;
416
420
  if (add) { add.disabled = true; add.classList.add('used-choice'); }
421
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
417
422
  finalizeIfNeeded();
418
423
  } else {
419
424
  if (!duelPendingTeams.has(i)) return;
@@ -426,7 +431,14 @@
426
431
  };
427
432
 
428
433
  const handleZero = ()=>{
429
- if (gameMode === 'duel') {
434
+ if (gameMode === 'turn') {
435
+ if (scored) return;
436
+ // Explicitly choose 0 points: no score change, but consume the question
437
+ scored = true;
438
+ if (add) { add.disabled = true; add.classList.add('used-choice'); }
439
+ if (zeroBtn) { zeroBtn.disabled = true; zeroBtn.classList.add('used-choice'); }
440
+ finalizeIfNeeded();
441
+ } else if (gameMode === 'duel') {
430
442
  if (!duelPendingTeams.has(i)) return;
431
443
  // Zero points: just clear pending state for this team
432
444
  duelPendingTeams.delete(i);
@@ -444,8 +456,9 @@
444
456
  if (zeroBtn) zeroBtn.disabled = true;
445
457
  }
446
458
 
447
- teamActions.appendChild(add);
448
- if (zeroBtn) teamActions.appendChild(zeroBtn);
459
+ teamCol.appendChild(add);
460
+ if (zeroBtn) teamCol.appendChild(zeroBtn);
461
+ teamActions.appendChild(teamCol);
449
462
  });
450
463
  const footerRight = document.createElement('div');
451
464
  footerRight.className = 'jeopardy-footer-right';
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: munchboka-edutools
3
- Version: 0.1.14
3
+ Version: 0.1.19
4
4
  Summary: Sphinx/Jupyter Book directives and assets for Munchboka and VGS books
5
5
  Project-URL: Homepage, https://github.com/reneaas/munchboka-edutools
6
6
  Project-URL: Issues, https://github.com/reneaas/munchboka-edutools/issues
@@ -1,6 +1,6 @@
1
1
  munchboka_edutools/__init__.py,sha256=fgHZaxSDzU7Didm5UN2_H2-ZYZIUowU8MJaRYxNmKNA,7380
2
2
  munchboka_edutools/_plotmath_shim.py,sha256=kWZV-SO4idGHCVdBcfQmrhFF9DEgxPY5Xg8d1h1Bb6c,3348
3
- munchboka_edutools/_version.py,sha256=N9x0dPNNIgG8onJijVEeDulgDj_3QJTk0cksf5Vs5IA,49
3
+ munchboka_edutools/_version.py,sha256=bmv5BPFZ5wOAf5EHfelg_MGwWrQ0-YajL51koWqQQAQ,49
4
4
  munchboka_edutools/directives/__init__.py,sha256=wxsmI7Wt_Rpft925y9BmU7urgKhGNlpqnzN2cFbCtoI,83
5
5
  munchboka_edutools/directives/admonitions.py,sha256=yJB5F1Wlu_AZdiWQWFR7ArBO0OFE5bsx3DirLV_ujaw,10226
6
6
  munchboka_edutools/directives/cas_popup.py,sha256=WED8V1DT55sqYRr_CpgbkanadXRkNmddeW9KSw11IKA,9113
@@ -8,6 +8,7 @@ munchboka_edutools/directives/clear.py,sha256=LouDw6yPUccbHCKOt-64vv34xyaBZwbL_4
8
8
  munchboka_edutools/directives/dialogue.py,sha256=qgG8AW0vNvwpnGlGruqTm2KK6KG2gcHJoZ8Ibo8RGEQ,4203
9
9
  munchboka_edutools/directives/escape_room.py,sha256=ZVnxcINvMGis1AOpHZLSOuGDN1zVRAyWJ3Mm7RUDBvk,10986
10
10
  munchboka_edutools/directives/factor_tree.py,sha256=BW6-OQO_9x4H5ktfmkz0zOnNmp0N8azj17LQMgzm9Jo,18451
11
+ munchboka_edutools/directives/flashcards.py,sha256=yyzBGXFa2IpeephiRP9qWWOttFf4HZwa93XK3wruUKQ,8879
11
12
  munchboka_edutools/directives/ggb.py,sha256=zHlO_zAFXNqAnvH8xUYP1b0khjpbcUwuPb0QMzkObGc,6232
12
13
  munchboka_edutools/directives/ggb_icon.py,sha256=tgOLBQt7CzMpEz5WmAL9-VjPYCMd4zDP973zsanIw0c,3118
13
14
  munchboka_edutools/directives/ggb_popup.py,sha256=G84ZgYExh9nLav8b_8ZBe8825nU8wcedOIvSGlDK2FM,5384
@@ -17,7 +18,7 @@ munchboka_edutools/directives/jeopardy.py,sha256=FoYvyuGbRXpF8KjjKJ-FUzonvPpqm3v
17
18
  munchboka_edutools/directives/multi_plot.py,sha256=q2ocaAWzf_ZnDcUi3vKAEt_M_d7zNTEJwveyHrEK5mc,42739
18
19
  munchboka_edutools/directives/pair_puzzle.py,sha256=8x6RXVT7I3EcEZEX2TJxPJxOIkwdhd-YPh_DnoKQarI,5277
19
20
  munchboka_edutools/directives/parsons.py,sha256=fpKcbyiHcJQlYsiMBaHKAy5vJ0eCUMUVV8h1clb5s7A,3384
20
- munchboka_edutools/directives/plot.py,sha256=oYnTUI5sbUn-A4pfYwBbd-TKif78l3i27NHXl6MhUok,132446
21
+ munchboka_edutools/directives/plot.py,sha256=s4TscvwfXXt2D8Jul1QkA1ITSn_JCixUJ819U_-K5Eg,135225
21
22
  munchboka_edutools/directives/poly_icon.py,sha256=hT0NUrgkcYqcHk4_lKGiqam4RT6LXTarTim08j4BsJc,3558
22
23
  munchboka_edutools/directives/polydiv.py,sha256=vd8rwNqpn6_Wu52P7_LAGRhL36hufFB1aRskuxEiko4,11735
23
24
  munchboka_edutools/directives/popup.py,sha256=Nni7uxmXD4YguC7L-tgiowkDNsXZ8S_XlKGU-LRroCY,8072
@@ -29,12 +30,13 @@ munchboka_edutools/static/css/admonitions.css,sha256=qwA4Xvj6dJ5pd-vKi9CiMqvNJCl
29
30
  munchboka_edutools/static/css/cas_popup.css,sha256=-tpz-YJHrmcZtRcVWzPLiKZMF7QYaAcVIzXZw6ER8W0,5731
30
31
  munchboka_edutools/static/css/dialogue.css,sha256=9G8bswTTVjTaqWVStFR7PaS7uT-mpQrq2Sf07zEgS6g,1494
31
32
  munchboka_edutools/static/css/figures.css,sha256=E-CeY02Ac-T1H6EGuCBxeoKvug307_oG2Iq9vVxSCiY,6782
33
+ munchboka_edutools/static/css/flashcards.css,sha256=oe8IDsT873dCbkevJ_8HYgYsDXk2VvRWs01ArYJ1tA0,4692
32
34
  munchboka_edutools/static/css/general_style.css,sha256=6m07K84uQf7iaFMpnVmLUt1V4ZrV-673bo7n2sKS9Yo,2298
33
35
  munchboka_edutools/static/css/github-dark-high-contrast.css,sha256=Dk7DDwe7x66NA3vwNIzB4mGD2GD6pODUCoSippd56Hk,2883
34
- munchboka_edutools/static/css/github-dark.css,sha256=zEty5-T_Z0f6Vs7QcqBvoTeRvsLysKQYZ8YLOEyDZoo,1933
35
- munchboka_edutools/static/css/github-light.css,sha256=dfkR_YEobEKt1IVkC8z3sIeliLGQcWHTKvLHjxi88II,1855
36
+ munchboka_edutools/static/css/github-dark.css,sha256=XhVzY424aY3qvMwlwowsn_ncW9y2WHhUsXncvPsPk-U,3150
37
+ munchboka_edutools/static/css/github-light.css,sha256=G7yNZ0s7qjwigyr6gN4qhQdUo3_bhJIFtJOZ41cMKNw,3191
36
38
  munchboka_edutools/static/css/interactive_code.css,sha256=ZD0NEw_VM2fczaxOkKREPB8GykDJVDoQ9rDbttw0m0Y,13808
37
- munchboka_edutools/static/css/jeopardy.css,sha256=jY2rSfWLfqzGqCZ4GXA2uPfcjpXoZyoAvrGZvZWybMA,20408
39
+ munchboka_edutools/static/css/jeopardy.css,sha256=N375ADhb9ElKzdkdMHKbkwJsmsy2NusT9kfB8quIjYo,20726
38
40
  munchboka_edutools/static/css/popup.css,sha256=0-wc2b6zXwZXecxSey5S5Y9036BBXjNFRQpf9ft5dUE,2514
39
41
  munchboka_edutools/static/css/quiz.css,sha256=gLvmSuQalYgFXP3-fkLOwfE6LpX6oHkm4vHPDMhUFPE,8209
40
42
  munchboka_edutools/static/css/timedQuiz.css,sha256=wb0u7CmbMJ-JkYkHQYksrir329sTAtKyL7q9K-eYY14,9912
@@ -123,9 +125,10 @@ munchboka_edutools/static/icons/solid/light_mode/stop.svg,sha256=MHKp5NcZ6dV_oN7
123
125
  munchboka_edutools/static/icons/stickers/edit.svg,sha256=P1XbvIodj08IQkv7MJMKatCFOuu4EABhhWVXJLGx5og,317
124
126
  munchboka_edutools/static/icons/stickers/pencil_square.svg,sha256=NQ0nT7vhRzrznFC6rt7Tiqq9EeJJwiDdphRs1-A5vvM,455
125
127
  munchboka_edutools/static/js/casThemeManager.js,sha256=DCHqn7c2NhyGo8r4Q6KF5hs5eskHKUhhb-OHZJxGM38,3189
128
+ munchboka_edutools/static/js/flashcards.js,sha256=-PIxkxqcatd1sCNsOFg-QBsQwv9uxRw6_mZKCBo_9FA,5976
126
129
  munchboka_edutools/static/js/geogebra-setup.js,sha256=iLMH-Ja_PJv2Yg8Ujy_LoiVQkd_x1urYoK5KCgIjPtU,279
127
130
  munchboka_edutools/static/js/highlight-init.js,sha256=4sTpB5Z8uLp4-iczmsmRBt3zBjID6dIbeFSfad2Fz3E,188
128
- munchboka_edutools/static/js/jeopardy.js,sha256=35nHhk-DQ6zXM1BerI7sYbYyD7V3Sw1ycbUNQH1ISG4,25436
131
+ munchboka_edutools/static/js/jeopardy.js,sha256=xEGMIN6gIvHyQprxj1Ur8GjHaUseMkHLu_oNwFvFa_4,26214
129
132
  munchboka_edutools/static/js/popup.js,sha256=5zI8AmR1-LcUUqlFn210lUDdfFCY0UmzqoSsIZ899JQ,2740
130
133
  munchboka_edutools/static/js/quiz.js,sha256=9dpH3B4KM3nPscjE41seQNneXJGj1hrMPjirh3DYunI,15468
131
134
  munchboka_edutools/static/js/utils.js,sha256=p5qSGOB1orGhQLft6bZ3qpx1Mpog4y6yAvh4WyXDt6k,160
@@ -143,7 +146,7 @@ munchboka_edutools/static/js/skulpt/skulpt.js,sha256=xzS8AVz4mGi2v4lytAEeH0tm7Ip
143
146
  munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js,sha256=OleHi24leM3Ejc2dvHYSwQGarIA_ZoqjtA3WrPvaX8s,6571
144
147
  munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js,sha256=TFKnF80ZsnYbzdSKuOsKcHfEg1ZiUok9vgraOTFE5gM,9030
145
148
  munchboka_edutools/static/js/timedQuiz/utils.js,sha256=CvlHVW3Uq-DQapRRuMC61q785GN5NZX5VmZj0zeP2hE,195
146
- munchboka_edutools-0.1.14.dist-info/METADATA,sha256=qGTN1DDlkbi3jsEsorMheTP96cmc6Nf8n8TMn8zQKGs,3270
147
- munchboka_edutools-0.1.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
148
- munchboka_edutools-0.1.14.dist-info/licenses/LICENSE,sha256=7nP4mEfu9fXD-OFxavvJyiudI9LGiEvLk9a0ucEZ6NE,1079
149
- munchboka_edutools-0.1.14.dist-info/RECORD,,
149
+ munchboka_edutools-0.1.19.dist-info/METADATA,sha256=vscsVJhxMAYX-aHKUMYZbS5olMCTHDmBwDvECWaiMwA,3270
150
+ munchboka_edutools-0.1.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
151
+ munchboka_edutools-0.1.19.dist-info/licenses/LICENSE,sha256=7nP4mEfu9fXD-OFxavvJyiudI9LGiEvLk9a0ucEZ6NE,1079
152
+ munchboka_edutools-0.1.19.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any