Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Created Modals for Latex command by JefeThePug · Pull Request #179 · practical-python-org/Eos · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All@@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
try:
if not self.session:
return False
async with self.session.get(image_path) as response:
return response.status == 200
except aiohttp.ClientError:
logger.exception("Failed to fetch LaTeX image from CodeCogs")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with CodeCogs")
return False

def normalize_color(self, value: str, default: str) -> str:
normalized = value.strip().replace(" ", "").lower()
return LATEX_COLORS.get(normalized, default)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–500).
"""
logger.info(
"%s used the %s command.",
interaction.user.display_name,
interaction.command,
)

# check for valid user specified color for text and background
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# Check for valid user specified color for text and background.
bg_color = self.normalize_color(bg_color, "Black")
txt_color = self.normalize_color(txt_color, "White")

if txt_color == bg_color:
txt_color = "White" if bg_color == "Black" else "Black"

# handle necessary character replacements
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All@@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

for e, c in character_replacements.items():
equation = equation.replace(e, c)

# prevent user specified dpi from being too big/small
dpi = max(200, min(dpi, 500))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All@@ -141,18 +133,52 @@ async def latex(
rf"\colorbox{{{bg_color}}}{{${equation}$}}"
)

if await self.latex_available(image_path):
embed = embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)
# Send the embed with the image
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message(
f"Failed to fetch the image ({image_path}) from the API", ephemeral=True
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@app_commands.command()
async def latex(
self,
interaction: discord.Interaction,
equation: str,
*,
bg_color: str = "Black",
txt_color: str = "White",
dpi: int = 300,
) -> None:
"""
Send a mathematical equation using LaTeX.

Parameters
----------
equation : LaTeX equation (do not include `$` delimiters).
bg_color : Background color. See `/latex_colors` for available colors.
txt_color : Text color. See `/latex_colors` for available colors.
dpi : Image resolution in DPI (200–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file addedsrc/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading