munchboka-edutools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- munchboka_edutools/__init__.py +182 -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 +356 -0
- munchboka_edutools/directives/cas_popup.py +272 -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 +62 -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 +3012 -0
- munchboka_edutools/directives/poly_icon.py +91 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +474 -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 +476 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -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/interactive_code/codeEditor.js +662 -0
- munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
- munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
- munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
- munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
- munchboka_edutools/static/js/jeopardy.js +457 -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/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.0.dist-info/METADATA +107 -0
- munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
- munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polynomial Icon Role for Munchboka Edutools
|
|
3
|
+
===========================================
|
|
4
|
+
|
|
5
|
+
This module provides a custom inline role for displaying polynomial/cubic function
|
|
6
|
+
icons in Sphinx documentation. It's designed for mathematical content where you need
|
|
7
|
+
to refer to cubic function shapes visually.
|
|
8
|
+
|
|
9
|
+
Usage in MyST Markdown:
|
|
10
|
+
{poly-icon}`cubicup`
|
|
11
|
+
{poly-icon}`cubicdown`
|
|
12
|
+
{poly-icon}`smile`
|
|
13
|
+
{poly-icon}`frown`
|
|
14
|
+
|
|
15
|
+
Available Icons:
|
|
16
|
+
- cubicup: Cubic function going up (standard cubic)
|
|
17
|
+
- cubicdown: Cubic function going down (negative cubic)
|
|
18
|
+
- smile: Positive quadratic (parabola opening up)
|
|
19
|
+
- frown: Negative quadratic (parabola opening down)
|
|
20
|
+
|
|
21
|
+
The icons are displayed inline with the text and adapt to the current theme
|
|
22
|
+
(light/dark mode). They use the .inline-image class for consistent styling.
|
|
23
|
+
|
|
24
|
+
Author: René Aasen (ported from matematikk_r1)
|
|
25
|
+
Date: November 2025
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from docutils import nodes
|
|
29
|
+
from docutils.parsers.rst import roles
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def poly_icon_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
|
33
|
+
"""
|
|
34
|
+
Custom role for polynomial/cubic function icons.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
name: The role name used in the document
|
|
38
|
+
rawtext: The entire role markup string
|
|
39
|
+
text: The icon name (e.g., 'cubicup', 'smile')
|
|
40
|
+
lineno: Line number where the role appears
|
|
41
|
+
inliner: The inliner instance
|
|
42
|
+
options: Dictionary of directive options
|
|
43
|
+
content: List of strings, the content of the role
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
tuple: (list of nodes, list of system messages)
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
Input: {poly-icon}`cubicup`
|
|
50
|
+
Output: <img src="/_static/munchboka/icons/polyicons/cubicup.svg"
|
|
51
|
+
alt="Cubic cubicup icon" class="inline-image">
|
|
52
|
+
"""
|
|
53
|
+
# Clean up the icon name (remove any extra whitespace)
|
|
54
|
+
icon_name = text.strip()
|
|
55
|
+
|
|
56
|
+
# Build the image path relative to the source directory
|
|
57
|
+
# Note: Using /_static/munchboka/ path structure for built HTML
|
|
58
|
+
img_src = f"/_static/munchboka/icons/polyicons/{icon_name}.svg"
|
|
59
|
+
|
|
60
|
+
# Create a proper image node that Sphinx can process
|
|
61
|
+
node = nodes.image(uri=img_src, alt=f"Cubic {icon_name} icon")
|
|
62
|
+
|
|
63
|
+
# Add the inline-image class for consistent styling
|
|
64
|
+
# (defined in figures.css with theme-aware styling)
|
|
65
|
+
node["classes"] = ["inline-image"]
|
|
66
|
+
|
|
67
|
+
return [node], []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def setup(app):
|
|
71
|
+
"""
|
|
72
|
+
Setup function to register the role with Sphinx.
|
|
73
|
+
|
|
74
|
+
This function is called automatically by Sphinx when the extension is loaded.
|
|
75
|
+
It registers the poly-icon role for use in documentation.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
app: The Sphinx application instance
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
dict: Extension metadata including version and parallel processing flags
|
|
82
|
+
"""
|
|
83
|
+
# Register the role with both hyphenated and unhyphenated names for compatibility
|
|
84
|
+
roles.register_local_role("poly-icon", poly_icon_role)
|
|
85
|
+
roles.register_local_role("polyicon", poly_icon_role)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"version": "0.1",
|
|
89
|
+
"parallel_read_safe": True,
|
|
90
|
+
"parallel_write_safe": True,
|
|
91
|
+
}
|
|
@@ -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
|
+
}
|