1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
import asyncio
import re
from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
import discord
import matplotlib.pyplot as plt
from discord.ext import commands
# configure fonts and colors for matplotlib
plt.rcParams.update(
{
"font.size": 16,
"mathtext.fontset": "cm", # Computer Modern font set
"mathtext.rm": "serif",
"figure.facecolor": "38383F", # matches Discord's dark mode background color
"text.color": "white",
}
)
FORMATTED_CODE_REGEX = re.compile(
r"(?P<delim>(?P<block>```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block
r"(?(block)(?:(?P<lang>[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline)
r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code
r"(?P<code>.*?)" # extract all code inside the markup
r"\s*" # any more whitespace before the end of the code markup
r"(?P=delim)", # match the exact same delimiter from the start again
re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive
)
class Latex(commands.Cog):
"""Renders latex."""
@staticmethod
def _render(text: str) -> BytesIO:
"""Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception."""
fig = plt.figure()
try:
fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top")
rendered_image = BytesIO()
plt.savefig(rendered_image, bbox_inches="tight", dpi=600)
rendered_image.seek(0)
return rendered_image
except ValueError as e:
raise commands.BadArgument(str(e))
@staticmethod
def _prepare_input(text: str) -> str:
text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\
if match := FORMATTED_CODE_REGEX.match(text):
return match.group("code")
else:
return text
@commands.command()
async def latex(self, ctx: commands.Context, *, text: str) -> None:
"""Renders the text in latex and sends the image."""
text = self._prepare_input(text)
async with ctx.typing():
with ThreadPoolExecutor() as pool:
image = await asyncio.get_running_loop().run_in_executor(
pool, self._render, text
)
await ctx.send(file=discord.File(image, "latex.png"))
def setup(bot: commands.Bot) -> None:
"""Load the Latex Cog."""
bot.add_cog(Latex(bot))
|