python-color-math 0.2.12__py3-none-any.whl → 0.2.14__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.
color_math/__init__.py CHANGED
@@ -5,13 +5,19 @@ from .config import ColorMathOptions
5
5
  from .converters.block import convert_text
6
6
  from .undo import uncolor_text
7
7
 
8
- __version__ = "0.2.12"
8
+ from .ipython import ColorMath, colormath, load_ipython_extension, unload_ipython_extension
9
+
10
+ __version__ = "0.2.14"
9
11
 
10
12
  __all__ = [
11
13
  "__version__",
14
+ "ColorMath",
12
15
  "ColorMathOptions",
16
+ "colormath",
13
17
  "convert_text",
14
18
  "detect_format",
19
+ "load_ipython_extension",
15
20
  "transform_document",
16
21
  "uncolor_text",
22
+ "unload_ipython_extension",
17
23
  ]
color_math/ipython.py ADDED
@@ -0,0 +1,183 @@
1
+ """IPython and Jupyter integration for color-math.
2
+
3
+ Enables seamless colorized math in Jupyter Notebook 7, JupyterLab, Classic
4
+ Notebook, and Google Colab via `%load_ext color_math`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import shlex
10
+ from typing import Any
11
+
12
+ from .adapters import transform_document
13
+ from .config import ColorMathOptions, THEMES
14
+ from .converters.block import convert_math_block, convert_text
15
+
16
+ try:
17
+ from IPython.core.magic import Magics, cell_magic, line_magic, magics_class
18
+ from IPython.display import Markdown, Math, display
19
+ HAS_IPYTHON = True
20
+ except ImportError:
21
+ HAS_IPYTHON = False
22
+ Magics = object # type: ignore[misc,assignment]
23
+ def magics_class(cls: Any) -> Any: return cls
24
+ def line_magic(func: Any) -> Any: return func
25
+ def cell_magic(func: Any) -> Any: return func
26
+
27
+
28
+ def colormath(
29
+ text: str,
30
+ palette: dict[str, str] | None = None,
31
+ options: ColorMathOptions | None = None,
32
+ theme: str | None = None,
33
+ is_fragment: bool = False,
34
+ ) -> str:
35
+ """Convenience helper to colorize LaTeX math or Markdown math.
36
+
37
+ If is_fragment is True, text is treated as raw math without $ delimiters.
38
+ """
39
+ active_palette = THEMES.get(theme) if theme else palette
40
+ if is_fragment:
41
+ converted = convert_math_block(f"$${text}$$", palette=active_palette, options=options)
42
+ return converted[2:-2]
43
+ return convert_text(text, palette=active_palette, options=options)
44
+
45
+
46
+ if HAS_IPYTHON:
47
+ class ColorMath(Math):
48
+ """A drop-in replacement for IPython.display.Math that renders with semantic colors."""
49
+
50
+ def __init__(
51
+ self,
52
+ data: Any = None,
53
+ palette: dict[str, str] | None = None,
54
+ options: ColorMathOptions | None = None,
55
+ theme: str | None = None,
56
+ **kwargs: Any,
57
+ ) -> None:
58
+ raw = "" if data is None else str(data)
59
+ colored = colormath(raw, palette=palette, options=options, theme=theme, is_fragment=True)
60
+ super().__init__(data=colored, **kwargs)
61
+
62
+
63
+ @magics_class
64
+ class ColorMathMagics(Magics):
65
+ """IPython magics for semantic LaTeX colorization."""
66
+
67
+ def __init__(self, shell: Any) -> None:
68
+ super().__init__(shell)
69
+ self._auto_hook_active = False
70
+ self._orig_math_repr = None
71
+
72
+ @line_magic("color_math")
73
+ @line_magic("colormath")
74
+ def color_math_line(self, line: str) -> Any:
75
+ r"""Colorize and display a single LaTeX formula.
76
+
77
+ Usage:
78
+ %color_math \int_0^1 x^2 dx = \frac{1}{3}
79
+ %color_math --theme catppuccin \hat{H}\psi = E\psi
80
+ """
81
+ theme = None
82
+ raw_line = line.strip()
83
+ if raw_line.startswith("--theme "):
84
+ parts = shlex.split(raw_line)
85
+ if len(parts) >= 3 and parts[0] == "--theme":
86
+ theme = parts[1]
87
+ raw_line = " ".join(parts[2:])
88
+
89
+ if not raw_line:
90
+ display(Markdown("*Color Math: please provide a LaTeX expression.*"))
91
+ return None
92
+
93
+ # Render as display math
94
+ colored = colormath(raw_line, theme=theme, is_fragment=True)
95
+ return Math(colored)
96
+
97
+ @cell_magic("color_math")
98
+ @cell_magic("colormath")
99
+ def color_math_cell(self, line: str, cell: str) -> Any:
100
+ r"""Colorize and display a cell containing LaTeX or Markdown math.
101
+
102
+ Usage:
103
+ %%color_math
104
+ Here is the equation:
105
+ $$ \frac{df}{dx} = f'(x) $$
106
+ """
107
+ theme = None
108
+ args = shlex.split(line.strip()) if line.strip() else []
109
+ if len(args) >= 2 and args[0] == "--theme":
110
+ theme = args[1]
111
+
112
+ converted = colormath(cell, theme=theme, is_fragment=False)
113
+ display(Markdown(converted))
114
+
115
+ @line_magic("color_math_auto")
116
+ def color_math_auto(self, line: str) -> None:
117
+ """Toggle automatic semantic colorization for all IPython.display.Math outputs.
118
+
119
+ Usage:
120
+ %color_math_auto on
121
+ %color_math_auto off
122
+ %color_math_auto status
123
+ """
124
+ cmd = line.strip().lower()
125
+ if cmd in ("on", "enable", "1", "true"):
126
+ self.enable_auto_display()
127
+ print("Color Math: Automatic display coloring enabled for IPython.display.Math.")
128
+ elif cmd in ("off", "disable", "0", "false"):
129
+ self.disable_auto_display()
130
+ print("Color Math: Automatic display coloring disabled.")
131
+ else:
132
+ status = "ENABLED" if self._auto_hook_active else "DISABLED"
133
+ print(f"Color Math: Automatic display coloring is currently {status}.")
134
+
135
+ def enable_auto_display(self) -> None:
136
+ """Patch IPython.display.Math to automatically colorize its LaTeX payload."""
137
+ if self._auto_hook_active:
138
+ return
139
+ self._orig_math_repr = Math._repr_latex_
140
+
141
+ def _colorized_repr_latex(math_self: Math) -> str:
142
+ raw = self._orig_math_repr(math_self) # type: ignore[misc]
143
+ if not raw:
144
+ return raw
145
+ # Raw may be surrounded by $$ or not
146
+ if raw.startswith("$$") and raw.endswith("$$"):
147
+ body = raw[2:-2]
148
+ return f"$${colormath(body, is_fragment=True)}$$"
149
+ if raw.startswith("$") and raw.endswith("$"):
150
+ body = raw[1:-1]
151
+ return f"${colormath(body, is_fragment=True)}$"
152
+ return colormath(raw, is_fragment=True)
153
+
154
+ Math._repr_latex_ = _colorized_repr_latex # type: ignore[assignment]
155
+ self._auto_hook_active = True
156
+
157
+ def disable_auto_display(self) -> None:
158
+ """Restore original IPython.display.Math behavior."""
159
+ if not self._auto_hook_active:
160
+ return
161
+ if self._orig_math_repr is not None:
162
+ Math._repr_latex_ = self._orig_math_repr # type: ignore[assignment]
163
+ self._auto_hook_active = False
164
+
165
+
166
+ def load_ipython_extension(ipython: Any) -> None:
167
+ """Standard IPython extension loader."""
168
+ if not HAS_IPYTHON:
169
+ return
170
+ magics = ColorMathMagics(ipython)
171
+ ipython.register_magics(magics)
172
+ # Store reference so it can be cleanly unloaded
173
+ ipython.custom_exceptions = getattr(ipython, "custom_exceptions", {})
174
+ setattr(ipython, "_color_math_magics", magics)
175
+
176
+
177
+ def unload_ipython_extension(ipython: Any) -> None:
178
+ """Standard IPython extension unloader."""
179
+ if not HAS_IPYTHON:
180
+ return
181
+ magics = getattr(ipython, "_color_math_magics", None)
182
+ if magics is not None and hasattr(magics, "disable_auto_display"):
183
+ magics.disable_auto_display()
color_math/jupyter.py ADDED
@@ -0,0 +1,99 @@
1
+ """Jupyter Server & Notebook 7 integration for color-math.
2
+
3
+ Provides automated pre-save formatting so that whenever a notebook is saved
4
+ in Jupyter Notebook 7 or JupyterLab, all LaTeX formulas in Markdown cells
5
+ are automatically colorized.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .config import ColorMathOptions
15
+ from .converters.block import convert_text
16
+ from .undo import uncolor_text
17
+
18
+
19
+ HOOK_SNIPPET = """
20
+ # >>> color-math Jupyter pre-save hook >>>
21
+ try:
22
+ from color_math.jupyter import notebook_pre_save_hook
23
+ c.FileContentsManager.pre_save_hook = notebook_pre_save_hook
24
+ except ImportError:
25
+ pass
26
+ # <<< color-math Jupyter pre-save hook <<<
27
+ """
28
+
29
+
30
+ def notebook_pre_save_hook(
31
+ model: dict[str, Any],
32
+ os_path: str = "",
33
+ contents_manager: Any = None,
34
+ options: ColorMathOptions | None = None,
35
+ **kwargs: Any,
36
+ ) -> None:
37
+ """Jupyter FileContentsManager pre_save_hook.
38
+
39
+ Modifies the notebook model in-place before it is written to disk.
40
+ """
41
+ if model.get("type") != "notebook":
42
+ return
43
+
44
+ content = model.get("content")
45
+ if not isinstance(content, dict):
46
+ return
47
+
48
+ cells = content.get("cells")
49
+ if not isinstance(cells, list):
50
+ return
51
+
52
+ for cell in cells:
53
+ if not isinstance(cell, dict) or cell.get("cell_type") != "markdown":
54
+ continue
55
+
56
+ source = cell.get("source", "")
57
+ if isinstance(source, str):
58
+ joined = source
59
+ elif isinstance(source, list) and all(isinstance(part, str) for part in source):
60
+ joined = "".join(source)
61
+ else:
62
+ continue
63
+
64
+ converted = convert_text(joined, options=options)
65
+ if converted != joined:
66
+ if isinstance(source, str):
67
+ cell["source"] = converted
68
+ else:
69
+ cell["source"] = converted.splitlines(keepends=True) or ([""] if source else [])
70
+
71
+
72
+ def get_jupyter_config_dir() -> Path:
73
+ """Locate the active Jupyter user configuration directory."""
74
+ env_dir = os.environ.get("JUPYTER_CONFIG_DIR")
75
+ if env_dir:
76
+ return Path(env_dir)
77
+ return Path.home() / ".jupyter"
78
+
79
+
80
+ def install_jupyter_hook(config_dir: Path | str | None = None) -> Path:
81
+ """Install the pre-save hook into the user's Jupyter configuration."""
82
+ target_dir = Path(config_dir) if config_dir else get_jupyter_config_dir()
83
+ target_dir.mkdir(parents=True, exist_ok=True)
84
+
85
+ # In modern Jupyter (Notebook 7 / JupyterLab 4), jupyter_server_config.py is preferred
86
+ config_file = target_dir / "jupyter_server_config.py"
87
+ if not config_file.exists():
88
+ legacy_file = target_dir / "jupyter_notebook_config.py"
89
+ if legacy_file.exists():
90
+ config_file = legacy_file
91
+
92
+ existing_content = config_file.read_text(encoding="utf-8") if config_file.exists() else ""
93
+ if "color_math.jupyter" in existing_content:
94
+ return config_file
95
+
96
+ with open(config_file, "a", encoding="utf-8") as f:
97
+ f.write(HOOK_SNIPPET + "\n")
98
+
99
+ return config_file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-color-math
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: A fast, local semantic LaTeX colorizer for Markdown, Jupyter, and LaTeX math equations.
5
5
  Author: Aditya
