Repository: python-discord/bot

Issue:
Allow text to be inserted before and after eval codeblocks
## Rationale

During an eval demonstration, it'd be nice to be able to insert text before and after the codeblock to comment it, instead of putting actual comments in the code, or sending two messages.

## Specifications

Here is the current regex used to extract the code of a message is this one : 

https://github.com/python-discord/bot/blob/b27c286f2ce648d021eed0c8e07476066b86dd98/bot/cogs/snekbox.py#L22-L32

We could simply insert a `.*` before and after to make it work, as in [this example](https://regex101.com/r/Xac4It/1)


Discussion:
Sounds good to me. I don't see a reason to not support this. I'm not worried about cases where people have code that contains back ticks.

I think it's best for the end back tick to be a lazy search rather than a greedy search (if it isn't already). This will ensure it always matches the first code block.
This would be a great update. I definitely support this.
@Akarys42 Sounds nice
I wonder though if we'd like to take a step further and allow interleaving code blocks and text. This would allow you to do something such as:
___
!e
This is how you assign:
\```py
x = 1
\```
And then you can print it:
\```py
print(x)
\```
___
By concatenating the code blocks and then running it as usual. This should still be a fairly simple change, and it would include your idea as a private case.
If that's good then I wouldn't mind writing it.
That's a good idea, yeah.

Resolving patch:
diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py
--- a/bot/exts/utils/snekbox.py
+++ b/bot/exts/utils/snekbox.py
@@ -21,14 +21,12 @@
 
 ESCAPE_REGEX = re.compile("[`\u202E\u200B]{3,}")
 FORMATTED_CODE_REGEX = re.compile(
-    r"^\s*"                                 # any leading whitespace from the beginning of the string
     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
-    r"\s*$",                                # any trailing whitespace until the end of the string
+    r"(?P=delim)",                          # match the exact same delimiter from the start again
     re.DOTALL | re.IGNORECASE               # "." also matches newlines, case insensitive
 )
 RAW_CODE_REGEX = re.compile(
@@ -76,23 +74,32 @@ async def upload_output(self, output: str) -> Optional[str]:
 
     @staticmethod
     def prepare_input(code: str) -> str:
-        """Extract code from the Markdown, format it, and insert it into the code template."""
-        match = FORMATTED_CODE_REGEX.fullmatch(code)
-        if match:
-            code, block, lang, delim = match.group("code", "block", "lang", "delim")
-            code = textwrap.dedent(code)
-            if block:
-                info = (f"'{lang}' highlighted" if lang else "plain") + " code block"
+        """
+        Extract code from the Markdown, format it, and insert it into the code template.
+
+        If there is any code block, ignore text outside the code block.
+        Use the first code block, but prefer a fenced code block.
+        If there are several fenced code blocks, concatenate only the fenced code blocks.
+        """
+        if match := list(FORMATTED_CODE_REGEX.finditer(code)):
+            blocks = [block for block in match if block.group("block")]
+
+            if len(blocks) > 1:
+                code = '\n'.join(block.group("code") for block in blocks)
+                info = "several code blocks"
             else:
-                info = f"{delim}-enclosed inline code"
-            log.trace(f"Extracted {info} for evaluation:\n{code}")
+                match = match[0] if len(blocks) == 0 else blocks[0]
+                code, block, lang, delim = match.group("code", "block", "lang", "delim")
+                if block:
+                    info = (f"'{lang}' highlighted" if lang else "plain") + " code block"
+                else:
+                    info = f"{delim}-enclosed inline code"
         else:
-            code = textwrap.dedent(RAW_CODE_REGEX.fullmatch(code).group("code"))
-            log.trace(
-                f"Eval message contains unformatted or badly formatted code, "
-                f"stripping whitespace only:\n{code}"
-            )
+            code = RAW_CODE_REGEX.fullmatch(code).group("code")
+            info = "unformatted or badly formatted code"
 
+        code = textwrap.dedent(code)
+        log.trace(f"Extracted {info} for evaluation:\n{code}")
         return code
 
     @staticmethod


Test patch:
diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py
--- a/tests/bot/exts/utils/test_snekbox.py
+++ b/tests/bot/exts/utils/test_snekbox.py
@@ -52,6 +52,13 @@ def test_prepare_input(self):
             ('`print("Hello world!")`', 'print("Hello world!")', 'one line code block'),
             ('```\nprint("Hello world!")```', 'print("Hello world!")', 'multiline code block'),
             ('```py\nprint("Hello world!")```', 'print("Hello world!")', 'multiline python code block'),
+            ('text```print("Hello world!")```text', 'print("Hello world!")', 'code block surrounded by text'),
+            ('```print("Hello world!")```\ntext\n```py\nprint("Hello world!")```',
+             'print("Hello world!")\nprint("Hello world!")', 'two code blocks with text in-between'),
+            ('`print("Hello world!")`\ntext\n```print("How\'s it going?")```',
+             'print("How\'s it going?")', 'code block preceded by inline code'),
+            ('`print("Hello world!")`\ntext\n`print("Hello world!")`',
+             'print("Hello world!")', 'one inline code block of two')
         )
         for case, expected, testname in cases:
             with self.subTest(msg=f'Extract code from {testname}.'):