python-color-math 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.
- color_math/__init__.py +8 -0
- color_math/__main__.py +5 -0
- color_math/adapters.py +288 -0
- color_math/config.py +351 -0
- color_math/converters/__init__.py +27 -0
- color_math/converters/align.py +18 -0
- color_math/converters/block.py +126 -0
- color_math/converters/derivative.py +266 -0
- color_math/converters/equation.py +5 -0
- color_math/converters/generic.py +101 -0
- color_math/converters/integral.py +16 -0
- color_math/converters/limit.py +16 -0
- color_math/converters/matrix.py +143 -0
- color_math/converters/semantic.py +76 -0
- color_math/io.py +53 -0
- color_math/main.py +162 -0
- color_math/parsers/__init__.py +64 -0
- color_math/parsers/braket.py +109 -0
- color_math/parsers/delimiters.py +151 -0
- color_math/parsers/differentials.py +71 -0
- color_math/parsers/dimensionless.py +74 -0
- color_math/parsers/latex_spans.py +1050 -0
- color_math/parsers/markdown_scanner.py +463 -0
- color_math/parsers/math_parser.py +366 -0
- color_math/parsers/scanner.py +351 -0
- color_math/parsers/taxonomy.py +124 -0
- color_math/parsers/units.py +98 -0
- color_math/parsers/variable_hash.py +126 -0
- color_math/self_test.py +224 -0
- color_math/undo.py +63 -0
- color_math/utils/__init__.py +30 -0
- color_math/utils/coloring.py +61 -0
- color_math/utils/latex_helpers.py +232 -0
- color_math/utils/spans.py +77 -0
- python_color_math-0.1.0.dist-info/METADATA +167 -0
- python_color_math-0.1.0.dist-info/RECORD +40 -0
- python_color_math-0.1.0.dist-info/WHEEL +5 -0
- python_color_math-0.1.0.dist-info/entry_points.txt +2 -0
- python_color_math-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_color_math-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# utils/coloring.py
|
|
2
|
+
|
|
3
|
+
from ..config import (
|
|
4
|
+
COLORS,
|
|
5
|
+
BIG_OPERATORS,
|
|
6
|
+
INTEGRALS,
|
|
7
|
+
LIMIT_OPERATORS,
|
|
8
|
+
ARROWS,
|
|
9
|
+
SET_SYMBOLS,
|
|
10
|
+
SPACING_COMMANDS,
|
|
11
|
+
MULTIPLICATION_SYMBOLS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def latex_color(color: str, value: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Wrap LaTeX text in a color command.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
("red", "x")
|
|
21
|
+
-> "\\textcolor{red}{x}"
|
|
22
|
+
"""
|
|
23
|
+
return rf"\textcolor{{{color}}}{{{value}}}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def command_color(command: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Determine which color category a LaTeX command belongs to.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
"\\sum" -> orange
|
|
32
|
+
"\\rightarrow" -> arrow color
|
|
33
|
+
"\\in" -> set color
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Big math operators
|
|
37
|
+
if (
|
|
38
|
+
command in BIG_OPERATORS
|
|
39
|
+
or command in INTEGRALS
|
|
40
|
+
or command in LIMIT_OPERATORS
|
|
41
|
+
):
|
|
42
|
+
return COLORS["orange"]
|
|
43
|
+
|
|
44
|
+
# Arrows
|
|
45
|
+
if command in ARROWS:
|
|
46
|
+
return COLORS["arrow"]
|
|
47
|
+
|
|
48
|
+
# Set theory symbols
|
|
49
|
+
if command in SET_SYMBOLS:
|
|
50
|
+
return COLORS["set"]
|
|
51
|
+
|
|
52
|
+
# Spacing commands
|
|
53
|
+
if command in SPACING_COMMANDS:
|
|
54
|
+
return COLORS["spacing"]
|
|
55
|
+
|
|
56
|
+
# Multiplication symbols
|
|
57
|
+
if command in MULTIPLICATION_SYMBOLS:
|
|
58
|
+
return COLORS["dot"]
|
|
59
|
+
|
|
60
|
+
# Default relations (=, <, >, etc.)
|
|
61
|
+
return COLORS["relation"]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# latex_helpers.py
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
COMMAND_RE = re.compile(r"\\[A-Za-z]+|\\.")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_comment_end(text: str, start: int) -> int:
|
|
10
|
+
"""Return the index after one TeX comment, including its newline."""
|
|
11
|
+
index = start + 1
|
|
12
|
+
while index < len(text) and text[index] not in "\r\n":
|
|
13
|
+
index += 1
|
|
14
|
+
if text.startswith("\r\n", index):
|
|
15
|
+
return index + 2
|
|
16
|
+
return min(index + 1, len(text))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_verb_end(text: str, start: int) -> tuple[int, bool] | None:
|
|
20
|
+
r"""Read an exact ``\verb``/``\verb*`` payload without inspecting it."""
|
|
21
|
+
if not text.startswith(r"\verb", start):
|
|
22
|
+
return None
|
|
23
|
+
command_end = start + len(r"\verb")
|
|
24
|
+
if command_end < len(text) and text[command_end].isalpha():
|
|
25
|
+
return None
|
|
26
|
+
if command_end < len(text) and text[command_end] == "*":
|
|
27
|
+
command_end += 1
|
|
28
|
+
if command_end >= len(text) or text[command_end].isspace():
|
|
29
|
+
return len(text), False
|
|
30
|
+
|
|
31
|
+
delimiter = text[command_end]
|
|
32
|
+
closing = text.find(delimiter, command_end + 1)
|
|
33
|
+
return (
|
|
34
|
+
(len(text), False)
|
|
35
|
+
if closing < 0
|
|
36
|
+
else (closing + 1, True)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_braced(text: str, start: int) -> tuple[str, int] | None:
|
|
41
|
+
"""
|
|
42
|
+
Read a balanced {...} group starting at index 'start'.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
(captured_text, end_index)
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
"{abc}" -> ("{abc}", 5)
|
|
49
|
+
"""
|
|
50
|
+
if start >= len(text) or text[start] != "{":
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
depth = 0
|
|
54
|
+
index = start
|
|
55
|
+
|
|
56
|
+
while index < len(text):
|
|
57
|
+
char = text[index]
|
|
58
|
+
|
|
59
|
+
if char == "%":
|
|
60
|
+
index = read_comment_end(text, index)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if char == "\\":
|
|
64
|
+
verb = read_verb_end(text, index)
|
|
65
|
+
if verb is not None:
|
|
66
|
+
index, closed = verb
|
|
67
|
+
if not closed:
|
|
68
|
+
return None
|
|
69
|
+
continue
|
|
70
|
+
command = COMMAND_RE.match(text, index)
|
|
71
|
+
index = command.end() if command is not None else index + 1
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if char == "{":
|
|
75
|
+
depth += 1
|
|
76
|
+
elif char == "}":
|
|
77
|
+
depth -= 1
|
|
78
|
+
if depth == 0:
|
|
79
|
+
return text[start:index + 1], index + 1
|
|
80
|
+
|
|
81
|
+
index += 1
|
|
82
|
+
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_script_argument(text: str, start: int) -> tuple[str, int] | None:
|
|
87
|
+
"""
|
|
88
|
+
Read argument after _ or ^
|
|
89
|
+
|
|
90
|
+
Handles:
|
|
91
|
+
x^{abc}
|
|
92
|
+
x^n
|
|
93
|
+
x^\\alpha
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if start >= len(text):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Braced argument
|
|
100
|
+
if text[start] == "{":
|
|
101
|
+
return read_braced(text, start)
|
|
102
|
+
|
|
103
|
+
if text[start] in "$\r\n":
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
# Latex command
|
|
107
|
+
if text[start] == "\\":
|
|
108
|
+
match = re.match(r"\\[A-Za-z]+|\\.", text[start:])
|
|
109
|
+
if match:
|
|
110
|
+
return match.group(0), start + len(match.group(0))
|
|
111
|
+
|
|
112
|
+
# Single character
|
|
113
|
+
return text[start], start + 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_script(text: str, start: int) -> tuple[str, int, bool] | None:
|
|
117
|
+
"""
|
|
118
|
+
Read full superscript/subscript.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
"^2"
|
|
122
|
+
"_{abc}"
|
|
123
|
+
"^\\alpha"
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
marker = text[start] # _ or ^
|
|
127
|
+
|
|
128
|
+
argument_start = start + 1
|
|
129
|
+
|
|
130
|
+
if argument_start < len(text) and text[argument_start] == "{":
|
|
131
|
+
argument_data = read_braced(text, argument_start)
|
|
132
|
+
|
|
133
|
+
if argument_data is None:
|
|
134
|
+
return text[start:], len(text), False
|
|
135
|
+
|
|
136
|
+
argument, end = argument_data
|
|
137
|
+
return f"{marker}{argument}", end, True
|
|
138
|
+
|
|
139
|
+
argument_data = read_script_argument(text, argument_start)
|
|
140
|
+
|
|
141
|
+
if argument_data is None:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
argument, end = argument_data
|
|
145
|
+
|
|
146
|
+
return f"{marker}{argument}", end, True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_color_wrapper(text: str, start: int) -> tuple[str, int] | None:
|
|
150
|
+
"""
|
|
151
|
+
Read a scoped color wrapper and return its unbraced value and end index.
|
|
152
|
+
|
|
153
|
+
Supports current ``\\textcolor{red}{x}`` output and legacy
|
|
154
|
+
``\\color{red}{x}`` output. Command names are matched exactly, so macros
|
|
155
|
+
such as ``\\colorbox`` and ``\\colorful`` are left alone.
|
|
156
|
+
"""
|
|
157
|
+
command = next(
|
|
158
|
+
(
|
|
159
|
+
candidate
|
|
160
|
+
for candidate in (r"\textcolor", r"\color")
|
|
161
|
+
if text.startswith(candidate, start)
|
|
162
|
+
and (
|
|
163
|
+
start + len(candidate) == len(text)
|
|
164
|
+
or not text[start + len(candidate)].isalpha()
|
|
165
|
+
)
|
|
166
|
+
),
|
|
167
|
+
None,
|
|
168
|
+
)
|
|
169
|
+
if command is None:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
index = start + len(command)
|
|
173
|
+
|
|
174
|
+
while index < len(text) and text[index].isspace():
|
|
175
|
+
index += 1
|
|
176
|
+
|
|
177
|
+
color_data = read_braced(text, index)
|
|
178
|
+
if color_data is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
_, index = color_data
|
|
182
|
+
|
|
183
|
+
while index < len(text) and text[index].isspace():
|
|
184
|
+
index += 1
|
|
185
|
+
|
|
186
|
+
value_data = read_braced(text, index)
|
|
187
|
+
if value_data is None:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
value, end = value_data
|
|
191
|
+
return value[1:-1], end
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def read_color_command(text: str, start: int) -> tuple[str, int] | None:
|
|
195
|
+
"""
|
|
196
|
+
Detect an existing scoped color command.
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
\\textcolor{red}{x}
|
|
200
|
+
\\color{red}{x}
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
(full_command, end_index)
|
|
204
|
+
or None
|
|
205
|
+
"""
|
|
206
|
+
wrapper = read_color_wrapper(text, start)
|
|
207
|
+
if wrapper is None:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
_, end = wrapper
|
|
211
|
+
return text[start:end], end
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def contains_color_wrapper(text: str) -> bool:
|
|
215
|
+
"""Find active wrappers, ignoring TeX comments and verbatim payloads."""
|
|
216
|
+
index = 0
|
|
217
|
+
while index < len(text):
|
|
218
|
+
if text[index] == "%":
|
|
219
|
+
index = read_comment_end(text, index)
|
|
220
|
+
continue
|
|
221
|
+
if text[index] == "\\":
|
|
222
|
+
if read_color_wrapper(text, index) is not None:
|
|
223
|
+
return True
|
|
224
|
+
verb = read_verb_end(text, index)
|
|
225
|
+
if verb is not None:
|
|
226
|
+
index = verb[0]
|
|
227
|
+
continue
|
|
228
|
+
command = COMMAND_RE.match(text, index)
|
|
229
|
+
index = command.end() if command is not None else index + 1
|
|
230
|
+
continue
|
|
231
|
+
index += 1
|
|
232
|
+
return False
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Apply source-preserving color wrappers to exact LaTeX ranges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ColorSpan:
|
|
10
|
+
"""A half-open source range that should receive one color."""
|
|
11
|
+
|
|
12
|
+
start: int
|
|
13
|
+
end: int
|
|
14
|
+
color: str
|
|
15
|
+
priority: int = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _crosses(left: ColorSpan, right: ColorSpan) -> bool:
|
|
19
|
+
return (
|
|
20
|
+
left.start < right.start < left.end < right.end
|
|
21
|
+
or right.start < left.start < right.end < left.end
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def select_color_spans(source: str, spans: list[ColorSpan]) -> list[ColorSpan]:
|
|
26
|
+
"""Keep valid nested/disjoint spans, preferring higher-priority edits."""
|
|
27
|
+
|
|
28
|
+
candidates: dict[tuple[int, int], ColorSpan] = {}
|
|
29
|
+
for span in spans:
|
|
30
|
+
if not (0 <= span.start < span.end <= len(source)):
|
|
31
|
+
continue
|
|
32
|
+
key = (span.start, span.end)
|
|
33
|
+
previous = candidates.get(key)
|
|
34
|
+
if previous is None or span.priority > previous.priority:
|
|
35
|
+
candidates[key] = span
|
|
36
|
+
|
|
37
|
+
accepted: list[ColorSpan] = []
|
|
38
|
+
for span in sorted(
|
|
39
|
+
candidates.values(),
|
|
40
|
+
key=lambda item: (-item.priority, item.start, -(item.end - item.start)),
|
|
41
|
+
):
|
|
42
|
+
if any(_crosses(span, other) for other in accepted):
|
|
43
|
+
continue
|
|
44
|
+
accepted.append(span)
|
|
45
|
+
|
|
46
|
+
return sorted(accepted, key=lambda item: (item.start, -item.end))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def apply_color_spans(source: str, spans: list[ColorSpan]) -> str:
|
|
50
|
+
"""Insert ``\\textcolor`` wrappers without changing the source itself."""
|
|
51
|
+
|
|
52
|
+
selected = select_color_spans(source, spans)
|
|
53
|
+
openings: dict[int, list[ColorSpan]] = {}
|
|
54
|
+
closings: dict[int, list[ColorSpan]] = {}
|
|
55
|
+
for span in selected:
|
|
56
|
+
openings.setdefault(span.start, []).append(span)
|
|
57
|
+
closings.setdefault(span.end, []).append(span)
|
|
58
|
+
|
|
59
|
+
pieces: list[str] = []
|
|
60
|
+
for index in range(len(source) + 1):
|
|
61
|
+
# Close inner spans first, then open outer spans first.
|
|
62
|
+
for _ in sorted(
|
|
63
|
+
closings.get(index, ()),
|
|
64
|
+
key=lambda item: item.start,
|
|
65
|
+
reverse=True,
|
|
66
|
+
):
|
|
67
|
+
pieces.append("}")
|
|
68
|
+
for span in sorted(
|
|
69
|
+
openings.get(index, ()),
|
|
70
|
+
key=lambda item: item.end,
|
|
71
|
+
reverse=True,
|
|
72
|
+
):
|
|
73
|
+
pieces.append(rf"\textcolor{{{span.color}}}{{")
|
|
74
|
+
if index < len(source):
|
|
75
|
+
pieces.append(source[index])
|
|
76
|
+
|
|
77
|
+
return "".join(pieces)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-color-math
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure Python semantic LaTeX and MathJax colorizer for Obsidian Markdown, KaTeX, and math notes.
|
|
5
|
+
Author: 36ty-blip
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Aditya
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/36ty-blip/python-color-math
|
|
29
|
+
Project-URL: Repository, https://github.com/36ty-blip/python-color-math
|
|
30
|
+
Project-URL: Issues, https://github.com/36ty-blip/python-color-math/issues
|
|
31
|
+
Keywords: obsidian,latex,math,markdown,colorizer,katex,mathjax,cli
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: Intended Audience :: Education
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
45
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
46
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
47
|
+
Requires-Python: >=3.10
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
License-File: LICENSE
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# ๐จ color-math (Python)
|
|
53
|
+
|
|
54
|
+
> Pure Python semantic LaTeX and MathJax colorizer for Obsidian Markdown notes, KaTeX documents, and scientific workflows. Zero dependencies.
|
|
55
|
+
|
|
56
|
+
[](https://github.com/36ty-blip/python-color-math/actions/workflows/ci.yml)
|
|
57
|
+
[](https://pypi.org/project/python-color-math/)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
[](https://www.python.org/)
|
|
60
|
+
|
|
61
|
+
`python-color-math` is a fast, standalone command-line tool and Python library that automatically parses LaTeX and MathJax expressions and wraps elements in semantic `\textcolor{...}{...}` annotations. It works across plain Markdown, Obsidian notes, Quarto documents, Jupyter notebooks, and raw LaTeX files without modifying surrounding prose or code blocks.
|
|
62
|
+
|
|
63
|
+
> [!NOTE]
|
|
64
|
+
> **Looking for the Obsidian Plugin?** Check out [obsidian-color-math](https://github.com/36ty-blip/obsidian-color-math) for real-time live preview math coloring directly inside Obsidian!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## โจ Features
|
|
69
|
+
|
|
70
|
+
- **Zero External Dependencies**: Runs entirely on the Python Standard Library (Python 3.10+).
|
|
71
|
+
- **Physical Units & Metric Prefixes**: Distinguishes units (`\mu m`, `m/s`, `nm`, `kg`) from variables, shielding algebraic variables like $m$ in $F = ma$ or $E = mc^2$.
|
|
72
|
+
- **Calculus Differentials & Derivatives**: Recognizes infinitesimal differentials ($dx$, $dt$, $d\theta$) and derivative fractions ($\frac{d}{dx}$, $\frac{\partial \psi}{\partial t}$), while preserving standalone distance $d$ and relations like $d\iff e$.
|
|
73
|
+
- **Dirac Quantum Bra-Ket Notation**: Formats kets ($|\psi\rangle$), bras ($\langle\phi|$), and expectation values ($\langle\phi|\hat{H}|\psi\rangle$) while protecting absolute values ($|x| < 5$).
|
|
74
|
+
- **Dimensionless Numbers**: Identifies contiguous engineering numbers ($Re$, $Ma$, $Pr$, $Nu$) without capturing separated variables ($R\,e$).
|
|
75
|
+
- **Rainbow Delimiters**: Stack-based delimiter matching that colors nested parentheses, brackets, and braces by nesting depth.
|
|
76
|
+
- **Mathematical Symbol Taxonomy**: Categorizes constants ($\pi, \hbar, \infty$), Greek parameters ($\alpha, \theta, \lambda$), functions ($\sin, \cos, \ln$), and bound summation/limit indices.
|
|
77
|
+
- **Variable Data-Flow Hashing**: Deterministically hashes identifiers across an equation so each unique variable maintains a consistent color across terms.
|
|
78
|
+
- **Boxed Equations**: Preserves `\boxed{...}` wrappers while coloring internal mathematical structures.
|
|
79
|
+
- **Signature Tokyo Night Palette**: Muted pastel tones calibrated for readability and reduced eye strain.
|
|
80
|
+
- **Markdown & TeX Safety**: Fenced code blocks, inline code, TeX comments, and `\verb` blocks are protected and left unmodified.
|
|
81
|
+
- **Lossless & Reversible**: Includes a `--undo` command to cleanly strip all injected colors back to original LaTeX notation.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ๐ Installation
|
|
86
|
+
|
|
87
|
+
Install from PyPI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install python-color-math
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or install from source:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/36ty-blip/python-color-math.git
|
|
97
|
+
cd python-color-math
|
|
98
|
+
pip install .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ๐ป CLI Usage
|
|
104
|
+
|
|
105
|
+
### Process a Markdown or LaTeX file
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Colorize equations in a single note
|
|
109
|
+
color-math note.md
|
|
110
|
+
|
|
111
|
+
# Colorize an entire directory of notes recursively
|
|
112
|
+
color-math ./vault/
|
|
113
|
+
|
|
114
|
+
# Preview changes without modifying files (Dry Run)
|
|
115
|
+
color-math --dry-run note.md
|
|
116
|
+
|
|
117
|
+
# Revert and clean colors back to plain LaTeX
|
|
118
|
+
color-math --undo note.md
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Stdin / Pipe Support
|
|
122
|
+
|
|
123
|
+
Pipe equations directly through the CLI:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
echo "$$\frac{d}{dx}f(g(x)) = f'(g(x)) \cdot g'(x)$$" | color-math
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## ๐ Python API
|
|
132
|
+
|
|
133
|
+
Use `color-math` directly as a library in your Python applications, scripts, or data pipelines:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from color_math import colorize_latex, ColorMathOptions
|
|
137
|
+
|
|
138
|
+
latex = r"\frac{d}{dx}f(g(x)) = f'(g(x)) \cdot g'(x)"
|
|
139
|
+
|
|
140
|
+
# Basic coloring with default Tokyo Night palette
|
|
141
|
+
colored = colorize_latex(latex)
|
|
142
|
+
print(colored)
|
|
143
|
+
|
|
144
|
+
# Enable extended features (Rainbow delimiters, Variable hashing, etc.)
|
|
145
|
+
opts = ColorMathOptions.all_enabled()
|
|
146
|
+
extended_colored = colorize_latex(latex, options=opts)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## ๐งช Testing
|
|
152
|
+
|
|
153
|
+
Run the comprehensive test suites (51 tests):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Run feature test suite
|
|
157
|
+
python -m unittest discover tests
|
|
158
|
+
|
|
159
|
+
# Run regression self-test suite
|
|
160
|
+
python -m tests.self_test
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## ๐ License
|
|
166
|
+
|
|
167
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
color_math/__init__.py,sha256=Mzp9sKJJHZPEHWiy0UnH1tu6WJ0-VltFtyx_-pU7Dek,336
|
|
2
|
+
color_math/__main__.py,sha256=lU6t3BklI-5H9EdprlceWLr0UcDmfXUu3Th0tjk2ANA,81
|
|
3
|
+
color_math/adapters.py,sha256=4WtoioVJqTRsgiukHvxHHGEd4Mr9v0ATnZituriaXu8,10215
|
|
4
|
+
color_math/config.py,sha256=S_fq5uSoIXPMI8TSlIUc9d4uPsp-zFlR3hvAINIDf9k,5835
|
|
5
|
+
color_math/io.py,sha256=iCWMY1ufPZzFCwuHTn_axNetek8PRXGNnrmz59CrlGw,1591
|
|
6
|
+
color_math/main.py,sha256=lBbgYkNGZLsyxDIF_LXnFNvBZUsTwUpuRXoe4HZa1gU,4393
|
|
7
|
+
color_math/self_test.py,sha256=O-BLG2c_3jXPl9NfuMIKChUxGFih8DtVchdBMI3X2-o,7735
|
|
8
|
+
color_math/undo.py,sha256=2LZCYybQICeDbHGk-KBcGczf0qp8D4_3Af8clD2nYlw,1773
|
|
9
|
+
color_math/converters/__init__.py,sha256=1D8U549JFGecslNKnvdpS1yslzMk30xkFM9ymjeYtDE,718
|
|
10
|
+
color_math/converters/align.py,sha256=bfOuoX_GQXLCqPtONlHCRZIsTv9DA8am5J0S8X8AVO8,461
|
|
11
|
+
color_math/converters/block.py,sha256=VB4CpgvW2Ms1vXG0biUSQdcMWD1bIw562Kuij3iZoUY,3139
|
|
12
|
+
color_math/converters/derivative.py,sha256=hVreur2qv2uH2wrvJQ5j00y5Jpefa4hwUCwmKkVsP20,8241
|
|
13
|
+
color_math/converters/equation.py,sha256=NoUXfLayzkoJJ2cBZmV-9-LLy156K4Ak03-wPx7t67M,105
|
|
14
|
+
color_math/converters/generic.py,sha256=iVg-kTsvadZrQ3MSdpBhEye-wf6MW_Qnru9jO0e5WbA,3426
|
|
15
|
+
color_math/converters/integral.py,sha256=V-HkZVcCLzELR9wCbUz0Lgc3cTIvy4DCuEfEFzCy_jg,414
|
|
16
|
+
color_math/converters/limit.py,sha256=xcakNIFUb_2zC9jHOZmAx9QgzWUbzVp31b1jgfCgP5Q,425
|
|
17
|
+
color_math/converters/matrix.py,sha256=0AHpCLMmGIzMwiMSZ8PpK1-JdCYnoCXzhkmlhmNYyxw,4672
|
|
18
|
+
color_math/converters/semantic.py,sha256=fxAu76l8K57GI2oLvfYSrHpFt1hMWBhBHyt9giLbPy0,1902
|
|
19
|
+
color_math/parsers/__init__.py,sha256=EiZFGRm7aanSJzh5mAA4F8_I94T85kOtJrz9iZBADwc,1722
|
|
20
|
+
color_math/parsers/braket.py,sha256=9FOLa26Yvf9vl-AKF5fjSITw3gS0MEtM-Ke9XkFdmhE,3919
|
|
21
|
+
color_math/parsers/delimiters.py,sha256=vcf29d0QRtAPN5wuxHqdxW8EQ6kVvuwG2fqDLW9lAGE,5057
|
|
22
|
+
color_math/parsers/differentials.py,sha256=VjrVNX38JcGreTeAWo_azwqK3c2hsUrIx38FyX0NXRA,3009
|
|
23
|
+
color_math/parsers/dimensionless.py,sha256=_pGw10Os5iVenry7JWcEE3vtodmt7EAacrkE4obYwOA,2307
|
|
24
|
+
color_math/parsers/latex_spans.py,sha256=UGcSh4qzuCcZjT_tZMsB2RsoSjyLSckXDhzukpioe4Q,31343
|
|
25
|
+
color_math/parsers/markdown_scanner.py,sha256=6KfEeHJ-F9xsaarIKmdyeN2CwQORQLs95q26uY7_JXI,12790
|
|
26
|
+
color_math/parsers/math_parser.py,sha256=pbYfIfJDjUMhfuPzMuLukqtzZXW6mMshQqE4uulKqRM,10415
|
|
27
|
+
color_math/parsers/scanner.py,sha256=eTolv8sg8q7ILg4-CfOFbgSw1ZMpidQDYi0HRnN_R8U,11338
|
|
28
|
+
color_math/parsers/taxonomy.py,sha256=XDCirS4kOKcHQf-6ldqdaeQfr81rJXLnXmdYY9X68FA,5036
|
|
29
|
+
color_math/parsers/units.py,sha256=EhvTdIlels3Tw2MSyt4c7XRSlC4eG4Bz4Hg351SdNSg,3656
|
|
30
|
+
color_math/parsers/variable_hash.py,sha256=-OhQbE7psG4WfqsENFQL75lk9YsFwelRDEmRDXTkTis,5200
|
|
31
|
+
color_math/utils/__init__.py,sha256=y5qebQLJwQjuOuI54QnbDeCr7SEdFQm4-9KgIruWQfc,497
|
|
32
|
+
color_math/utils/coloring.py,sha256=nCihj00f_lOuQ79l_crc7BJ30e6rh9nztOwDMwvEMwc,1240
|
|
33
|
+
color_math/utils/latex_helpers.py,sha256=JMNApj--EvEDMXkRyoRpUqbaJysaMGD0BiEAIvRE_vY,5886
|
|
34
|
+
color_math/utils/spans.py,sha256=vYsvSk3QfRqZswFDQ1OtiXOBIzN_36OtE5AP4xaenFI,2396
|
|
35
|
+
python_color_math-0.1.0.dist-info/licenses/LICENSE,sha256=hveQNxqYFqK9Brs_ruH-8shF2D61hIVKYdCjiE-1wMY,1063
|
|
36
|
+
python_color_math-0.1.0.dist-info/METADATA,sha256=LkbUg3wjQvmAWp8hMi6zb2h7YV6qdQF4Zr5b1p3prRA,7079
|
|
37
|
+
python_color_math-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
38
|
+
python_color_math-0.1.0.dist-info/entry_points.txt,sha256=tYE8pBqTiwj22sVIVaSAiP-ZWbFry_ZN1nssVb6wOPc,52
|
|
39
|
+
python_color_math-0.1.0.dist-info/top_level.txt,sha256=Pxuur7TFdlMD0AW61wir5nCIgzFAupL67n1tWe1B5A8,11
|
|
40
|
+
python_color_math-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aditya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
color_math
|