diff --git a/README.md b/README.md
index dbaabc7..3372d3f 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ for LLM responses, GitHub README files, and more.
ensure
its effectiveness.
- We also support Latex Visualization(escape) and Expanded Citation.
+- Mermaid Diagrams render supported.
> [!NOTE]
> If you're interested, there's also a Node.js version of the library
@@ -40,10 +41,14 @@ pdm add telegramify-markdown
### 🤔 What you want to do?
- If you just want to send *static text* and don't want to worry about formatting,
- check: **[playground/markdownify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/markdownify_case.py)**
+ check: *
+ *[playground/markdownify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/markdownify_case.py)
+ **
- If you are developing an *LLM application* and need to send potentially **super-long text**, please
- check: **[playground/telegramify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/telegramify_case.py)**
+ check: *
+ *[playground/telegramify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/telegramify_case.py)
+ **
## 👀 Use case
@@ -139,7 +144,9 @@ print(converted)
### `telegramify_case`
-please check: **[playground/telegramify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/telegramify_case.py)**
+please check: *
+*[playground/telegramify_case.py](https://github.com/sudoskys/telegramify-markdown/blob/main/playground/telegramify_case.py)
+**
## 🔨 Supported Input
diff --git a/pdm.lock b/pdm.lock
index 0c5eee1..04dd1da 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,7 +5,7 @@
groups = ["default", "dev"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
-content_hash = "sha256:55c8b76d4a044f00f1ea8950a29219197d8671402664cd7fb27e58a9aea8a1b7"
+content_hash = "sha256:0aefcf000fd00e4fd856ff26e00c585d9a2902b872e94cd1bf5159726d8d55e0"
[[metadata.targets]]
requires_python = ">=3.8"
@@ -168,6 +168,95 @@ files = [
{file = "mistletoe-1.4.0.tar.gz", hash = "sha256:1630f906e5e4bbe66fdeb4d29d277e2ea515d642bb18a9b49b136361a9818c9d"},
]
+[[package]]
+name = "pillow"
+version = "10.4.0"
+requires_python = ">=3.8"
+summary = "Python Imaging Library (Fork)"
+groups = ["default"]
+files = [
+ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
+ {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
+ {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
+ {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
+ {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
+ {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
+ {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
+ {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
+ {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
+ {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
+ {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
+ {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
+ {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
+ {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
+ {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
+ {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
+ {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
+ {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
+ {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
+ {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
+]
+
[[package]]
name = "pytelegrambotapi"
version = "4.23.0"
diff --git a/playground/inspect_telegramify.py b/playground/inspect_telegramify.py
new file mode 100644
index 0000000..e03faed
--- /dev/null
+++ b/playground/inspect_telegramify.py
@@ -0,0 +1,40 @@
+import os
+import pathlib
+from time import sleep
+
+from dotenv import load_dotenv
+from telebot import TeleBot
+
+import telegramify_markdown
+from telegramify_markdown.customize import markdown_symbol
+from telegramify_markdown.type import ContentTypes
+
+tips = """
+telegramify_markdown.telegramify
+
+The stability of telegramify_markdown.telegramify is unproven, please keep good log records.
+
+Feel free to check it out, if you have any questions please open an issue
+"""
+
+load_dotenv()
+telegram_bot_token = os.getenv("TELEGRAM_BOT_TOKEN", None)
+chat_id = os.getenv("TELEGRAM_CHAT_ID", None)
+bot = TeleBot(telegram_bot_token)
+
+markdown_symbol.head_level_1 = "📌" # If you want, Customizing the head level 1 symbol
+markdown_symbol.link = "🔗" # If you want, Customizing the link symbol
+md = pathlib.Path(__file__).parent.joinpath("t_longtext.md").read_text(encoding="utf-8")
+boxs = telegramify_markdown.telegramify(md)
+for item in boxs:
+ print("Sent one item")
+ sleep(0.2)
+ if item.content_type == ContentTypes.TEXT:
+ print("TEXT")
+ print(item.content)
+ elif item.content_type == ContentTypes.PHOTO:
+ print("PHOTO")
+ print(item.caption)
+ elif item.content_type == ContentTypes.FILE:
+ print("FILE")
+ print(item.file_name)
diff --git a/playground/t_longtext.md b/playground/t_longtext.md
index c533cd2..b2265f1 100644
--- a/playground/t_longtext.md
+++ b/playground/t_longtext.md
@@ -15,6 +15,29 @@ The **0-1 Knapsack Problem** involves finding the optimal way to fill a knapsack
---
+**mermaid**
+
+```mermaid
+graph TD
+ A[Start] --> B{Condition}
+ B -->|是的| C[Process 1]
+ C --> D[Process 2]
+ D --> E[End]
+ B -->|No| F[Process 3]
+ F --> E
+```
+
+**wrong mermaid**
+
+```mermaid
+A[Start] --> B{Condition}
+B -->|Yes| C[Process 1]
+C --> D[Process 2]
+D --> E[End]
+B -->|No| F[Process 3]
+F --> E
+```
+
### Method 1: Recursive + Memoization (Top-Down)
```python
@@ -70,14 +93,14 @@ print(f"Maximum Value (Recursive + Memoization): {max_value}")
**Key Points:**
1. **Base Condition**:
- - If no items are left (`n == 0`) or capacity is zero (`capacity == 0`), the value is `0`.
+ - If no items are left (`n == 0`) or capacity is zero (`capacity == 0`), the value is `0`.
2. **Memoization**:
- - Results of subproblems `(n, capacity)` are stored in the `memo` dictionary to avoid redundant calculations.
+ - Results of subproblems `(n, capacity)` are stored in the `memo` dictionary to avoid redundant calculations.
3. **Recursive Decisions**:
- - If an item is too heavy, it is skipped.
- - Otherwise, choose from two options:
- - Exclude the current item.
- - Include the current item and add its value.
+ - If an item is too heavy, it is skipped.
+ - Otherwise, choose from two options:
+ - Exclude the current item.
+ - Include the current item and add its value.
4. **Time Complexity**: `O(n * W)`, where `n` is the number of items and `W` is the capacity.
5. **Space Complexity**: `O(n * W)` for memo storage.
@@ -123,15 +146,15 @@ print(f"Maximum Value (Dynamic Programming): {max_value}")
**Key Points:**
1. **State Definition**:
- - `dp[i][j]` denotes the maximum value attained with the first `i` items and a backpack capacity of `j`.
+ - `dp[i][j]` denotes the maximum value attained with the first `i` items and a backpack capacity of `j`.
2. **Transition Formula**:
- - If the item is too heavy, `dp[i][j] = dp[i-1][j]`.
- - Otherwise, calculate `dp[i][j] = max(dp[i-1][j], dp[i-1][j - weights[i-1]] + values[i-1])`.
+ - If the item is too heavy, `dp[i][j] = dp[i-1][j]`.
+ - Otherwise, calculate `dp[i][j] = max(dp[i-1][j], dp[i-1][j - weights[i-1]] + values[i-1])`.
3. **Initialization**:
- - `dp[i][0] = 0` and `dp[0][j] = 0`, meaning no items or zero capacity yields zero value.
+ - `dp[i][0] = 0` and `dp[0][j] = 0`, meaning no items or zero capacity yields zero value.
4. Complexity:
- - **Time**: `O(n * W)`.
- - **Space**: `O(n * W)`.
+ - **Time**: `O(n * W)`.
+ - **Space**: `O(n * W)`.
---
@@ -169,6 +192,7 @@ print(f"Maximum Value (Optimized Dynamic Programming): {max_value}")
```
**Advantages**:
+
- **Time Complexity**: `O(n * W)`.
- **Space Complexity**: Reduced to `O(W)`.
@@ -176,129 +200,131 @@ print(f"Maximum Value (Optimized Dynamic Programming): {max_value}")
### Summary
-| Method | Time Complexity | Space Complexity | Comment |
-|---------------------------|-----------------|------------------|----------------------------------------|
-| Recursion + Memoization | `O(n * W)` | `O(n * W)` | Conceptually simple and intuitive. |
-| DP (Bottom-Up, 2D Table) | `O(n * W)` | `O(n * W)` | Easy to implement and understand. |
-| DP (Space-Optimized) | `O(n * W)` | `O(W)` | Efficient in space usage. |
+| Method | Time Complexity | Space Complexity | Comment |
+|--------------------------|-----------------|------------------|------------------------------------|
+| Recursion + Memoization | `O(n * W)` | `O(n * W)` | Conceptually simple and intuitive. |
+| DP (Bottom-Up, 2D Table) | `O(n * W)` | `O(n * W)` | Easy to implement and understand. |
+| DP (Space-Optimized) | `O(n * W)` | `O(W)` | Efficient in space usage. |
---
### React UI Example for 0-1 Knapsack
-Here is a React component that accepts user input for weights, values, and capacity, then computes the maximum achievable value in the knapsack:
+Here is a React component that accepts user input for weights, values, and capacity, then computes the maximum
+achievable value in the knapsack:
```jsx
// KnapsackProblem.js
import React, {useState} from "react";
const KnapsackProblem = () => {
- const [weights, setWeights] = useState("");
- const [values, setValues] = useState("");
- const [capacity, setCapacity] = useState("");
- const [result, setResult] = useState(null);
-
- const solveKnapsack = (weights, values, capacity) => {
- const n = weights.length;
- const dp = Array.from({ length: n + 1 }, () =>
- Array(capacity + 1).fill(0)
- );
+ const [weights, setWeights] = useState("");
+ const [values, setValues] = useState("");
+ const [capacity, setCapacity] = useState("");
+ const [result, setResult] = useState(null);
+
+ const solveKnapsack = (weights, values, capacity) => {
+ const n = weights.length;
+ const dp = Array.from({length: n + 1}, () =>
+ Array(capacity + 1).fill(0)
+ );
+
+ for (let i = 1; i <= n; i++) {
+ for (let j = 1; j <= capacity; j++) {
+ if (weights[i - 1] > j) {
+ dp[i][j] = dp[i - 1][j];
+ } else {
+ dp[i][j] = Math.max(
+ dp[i - 1][j],
+ dp[i - 1][j - weights[i - 1]] + values[i - 1]
+ );
+ }
+ }
+ }
- for (let i = 1; i <= n; i++) {
- for (let j = 1; j <= capacity; j++) {
- if (weights[i - 1] > j) {
- dp[i][j] = dp[i - 1][j];
- } else {
- dp[i][j] = Math.max(
- dp[i - 1][j],
- dp[i - 1][j - weights[i - 1]] + values[i - 1]
- );
+ return dp[n][capacity];
+ };
+
+ const handleCalculate = () => {
+ const weightArray = weights
+ .split(",")
+ .map((w) => parseInt(w.trim(), 10));
+ const valueArray = values
+ .split(",")
+ .map((v) => parseInt(v.trim(), 10));
+ const maxCapacity = parseInt(capacity, 10);
+
+ if (weightArray.length !== valueArray.length || isNaN(maxCapacity)) {
+ alert("Invalid inputs!");
+ return;
}
- }
- }
-
- return dp[n][capacity];
- };
-
- const handleCalculate = () => {
- const weightArray = weights
- .split(",")
- .map((w) => parseInt(w.trim(), 10));
- const valueArray = values
- .split(",")
- .map((v) => parseInt(v.trim(), 10));
- const maxCapacity = parseInt(capacity, 10);
-
- if (weightArray.length !== valueArray.length || isNaN(maxCapacity)) {
- alert("Invalid inputs!");
- return;
- }
-
- const maxValue = solveKnapsack(weightArray, valueArray, maxCapacity);
- setResult(maxValue);
- };
-
- return (
-
-
0-1 Knapsack Problem
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
-
Enter weights, values, and capacity to calculate the maximum value:
-
-
-
-
-
-
-
-
-
-
- {result !== null &&
Maximum Value: {result}
}
-
- );
+
+ const maxValue = solveKnapsack(weightArray, valueArray, maxCapacity);
+ setResult(maxValue);
+ };
+
+ return (
+
+
0-1 Knapsack Problem
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
UUID: 0-1-knapsack-problem-1 00000000000000000000000000000000000000000000000000000000
+
Enter weights, values, and capacity to calculate the maximum value:
+
+
+
+
+
+
+
+
+
+
+ {result !== null &&
Maximum Value: {result}
}
+
+ );
};
export default KnapsackProblem;
```
-This React code provides a simple user interface to input weights, values, and capacity, calculates the result using a DP function, and renders the maximum value.
\ No newline at end of file
+This React code provides a simple user interface to input weights, values, and capacity, calculates the result using a
+DP function, and renders the maximum value.
\ No newline at end of file
diff --git a/playground/telegramify_case.py b/playground/telegramify_case.py
index cd2b6fd..05c0884 100644
--- a/playground/telegramify_case.py
+++ b/playground/telegramify_case.py
@@ -6,7 +6,7 @@
from telebot import TeleBot
import telegramify_markdown
-from telegramify_markdown import ContentTypes
+from telegramify_markdown.type import ContentTypes
from telegramify_markdown.customize import markdown_symbol
tips = """
@@ -29,24 +29,36 @@
for item in boxs:
print("Sent one item")
sleep(0.2)
- if item.content_type == ContentTypes.TEXT:
- print("TEXT")
- bot.send_message(
- chat_id,
- item.content,
- parse_mode="MarkdownV2"
- )
- elif item.content_type == ContentTypes.PHOTO:
- print("PHOTO")
- bot.send_photo(
- chat_id,
- (item.file_name, item.file_data),
- caption=item.caption
- )
- elif item.content_type == ContentTypes.FILE:
- print("FILE")
- bot.send_document(
- chat_id,
- (item.file_name, item.file_data),
- caption=item.caption,
- )
+ try:
+ if item.content_type == ContentTypes.TEXT:
+ print("TEXT")
+ bot.send_message(
+ chat_id,
+ item.content,
+ parse_mode="MarkdownV2"
+ )
+ elif item.content_type == ContentTypes.PHOTO:
+ print("PHOTO")
+ """
+ bot.send_sticker(
+ chat_id,
+ (item.file_name, item.file_data),
+ )
+ """
+ bot.send_photo(
+ chat_id,
+ (item.file_name, item.file_data),
+ caption=item.caption,
+ parse_mode="MarkdownV2"
+ )
+ elif item.content_type == ContentTypes.FILE:
+ print("FILE")
+ bot.send_document(
+ chat_id,
+ (item.file_name, item.file_data),
+ caption=item.caption,
+ parse_mode="MarkdownV2"
+ )
+ except Exception as e:
+ print(f"Error: {item}")
+ raise e
diff --git a/pyproject.toml b/pyproject.toml
index ea5cf5e..39cea1d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "telegramify-markdown"
-version = "0.2.1"
+version = "0.2.2"
description = "Makes it easy to send Markdown in Telegram MarkdownV2 style"
authors = [
{ name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -9,6 +9,7 @@ dependencies = [
"mistletoe==1.4.0",
"pytelegrambotapi>=4.22.0",
"docutils>=0.20.1",
+ "Pillow>=10.4.0",
]
requires-python = ">=3.8"
readme = "README.md"
diff --git a/src/telegramify_markdown/__init__.py b/src/telegramify_markdown/__init__.py
index 18717a0..72aea9d 100644
--- a/src/telegramify_markdown/__init__.py
+++ b/src/telegramify_markdown/__init__.py
@@ -1,8 +1,5 @@
-import dataclasses
import re
-from abc import ABCMeta
-from enum import Enum
-from typing import Union, List
+from typing import Union, List, Tuple, Any
import mistletoe
from mistletoe.block_token import BlockToken, ThematicBreak # noqa
@@ -10,19 +7,24 @@
from mistletoe.span_token import SpanToken # noqa
from . import customize
+from .interpreters import Text, File, Photo, BaseInterpreter, MermaidInterpreter, Interpreters
from .latex_escape.const import LATEX_SYMBOLS, NOT_MAP, LATEX_STYLES
from .latex_escape.helper import LatexToUnicodeHelper
+from .logger import logger
+from .mermaid import render_mermaid
from .mime import get_filename
from .render import TelegramMarkdownRenderer, escape_markdown
+from .type import Text, File, Photo, ContentTypes
__all__ = [
"escape_markdown",
"customize",
"markdownify",
"telegramify",
+ "BaseInterpreter",
+ "Interpreters",
"ContentTypes",
]
-
latex_escape_helper = LatexToUnicodeHelper()
@@ -94,42 +96,55 @@ def _update_block(token: BlockToken):
_update_text(token)
-class ContentTypes(Enum):
- TEXT = "text"
- FILE = "file"
- PHOTO = "photo"
-
-
-class RenderedContent(object, metaclass=ABCMeta):
- """
- The rendered content.
-
- - content: str
- - content_type: ContentTypes
- """
- content_type: ContentTypes
-
-
-@dataclasses.dataclass
-class Text(RenderedContent):
- content: str
- content_type: ContentTypes = ContentTypes.TEXT
-
-
-@dataclasses.dataclass
-class File(RenderedContent):
- file_name: str
- file_data: bytes
- caption: str = ""
- content_type: ContentTypes = ContentTypes.FILE
-
-
-@dataclasses.dataclass
-class Photo(RenderedContent):
- file_name: str
- file_data: bytes
- caption: str = ""
- content_type: ContentTypes = ContentTypes.PHOTO
+class PackHelper(object):
+ @staticmethod
+ def process_long_pack(__token1_l: list, __token2_l: list, render_func: callable):
+ """
+ Process the long pack.
+ :param __token1_l: Escaped tokens
+ :param __token2_l: Unescaped tokens
+ :param render_func: The render function
+ :return:
+ """
+ # 如果超过最大字数限制
+ if all(isinstance(_per_token1, mistletoe.block_token.CodeFence) for _per_token1 in __token1_l) and len(
+ __token1_l) == 1 and len(__token2_l) == 1:
+ # 如果这个 pack 是完全的 code block,那么采用文件形式发送。否则采用文本形式发送。
+ _escaped_code = __token1_l[0]
+ _unescaped_code_child = list(__token2_l[0].children)
+ file_content = render_func(__token2_l)
+ if _unescaped_code_child:
+ _code_text = _unescaped_code_child[0]
+ if isinstance(_code_text, mistletoe.span_token.RawText):
+ file_content = _code_text.content
+ lang = "txt"
+ if isinstance(_escaped_code, mistletoe.block_token.CodeFence):
+ lang = _escaped_code.language
+ if lang.lower() == "mermaid":
+ try:
+ image_io, caption = render_mermaid(file_content.replace("```mermaid", "").replace("```", ""))
+ return [Photo(file_name="mermaid.png", file_data=image_io.getvalue(), caption=caption)]
+ except Exception as e:
+ pass
+ file_name = get_filename(line=render_func(__token1_l), language=lang)
+ return [File(file_name=file_name, file_data=file_content.encode(), caption="")]
+ # 如果超过最大字数限制
+ return [File(file_name="letter.txt", file_data=render_func(__token2_l).encode(), caption="")]
+
+ @staticmethod
+ def process_short_pack(__token1_l, __token2_l, render_func):
+ """
+ Process the short pack.
+ :param __token1_l: Escaped tokens
+ :param __token2_l: Unescaped tokens
+ :param render_func: The render function
+ :return:
+ """
+ _processed = []
+ escaped_cell = render_func(__token1_l)
+ # 没有超过最大字数限制
+ _processed.append(Text(content=escaped_cell))
+ return _processed
def telegramify(
@@ -173,18 +188,23 @@ def telegramify(
raise ValueError("Token length mismatch")
# 对内容进行分块渲染
- def is_over_max_word_count(doc_t: list):
+ def is_over_max_word_count(doc_t: List[Tuple[Any, Any]]):
doc = mistletoe.Document(lines=[])
doc.children = [___token for ___token, ___token2 in doc_t]
return len(renderer.render(doc)) > max_word_count
- def render_block(doc_t: list):
+ def render_block(doc_t: List[Any]):
doc = mistletoe.Document(lines=[])
doc.children = doc_t.copy()
return renderer.render(doc)
+ def render_lines(lines: str):
+ doc = mistletoe.Document(lines=lines)
+ return renderer.render(doc)
+
_stack = []
_packed = []
+
# 步进推送
for _token, _token2 in zip(tokens, tokens2):
# 计算如果推送当前 Token 是否会超过最大字数限制
@@ -193,38 +213,31 @@ def render_block(doc_t: list):
_stack = [(_token, _token2)]
else:
_stack.append((_token, _token2))
- # 推送剩余的 Token
if _stack:
_packed.append(_stack)
- for pack in _packed:
- # 混拆解包
- __token1_l = list(__token1 for __token1, __token2 in pack)
- __token2_l = list(__token2 for __token1, __token2 in pack)
- escaped_cell = render_block(__token1_l)
- unescaped_cell = render_block(__token2_l)
- # 如果这个 pack 是完全的 code block,那么采用文件形式发送。否则采用文本形式发送。
- if len(escaped_cell) > max_word_count:
- if all(
- isinstance(_per_token1, mistletoe.block_token.CodeFence) for _per_token1 in __token1_l
- ) and len(__token1_l) == 1 and len(__token2_l) == 1:
- _escaped_code = __token1_l[0]
- _unescaped_code_child = list(__token2_l[0].children)
- file_content = unescaped_cell
- if _unescaped_code_child:
- _code_text = _unescaped_code_child[0]
- if isinstance(_code_text, mistletoe.span_token.RawText):
- file_content = _code_text.content
- lang = "txt"
- if isinstance(_escaped_code, mistletoe.block_token.CodeFence):
- lang = _escaped_code.language
- file_name = get_filename(line=escaped_cell, language=lang)
- _rendered.append(
- File(file_name=file_name, file_data=file_content.encode(), caption="")
- )
- else:
- _rendered.append(File(file_name="letter.txt", file_data=unescaped_cell.encode(), caption=""))
- else:
- _rendered.append(Text(content=escaped_cell))
+ _task = [("base", cell) for cell in _packed]
+ # [(base, [(token1,token2),(token1,token2)]), (base, [(token1,token2),(token1,token2)])]
+
+ interpreters_map = {interpreter.name: interpreter for interpreter in Interpreters}
+ for interpreter in Interpreters:
+ _task = interpreter.merge(_task)
+ for interpreter in Interpreters:
+ _new_task = []
+ for _per_task in _task:
+ _new_task.extend(interpreter.split(_per_task))
+ _task = _new_task
+
+ for _per_task in _task:
+ task_type, token_pairs = _per_task
+ if task_type not in interpreters_map:
+ raise ValueError("Invalid task type.")
+ interpreter = interpreters_map[task_type]
+ _rendered.extend(interpreter.render_task(
+ task=_per_task,
+ render_lines_func=render_lines,
+ render_block_func=render_block,
+ max_word_count=max_word_count
+ ))
return _rendered
diff --git a/src/telegramify_markdown/interpreters.py b/src/telegramify_markdown/interpreters.py
new file mode 100644
index 0000000..4ea6d2b
--- /dev/null
+++ b/src/telegramify_markdown/interpreters.py
@@ -0,0 +1,183 @@
+from typing import List, Any, Callable
+
+import mistletoe
+
+from telegramify_markdown.logger import logger
+from telegramify_markdown.mermaid import render_mermaid
+from telegramify_markdown.mime import get_filename
+from telegramify_markdown.type import TaskType, File, Text, Photo, SentType
+
+
+class BaseInterpreter(object):
+ name = "base"
+
+ def merge(self, tasks: List[TaskType]) -> List[TaskType]:
+ """
+ Merge the tasks.
+ :param tasks: [(base, [(token1,token2),(token1,token2)]), (base, [(token1,token2),(token1,token2)])]
+ :return:
+ """
+ return tasks
+
+ def split(self, task: TaskType) -> List[TaskType]:
+ """
+ Split the task.
+ :param task: (base, [(token1,token2),(token1,token2)])
+ :return: [(base, [(token1,token2),(token1,token2)]),....newTask]
+ """
+ return [task]
+
+ def render_task(self,
+ task: TaskType,
+ render_block_func: Callable[[List[Any]], str],
+ render_lines_func: Callable[[str], str],
+ max_word_count: int = 4090
+ ) -> SentType:
+ """
+ Render the task.
+ :param render_block_func: The render block function
+ :param render_lines_func: The render lines function
+ :param task: (base, [(token1,token2),(token1,token2)])
+ :param max_word_count: The maximum number of words in a single message.
+ :return: SentType
+ """
+ task_type, token_pairs = task
+ if task_type != "base":
+ logger.warn("Invalid task type for BaseInterpreter.")
+ token1_l = list(__token1 for __token1, __token2 in token_pairs)
+ token2_l = list(__token2 for __token1, __token2 in token_pairs)
+ # 处理超过最大字数限制的情况
+ if len(render_block_func(token1_l)) > max_word_count:
+ # 如果超过最大字数限制
+ if all(isinstance(_per_token1, mistletoe.block_token.CodeFence) for _per_token1 in token1_l) and len(
+ token1_l) == 1 and len(token2_l) == 1:
+ # 如果这个 pack 是完全的 code block,那么采用文件形式发送。否则采用文本形式发送。
+ _escaped_code = token1_l[0]
+ _unescaped_code_child = list(token1_l[0].children)
+ file_content = render_block_func(token2_l)
+ if _unescaped_code_child:
+ _code_text = _unescaped_code_child[0]
+ if isinstance(_code_text, mistletoe.span_token.RawText):
+ file_content = _code_text.content
+ lang = "txt"
+ if isinstance(_escaped_code, mistletoe.block_token.CodeFence):
+ lang = _escaped_code.language
+ """
+ if lang.lower() == "mermaid":
+ try:
+ image_io, caption = render_mermaid(file_content.replace("```mermaid", "").replace("```", ""))
+ return [Photo(file_name="mermaid.png", file_data=image_io.getvalue(), caption=caption)]
+ except Exception as e:
+ pass
+ """
+ file_name = get_filename(line=render_block_func(token1_l), language=lang)
+ return [File(file_name=file_name, file_data=file_content.encode(), caption="")]
+ # 如果超过最大字数限制
+ return [File(file_name="letter.txt", file_data=render_block_func(token2_l).encode(), caption="")]
+ # 没有超过最大字数限制
+ return [Text(content=render_block_func(token1_l))]
+
+
+class MermaidInterpreter(BaseInterpreter):
+ name = "mermaid"
+
+ def merge(self, tasks: List[TaskType]) -> List[TaskType]:
+ """
+ Merge the tasks.
+ :param tasks: [(base, [(token1,token2),(token1,token2)]), (base, [(token1,token2),(token1,token2)])]
+ :return:
+ """
+ return tasks
+
+ def split(self, task: TaskType) -> List[TaskType]:
+ """
+ Split the task.
+ :param task: (base, [(token1,token2),(token1,token2)])
+ :return: [(mermaid, [(token1,token2),(token1,token2)]),....newTask]
+ """
+ task_type, token_pairs = task
+ # 只处理 base 块
+ if task_type != "base":
+ return [task]
+ # 用于存放生成的新任务
+ tasks = []
+ # 临时缓存非 Mermaid 块
+ current_base_tokens = []
+ for token_pair in token_pairs:
+ token1, _ = token_pair
+ # 检查是否为 Mermaid 块
+ if isinstance(token1, mistletoe.block_token.CodeFence) and token1.language.lower() == "mermaid":
+ if current_base_tokens:
+ # 将缓存的非 Mermaid 块生成新的 base 任务
+ tasks.append(("base", current_base_tokens))
+ current_base_tokens = []
+ # 单独添加 Mermaid 块任务
+ tasks.append(("mermaid", [token_pair]))
+ else:
+ # 累积 base 块
+ current_base_tokens.append(token_pair)
+ # 处理剩余的 base 块
+ if current_base_tokens:
+ tasks.append(("base", current_base_tokens))
+ return tasks
+
+ def render_task(self,
+ task: TaskType,
+ render_block_func: Callable[[List[Any]], str],
+ render_lines_func: Callable[[str], str],
+ max_word_count: int = 4090
+ ) -> SentType:
+ """
+ Render the task.#
+ :param task: (base, [(token1,token2),(token1,token2)]) of [(base, [(token1,token2),(token1,token2)]), (base, [(token1,token2),(token1,token2)])]
+ :param render_block_func: The render block function
+ :param render_lines_func: The render lines function
+ :param max_word_count: The maximum number of words in a single message.
+ :return: SentType
+ """
+ task_type, token_pairs = task
+ if task_type != "mermaid":
+ raise ValueError("Invalid task type for MermaidInterpreter.")
+ # 仅处理 Mermaid 块
+ if len(token_pairs) != 1:
+ raise ValueError("Invalid token length for MermaidInterpreter.")
+ token1_l = list(__token1 for __token1, __token2 in token_pairs)
+ token2_l = list(__token2 for __token1, __token2 in token_pairs)
+ if not all(isinstance(_per_token, mistletoe.block_token.CodeFence) for _per_token in token1_l):
+ raise ValueError("Invalid token type for MermaidInterpreter.")
+ _escaped_code = token2_l[0]
+ if (isinstance(
+ _escaped_code,
+ mistletoe.block_token.CodeFence
+ ) and _escaped_code.language.lower() == "mermaid"):
+ file_content = render_block_func(token1_l)
+ _unescaped_code_child = list(_escaped_code.children)
+ if _unescaped_code_child:
+ _raw_text = _unescaped_code_child[0]
+ if isinstance(_raw_text, mistletoe.span_token.RawText):
+ file_content = _raw_text.content
+ try:
+ img_io, url = render_mermaid(file_content.replace("```mermaid", "").replace("```", ""))
+ message = f"[edit in mermaid.live]({url})"
+ except Exception as e:
+ return [
+ File(
+ file_name="mermaid_code.txt",
+ file_data=render_block_func(token2_l).encode(),
+ caption=""
+ )
+ ]
+ else:
+ return [
+ Photo(
+ file_name="mermaid.png",
+ file_data=img_io.getvalue(),
+ caption=render_lines_func(message)
+ )
+ ]
+ return [
+ File(file_name="mermaid_code.txt", file_data=render_block_func(token2_l).encode(), caption="")
+ ]
+
+
+Interpreters = [BaseInterpreter(), MermaidInterpreter()]
diff --git a/src/telegramify_markdown/logger.py b/src/telegramify_markdown/logger.py
new file mode 100644
index 0000000..cd8dda6
--- /dev/null
+++ b/src/telegramify_markdown/logger.py
@@ -0,0 +1,5 @@
+import logging
+
+# Configure the logger
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
diff --git a/src/telegramify_markdown/mermaid.py b/src/telegramify_markdown/mermaid.py
new file mode 100644
index 0000000..41d002c
--- /dev/null
+++ b/src/telegramify_markdown/mermaid.py
@@ -0,0 +1,162 @@
+import base64
+import dataclasses
+import json
+import zlib
+from functools import lru_cache
+from io import BytesIO
+from typing import Union, Tuple
+
+import requests
+from PIL import Image
+
+from telegramify_markdown.logger import logger
+
+
+@dataclasses.dataclass
+class MermaidConfig:
+ theme: str = "neutral"
+
+
+# 设置基于 URL 的缓存
+@lru_cache(maxsize=128)
+def download_image(url: str) -> BytesIO:
+ """
+ Download the image from the URL.
+ :param url: Image URL
+ :raises: requests.HTTPError, requests.ConnectionError, requests.Timeout
+ """
+ logger.debug(f"telegramify_markdown: Downloading mermaid image from {url}")
+ headers = {
+ "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
+ }
+ response = requests.get(url, headers=headers, timeout=10, stream=True)
+ try:
+ response.raise_for_status()
+ except requests.HTTPError as e:
+ logger.error(f"telegramify_markdown: HTTP Error: {e}")
+ raise ValueError("telegramify_markdown: Cant render the mermaid graph") from e
+ return BytesIO(response.content)
+
+
+def is_image(data: BytesIO) -> bool:
+ """
+ Check if the data is an image
+ :param data: BytesIO Stream
+ :return: If the data is an image, return True; otherwise, return False
+ """
+ try:
+ # 使用 Pillow 验证是否是合法图片
+ with Image.open(data) as img:
+ img.verify() # 验证图片
+ return True
+ except Exception as e:
+ logger.debug(f"telegramify_markdown: Image verification failed: {e}")
+ return False
+
+
+def compress_to_deflate(data: Union[bytes]) -> bytes:
+ """
+ Compress the data using the DEFLATE algorithm.
+ :param data: The data to compress
+ :return: The compressed data
+ """
+ compressor = zlib.compressobj(
+ level=9, # Maximum compression level
+ method=zlib.DEFLATED, # Use the DEFLATE algorithm
+ wbits=15, # Window size
+ memLevel=8, # Memory usage level
+ strategy=zlib.Z_DEFAULT_STRATEGY # Default compression strategy
+ )
+ compressed_data = compressor.compress(data)
+ compressed_data += compressor.flush()
+ return compressed_data
+
+
+def safe_base64_encode(data):
+ """
+ URL-safe base64 encoding
+ :param data: waiting for encoding
+ :return: Encoded data
+ """
+ return base64.urlsafe_b64encode(data)
+
+
+def generate_pako(graph_markdown: str, mermaid_config: MermaidConfig = None) -> str:
+ """
+ Generate the pako URL for the Mermaid graph.
+ :param graph_markdown: Input Mermaid graph markdown
+ :param mermaid_config: Mermaid configuration
+ :return: The pako URL
+ """
+ if mermaid_config is None:
+ mermaid_config = MermaidConfig()
+ graph_data = {
+ "code": graph_markdown,
+ "mermaid": mermaid_config.__dict__
+ }
+ json_bytes = json.dumps(graph_data).encode('ascii')
+ compressed_data = compress_to_deflate(json_bytes)
+ base64_encoded = safe_base64_encode(compressed_data)
+ return f"pako:{base64_encoded.decode('ascii')}"
+
+
+def b64_mermaid_url(diagram: str) -> str:
+ """
+ ***NOT USED***
+
+ Get the Mermaid Ink URL for the graph.
+ :param diagram: The Mermaid graph Markdown
+ :return: Link
+ """
+ diagram_encoded = safe_base64_encode(diagram.encode('utf8')).decode('ascii')
+ return f'https://mermaid.ink/img/{diagram_encoded}?theme=neutral&width=500&scale=2'
+
+
+def get_mermaid_live_url(graph_markdown: str) -> str:
+ """
+ Get the Mermaid Live URL for the graph.
+ Can be used to edit the graph in the browser.
+ :param graph_markdown:
+ :return:
+ """
+ return f'https://mermaid.live/edit/#{generate_pako(graph_markdown)}'
+
+
+def get_mermaid_ink_url(graph_markdown: str) -> str:
+ """
+ Get the Mermaid Ink URL for the graph.
+ Can be used to download the image.
+ :param graph_markdown: The Mermaid graph Markdown
+ :return: Link
+ """
+ return f'https://mermaid.ink/img/{generate_pako(graph_markdown)}?theme=neutral&width=500&scale=2&type=webp'
+
+
+def render_mermaid(diagram: str) -> Tuple[BytesIO, str]:
+ # render picture
+ img_url = get_mermaid_ink_url(diagram)
+ caption = get_mermaid_live_url(diagram)
+ # Download the image
+ img_data = download_image(img_url)
+ if not is_image(img_data):
+ raise ValueError("The URL does not return an image.")
+ img_data.seek(0) # Reset the file pointer to the beginning
+ return img_data, caption
+
+
+if __name__ == '__main__':
+ mermaid_md = """
+ ```
+ graph TD
+ A[Christmas] -->|Get money| B(Go shopping)
+ B --> C{Let me think}
+ C -->|One| D[Laptop]
+ C -->|Two| E[你好]
+ C -->|Three| F[fa:fa-car Car]
+ ```
+ """
+ t1 = render_mermaid(mermaid_md)
+ print(t1)
+ # 展示图片
+ Image.open(t1[0]).show()
diff --git a/src/telegramify_markdown/mime.py b/src/telegramify_markdown/mime.py
index db5620f..d189d33 100644
--- a/src/telegramify_markdown/mime.py
+++ b/src/telegramify_markdown/mime.py
@@ -1,12 +1,11 @@
# NOTE: Maybe we can use https://github.com/google/magika/ instead.
-import logging
+
import re
from pathlib import Path
from typing import Optional
-# Configure the logger
-logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
-logger = logging.getLogger(__name__)
+from telegramify_markdown.logger import logger
+
default_language_to_ext = {
"python": "py",
"javascript": "js",
diff --git a/src/telegramify_markdown/type.py b/src/telegramify_markdown/type.py
new file mode 100644
index 0000000..8274318
--- /dev/null
+++ b/src/telegramify_markdown/type.py
@@ -0,0 +1,45 @@
+import dataclasses
+from abc import ABCMeta
+from enum import Enum
+from typing import Tuple, List, Any, Union
+
+TaskType = Tuple[str, List[Tuple[Any, Any]]]
+SentType = List[Union["Text", "File", "Photo"]]
+
+
+class ContentTypes(Enum):
+ TEXT = "text"
+ FILE = "file"
+ PHOTO = "photo"
+
+
+class RenderedContent(object, metaclass=ABCMeta):
+ """
+ The rendered content.
+
+ - content: str
+ - content_type: ContentTypes
+ """
+ content_type: ContentTypes
+
+
+@dataclasses.dataclass
+class Text(RenderedContent):
+ content: str
+ content_type: ContentTypes = ContentTypes.TEXT
+
+
+@dataclasses.dataclass
+class File(RenderedContent):
+ file_name: str
+ file_data: bytes
+ caption: str = ""
+ content_type: ContentTypes = ContentTypes.FILE
+
+
+@dataclasses.dataclass
+class Photo(RenderedContent):
+ file_name: str
+ file_data: bytes
+ caption: str = ""
+ content_type: ContentTypes = ContentTypes.PHOTO