Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formatting math (much like black formats Python code) #111

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
30 changes: 30 additions & 0 deletions tests/test_formatter_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import texplain


def test_arithmetic():
assert texplain.formatter_math(r"i-1") == r"i - 1"
assert texplain.formatter_math(r"a=b") == r"a = b"
assert texplain.formatter_math(r"a:=b") == r"a := b"
assert texplain.formatter_math(r"a-b") == r"a - b"
assert texplain.formatter_math(r"a\leq b") == r"a \leq b"
assert texplain.formatter_math(r"\theta>0") == r"\theta > 0"
assert texplain.formatter_math(r"P(x)\sim x^\theta") == r"P(x) \sim x^\theta"
assert texplain.formatter_math(r"a\simeq 0.2") == r"a \simeq 0.2"
assert texplain.formatter_math(r"\lambda_+") == r"\lambda_+"


def test_comma():
assert texplain.formatter_math(r"1,2") == r"1,2"
assert texplain.formatter_math(r"\tau, \nu, \ldots") == r"\tau, \nu, \ldots"


def test_sign():
assert texplain.formatter_math(r" - b") == r"-b"
assert texplain.formatter_math(r"- b") == r"-b"
assert texplain.formatter_math(r"-b") == r"-b"
assert texplain.formatter_math(r"{ - b}") == r"{-b}"
assert texplain.formatter_math(r"{- b}") == r"{-b}"
assert texplain.formatter_math(r"{-b}") == r"{-b}"
assert texplain.formatter_math(r"\gamma=-0.30") == r"\gamma = -0.30"
assert texplain.formatter_math(r"a = - 0.30") == r"a = -0.30"
assert texplain.formatter_math(r"r_0 = -w / 2") == r"r_0 = -w / 2"
40 changes: 36 additions & 4 deletions tests/test_indent_long.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ def test_latexindent_one_sentence_per_line_issue_376():
For a slip event at interface $s$, we have $\Delta R_s > 0$ and $\Delta R_i = 0$ for $i \neq s$, inducing
\begin{equation}
\label{eq:delta_fi}
\Delta f_i = K\Delta R_s \,\, \mathrm{for} \,\, i \geq s ; \quad \Delta f_i=0 \,\, \mathrm{otherwise}.
\Delta f_i = K\Delta R_s \,\, \mathrm{for} \,\, i \geq s ; \quad \Delta f_i = 0 \,\, \mathrm{otherwise}.
\end{equation}
We can then deduce that
\begin{equation}
\label{eq:DR_DF_multi}
\Delta F = \frac{h K}{H} \Delta R_s \sum\limits_{i=s}^n i \, = \frac{h K}{H}\Delta R_s (n+s)(n-s+1) / 2.
\Delta F = \frac{h K}{H} \Delta R_s \sum\limits_{i = s}^n i \, = \frac{h K}{H}\Delta R_s (n + s)(n - s + 1) / 2.
\end{equation}
which we verify in \cref{fig:2c}.
"""
Expand Down Expand Up @@ -295,7 +295,7 @@ def test_latexindent_one_sentence_per_line_mlep():
The unit is $\rm kg.m^{-2}.s^{-1}$.
Values goes from 0.02 to 0.74 Pa.
Here some space needed \hspace{0.5cm}.
The value is $\lambda=3.67$.
The value is $\lambda = 3.67$.
The URL is \url{www.scilab.org}.
"""

Expand Down Expand Up @@ -331,6 +331,38 @@ def test_latexindent_one_sentence_per_line_mlep2():

This is a sentence.

\begin{table}[htbp]
\caption{\label{tab1} Here is the legend.}
\begin{tabular}{cc}
1 & 1 \\
\end{tabular}
\end{table}
"""

formatted = r"""
The names (Smith, Doe, etc.) are inherited.

Two items are used:
\begin{itemize}
\item Item 1.
\item Item 2.
\end{itemize}

The energy is defined as
\begin{equation}
E = m c^2
\end{equation}
where m is the mass.

\begin{table}[htbp]
\caption{\label{tab1} Here is the legend.}
\begin{tabular}{cc}
1 & 1 \\
\end{tabular}
\end{table}

This is a sentence.

\begin{table}[htbp]
\caption{\label{tab1} Here is the legend.}
\begin{tabular}{cc}
Expand All @@ -340,7 +372,7 @@ def test_latexindent_one_sentence_per_line_mlep2():
"""

ret = texplain.indent(text)
assert ret.strip() == text.strip()
assert ret.strip() == formatted.strip()


def test_latexindent_one_sentence_per_line_more_code_blocks():
Expand Down
3 changes: 1 addition & 2 deletions tests/test_indent_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ def test_environment_inline():
"""

