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