-
Notifications
You must be signed in to change notification settings - Fork 7
Feature/prompt user to paste code properly 35 - Take 2 #137
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import enum | ||
|
||
from codeblocks.utils import black_string | ||
|
||
|
||
class Language(enum.Enum): | ||
PYTHON = "Python" | ||
HTML = "HTML" | ||
JAVASCRIPT = "JavaScript" | ||
NONE = "" | ||
|
||
@classmethod | ||
def choices(cls): | ||
return [lang.value for lang in cls if lang.value] | ||
|
||
def get_markdown_name(self): | ||
_mapping = {"Python": "py", "HTML": "html", "JavaScript": "js"} | ||
return _mapping.get(self.value) | ||
|
||
|
||
_EXAMPLE_CODE = black_string("def add_one(foo: int) -> int: return foo + 1") | ||
_ESCAPED_CODEBLOCK = f"\`\`\`python\n{_EXAMPLE_CODE}\n\`\`\`" # noqa: W605 | ||
|
||
|
||
def get_escaped_codeblock(): | ||
return _ESCAPED_CODEBLOCK | ||
|
||
|
||
def get_example_code(): | ||
return _EXAMPLE_CODE | ||
|
||
|
||
LANGUAGE_GUESS_TRESHOLD = 0.5 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import abc | ||
import ast | ||
import logging | ||
from typing import Optional, Union | ||
|
||
import bs4 | ||
import guesslang | ||
import hikari | ||
from codeblocks.constants import LANGUAGE_GUESS_TRESHOLD, Language | ||
from codeblocks.types import Codeblock, MarkdownCodeblock | ||
from codeblocks.utils import black_string | ||
|
||
logger = logging.getLogger(__name__) | ||
_LOG_PREFIX = "[CODEBLOCK-PARSER]" | ||
|
||
|
||
class CodeblockParser(abc.ABC): | ||
def __init__(self, message: hikari.Message) -> None: | ||
self.codeblock = Codeblock.from_message(message=message) | ||
|
||
def validate_and_format(self): | ||
return MarkdownCodeblock.set_content( | ||
content=self.format_code(self.codeblock.content), | ||
language=self.validate_and_get_language(), | ||
) | ||
|
||
@abc.abstractmethod | ||
def validate_and_get_language(self): | ||
pass | ||
|
||
@abc.abstractmethod | ||
def format_code(self, content: str) -> str: | ||
pass | ||
|
||
|
||
class PythonParser(CodeblockParser): | ||
def validate_and_get_language(self): | ||
if self.is_valid_python(self.codeblock.content): | ||
return Language.PYTHON | ||
|
||
def format_code(self, content: str) -> str: | ||
return black_string(content) | ||
|
||
@classmethod | ||
def is_valid_python(cls, content: str) -> bool: | ||
logger.info(f"{_LOG_PREFIX} Checking if message is valid python code.") | ||
try: | ||
content = content.replace("\x00", "") | ||
tree = ast.parse(content) | ||
except SyntaxError: | ||
return False | ||
return not all(isinstance(node, ast.Expr) for node in tree.body) | ||
|
||
|
||
class HTMLParser(CodeblockParser): | ||
def validate_and_get_language(self): | ||
logger.info(f"{_LOG_PREFIX} Checking if message is valid html code.") | ||
if self.is_valid_html(content=self.codeblock.content): | ||
return Language.HTML | ||
|
||
@classmethod | ||
def is_valid_html(cls, content: str) -> bool: | ||
logger.info(f"{_LOG_PREFIX} Checking if message is valid html code.") | ||
return bool(bs4.BeautifulSoup(content, "html.parser").find()) | ||
|
||
def format_code(self, content: str) -> str: | ||
return content | ||
|
||
|
||
class JavascriptParser(CodeblockParser): | ||
def validate_and_get_language(self): | ||
if self.is_javascript_code(content=self.codeblock.content): | ||
return Language.JAVASCRIPT | ||
|
||
def format_code(self, content: str) -> str: | ||
return content | ||
|
||
@classmethod | ||
def is_javascript_code(cls, content: str) -> Optional[bool]: | ||
logger.info(f"{_LOG_PREFIX} Checking if message is valid javascript code.") | ||
guess = guesslang.Guess() | ||
if ( | ||
dict(guess.probabilities(content))[Language.JAVASCRIPT.value] | ||
> LANGUAGE_GUESS_TRESHOLD | ||
): | ||
return guess.language_name(content) in Language.choices() | ||
return None | ||
|
||
|
||
def get_parser( | ||
message: hikari.Message, | ||
) -> Optional[Union[HTMLParser, PythonParser, JavascriptParser]]: | ||
if message.content is not None: | ||
if PythonParser.is_valid_python(content=message.content): | ||
logger.info(f"{_LOG_PREFIX} Message is unformatted python code.") | ||
return PythonParser(message=message) | ||
elif HTMLParser.is_valid_html(content=message.content): | ||
logger.info(f"{_LOG_PREFIX} Message is unformatted html code.") | ||
return HTMLParser(message=message) | ||
elif JavascriptParser.is_javascript_code(content=message.content): | ||
logger.info(f"{_LOG_PREFIX} Message is unformatted javascript code.") | ||
return JavascriptParser(message=message) | ||
logger.info(f"{_LOG_PREFIX} Message is not valid code, no action necessary.") | ||
return None | ||
|
||
|
||
def should_parse_message(content: str) -> bool: | ||
return not (_is_codeblock(content=content) or _is_inline_block(content=content)) | ||
|
||
|
||
def _is_codeblock(content: str): | ||
return content.startswith("```") and content.endswith("```") | ||
|
||
|
||
def _is_inline_block(content: str): | ||
return content.startswith("`") and content.endswith("`") |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,41 @@ | ||||||
from __future__ import annotations | ||||||
|
||||||
from typing import NamedTuple, Optional | ||||||
|
||||||
import hikari | ||||||
from codeblocks.constants import Language | ||||||
from markup import services as markdown_services | ||||||
|
||||||
|
||||||
class Codeblock( | ||||||
NamedTuple( | ||||||
"Codeblock", | ||||||
[ | ||||||
("content", Optional[str]), | ||||||
("length", int), | ||||||
], | ||||||
) | ||||||
): | ||||||
@classmethod | ||||||
def from_message(cls, message: hikari.Message) -> Codeblock: | ||||||
return cls( | ||||||
content=message.content, | ||||||
length=len(message.content.split("\n")) | ||||||
if message.content is not None | ||||||
else 0, | ||||||
) | ||||||
|
||||||
|
||||||
class MarkdownCodeblock( | ||||||
NamedTuple( | ||||||
"MarkdownCodeblock", | ||||||
[ | ||||||
("content", Optional[str]), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
], | ||||||
) | ||||||
): | ||||||
@classmethod | ||||||
def set_content(cls, content: str, language: Language) -> MarkdownCodeblock: | ||||||
return cls( | ||||||
content=markdown_services.codeblock(text=content, language=language), | ||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import black | ||
|
||
|
||
def black_string(content): | ||
return black.format_str( | ||
src_contents=content, | ||
mode=black.Mode( | ||
target_versions={black.TargetVersion.PY36}, | ||
line_length=80, | ||
string_normalization=False, | ||
is_pyi=False, | ||
), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__all__ = ["codeblock"] | ||
|
||
from . import codeblock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import crescent | ||
import hikari | ||
from codeblocks.parsers import get_parser, should_parse_message | ||
|
||
plugin = crescent.Plugin("codeblocks") | ||
|
||
|
||
@plugin.include | ||
@crescent.event | ||
async def on_message_format(event: hikari.MessageCreateEvent): | ||
if event.message.author.is_bot or event.message.content is None: | ||
return | ||
if not should_parse_message(event.message.content): | ||
return | ||
|
||
codeblock = get_parser(message=event.message) | ||
if codeblock is not None: | ||
await event.message.respond( | ||
"Please use the slash command `/markdown` for more information on how to properly format your code.", | ||
reply=True, | ||
mentions_reply=True, | ||
) | ||
await event.message.respond( | ||
codeblock.validate_and_format().content, | ||
reply=True, | ||
mentions_reply=True, | ||
) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,23 @@ | ||||||
from typing import Optional | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't much like the name of this file, can it be utils? Or whatever is more appropriate. |
||||||
|
||||||
from codeblocks.constants import Language | ||||||
|
||||||
|
||||||
def bold(text: str) -> str: | ||||||
return f"**{text}**" | ||||||
|
||||||
|
||||||
def italics(text: str) -> str: | ||||||
return f"*{text}*" | ||||||
|
||||||
|
||||||
def inline_code(text: str) -> str: | ||||||
return f"`{text}`" | ||||||
|
||||||
|
||||||
def codeblock(text: str, language: Language) -> Optional[str]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
try: | ||||||
return f"```{language.get_markdown_name()}\n{text}\n```" | ||||||
except AttributeError: | ||||||
pass | ||||||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're on 3.10 (which reminds me, should probably upgrade to 3.11), so we can do:
Additionally... Couldn't this be: