Skip to content
1 change: 1 addition & 0 deletions monty/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class Emojis:

confirmation = "\u2705"
decline = "\u274c"
no_choice_light = "\u25fb\ufe0f"

x = "\U0001f1fd"
o = "\U0001f1f4"
Expand Down
17 changes: 14 additions & 3 deletions monty/exts/info/github_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,18 @@ async def fetch_data(
await self.request_cache.set(cache_key, (None, body), timeout=timedelta(minutes=30).total_seconds())
return body

def render_github_markdown(self, body: str, *, context: RenderContext = None, limit: int = 700) -> str:
def render_github_markdown(self, body: str, *, context: RenderContext = None, limit: int = 2700) -> str:
"""Render GitHub Flavored Markdown to Discord flavoured markdown."""
markdown = mistune.create_markdown(escape=False, renderer=DiscordRenderer())
url_prefix = context and context.html_url
markdown = mistune.create_markdown(
escape=False,
renderer=DiscordRenderer(repo=url_prefix),
plugins=[
"strikethrough",
"task_lists",
"url",
],
)
body = markdown(body) or ""

if len(body) > limit:
Expand Down Expand Up @@ -654,7 +663,9 @@ def format_embed_expanded_issue(
body: Optional[str] = json_data["body"]
if body and not body.isspace():
# escape wack stuff from the markdown
embed.description = self.render_github_markdown(body, context=None)
embed.description = self.render_github_markdown(
body, context=RenderContext(user=issue.organisation, repo=issue.repository)
)
if not body or body.isspace():
embed.description = "*No description provided.*"
return embed
Expand Down
51 changes: 39 additions & 12 deletions monty/utils/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
from bs4.element import PageElement, Tag
from markdownify import MarkdownConverter

from monty import constants


__all__ = (
"remove_codeblocks",
"DocMarkdownConverter",
"DiscordRenderer",
)
# taken from version 0.6.1 of markdownify
WHITESPACE_RE = re.compile(r"[\r\n\s\t ]+")

Expand All @@ -16,6 +23,8 @@
re.DOTALL | re.MULTILINE,
)

GH_ISSUE_RE = re.compile(r"(?:GH-|#)(\d+)")


def remove_codeblocks(content: str) -> str:
"""Remove any codeblock in a message."""
Expand Down Expand Up @@ -96,8 +105,18 @@ def convert_hr(self, el: PageElement, text: str, convert_as_inline: bool) -> str
class DiscordRenderer(mistune.renderers.BaseRenderer):
"""Custom renderer for markdown to discord compatiable markdown."""

def __init__(self, repo: str = None):
self._repo = (repo or "").rstrip("/")

def text(self, text: str) -> str:
"""No op."""
"""Replace GitHub links with their expanded versions."""
if self._repo:
# todo: expand this to all different varieties of automatic links
# if a repository is provided we replace all snippets with the correct thing
def replacement(match: re.Match[str]) -> str:
return self.link(self._repo + "/issues/" + match[1], text=match[0])

return GH_ISSUE_RE.sub(replacement, text)
return text

def link(self, link: str, text: Optional[str] = None, title: Optional[str] = None) -> str:
Expand All @@ -113,9 +132,9 @@ def link(self, link: str, text: Optional[str] = None, title: Optional[str] = Non
else:
return link

def image(self, src: str, alt: str = "", title: str = None) -> str:
def image(self, src: str, alt: str = None, title: str = None) -> str:
"""Return a link to the provided image."""
return self.link(src, text="image", title=title)
return self.link(src, text="!image", title=alt)

def emphasis(self, text: str) -> str:
"""Return italiced text."""
Expand All @@ -128,9 +147,9 @@ def strong(self, text: str) -> str:
def heading(self, text: str, level: int) -> str:
"""Format the heading to be bold if its large enough. Otherwise underline it."""
if level in (1, 2, 3):
return f"**{text}**\n"
return "\n" f"**{text}**\n"
else:
return f"__{text}__\n"
return "\n" f"__{text}__\n"

def newline(self) -> str:
"""Return a new line."""
Expand Down Expand Up @@ -161,12 +180,12 @@ def block_code(self, code: str, info: str = None) -> str:
lang = info.split(None, 1)[0]
md += lang
md += "\n"
return md + code.replace("`" * 3, "`\u200b" * 3) + "\n```"
return md + code.replace("`" * 3, "`\u200b" * 3) + "\n```\n"

def block_quote(self, text: str) -> str:
"""Quote the provided text."""
if text:
return "> " + "> ".join(text) + "\n"
return "> " + "> ".join(text.rstrip().splitlines(keepends=True)) + "\n"
return ""

def block_html(self, html: str) -> str:
Expand All @@ -184,15 +203,23 @@ def codespan(self, text: str) -> str:

def paragraph(self, text: str) -> str:
"""Return a paragraph with a newline postceeding."""
return text + "\n"
return f"{text}\n\n"

def list(self, text: str, ordered: bool, level: int, start: Any = None) -> str:
"""Do nothing when encountering a list."""
return ""
"""Return the unedited list."""
# todo: figure out how this should actually work
if level != 1:
return text
return text.lstrip("\n") + "\n\n"

def list_item(self, text: Any, level: int) -> str:
"""Do nothing when encountering a list."""
return ""
"""Show the list, indented to its proper level."""
return "\n" + "\u200b " * (level - 1) * 8 + f"- {text}"

def task_list_item(self, text: Any, level: int, checked: bool = False, **attrs) -> str:
"""Convert task list options to emoji."""
emoji = constants.Emojis.confirmation if checked else constants.Emojis.no_choice_light
return self.list_item(emoji + " " + text, level=level)

def finalize(self, data: Any) -> str:
"""Finalize the data."""
Expand Down