munchboka-edutools 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of munchboka-edutools might be problematic. Click here for more details.
- munchboka_edutools/__init__.py +184 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +389 -0
- munchboka_edutools/directives/cas_popup.py +272 -0
- munchboka_edutools/directives/clear.py +103 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/factor_tree.py +549 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +105 -0
- munchboka_edutools/directives/ggb_popup.py +165 -0
- munchboka_edutools/directives/horner.py +324 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/multi_plot.py +1126 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3105 -0
- munchboka_edutools/directives/poly_icon.py +111 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/popup.py +245 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +516 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +274 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +112 -0
- munchboka_edutools/static/css/github-light.css +120 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +529 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/popup.css +115 -0
- munchboka_edutools/static/css/quiz.css +312 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/jeopardy.js +523 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/popup.js +85 -0
- munchboka_edutools/static/js/quiz.js +422 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
- munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
- munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polynomial function shape icon role for inline SVG icons.
|
|
3
|
+
|
|
4
|
+
This role allows you to insert icons representing different polynomial function shapes.
|
|
5
|
+
The icons help visualize the general shape of polynomial functions.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
A quadratic function with positive leading coefficient has a {poly-icon}`smile` shape.
|
|
9
|
+
|
|
10
|
+
A cubic function with negative leading coefficient has a {poly-icon}`cubicdown` shape.
|
|
11
|
+
|
|
12
|
+
Available icons:
|
|
13
|
+
- smile: U-shaped (parabola opening upward, like a smile)
|
|
14
|
+
- frown: ∩-shaped (parabola opening downward, like a frown)
|
|
15
|
+
- cubicup: Cubic function starting low and ending high (∪∩ shape)
|
|
16
|
+
- cubicdown: Cubic function starting high and ending low (∩∪ shape)
|
|
17
|
+
|
|
18
|
+
The icons are rendered as inline images with appropriate alt text.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from docutils import nodes
|
|
22
|
+
from docutils.parsers.rst import roles
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Custom node for polynomial icons
|
|
26
|
+
class poly_icon_node(nodes.Inline, nodes.Element):
|
|
27
|
+
"""Custom node for polynomial icons."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def poly_icon_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
|
33
|
+
"""
|
|
34
|
+
Custom role for polynomial function shape icons.
|
|
35
|
+
|
|
36
|
+
Usage: {poly-icon}`smile`
|
|
37
|
+
|
|
38
|
+
This generates a custom node that will be rendered with relative paths.
|
|
39
|
+
"""
|
|
40
|
+
# Clean up the icon name (remove any extra whitespace)
|
|
41
|
+
icon_name = text.strip().lower()
|
|
42
|
+
|
|
43
|
+
# Validate icon name
|
|
44
|
+
valid_icons = ["smile", "frown", "cubicup", "cubicdown"]
|
|
45
|
+
if icon_name not in valid_icons:
|
|
46
|
+
msg = inliner.reporter.error(
|
|
47
|
+
f'Invalid poly-icon name "{icon_name}". Must be one of: {", ".join(valid_icons)}',
|
|
48
|
+
line=lineno,
|
|
49
|
+
)
|
|
50
|
+
prb = inliner.problematic(rawtext, rawtext, msg)
|
|
51
|
+
return [prb], [msg]
|
|
52
|
+
|
|
53
|
+
# Create our custom node
|
|
54
|
+
node = poly_icon_node()
|
|
55
|
+
node["icon_name"] = icon_name
|
|
56
|
+
node["classes"] = ["inline-image"]
|
|
57
|
+
|
|
58
|
+
return [node], []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def visit_poly_icon_html(self, node):
|
|
62
|
+
"""HTML visitor for poly_icon_node - generates relative path."""
|
|
63
|
+
icon_name = node["icon_name"]
|
|
64
|
+
|
|
65
|
+
# Get the relative path from current document to _static directory
|
|
66
|
+
# self.builder.current_docname is like "examples/poly_icon"
|
|
67
|
+
# We want path from there to ../_static/munchboka/icons/polyicons/
|
|
68
|
+
from os.path import dirname
|
|
69
|
+
|
|
70
|
+
# Current document directory depth
|
|
71
|
+
doc_dir = dirname(self.builder.current_docname) if "/" in self.builder.current_docname else ""
|
|
72
|
+
|
|
73
|
+
# Calculate relative path
|
|
74
|
+
if doc_dir:
|
|
75
|
+
# For documents in subdirectories like "examples/poly_icon"
|
|
76
|
+
depth = doc_dir.count("/") + 1
|
|
77
|
+
rel_prefix = "../" * depth
|
|
78
|
+
else:
|
|
79
|
+
# For top-level documents
|
|
80
|
+
rel_prefix = ""
|
|
81
|
+
|
|
82
|
+
img_path = f"{rel_prefix}_static/munchboka/icons/polyicons/{icon_name}.svg"
|
|
83
|
+
|
|
84
|
+
# Generate the HTML with relative path
|
|
85
|
+
html = f'<img src="{img_path}" alt="{icon_name} polynomial icon" class="inline-image" />'
|
|
86
|
+
self.body.append(html)
|
|
87
|
+
raise nodes.SkipNode
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def depart_poly_icon_html(self, node):
|
|
91
|
+
"""Depart function (not needed as we raise SkipNode)."""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def setup(app):
|
|
96
|
+
"""Setup function to register the role with Sphinx."""
|
|
97
|
+
# Register the custom node
|
|
98
|
+
app.add_node(
|
|
99
|
+
poly_icon_node,
|
|
100
|
+
html=(visit_poly_icon_html, depart_poly_icon_html),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Register the role with both hyphenated and unhyphenated names
|
|
104
|
+
roles.register_local_role("poly-icon", poly_icon_role)
|
|
105
|
+
roles.register_local_role("polyicon", poly_icon_role)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"version": "0.1.0",
|
|
109
|
+
"parallel_read_safe": True,
|
|
110
|
+
"parallel_write_safe": True,
|
|
111
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polynomial long division directive for Sphinx/Jupyter Book.
|
|
3
|
+
|
|
4
|
+
Generates SVG visualizations of polynomial long division using LaTeX.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
import uuid
|
|
12
|
+
from docutils import nodes
|
|
13
|
+
from docutils.parsers.rst import directives
|
|
14
|
+
from sphinx.util.docutils import SphinxDirective
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _hash_key(*parts):
|
|
18
|
+
"""Generate a hash key from multiple parts for caching."""
|
|
19
|
+
h = hashlib.sha1()
|
|
20
|
+
for p in parts:
|
|
21
|
+
if p is None:
|
|
22
|
+
p = "__NONE__"
|
|
23
|
+
h.update(str(p).encode("utf-8"))
|
|
24
|
+
h.update(b"||")
|
|
25
|
+
return h.hexdigest()[:12]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def polylongdiv(fname: str, p: str, q: str, stage: int = None, svg: bool = True, vars=None):
|
|
29
|
+
"""
|
|
30
|
+
Generate polynomial long division figure using LaTeX.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
fname: Base filename (without extension)
|
|
34
|
+
p: Dividend polynomial as string (e.g., "x^3 + 2x^2 - 3x - 6")
|
|
35
|
+
q: Divisor polynomial as string (e.g., "x - 2")
|
|
36
|
+
stage: Optional stage number for step-by-step display
|
|
37
|
+
svg: If True, convert to SVG; otherwise keep as PDF
|
|
38
|
+
vars: Variable(s) used in polynomials (default: "x")
|
|
39
|
+
"""
|
|
40
|
+
if not vars:
|
|
41
|
+
vars = "x"
|
|
42
|
+
|
|
43
|
+
# Generate unique temp filenames
|
|
44
|
+
temp_id = uuid.uuid4().hex[:8]
|
|
45
|
+
tex_file = f"tmp_{temp_id}.tex"
|
|
46
|
+
pdf_file = f"tmp_{temp_id}.pdf"
|
|
47
|
+
|
|
48
|
+
# Format LaTeX command
|
|
49
|
+
if stage is None:
|
|
50
|
+
div_cmd = r"\polylongdiv[style=C, div=:, vars=None]{{p}}{{q}}"
|
|
51
|
+
div_cmd = div_cmd.replace("{p}", p).replace("{q}", q).replace("None", str(vars))
|
|
52
|
+
else:
|
|
53
|
+
div_cmd = r"\polylongdiv[style=C, div=:, stage={stage}]{{p}}{{q}}"
|
|
54
|
+
div_cmd = div_cmd.replace("{p}", p).replace("{q}", q).replace("{stage}", str(stage))
|
|
55
|
+
|
|
56
|
+
# Create LaTeX file
|
|
57
|
+
s = f"""\\documentclass[border=0.2cm]{{standalone}}
|
|
58
|
+
\\usepackage{{polynom}}
|
|
59
|
+
\\begin{{document}}
|
|
60
|
+
{div_cmd}
|
|
61
|
+
\\end{{document}}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Remove .svg extension properly if present
|
|
65
|
+
if fname.endswith(".svg"):
|
|
66
|
+
fname = fname[:-4]
|
|
67
|
+
|
|
68
|
+
# Write and process files
|
|
69
|
+
with open(tex_file, "w") as f:
|
|
70
|
+
f.write(s)
|
|
71
|
+
|
|
72
|
+
os.system(f"pdflatex {tex_file}")
|
|
73
|
+
|
|
74
|
+
if svg:
|
|
75
|
+
os.system(f"pdf2svg {pdf_file} {fname}.svg")
|
|
76
|
+
else:
|
|
77
|
+
os.system(f"mv {pdf_file} {fname}.pdf")
|
|
78
|
+
|
|
79
|
+
# Cleanup temp files
|
|
80
|
+
os.system(f"rm tmp_{temp_id}.*")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PolyDivDirective(SphinxDirective):
|
|
84
|
+
"""
|
|
85
|
+
Generate (and cache) a polynomial long division figure as SVG and embed it.
|
|
86
|
+
|
|
87
|
+
Usage (MyST):
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
::::{polydiv}
|
|
91
|
+
:p: x^3 + 2x^2 - 3x - 6
|
|
92
|
+
:q: x - 2
|
|
93
|
+
:stage: 2 # optional
|
|
94
|
+
:vars: x # optional (default x)
|
|
95
|
+
:align: center # optional (left|center|right)
|
|
96
|
+
:class: small # optional extra CSS classes on figure
|
|
97
|
+
|
|
98
|
+
Optional caption here.
|
|
99
|
+
::::
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or classic reStructuredText:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
.. polydiv::
|
|
106
|
+
:p: x^3 + 2x^2 - 3x - 6
|
|
107
|
+
:q: x - 2
|
|
108
|
+
:stage: 2
|
|
109
|
+
:vars: x
|
|
110
|
+
|
|
111
|
+
Optional caption here.
|
|
112
|
+
```
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
has_content = True
|
|
116
|
+
required_arguments = 0
|
|
117
|
+
optional_arguments = 0
|
|
118
|
+
final_argument_whitespace = False
|
|
119
|
+
option_spec = {
|
|
120
|
+
"p": directives.unchanged_required,
|
|
121
|
+
"q": directives.unchanged_required,
|
|
122
|
+
"stage": directives.nonnegative_int,
|
|
123
|
+
"vars": directives.unchanged,
|
|
124
|
+
"align": lambda a: directives.choice(a, ["left", "center", "right"]),
|
|
125
|
+
"class": directives.class_option,
|
|
126
|
+
"name": directives.unchanged, # optional explicit base filename / ref name
|
|
127
|
+
"cache": directives.flag, # include to enable (default on)
|
|
128
|
+
"nocache": directives.flag, # include to force regeneration
|
|
129
|
+
"alt": directives.unchanged,
|
|
130
|
+
"width": directives.length_or_percentage_or_unitless,
|
|
131
|
+
# Always inline now; legacy 'inline' option kept (ignored) for backward compatibility
|
|
132
|
+
"inline": directives.flag,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def run(self):
|
|
136
|
+
"""Main directive entry: always inline-embed generated SVG and allow width control & centering."""
|
|
137
|
+
env = self.state.document.settings.env
|
|
138
|
+
app = env.app
|
|
139
|
+
|
|
140
|
+
# Options
|
|
141
|
+
p = self.options.get("p")
|
|
142
|
+
q = self.options.get("q")
|
|
143
|
+
if p is None or q is None:
|
|
144
|
+
return [
|
|
145
|
+
self.state_machine.reporter.error(
|
|
146
|
+
"Directive 'polydiv' requires both :p: and :q: options.",
|
|
147
|
+
line=self.lineno,
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
stage = self.options.get("stage")
|
|
151
|
+
vars_opt = self.options.get("vars", "x")
|
|
152
|
+
explicit_name = self.options.get("name")
|
|
153
|
+
content_hash = _hash_key(p, q, stage, vars_opt)
|
|
154
|
+
base_name = explicit_name or f"polydiv_{content_hash}"
|
|
155
|
+
|
|
156
|
+
# Paths
|
|
157
|
+
src_dir = app.srcdir
|
|
158
|
+
rel_dir = os.path.join("_static", "polydiv")
|
|
159
|
+
abs_dir = os.path.join(src_dir, rel_dir)
|
|
160
|
+
os.makedirs(abs_dir, exist_ok=True)
|
|
161
|
+
svg_filename = f"{base_name}.svg"
|
|
162
|
+
abs_svg_path = os.path.join(abs_dir, svg_filename)
|
|
163
|
+
|
|
164
|
+
regenerate = "nocache" in self.options or not os.path.exists(abs_svg_path)
|
|
165
|
+
if regenerate:
|
|
166
|
+
cwd = os.getcwd()
|
|
167
|
+
try:
|
|
168
|
+
os.chdir(abs_dir)
|
|
169
|
+
polylongdiv(
|
|
170
|
+
fname=base_name,
|
|
171
|
+
p=p,
|
|
172
|
+
q=q,
|
|
173
|
+
stage=stage,
|
|
174
|
+
vars=vars_opt,
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return [
|
|
178
|
+
self.state_machine.reporter.error(
|
|
179
|
+
f"Error generating polynomial division: {e}",
|
|
180
|
+
line=self.lineno,
|
|
181
|
+
)
|
|
182
|
+
]
|
|
183
|
+
finally:
|
|
184
|
+
os.chdir(cwd)
|
|
185
|
+
|
|
186
|
+
# Post-process: strip width/height for responsiveness if viewBox present
|
|
187
|
+
try:
|
|
188
|
+
if os.path.exists(abs_svg_path):
|
|
189
|
+
with open(abs_svg_path, "r", encoding="utf-8") as f_svg:
|
|
190
|
+
svg_text_tmp = f_svg.read()
|
|
191
|
+
if "viewBox" in svg_text_tmp:
|
|
192
|
+
cleaned = re.sub(r'\swidth="[^"]+"', "", svg_text_tmp)
|
|
193
|
+
cleaned = re.sub(r'\sheight="[^"]+"', "", cleaned)
|
|
194
|
+
if cleaned != svg_text_tmp:
|
|
195
|
+
with open(abs_svg_path, "w", encoding="utf-8") as f_out:
|
|
196
|
+
f_out.write(cleaned)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if not os.path.exists(abs_svg_path):
|
|
201
|
+
return [
|
|
202
|
+
self.state_machine.reporter.error(
|
|
203
|
+
(
|
|
204
|
+
f"polydiv: failed to generate SVG '{svg_filename}'. "
|
|
205
|
+
"Check that 'pdflatex' and 'pdf2svg' are installed."
|
|
206
|
+
),
|
|
207
|
+
line=self.lineno,
|
|
208
|
+
)
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
env.note_dependency(abs_svg_path)
|
|
212
|
+
try:
|
|
213
|
+
out_static = os.path.join(app.outdir, "_static", "polydiv")
|
|
214
|
+
os.makedirs(out_static, exist_ok=True)
|
|
215
|
+
shutil.copy2(abs_svg_path, os.path.join(out_static, svg_filename))
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
# Alt text
|
|
220
|
+
if stage is not None:
|
|
221
|
+
default_alt = f"Polynomial division of ({p}) : ({q}) – stage {stage}"
|
|
222
|
+
else:
|
|
223
|
+
default_alt = f"Polynomial division of ({p}) : ({q})"
|
|
224
|
+
alt = self.options.get("alt", default_alt)
|
|
225
|
+
|
|
226
|
+
width_opt = self.options.get("width")
|
|
227
|
+
percentage_width = isinstance(width_opt, str) and width_opt.strip().endswith("%")
|
|
228
|
+
|
|
229
|
+
# Read final SVG
|
|
230
|
+
try:
|
|
231
|
+
with open(abs_svg_path, "r", encoding="utf-8") as f_svg:
|
|
232
|
+
raw_svg = f_svg.read()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
return [
|
|
235
|
+
self.state_machine.reporter.error(
|
|
236
|
+
f"polydiv inline: could not read SVG: {e}",
|
|
237
|
+
line=self.lineno,
|
|
238
|
+
)
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Uniquify IDs to prevent collisions
|
|
242
|
+
def _uniquify_ids(svg_text: str, prefix: str) -> str:
|
|
243
|
+
ids = set(re.findall(r'\bid="([^"]+)"', svg_text))
|
|
244
|
+
if not ids:
|
|
245
|
+
return svg_text
|
|
246
|
+
mapping = {old: f"{prefix}{old}" for old in ids}
|
|
247
|
+
for old, new in mapping.items():
|
|
248
|
+
svg_text = re.sub(rf'\bid="{re.escape(old)}"', f'id="{new}"', svg_text)
|
|
249
|
+
for old, new in mapping.items():
|
|
250
|
+
svg_text = re.sub(
|
|
251
|
+
rf'(?:xlink:)?href="#?{re.escape(old)}"', f'href="#{new}"', svg_text
|
|
252
|
+
)
|
|
253
|
+
svg_text = re.sub(
|
|
254
|
+
rf'xlink:href="#?{re.escape(old)}"',
|
|
255
|
+
f'xlink:href="#{new}"',
|
|
256
|
+
svg_text,
|
|
257
|
+
)
|
|
258
|
+
for old, new in mapping.items():
|
|
259
|
+
svg_text = re.sub(rf"url\(#\s*{re.escape(old)}\s*\)", f"url(#{new})", svg_text)
|
|
260
|
+
for old, new in mapping.items():
|
|
261
|
+
svg_text = re.sub(rf"#({re.escape(old)})\b", f"#{new}", svg_text)
|
|
262
|
+
return svg_text
|
|
263
|
+
|
|
264
|
+
unique_prefix = f"pd_{_hash_key(p, q, stage, vars_opt)}_{uuid.uuid4().hex[:6]}_"
|
|
265
|
+
raw_svg = _uniquify_ids(raw_svg, unique_prefix)
|
|
266
|
+
|
|
267
|
+
# Augment root <svg> (single pass handles both percent and fixed widths)
|
|
268
|
+
def _augment(match):
|
|
269
|
+
tag = match.group(0)
|
|
270
|
+
if "class=" not in tag:
|
|
271
|
+
tag = tag[:-1] + ' class="polydiv-inline-svg"' + ">"
|
|
272
|
+
else:
|
|
273
|
+
tag = tag.replace('class="', 'class="polydiv-inline-svg ')
|
|
274
|
+
if alt and "aria-label=" not in tag:
|
|
275
|
+
tag = tag[:-1] + f' role="img" aria-label="{alt}"' + ">"
|
|
276
|
+
if width_opt:
|
|
277
|
+
w_raw = width_opt.strip()
|
|
278
|
+
if percentage_width:
|
|
279
|
+
# percentage width: keep percent, center block
|
|
280
|
+
w_css = w_raw
|
|
281
|
+
margin = "margin:0 auto;" if "margin:" not in tag else ""
|
|
282
|
+
style_frag = f"width:{w_css}; height:auto; display:block; {margin}".strip()
|
|
283
|
+
else:
|
|
284
|
+
# fixed / unit width: add px if bare number
|
|
285
|
+
w_css = (w_raw + "px") if w_raw.isdigit() else w_raw
|
|
286
|
+
style_frag = f"width:{w_css}; height:auto; display:block;"
|
|
287
|
+
if "style=" in tag:
|
|
288
|
+
tag = re.sub(
|
|
289
|
+
r'style="([^"]*)"',
|
|
290
|
+
lambda m: f'style="{m.group(1)}; {style_frag}"',
|
|
291
|
+
tag,
|
|
292
|
+
count=1,
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
tag = tag[:-1] + f' style="{style_frag}"' + ">"
|
|
296
|
+
return tag
|
|
297
|
+
|
|
298
|
+
raw_svg = re.sub(r"<svg\b[^>]*>", _augment, raw_svg, count=1)
|
|
299
|
+
|
|
300
|
+
figure = nodes.figure()
|
|
301
|
+
figure.setdefault("classes", []).extend(
|
|
302
|
+
[
|
|
303
|
+
"adaptive-figure",
|
|
304
|
+
"polydiv-figure",
|
|
305
|
+
"no-click",
|
|
306
|
+
]
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
raw_node = nodes.raw("", raw_svg, format="html")
|
|
310
|
+
raw_node.setdefault("classes", []).extend(
|
|
311
|
+
[
|
|
312
|
+
"polydiv-image",
|
|
313
|
+
"no-click",
|
|
314
|
+
"no-scaled-link",
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
figure += raw_node
|
|
318
|
+
|
|
319
|
+
# Extra classes & alignment
|
|
320
|
+
extra_classes = self.options.get("class")
|
|
321
|
+
if extra_classes:
|
|
322
|
+
figure["classes"].extend(extra_classes)
|
|
323
|
+
figure["align"] = self.options.get("align", "center")
|
|
324
|
+
|
|
325
|
+
# Caption
|
|
326
|
+
if self.content:
|
|
327
|
+
caption_text = "\n".join(self.content)
|
|
328
|
+
caption_node = nodes.caption()
|
|
329
|
+
caption_node += nodes.Text(caption_text)
|
|
330
|
+
figure += caption_node
|
|
331
|
+
|
|
332
|
+
if explicit_name:
|
|
333
|
+
self.add_name(figure)
|
|
334
|
+
|
|
335
|
+
return [figure]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def setup(app):
|
|
339
|
+
app.add_directive("polydiv", PolyDivDirective)
|
|
340
|
+
return {
|
|
341
|
+
"version": "0.1",
|
|
342
|
+
"parallel_read_safe": True,
|
|
343
|
+
"parallel_write_safe": True,
|
|
344
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Popup directive and role for creating modal dialogs.
|
|
3
|
+
|
|
4
|
+
This extension provides two ways to create popups:
|
|
5
|
+
|
|
6
|
+
1. **Directive**: Creates a button that opens a dialog with content
|
|
7
|
+
2. **Role**: Creates inline text that shows a tooltip/bubble on hover
|
|
8
|
+
|
|
9
|
+
Directive Usage:
|
|
10
|
+
```{popup} Button Text
|
|
11
|
+
:title: Dialog Title
|
|
12
|
+
:width: 600
|
|
13
|
+
:height: 400
|
|
14
|
+
|
|
15
|
+
Content goes here. Supports **markdown** and $\\LaTeX$ math.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Role Usage:
|
|
19
|
+
This is {popup}`hover text <tooltip content>` inline.
|
|
20
|
+
|
|
21
|
+
The popup directive creates a modal dialog using jQuery UI, with automatic
|
|
22
|
+
KaTeX rendering for mathematical expressions. The role creates a simple
|
|
23
|
+
hover bubble for quick information.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from docutils import nodes
|
|
27
|
+
from sphinx.util.docutils import SphinxDirective, SphinxRole
|
|
28
|
+
from docutils.parsers.rst import directives
|
|
29
|
+
import uuid
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PopupDirective(SphinxDirective):
|
|
34
|
+
"""
|
|
35
|
+
Directive for creating a button that opens a modal dialog.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
- title: Dialog title (default: "Mer informasjon")
|
|
39
|
+
- width: Dialog width in pixels (default: 500)
|
|
40
|
+
- height: Dialog height in pixels or "auto" (default: "auto")
|
|
41
|
+
|
|
42
|
+
Arguments:
|
|
43
|
+
- Button text (optional, default: "Vis mer")
|
|
44
|
+
|
|
45
|
+
Content:
|
|
46
|
+
- The content to display in the dialog (supports MyST markdown)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
has_content = True
|
|
50
|
+
option_spec = {
|
|
51
|
+
"title": directives.unchanged,
|
|
52
|
+
"width": directives.positive_int,
|
|
53
|
+
"height": directives.unchanged,
|
|
54
|
+
}
|
|
55
|
+
required_arguments = 0
|
|
56
|
+
optional_arguments = 1 # Button text
|
|
57
|
+
|
|
58
|
+
def run(self):
|
|
59
|
+
# Get button text from arguments or use default
|
|
60
|
+
button_text = self.arguments[0] if self.arguments else "Vis mer"
|
|
61
|
+
|
|
62
|
+
# Get title from options or use default
|
|
63
|
+
dialog_title = self.options.get("title", "Mer informasjon")
|
|
64
|
+
|
|
65
|
+
# Get width and height if specified
|
|
66
|
+
width = self.options.get("width", 500)
|
|
67
|
+
height = self.options.get("height", "auto")
|
|
68
|
+
|
|
69
|
+
# Generate unique IDs
|
|
70
|
+
popup_id = f"popup-{uuid.uuid4().hex[:8]}"
|
|
71
|
+
button_id = f"button-{popup_id}"
|
|
72
|
+
content_id = f"content-{popup_id}"
|
|
73
|
+
|
|
74
|
+
# Process the directive content as markdown
|
|
75
|
+
content_html = "\n".join(self.content)
|
|
76
|
+
|
|
77
|
+
# Create the raw HTML with jQuery UI and KaTeX
|
|
78
|
+
html = f"""
|
|
79
|
+
<!-- Button to open the popup -->
|
|
80
|
+
<button id="{button_id}" class="popup-button">{button_text}</button>
|
|
81
|
+
|
|
82
|
+
<!-- Dialog container -->
|
|
83
|
+
<div id="{content_id}" class="popup-content" style="display:none;">
|
|
84
|
+
{content_html}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Script for KaTeX and dialog setup -->
|
|
88
|
+
<script>
|
|
89
|
+
(function() {{
|
|
90
|
+
// Function to load scripts sequentially
|
|
91
|
+
function loadScript(src, id) {{
|
|
92
|
+
return new Promise((resolve, reject) => {{
|
|
93
|
+
// Check if script is already loaded
|
|
94
|
+
if (document.getElementById(id)) {{
|
|
95
|
+
resolve();
|
|
96
|
+
return;
|
|
97
|
+
}}
|
|
98
|
+
|
|
99
|
+
const script = document.createElement('script');
|
|
100
|
+
script.id = id;
|
|
101
|
+
script.src = src;
|
|
102
|
+
script.async = false; // Important: maintain loading order
|
|
103
|
+
script.onload = () => resolve();
|
|
104
|
+
script.onerror = () => reject(new Error(`Failed to load script: ${{src}}`));
|
|
105
|
+
document.head.appendChild(script);
|
|
106
|
+
}});
|
|
107
|
+
}}
|
|
108
|
+
|
|
109
|
+
// Function to load stylesheets
|
|
110
|
+
function loadStylesheet(href, id) {{
|
|
111
|
+
return new Promise((resolve) => {{
|
|
112
|
+
if (document.getElementById(id)) {{
|
|
113
|
+
resolve();
|
|
114
|
+
return;
|
|
115
|
+
}}
|
|
116
|
+
|
|
117
|
+
const link = document.createElement('link');
|
|
118
|
+
link.id = id;
|
|
119
|
+
link.rel = 'stylesheet';
|
|
120
|
+
link.href = href;
|
|
121
|
+
link.onload = resolve;
|
|
122
|
+
document.head.appendChild(link);
|
|
123
|
+
}});
|
|
124
|
+
}}
|
|
125
|
+
|
|
126
|
+
// Initialize dialog once everything is loaded
|
|
127
|
+
function initializeDialog() {{
|
|
128
|
+
$("#{content_id}").dialog({{
|
|
129
|
+
autoOpen: false,
|
|
130
|
+
title: "{dialog_title}",
|
|
131
|
+
width: {width},
|
|
132
|
+
dialogClass: "popup-dialog",
|
|
133
|
+
modal: false,
|
|
134
|
+
resizable: true,
|
|
135
|
+
draggable: true,
|
|
136
|
+
open: function() {{
|
|
137
|
+
// Safety check to ensure KaTeX is available
|
|
138
|
+
if (window.katex && window.renderMathInElement) {{
|
|
139
|
+
try {{
|
|
140
|
+
renderMathInElement(document.getElementById('{content_id}'), {{
|
|
141
|
+
delimiters: [
|
|
142
|
+
{{left: "$$", right: "$$", display: true}},
|
|
143
|
+
{{left: "$", right: "$", display: false}},
|
|
144
|
+
{{left: "\\\\(", right: "\\\\)", display: false}},
|
|
145
|
+
{{left: "\\\\[", right: "\\\\]", display: true}}
|
|
146
|
+
],
|
|
147
|
+
throwOnError: false
|
|
148
|
+
}});
|
|
149
|
+
}} catch (e) {{
|
|
150
|
+
console.error("KaTeX rendering error:", e);
|
|
151
|
+
}}
|
|
152
|
+
}}
|
|
153
|
+
}}
|
|
154
|
+
}});
|
|
155
|
+
|
|
156
|
+
// Button click handler
|
|
157
|
+
$("#{button_id}").on("click", function() {{
|
|
158
|
+
$("#{content_id}").dialog("open");
|
|
159
|
+
}});
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
// Main initialization function
|
|
163
|
+
async function initialize() {{
|
|
164
|
+
try {{
|
|
165
|
+
// First, load jQuery if not already available
|
|
166
|
+
if (!window.jQuery) {{
|
|
167
|
+
await loadScript('https://code.jquery.com/jquery-3.6.0.min.js', 'jquery-script');
|
|
168
|
+
}}
|
|
169
|
+
|
|
170
|
+
// Then jQuery UI - safer check
|
|
171
|
+
if (!window.jQuery || typeof window.jQuery.ui === 'undefined') {{
|
|
172
|
+
await loadScript('https://code.jquery.com/ui/1.13.2/jquery-ui.min.js', 'jquery-ui-script');
|
|
173
|
+
await loadStylesheet('https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css', 'jquery-ui-css');
|
|
174
|
+
}}
|
|
175
|
+
|
|
176
|
+
// Then KaTeX and auto-render in sequence
|
|
177
|
+
await loadStylesheet('https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css', 'katex-css');
|
|
178
|
+
await loadScript('https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js', 'katex-script');
|
|
179
|
+
await loadScript('https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js', 'katex-auto-render');
|
|
180
|
+
|
|
181
|
+
// Initialize dialog after all scripts are loaded
|
|
182
|
+
initializeDialog();
|
|
183
|
+
}} catch (error) {{
|
|
184
|
+
console.error('Error initializing popup:', error);
|
|
185
|
+
}}
|
|
186
|
+
}}
|
|
187
|
+
|
|
188
|
+
// Start initialization when document is ready
|
|
189
|
+
if (document.readyState === 'loading') {{
|
|
190
|
+
document.addEventListener('DOMContentLoaded', initialize);
|
|
191
|
+
}} else {{
|
|
192
|
+
initialize();
|
|
193
|
+
}}
|
|
194
|
+
}})();
|
|
195
|
+
</script>
|
|
196
|
+
"""
|
|
197
|
+
return [nodes.raw("", html, format="html")]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class PopupRole(SphinxRole):
|
|
201
|
+
"""
|
|
202
|
+
Role for creating inline popups/tooltips.
|
|
203
|
+
|
|
204
|
+
Usage:
|
|
205
|
+
{popup}`label <content>`
|
|
206
|
+
|
|
207
|
+
The label is what appears in the text, and the content appears
|
|
208
|
+
in a tooltip/bubble when hovering over the label.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def run(self):
|
|
212
|
+
text = self.text
|
|
213
|
+
content_html = ""
|
|
214
|
+
|
|
215
|
+
# Parse the input: label <content>
|
|
216
|
+
options_match = re.search(r"<([^>]+)>\s*(?:\(([^)]+)\))?", text)
|
|
217
|
+
if options_match:
|
|
218
|
+
label = text[: options_match.start()].strip()
|
|
219
|
+
content_html = options_match.group(1)
|
|
220
|
+
else:
|
|
221
|
+
label = text
|
|
222
|
+
|
|
223
|
+
popup_id = f"popup-role-{uuid.uuid4().hex[:8]}"
|
|
224
|
+
|
|
225
|
+
html = f"""
|
|
226
|
+
<span class="popup-wrapper" id="{popup_id}">
|
|
227
|
+
<span class="popup-trigger">{label}</span>
|
|
228
|
+
<span class="popup-bubble" style="display:none;">{content_html}</span>
|
|
229
|
+
</span>
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
return [nodes.raw("", html, format="html")], []
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def setup(app):
|
|
236
|
+
"""Setup function to register the directive and role with Sphinx."""
|
|
237
|
+
# Register the directive and role
|
|
238
|
+
app.add_directive("popup", PopupDirective)
|
|
239
|
+
app.add_role("popup", PopupRole())
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
"version": "0.1.0",
|
|
243
|
+
"parallel_read_safe": True,
|
|
244
|
+
"parallel_write_safe": True,
|
|
245
|
+
}
|