formatted = r"""
This a text \begin{math} a = b
\end{math} with
This a text \begin{math} a = b \end{math} with
\begin{equation}
a = b
\end{equation}
Expand Down
47 changes: 45 additions & 2 deletions tests/test_indent_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -1276,11 +1276,54 @@ def test_code_c():

def test_format_math_inline():
text = r"""
Some text $a = b$ with $c=d$ some more text and \(e=f\).
Some text $a = b$ with $c=d$ some more text and \(e=f\) to \begin{math}g=h\end{math}.
"""

formatted = r"""
Some text $a = b$ with $c=d$ some more text and \(e=f\).
Some text $a = b$ with $c = d$ some more text and \(e = f\) to \begin{math} g = h \end{math}.
"""

ret = texplain.indent(text)
assert ret.strip() == formatted.strip()


def test_format_math_equation():
text = r"""
\begin{equation} a= b \end{equation}
and
\begin{equation}
c=d \label{eq:foo}
\end{equation}
and
\begin{equation}
e=f
\label{eq:foo}
\end{equation}
and
\begin{equation}
\label{eq:foo}
g=h
\end{equation}
"""

formatted = r"""
\begin{equation}
a = b
\end{equation}
and
\begin{equation}
c = d \label{eq:foo}
\end{equation}
and
\begin{equation}
e = f
\label{eq:foo}
\end{equation}
and
\begin{equation}
\label{eq:foo}
g = h
\end{equation}
"""

ret = texplain.indent(text)
Expand Down
94 changes: 93 additions & 1 deletion texplain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,9 @@ def _detail_text_to_placholders(
skip = False
if re.match(r"(?<!\\)(\\begin{)", line):
skip = True
if re.match(r"(?<!\\)(\\end{)", line):
elif re.match(r"(?<!\\)(\\end{)", line):
skip = True
elif re.match(r"(\-%s\-)(\w*\-)*(\d+\-)" % base, line): # existing placeholder
skip = True
n = len(line)
if not skip:
Expand Down Expand Up @@ -1374,6 +1376,83 @@ def _detail_indent_custom(text, texindent, noindent) -> tuple[str, list[Placehol
return text, placeholders_noindent + placeholders_texindent


def formatter_math(text: str) -> str:
r"""
Format a math string.

* Add spaces around arithmetic, e.g.:

- ``a+b`` becomes ``a + b``
- ``a\leq b`` becomes ``a \leq b``

* Remove spaces around signs, e.g.:

- ``a^{- b}`` becomes ``a^{-b}``

:param text: Math string. Assumed one line in math mode (without e.g. ``$``).
:return: Formatted math string.
"""
assert len(text.splitlines()) == 1
text = text.strip()

# add spaces around arithmetic operators (simple)
text = re.sub(r"([a-zA-Z0-9\(\)\{\}])([=\+\*\-\/\<\>])", r"\1 \2", text)
text = re.sub(r"([=\+\*\-\/\<\>])([a-zA-Z0-9\\])", r"\1 \2", text)

# add spaces around arithmetic operators (\leq, \sim, ...)
for operator in [
"leq",
"geq",
"sim",
"simeq",
"approx",
"equiv",
"neq",
"rightarrow",
"leftarrow",
"uparrow",
"downarrow",
"leftrightarrow",
"updownarrow",
"leftrightarrows",
"updownarrows",
]:
text = re.sub(r"([a-zA-Z0-9\(\)\{\}])(\\%s)(\s)(\s*)" % operator, r"\1 \2\3", text)
text = re.sub(r"(\\%s)(\s)(\s*)([a-zA-Z0-9\\])" % operator, r"\1\2\4", text)

# add spaces around arithmetic operators (:=)
for operator in [":="]:
text = re.sub(r"([a-zA-Z0-9\(\)\{\}])(%s)(\s)(\s*)" % operator, r"\1 \2\3", text)
text = re.sub(r"(%s)(\s)(\s*)([a-zA-Z0-9\\])" % operator, r"\1\2\4", text)

# remove spaces for signs
text = re.sub(r"([\{\(])(\s*)([\\\+\-])(\s*)", r"\1\3", text)
text = re.sub(r"^(\s*)([\+\-])(\s*)(.*)", r"\2\4", text)
text = re.sub(r"(\=)(\s*)([\+\-])(\s*)([a-zA-Z0-9])", r"\1 \3\5", text)

return text


def _formatter_inline_math(text: str) -> str:
r"""
Apply :py:func:`formatter_math` to a string that includes inline math commands.
For example ``\(a+b\)`` applies ``"\(" + formatter_math("a+b") + "\)"``.

:param text: Math string, including inline math commands.
:return: Formatted math string, including inline math commands.
"""

if text[:2] == "$$" and text[-2:] == "$$":
return "$$" + formatter_math(text[2:-2]) + "$$"
if text[0] == "$" and text[-1] == "$":
return "$" + formatter_math(text[1:-1]) + "$"
if text[:2] == r"\(" and text[-2:] == r"\)":
return r"\(" + formatter_math(text[2:-2]) + r"\)"
if text[:12] == r"\begin{math}" and text[-10:] == r"\end{math}":
return r"\begin{math} " + formatter_math(text[12:-10]) + r" \end{math}"
return text


def _detail_indent_comments(text, lstrip) -> tuple[str, list[Placeholder]]:
"""
Format comments.
Expand Down Expand Up @@ -1415,6 +1494,7 @@ def indent(
alignment: bool = True,
texindent: bool = True,
noindent: bool = True,
format_math: bool = True,
) -> str:
r"""
Indent text.
Expand Down Expand Up @@ -1523,6 +1603,7 @@ def indent(

is not formatted.

format_math: Format math, see :py:func:`formatter_math`.
:return: The indented text.
"""

Expand Down Expand Up @@ -1564,6 +1645,10 @@ def indent(
placeholder.content = re.sub(r"(\ +)", r" ", placeholder.content)
placeholder.space_front = None
placeholder.space_back = None
# format inline math
if format_math:
for placeholder in placeholders["inline_math"]:
placeholder.content = _formatter_inline_math(placeholder.content)

# put ``\begin{...}``/ ``\end{...}`` and ``\[`` / ``\]`` on a newline
if environment:
Expand All @@ -1577,6 +1662,13 @@ def indent(
if linebreak:
text = re.sub(r"(?<!\\)(\\\\)(\ *\n?)", r"\1\n", text)

# format display math
if format_math:
text, placeholders["math_line"] = text_to_placeholders(text, [PlaceholderType.math_line])
for placeholder in placeholders["math_line"]:
placeholder.content = formatter_math(placeholder.content)
text = text_from_placeholders(text, placeholders.pop("math_line"))

# \item starts on a new line
# (any white line before \item is preserved)
if itemize:
Expand Down
Loading