Repository: aiogram/aiogram
Pull request #1263 (2023-08-13 13:58:42)

Fixed nested hashtag, cashtag and email entnties parsing
Fixed nested hashtag, cashtag and email message entities not being parsed correctly when these entities are inside another entity.

Fixes: #1259 

github-actions[bot]: # :heavy_check_mark: Changelog found.

Thank you for adding a description of the changes
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aiogram/aiogram/pull/1263?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1263](https://app.codecov.io/gh/aiogram/aiogram/pull/1263?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (bc55038) into [dev-3.x](https://app.codecov.io/gh/aiogram/aiogram/commit/4e7c66726f2cae51c76a34388846444caebcaa1a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (4e7c667) will **decrease** coverage by `0.02%`.
> Report is 7 commits behind head on dev-3.x.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://app.codecov.io/gh/aiogram/aiogram/pull/1263/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://app.codecov.io/gh/aiogram/aiogram/pull/1263?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@             Coverage Diff             @@
##           dev-3.x    #1263      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files          364      364              
  Lines         9309     9309              
===========================================
- Hits          9309     9308       -1     
- Misses           0        1       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.98% <ø> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/aiogram/aiogram/pull/1263?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/utils/text\_decorations.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1263?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS91dGlscy90ZXh0X2RlY29yYXRpb25zLnB5) | `99.00% <ø> (-1.00%)` | :arrow_down: |


Patch:
diff --git a/CHANGES/1259.bugfix.rst b/CHANGES/1259.bugfix.rst
new file mode 100644
index 00000000..e070549d
--- /dev/null
+++ b/CHANGES/1259.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed nested hashtag, cashtag and email message entities not being parsed correctly when these entities are inside another entity.
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
index 97430a32..eaff6f2c 100644
--- a/aiogram/utils/text_decorations.py
+++ b/aiogram/utils/text_decorations.py
@@ -43,8 +43,11 @@ class TextDecoration(ABC):
             MessageEntityType.URL,
             MessageEntityType.MENTION,
             MessageEntityType.PHONE_NUMBER,
+            MessageEntityType.HASHTAG,
+            MessageEntityType.CASHTAG,
+            MessageEntityType.EMAIL,
         }:
-            # This entities should not be changed
+            # These entities should not be changed
             return text
         if entity.type in {
             MessageEntityType.BOLD,
@@ -71,6 +74,8 @@ class TextDecoration(ABC):
         if entity.type == MessageEntityType.CUSTOM_EMOJI:
             return self.custom_emoji(value=text, custom_emoji_id=cast(str, entity.custom_emoji_id))
 
+        # This case is not possible because of `if` above, but if any new entity is added to
+        # API it will be here too
         return self.quote(text)
 
     def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:


Test patch:
diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py
index 056bd1cb..25f222f0 100644
--- a/tests/test_utils/test_text_decorations.py
+++ b/tests/test_utils/test_text_decorations.py
@@ -113,6 +113,14 @@ class TestTextDecoration:
     ):
         assert decorator.apply_entity(entity, "test") == result
 
+    def test_unknown_apply_entity(self):
+        assert (
+            html_decoration.apply_entity(
+                MessageEntity(type="unknown", offset=0, length=5), "<test>"
+            )
+            == "&lt;test&gt;"
+        )
+
     @pytest.mark.parametrize(
         "decorator,before,after",
         [
@@ -243,6 +251,33 @@ class TestTextDecoration:
                 [MessageEntity(type="bold", offset=0, length=8, url=None, user=None)],
                 "<b>👋🏾 Hi!</b>",
             ],
+            [
+                html_decoration,
+                "#test",
+                [
+                    MessageEntity(type="hashtag", offset=0, length=5),
+                    MessageEntity(type="bold", offset=0, length=5),
+                ],
+                "<b>#test</b>",
+            ],
+            [
+                html_decoration,
+                "$TEST",
+                [
+                    MessageEntity(type="cashtag", offset=0, length=5),
+                    MessageEntity(type="bold", offset=0, length=5),
+                ],
+                "<b>$TEST</b>",
+            ],
+            [
+                html_decoration,
+                "test@example.com",
+                [
+                    MessageEntity(type="email", offset=0, length=16),
+                    MessageEntity(type="bold", offset=0, length=16),
+                ],
+                "<b>test@example.com</b>",
+            ],
         ],
     )
     def test_unparse(