6
6
  License: MIT License
@@ -56,10 +56,13 @@ Requires-Dist: mcp>=1.0.0; extra == "mcp"
56
56
  Provides-Extra: tutorial
57
57
  Requires-Dist: rich>=13.0.0; extra == "tutorial"
58
58
  Requires-Dist: questionary>=2.0.0; extra == "tutorial"
59
+ Provides-Extra: jupyter
60
+ Requires-Dist: ipython>=8.0.0; extra == "jupyter"
59
61
  Provides-Extra: all
60
62
  Requires-Dist: mcp>=1.0.0; extra == "all"
61
63
  Requires-Dist: rich>=13.0.0; extra == "all"
62
64
  Requires-Dist: questionary>=2.0.0; extra == "all"
65
+ Requires-Dist: ipython>=8.0.0; extra == "all"
63
66
  Dynamic: license-file
64
67
 
65
68
  # 🎨 Python Color Math
@@ -1,4 +1,4 @@
1
- color_math/__init__.py,sha256=WfTE42IX0N4KjPXOJeIseaVrvyUwO-rehid3dwOOsp8,402
1
+ color_math/__init__.py,sha256=t5MGubTUUxguT0lkBW4gAF83lqFYCQgMNAp9XeNRisE,591
2
2
  color_math/__main__.py,sha256=lU6t3BklI-5H9EdprlceWLr0UcDmfXUu3Th0tjk2ANA,81
