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
color_math/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Public helpers for converting Obsidian LaTeX color markup."""
|
|
2
|
+
|
|
3
|
+
from .adapters import detect_format, transform_document
|
|
4
|
+
from .config import ColorMathOptions
|
|
5
|
+
from .converters.block import convert_text
|
|
6
|
+
from .undo import uncolor_text
|
|
7
|
+
|
|
8
|
+
__all__ = ["ColorMathOptions", "convert_text", "detect_format", "transform_document", "uncolor_text"]
|
color_math/__main__.py
ADDED
color_math/adapters.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Thin document adapters around the existing Markdown math converter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .converters.block import convert_math_block, convert_text
|
|
10
|
+
from .undo import uncolor_fragment, uncolor_text
|
|
11
|
+
from .utils.latex_helpers import read_comment_end, read_verb_end
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FORMATS = ("auto", "markdown", "jupyter", "anki", "tex")
|
|
15
|
+
ANKI_DELIMITERS = (
|
|
16
|
+
(r"\[", r"\]"),
|
|
17
|
+
(r"\(", r"\)"),
|
|
18
|
+
("[$$]", "[/$$]"),
|
|
19
|
+
("[$]", "[/$]"),
|
|
20
|
+
)
|
|
21
|
+
MATH_ENVIRONMENTS = {
|
|
22
|
+
"align", "align*", "alignat", "alignat*", "displaymath",
|
|
23
|
+
"equation", "equation*", "eqnarray", "eqnarray*", "flalign",
|
|
24
|
+
"flalign*", "gather", "gather*", "math", "multline", "multline*",
|
|
25
|
+
}
|
|
26
|
+
VERBATIM_ENVIRONMENTS = {"Verbatim", "lstlisting", "minted", "verbatim", "verbatim*"}
|
|
27
|
+
BEGIN_RE = re.compile(r"\\begin\{([^{}]+)\}")
|
|
28
|
+
HEX_COLOR_RE = re.compile(r"\\textcolor\{#([0-9A-Fa-f]{6})\}")
|
|
29
|
+
NATIVE_COLOR_RE = re.compile(r"\\textcolor\{colormath([0-9A-Fa-f]{6})\}")
|
|
30
|
+
NATIVE_BEGIN = "% color-math: begin generated xcolor support"
|
|
31
|
+
NATIVE_END = "% color-math: end generated xcolor support"
|
|
32
|
+
NATIVE_BLOCK_RE = re.compile(
|
|
33
|
+
rf"(?m)^{re.escape(NATIVE_BEGIN)}(?:\r?\n).*?^{re.escape(NATIVE_END)}(?:\r?\n)?",
|
|
34
|
+
re.DOTALL,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AdapterError(ValueError):
|
|
39
|
+
"""A document cannot be safely handled by its selected adapter."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_format(path: Path | None, requested: str = "auto") -> str:
|
|
43
|
+
if requested != "auto":
|
|
44
|
+
return requested
|
|
45
|
+
if path is not None and path.suffix.lower() == ".ipynb":
|
|
46
|
+
return "jupyter"
|
|
47
|
+
if path is not None and path.suffix.lower() in {".tex", ".latex"}:
|
|
48
|
+
return "tex"
|
|
49
|
+
return "markdown"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def transform_document(text: str, format_name: str, undo: bool = False) -> str:
|
|
53
|
+
if format_name == "markdown":
|
|
54
|
+
return uncolor_text(text) if undo else convert_text(text)
|
|
55
|
+
if format_name == "jupyter":
|
|
56
|
+
return _transform_notebook(text, undo)
|
|
57
|
+
if format_name == "anki":
|
|
58
|
+
return _transform_delimited(text, ANKI_DELIMITERS, undo)
|
|
59
|
+
if format_name == "tex":
|
|
60
|
+
return _transform_tex(text, undo)
|
|
61
|
+
raise AdapterError(f"unsupported format: {format_name}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _transform_fragment(text: str, undo: bool) -> str:
|
|
65
|
+
if undo:
|
|
66
|
+
return uncolor_fragment(text)
|
|
67
|
+
converted = convert_math_block(f"$${text}$$")
|
|
68
|
+
return converted[2:-2]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _transform_notebook(text: str, undo: bool) -> str:
|
|
72
|
+
try:
|
|
73
|
+
notebook = json.loads(text)
|
|
74
|
+
except json.JSONDecodeError as error:
|
|
75
|
+
raise AdapterError(f"invalid Jupyter JSON at line {error.lineno}: {error.msg}") from error
|
|
76
|
+
|
|
77
|
+
if not isinstance(notebook, dict) or not isinstance(notebook.get("cells"), list):
|
|
78
|
+
raise AdapterError("Jupyter notebook must contain a cells list")
|
|
79
|
+
|
|
80
|
+
changed = False
|
|
81
|
+
for index, cell in enumerate(notebook["cells"]):
|
|
82
|
+
if not isinstance(cell, dict) or cell.get("cell_type") != "markdown":
|
|
83
|
+
continue
|
|
84
|
+
source = cell.get("source", "")
|
|
85
|
+
if isinstance(source, str):
|
|
86
|
+
joined = source
|
|
87
|
+
elif isinstance(source, list) and all(isinstance(part, str) for part in source):
|
|
88
|
+
joined = "".join(source)
|
|
89
|
+
else:
|
|
90
|
+
raise AdapterError(f"markdown cell {index + 1} has an invalid source")
|
|
91
|
+
|
|
92
|
+
converted = uncolor_text(joined) if undo else convert_text(joined)
|
|
93
|
+
if converted == joined:
|
|
94
|
+
continue
|
|
95
|
+
cell["source"] = (
|
|
96
|
+
converted
|
|
97
|
+
if isinstance(source, str)
|
|
98
|
+
else converted.splitlines(keepends=True) or ([""] if source else [])
|
|
99
|
+
)
|
|
100
|
+
changed = True
|
|
101
|
+
|
|
102
|
+
if not changed:
|
|
103
|
+
return text
|
|
104
|
+
|
|
105
|
+
indentation = re.search(r"\n([ \t]+)\"", text)
|
|
106
|
+
if indentation is None:
|
|
107
|
+
output = json.dumps(notebook, ensure_ascii=False, separators=(",", ":"))
|
|
108
|
+
else:
|
|
109
|
+
output = json.dumps(notebook, ensure_ascii=False, indent=indentation.group(1))
|
|
110
|
+
ending = "\r\n" if text.endswith("\r\n") else "\n" if text.endswith("\n") else ""
|
|
111
|
+
return output + ending
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_escaped(text: str, index: int) -> bool:
|
|
115
|
+
backslashes = 0
|
|
116
|
+
index -= 1
|
|
117
|
+
while index >= 0 and text[index] == "\\":
|
|
118
|
+
backslashes += 1
|
|
119
|
+
index -= 1
|
|
120
|
+
return backslashes % 2 == 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _next_delimiter(
|
|
124
|
+
text: str,
|
|
125
|
+
start: int,
|
|
126
|
+
delimiters: tuple[tuple[str, str], ...],
|
|
127
|
+
) -> tuple[int, str, str] | None:
|
|
128
|
+
matches = [
|
|
129
|
+
(position, opening, closing)
|
|
130
|
+
for opening, closing in delimiters
|
|
131
|
+
if (position := _find_unescaped(text, opening, start)) >= 0
|
|
132
|
+
]
|
|
133
|
+
return min(matches, default=None, key=lambda match: match[0])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _find_unescaped(text: str, token: str, start: int) -> int:
|
|
137
|
+
while (position := text.find(token, start)) >= 0:
|
|
138
|
+
if not _is_escaped(text, position):
|
|
139
|
+
return position
|
|
140
|
+
start = position + len(token)
|
|
141
|
+
return -1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _transform_delimited(
|
|
145
|
+
text: str,
|
|
146
|
+
delimiters: tuple[tuple[str, str], ...],
|
|
147
|
+
undo: bool,
|
|
148
|
+
) -> str:
|
|
149
|
+
output: list[str] = []
|
|
150
|
+
index = 0
|
|
151
|
+
while match := _next_delimiter(text, index, delimiters):
|
|
152
|
+
start, opening, closing = match
|
|
153
|
+
end = _find_unescaped(text, closing, start + len(opening))
|
|
154
|
+
if end < 0:
|
|
155
|
+
break
|
|
156
|
+
output.append(text[index:start + len(opening)])
|
|
157
|
+
output.append(_transform_fragment(text[start + len(opening):end], undo))
|
|
158
|
+
output.append(closing)
|
|
159
|
+
index = end + len(closing)
|
|
160
|
+
output.append(text[index:])
|
|
161
|
+
return "".join(output)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _find_active(text: str, token: str, start: int) -> int:
|
|
165
|
+
index = start
|
|
166
|
+
while index < len(text):
|
|
167
|
+
if text[index] == "%" and not _is_escaped(text, index):
|
|
168
|
+
index = read_comment_end(text, index)
|
|
169
|
+
continue
|
|
170
|
+
if text[index] == "\\":
|
|
171
|
+
verb = read_verb_end(text, index)
|
|
172
|
+
if verb is not None:
|
|
173
|
+
index = verb[0]
|
|
174
|
+
continue
|
|
175
|
+
if text.startswith(token, index) and not _is_escaped(text, index):
|
|
176
|
+
return index
|
|
177
|
+
index += 1
|
|
178
|
+
return -1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _find_dollar(text: str, start: int, width: int) -> int:
|
|
182
|
+
token = "$" * width
|
|
183
|
+
index = start
|
|
184
|
+
while (index := _find_active(text, token, index)) >= 0:
|
|
185
|
+
before = index > 0 and text[index - 1] == "$"
|
|
186
|
+
after = index + width < len(text) and text[index + width] == "$"
|
|
187
|
+
if not before and not after:
|
|
188
|
+
return index
|
|
189
|
+
index += width
|
|
190
|
+
return -1
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _transform_tex_math(text: str, undo: bool) -> str:
|
|
194
|
+
output: list[str] = []
|
|
195
|
+
index = 0
|
|
196
|
+
while index < len(text):
|
|
197
|
+
if text[index] == "%" and not _is_escaped(text, index):
|
|
198
|
+
end = read_comment_end(text, index)
|
|
199
|
+
output.append(text[index:end])
|
|
200
|
+
index = end
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if text[index] == "\\":
|
|
204
|
+
verb = read_verb_end(text, index)
|
|
205
|
+
if verb is not None:
|
|
206
|
+
end = verb[0]
|
|
207
|
+
output.append(text[index:end])
|
|
208
|
+
index = end
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
environment = BEGIN_RE.match(text, index)
|
|
212
|
+
if environment is not None:
|
|
213
|
+
name = environment.group(1)
|
|
214
|
+
closing = rf"\end{{{name}}}"
|
|
215
|
+
end_start = _find_active(text, closing, environment.end())
|
|
216
|
+
if end_start >= 0 and name in VERBATIM_ENVIRONMENTS:
|
|
217
|
+
end = end_start + len(closing)
|
|
218
|
+
output.append(text[index:end])
|
|
219
|
+
index = end
|
|
220
|
+
continue
|
|
221
|
+
if end_start >= 0 and name in MATH_ENVIRONMENTS:
|
|
222
|
+
end = end_start + len(closing)
|
|
223
|
+
output.append(_transform_fragment(text[index:end], undo))
|
|
224
|
+
index = end
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
pair = next(
|
|
228
|
+
((opening, closing) for opening, closing in ((r"\[", r"\]"), (r"\(", r"\)")) if text.startswith(opening, index)),
|
|
229
|
+
None,
|
|
230
|
+
)
|
|
231
|
+
if pair is not None:
|
|
232
|
+
opening, closing = pair
|
|
233
|
+
end_start = _find_active(text, closing, index + len(opening))
|
|
234
|
+
if end_start >= 0:
|
|
235
|
+
output.append(opening)
|
|
236
|
+
output.append(_transform_fragment(text[index + len(opening):end_start], undo))
|
|
237
|
+
output.append(closing)
|
|
238
|
+
index = end_start + len(closing)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if text[index] == "$" and not _is_escaped(text, index):
|
|
242
|
+
width = 2 if text.startswith("$$", index) else 1
|
|
243
|
+
end_start = _find_dollar(text, index + width, width)
|
|
244
|
+
if end_start >= 0:
|
|
245
|
+
delimiter = "$" * width
|
|
246
|
+
output.append(delimiter)
|
|
247
|
+
output.append(_transform_fragment(text[index + width:end_start], undo))
|
|
248
|
+
output.append(delimiter)
|
|
249
|
+
index = end_start + width
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
output.append(text[index])
|
|
253
|
+
index += 1
|
|
254
|
+
return "".join(output)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _native_support(text: str) -> str:
|
|
258
|
+
translated = HEX_COLOR_RE.sub(
|
|
259
|
+
lambda match: rf"\textcolor{{colormath{match.group(1).lower()}}}",
|
|
260
|
+
text,
|
|
261
|
+
)
|
|
262
|
+
colors = sorted({match.lower() for match in NATIVE_COLOR_RE.findall(translated)})
|
|
263
|
+
if not colors:
|
|
264
|
+
return translated
|
|
265
|
+
|
|
266
|
+
newline = "\r\n" if "\r\n" in translated else "\n"
|
|
267
|
+
lines = [NATIVE_BEGIN]
|
|
268
|
+
user_document = NATIVE_BLOCK_RE.sub("", translated)
|
|
269
|
+
if not re.search(r"\\usepackage(?:\[[^]]*\])?\{[^}]*\bxcolor\b[^}]*\}", user_document):
|
|
270
|
+
lines.append(r"\usepackage{xcolor}")
|
|
271
|
+
lines.extend(
|
|
272
|
+
rf"\definecolor{{colormath{color}}}{{HTML}}{{{color.upper()}}}"
|
|
273
|
+
for color in colors
|
|
274
|
+
)
|
|
275
|
+
lines.append(NATIVE_END)
|
|
276
|
+
block = newline.join(lines) + newline
|
|
277
|
+
|
|
278
|
+
if NATIVE_BLOCK_RE.search(translated):
|
|
279
|
+
return NATIVE_BLOCK_RE.sub(lambda _: block, translated, count=1)
|
|
280
|
+
document_class = re.search(r"(?m)^\\documentclass[^\r\n]*(?:\r?\n|$)", translated)
|
|
281
|
+
insertion = document_class.end() if document_class else 0
|
|
282
|
+
return translated[:insertion] + block + translated[insertion:]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _transform_tex(text: str, undo: bool) -> str:
|
|
286
|
+
if undo:
|
|
287
|
+
return _transform_tex_math(NATIVE_BLOCK_RE.sub("", text), True)
|
|
288
|
+
return _native_support(_transform_tex_math(text, False))
|
color_math/config.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# config.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
DEFAULT_COLORS = {
|
|
6
|
+
"main": "#7aa2f7",
|
|
7
|
+
"orange": "#e0af68",
|
|
8
|
+
"dot": "white",
|
|
9
|
+
"derivative": "#bb9af7",
|
|
10
|
+
"chain": "#9ece6a",
|
|
11
|
+
"upper": "#bb9af7",
|
|
12
|
+
"relation": "white",
|
|
13
|
+
"arrow": "#f7768e",
|
|
14
|
+
"set": "#bb9af7",
|
|
15
|
+
"spacing": "white",
|
|
16
|
+
"parameter": "#bb9af7",
|
|
17
|
+
"unit": "#73daca",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
COLORS = dict(DEFAULT_COLORS)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BIG_OPERATORS = {
|
|
24
|
+
r"\sum",
|
|
25
|
+
r"\prod",
|
|
26
|
+
r"\coprod",
|
|
27
|
+
r"\bigcup",
|
|
28
|
+
r"\bigcap",
|
|
29
|
+
r"\bigsqcup",
|
|
30
|
+
r"\bigvee",
|
|
31
|
+
r"\bigwedge",
|
|
32
|
+
r"\bigoplus",
|
|
33
|
+
r"\bigotimes",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
INTEGRALS = {
|
|
38
|
+
r"\int",
|
|
39
|
+
r"\iint",
|
|
40
|
+
r"\iiint",
|
|
41
|
+
r"\oint",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
LIMIT_OPERATORS = {
|
|
46
|
+
r"\lim",
|
|
47
|
+
r"\sup",
|
|
48
|
+
r"\inf",
|
|
49
|
+
r"\max",
|
|
50
|
+
r"\min",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
RELATIONS = {
|
|
55
|
+
r"\le",
|
|
56
|
+
r"\ge",
|
|
57
|
+
r"\ne",
|
|
58
|
+
r"\neq",
|
|
59
|
+
r"\leq",
|
|
60
|
+
r"\geq",
|
|
61
|
+
r"\approx",
|
|
62
|
+
r"\sim",
|
|
63
|
+
r"\equiv",
|
|
64
|
+
r"\propto",
|
|
65
|
+
r"\simeq",
|
|
66
|
+
r"\cong",
|
|
67
|
+
r"\pm",
|
|
68
|
+
r"\mp",
|
|
69
|
+
r"\div",
|
|
70
|
+
r"\ast",
|
|
71
|
+
r"\star",
|
|
72
|
+
r"\circ",
|
|
73
|
+
r"\bullet",
|
|
74
|
+
"=",
|
|
75
|
+
"<",
|
|
76
|
+
">",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
ARROWS = {
|
|
81
|
+
r"\xrightarrow",
|
|
82
|
+
r"\xleftarrow",
|
|
83
|
+
r"\hookrightarrow",
|
|
84
|
+
r"\hookleftarrow",
|
|
85
|
+
r"\uparrow",
|
|
86
|
+
r"\downarrow",
|
|
87
|
+
r"\implies",
|
|
88
|
+
r"\iff",
|
|
89
|
+
r"\longrightarrow",
|
|
90
|
+
r"\longleftarrow",
|
|
91
|
+
r"\leftrightarrow",
|
|
92
|
+
r"\rightarrow",
|
|
93
|
+
r"\leftarrow",
|
|
94
|
+
r"\Rightarrow",
|
|
95
|
+
r"\Leftarrow",
|
|
96
|
+
r"\Leftrightarrow",
|
|
97
|
+
r"\mapsto",
|
|
98
|
+
r"\to",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
SET_SYMBOLS = {
|
|
103
|
+
r"\forall",
|
|
104
|
+
r"\exists",
|
|
105
|
+
r"\land",
|
|
106
|
+
r"\lor",
|
|
107
|
+
r"\ni",
|
|
108
|
+
r"\sqsubset",
|
|
109
|
+
r"\sqsubseteq",
|
|
110
|
+
r"\uplus",
|
|
111
|
+
r"\notin",
|
|
112
|
+
r"\subseteq",
|
|
113
|
+
r"\supseteq",
|
|
114
|
+
r"\subset",
|
|
115
|
+
r"\supset",
|
|
116
|
+
r"\setminus",
|
|
117
|
+
r"\emptyset",
|
|
118
|
+
r"\in",
|
|
119
|
+
r"\cup",
|
|
120
|
+
r"\cap",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
SPACING_COMMANDS = {
|
|
125
|
+
r"\,",
|
|
126
|
+
r"\:",
|
|
127
|
+
r"\;",
|
|
128
|
+
r"\quad",
|
|
129
|
+
r"\qquad",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
MULTIPLICATION_SYMBOLS = {
|
|
134
|
+
r"\cdot",
|
|
135
|
+
r"\times",
|
|
136
|
+
"·",
|
|
137
|
+
"*",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
FUNCTION_COMMANDS = {
|
|
142
|
+
r"\arccos",
|
|
143
|
+
r"\arcsin",
|
|
144
|
+
r"\arctan",
|
|
145
|
+
r"\cos",
|
|
146
|
+
r"\cosh",
|
|
147
|
+
r"\exp",
|
|
148
|
+
r"\ln",
|
|
149
|
+
r"\log",
|
|
150
|
+
r"\sec",
|
|
151
|
+
r"\sin",
|
|
152
|
+
r"\sinh",
|
|
153
|
+
r"\tan",
|
|
154
|
+
r"\tanh",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Combined commands that should receive special coloring
|
|
159
|
+
COLOR_COMMANDS = (
|
|
160
|
+
BIG_OPERATORS
|
|
161
|
+
| INTEGRALS
|
|
162
|
+
| LIMIT_OPERATORS
|
|
163
|
+
| RELATIONS
|
|
164
|
+
| ARROWS
|
|
165
|
+
| SET_SYMBOLS
|
|
166
|
+
| SPACING_COMMANDS
|
|
167
|
+
| MULTIPLICATION_SYMBOLS
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Longest first so scanner matches \longrightarrow before \to
|
|
172
|
+
SORTED_COLOR_COMMANDS = sorted(
|
|
173
|
+
COLOR_COMMANDS,
|
|
174
|
+
key=len,
|
|
175
|
+
reverse=True,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
MATH_CONSTANTS = {
|
|
180
|
+
r"\pi",
|
|
181
|
+
r"\varpi",
|
|
182
|
+
r"\hbar",
|
|
183
|
+
r"\infty",
|
|
184
|
+
r"\ell",
|
|
185
|
+
r"\aleph",
|
|
186
|
+
r"\Re",
|
|
187
|
+
r"\Im",
|
|
188
|
+
r"\top",
|
|
189
|
+
r"\bot",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
MATH_ACCENTS = {
|
|
194
|
+
r"\dot",
|
|
195
|
+
r"\ddot",
|
|
196
|
+
r"\dddot",
|
|
197
|
+
r"\ddddot",
|
|
198
|
+
r"\hat",
|
|
199
|
+
r"\widehat",
|
|
200
|
+
r"\tilde",
|
|
201
|
+
r"\widetilde",
|
|
202
|
+
r"\bar",
|
|
203
|
+
r"\vec",
|
|
204
|
+
r"\check",
|
|
205
|
+
r"\breve",
|
|
206
|
+
r"\acute",
|
|
207
|
+
r"\grave",
|
|
208
|
+
r"\mathring",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
MATH_PARAMETERS = {
|
|
213
|
+
r"\alpha",
|
|
214
|
+
r"\beta",
|
|
215
|
+
r"\gamma",
|
|
216
|
+
r"\delta",
|
|
217
|
+
r"\epsilon",
|
|
218
|
+
r"\varepsilon",
|
|
219
|
+
r"\zeta",
|
|
220
|
+
r"\eta",
|
|
221
|
+
r"\theta",
|
|
222
|
+
r"\vartheta",
|
|
223
|
+
r"\iota",
|
|
224
|
+
r"\kappa",
|
|
225
|
+
r"\lambda",
|
|
226
|
+
r"\mu",
|
|
227
|
+
r"\nu",
|
|
228
|
+
r"\xi",
|
|
229
|
+
r"\rho",
|
|
230
|
+
r"\varrho",
|
|
231
|
+
r"\sigma",
|
|
232
|
+
r"\varsigma",
|
|
233
|
+
r"\tau",
|
|
234
|
+
r"\upsilon",
|
|
235
|
+
r"\phi",
|
|
236
|
+
r"\varphi",
|
|
237
|
+
r"\chi",
|
|
238
|
+
r"\psi",
|
|
239
|
+
r"\omega",
|
|
240
|
+
r"\Gamma",
|
|
241
|
+
r"\Delta",
|
|
242
|
+
r"\Theta",
|
|
243
|
+
r"\Lambda",
|
|
244
|
+
r"\Xi",
|
|
245
|
+
r"\Pi",
|
|
246
|
+
r"\Sigma",
|
|
247
|
+
r"\Upsilon",
|
|
248
|
+
r"\Phi",
|
|
249
|
+
r"\Psi",
|
|
250
|
+
r"\Omega",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
MATH_FUNCTIONS = {
|
|
255
|
+
r"\sin",
|
|
256
|
+
r"\cos",
|
|
257
|
+
r"\tan",
|
|
258
|
+
r"\csc",
|
|
259
|
+
r"\sec",
|
|
260
|
+
r"\cot",
|
|
261
|
+
r"\arcsin",
|
|
262
|
+
r"\arccos",
|
|
263
|
+
r"\arctan",
|
|
264
|
+
r"\sinh",
|
|
265
|
+
r"\cosh",
|
|
266
|
+
r"\tanh",
|
|
267
|
+
r"\coth",
|
|
268
|
+
r"\ln",
|
|
269
|
+
r"\log",
|
|
270
|
+
r"\exp",
|
|
271
|
+
r"\det",
|
|
272
|
+
r"\gcd",
|
|
273
|
+
r"\max",
|
|
274
|
+
r"\min",
|
|
275
|
+
r"\dim",
|
|
276
|
+
r"\ker",
|
|
277
|
+
r"\hom",
|
|
278
|
+
r"\deg",
|
|
279
|
+
r"\arg",
|
|
280
|
+
r"\Pr",
|
|
281
|
+
r"\sup",
|
|
282
|
+
r"\inf",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
RAINBOW_DELIMITER_COLORS: list[str] = [
|
|
287
|
+
"#e0af68", # Tier 0: Gold
|
|
288
|
+
"#7aa2f7", # Tier 1: Cyan / Blue
|
|
289
|
+
"#bb9af7", # Tier 2: Purple / Lavender
|
|
290
|
+
"#f7768e", # Tier 3: Coral / Pink
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
VARIABLE_HASH_PALETTE: list[str] = [
|
|
295
|
+
"#7aa2f7", # Tokyo Blue
|
|
296
|
+
"#7dcfff", # Tokyo Cyan
|
|
297
|
+
"#bb9af7", # Tokyo Purple
|
|
298
|
+
"#f7768e", # Tokyo Pink
|
|
299
|
+
"#e0af68", # Tokyo Orange/Gold
|
|
300
|
+
"#9ece6a", # Tokyo Green
|
|
301
|
+
"#2ac3de", # Light Cyan
|
|
302
|
+
"#ff9e64", # Peach
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def hash_string_to_color(s: str, palette: list[str] = VARIABLE_HASH_PALETTE) -> str:
|
|
307
|
+
"""Deterministic string hashing for variable data-flow coloring."""
|
|
308
|
+
h = 0
|
|
309
|
+
for char in s:
|
|
310
|
+
h = (h * 31 + ord(char)) & 0xFFFFFFFF
|
|
311
|
+
if h >= 0x80000000:
|
|
312
|
+
h -= 0x100000000
|
|
313
|
+
idx = abs(h) % len(palette)
|
|
314
|
+
return palette[idx]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass
|
|
318
|
+
class ColorMathOptions:
|
|
319
|
+
enable_taxonomy: bool = False
|
|
320
|
+
rainbow_delimiters: bool = False
|
|
321
|
+
variable_data_flow: bool = False
|
|
322
|
+
color_units: bool = False
|
|
323
|
+
color_differentials: bool = False
|
|
324
|
+
color_braket: bool = False
|
|
325
|
+
color_dimensionless: bool = False
|
|
326
|
+
|
|
327
|
+
@classmethod
|
|
328
|
+
def extended(cls) -> ColorMathOptions:
|
|
329
|
+
"""Returns standard extended options matching Obsidian plugin defaults."""
|
|
330
|
+
return cls(
|
|
331
|
+
enable_taxonomy=True,
|
|
332
|
+
rainbow_delimiters=True,
|
|
333
|
+
variable_data_flow=False,
|
|
334
|
+
color_units=True,
|
|
335
|
+
color_differentials=True,
|
|
336
|
+
color_braket=True,
|
|
337
|
+
color_dimensionless=True,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def all_enabled(cls) -> ColorMathOptions:
|
|
342
|
+
"""Returns options with all features enabled including variable data-flow."""
|
|
343
|
+
return cls(
|
|
344
|
+
enable_taxonomy=True,
|
|
345
|
+
rainbow_delimiters=True,
|
|
346
|
+
variable_data_flow=True,
|
|
347
|
+
color_units=True,
|
|
348
|
+
color_differentials=True,
|
|
349
|
+
color_braket=True,
|
|
350
|
+
color_dimensionless=True,
|
|
351
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# converters/__init__.py
|
|
2
|
+
|
|
3
|
+
from .align import convert_align_block
|
|
4
|
+
from .derivative import convert_derivative_line
|
|
5
|
+
from .equation import convert_equation_line
|
|
6
|
+
from .generic import color_latex_body, color_generic_math_line
|
|
7
|
+
from .integral import convert_integral_line
|
|
8
|
+
from .limit import convert_limit_line
|
|
9
|
+
from .matrix import convert_matrix_block
|
|
10
|
+
from .block import convert_math_block, convert_line, convert_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"convert_align_block",
|
|
15
|
+
"convert_derivative_line",
|
|
16
|
+
"convert_equation_line",
|
|
17
|
+
"convert_integral_line",
|
|
18
|
+
"convert_limit_line",
|
|
19
|
+
"convert_matrix_block",
|
|
20
|
+
|
|
21
|
+
"color_latex_body",
|
|
22
|
+
"color_generic_math_line",
|
|
23
|
+
|
|
24
|
+
"convert_math_block",
|
|
25
|
+
"convert_line",
|
|
26
|
+
"convert_text",
|
|
27
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .generic import color_latex_body
|
|
6
|
+
from .semantic import parse_math_block
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ALIGN_ENV_RE = re.compile(
|
|
10
|
+
r"\\begin\s*\{\s*(?:align\*?|aligned|gather\*?|gathered|split)\s*\}"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_align_block(block: str) -> str | None:
|
|
15
|
+
parsed = parse_math_block(block)
|
|
16
|
+
if parsed is None or ALIGN_ENV_RE.search(parsed.body) is None:
|
|
17
|
+
return None
|
|
18
|
+
return parsed.render(color_latex_body(parsed.body))
|