From 6cb34ead26d24b99199e3c9d31e279ed005443fe Mon Sep 17 00:00:00 2001 From: Edern Deneuville Date: Fri, 11 Sep 2026 13:55:54 +0200 Subject: [PATCH] Add Unicode math formatter --- src/ai_typewriter/math_format.py | 134 +++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/ai_typewriter/math_format.py diff --git a/src/ai_typewriter/math_format.py b/src/ai_typewriter/math_format.py new file mode 100644 index 0000000..ef3449a --- /dev/null +++ b/src/ai_typewriter/math_format.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import re + +GREEK_AND_SYMBOLS = { + r"\alpha": "α", + r"\beta": "β", + r"\gamma": "γ", + r"\delta": "δ", + r"\epsilon": "ε", + r"\varepsilon": "ε", + r"\zeta": "ζ", + r"\eta": "η", + r"\theta": "θ", + r"\vartheta": "ϑ", + r"\iota": "ι", + r"\kappa": "κ", + r"\lambda": "λ", + r"\mu": "μ", + r"\nu": "ν", + r"\xi": "ξ", + r"\pi": "π", + r"\rho": "ρ", + r"\sigma": "σ", + r"\tau": "τ", + r"\upsilon": "υ", + r"\phi": "φ", + r"\varphi": "φ", + r"\chi": "χ", + r"\psi": "ψ", + r"\omega": "ω", + r"\Gamma": "Γ", + r"\Delta": "Δ", + r"\Theta": "Θ", + r"\Lambda": "Λ", + r"\Xi": "Ξ", + r"\Pi": "Π", + r"\Sigma": "Σ", + r"\Phi": "Φ", + r"\Psi": "Ψ", + r"\Omega": "Ω", + r"\infty": "∞", + r"\leq": "≤", + r"\le": "≤", + r"\geq": "≥", + r"\ge": "≥", + r"\neq": "≠", + r"\ne": "≠", + r"\approx": "≈", + r"\times": "×", + r"\cdot": "·", + r"\pm": "±", + r"\mp": "∓", + r"\to": "→", + r"\rightarrow": "→", + r"\leftarrow": "←", + r"\Rightarrow": "⇒", + r"\Leftrightarrow": "⇔", + r"\forall": "∀", + r"\exists": "∃", + r"\in": "∈", + r"\notin": "∉", + r"\subset": "⊂", + r"\subseteq": "⊆", + r"\cup": "∪", + r"\cap": "∩", + r"\emptyset": "∅", + r"\mathbb{R}": "ℝ", + r"\mathbb{C}": "ℂ", + r"\mathbb{N}": "ℕ", + r"\mathbb{Z}": "ℤ", + r"\mathbb{Q}": "ℚ", + r"\int": "∫", + r"\sum": "∑", + r"\prod": "∏", + r"\partial": "∂", + r"\nabla": "∇", +} + +SUPERSCRIPT = str.maketrans({ + "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", + "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", + "+": "⁺", "-": "⁻", "=": "⁼", "(": "⁽", ")": "⁾", + "n": "ⁿ", "i": "ⁱ", +}) + +SUBSCRIPT = str.maketrans({ + "0": "₀", "1": "₁", "2": "₂", "3": "₃", "4": "₄", + "5": "₅", "6": "₆", "7": "₇", "8": "₈", "9": "₉", + "+": "₊", "-": "₋", "=": "₌", "(": "₍", ")": "₎", + "a": "ₐ", "e": "ₑ", "h": "ₕ", "i": "ᵢ", "j": "ⱼ", "k": "ₖ", + "l": "ₗ", "m": "ₘ", "n": "ₙ", "o": "ₒ", "p": "ₚ", "r": "ᵣ", + "s": "ₛ", "t": "ₜ", "u": "ᵤ", "v": "ᵥ", "x": "ₓ", +}) + +_GROUP_PATTERN = re.compile(r"([_^])\(([^()]+)\)|([_^])\{([^{}]+)\}|([_^])([A-Za-z0-9+\-=])") + + +def _translate_script(value: str, marker: str) -> str: + table = SUPERSCRIPT if marker == "^" else SUBSCRIPT + converted = value.translate(table) + return converted if converted != value else marker + value + + +def _replace_script(match: re.Match[str]) -> str: + marker = match.group(1) or match.group(3) or match.group(5) + value = match.group(2) or match.group(4) or match.group(6) + return _translate_script(value, marker) + + +def format_math_text(text: str, mode: str = "plain") -> str: + r"""Convert common UnicodeMath/LaTeX-like linear math to Unicode plain text. + + This is meant for Word normal text insertion. It makes expressions such as + z_1, x^2, \alpha and \infty render as z₁, x², α and ∞ without requiring the + user to manually open Word's equation editor for every expression. + """ + if mode == "plain": + return text + if mode not in {"unicode", "unicode_math"}: + raise ValueError("math_text_format doit être 'plain' ou 'unicode'") + + formatted = text + # Replace longer commands first so \subseteq wins before \subset. + for command, replacement in sorted(GREEK_AND_SYMBOLS.items(), key=lambda item: len(item[0]), reverse=True): + formatted = formatted.replace(command, replacement) + + formatted = re.sub(r"\\vec\(([^()]+)\)", lambda match: match.group(1) + "⃗", formatted) + formatted = formatted.replace(r"\sqrt", "√") + previous = None + while previous != formatted: + previous = formatted + formatted = _GROUP_PATTERN.sub(_replace_script, formatted) + return formatted