3
3
  color_math/adapters.py,sha256=glMvD8Bma_865jmOvIGGkCRxQLIEGjToFDOqJy_WeWA,12293
4
4
  color_math/colormath.default.json,sha256=KmQb7BV7sMMIqPxR4x6i9RwuXuUqkpyiy4yHw84GxPQ,2756
@@ -7,6 +7,8 @@ color_math/config.py,sha256=z8Op8UzZz4D0jMaAd8GGYwAbTwqR_2ZUG4Lb9f03Ii0,20982
7
7
  color_math/custom_definitions.py,sha256=IYJ_L51d1vyFf5frbJoFMIagKRcI7TiNzELfoewZyjk,2392
8
8
  color_math/gui.py,sha256=cBsgNToQdZ7mNQ9JA5_jEv7QbELc4NGtgXowTxZ-IWQ,21413
9
9
  color_math/io.py,sha256=iCWMY1ufPZzFCwuHTn_axNetek8PRXGNnrmz59CrlGw,1591
10
+ color_math/ipython.py,sha256=P-cvBogtb3N6NHrgr8gZcrLlbqyKO32FR9wjwNAxfBo,6929
11
+ color_math/jupyter.py,sha256=CaCWNmtOPqUn0CVZdCie28M1324y0iU6ck2vxCvMn6w,3067
10
12
  color_math/main.py,sha256=Prk2pOGwqAlS9Yn62ACBnZv6XsQEco_NqrQYacU4kUo,34253
11
13
  color_math/mcp.py,sha256=M-JTnWC-xhJemIv2F0ZI4VBF5_MUmasid5B4PIIPgSo,1112
12
14
  color_math/mcp_server.py,sha256=AyDjwKVS-7nnaFu4hBNeBrFC542bs7_WuWosgMflvDg,16086
@@ -48,9 +50,9 @@ color_math/utils/latex_helpers.py,sha256=1cFj0zqv68mD-p0vej6yPrhwgqLfAWcIV1rX15F
48
50
  color_math/utils/spans.py,sha256=3Lq1_bN0s4vSYnS8Rz9tCHoFoJDY6Ff9CzawWWws3YE,3938
49
51
  python_color_math/__init__.py,sha256=Dj5YZXXjmNgbyhHKX2YQXvVggtB9UZy9zotLiV2j5So,329
50
52
  python_color_math/__main__.py,sha256=cqUM_fV0E6mtuHeBn2MjuozR2rlpev9AfXfcm2hSpi8,90
51
- python_color_math-0.2.12.dist-info/licenses/LICENSE,sha256=hveQNxqYFqK9Brs_ruH-8shF2D61hIVKYdCjiE-1wMY,1063
52
- python_color_math-0.2.12.dist-info/METADATA,sha256=5bhH05wLGgUGTVWeVTDwmf5q2zmEVuX2NGuikHCp2HY,14349
53
- python_color_math-0.2.12.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
54
- python_color_math-0.2.12.dist-info/entry_points.txt,sha256=ZgzGbO_D3csr693Fd-tVGhls6WzsZiOhCau-liSkE_M,130
55
- python_color_math-0.2.12.dist-info/top_level.txt,sha256=dkfmCdtILDAgX4wwDW5DKHh9YRLt9Mh2RU0gh1mBDDA,29
56
- python_color_math-0.2.12.dist-info/RECORD,,
53
+ python_color_math-0.2.14.dist-info/licenses/LICENSE,sha256=hveQNxqYFqK9Brs_ruH-8shF2D61hIVKYdCjiE-1wMY,1063
54
+ python_color_math-0.2.14.dist-info/METADATA,sha256=ErCd5J70JgF4i5a0Nm1DwF1BvuMreS4AlVzEJaPdA4o,14469
55
+ python_color_math-0.2.14.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
56
+ python_color_math-0.2.14.dist-info/entry_points.txt,sha256=ZgzGbO_D3csr693Fd-tVGhls6WzsZiOhCau-liSkE_M,130
57
+ python_color_math-0.2.14.dist-info/top_level.txt,sha256=dkfmCdtILDAgX4wwDW5DKHh9YRLt9Mh2RU0gh1mBDDA,29
58
+ python_color_math-0.2.14.dist-info/RECORD,,