tg-rich-render 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tg_rich_render-0.1.0/LICENSE +21 -0
- tg_rich_render-0.1.0/PKG-INFO +197 -0
- tg_rich_render-0.1.0/README.md +172 -0
- tg_rich_render-0.1.0/pyproject.toml +40 -0
- tg_rich_render-0.1.0/setup.cfg +4 -0
- tg_rich_render-0.1.0/tests/test_adapter.py +29 -0
- tg_rich_render-0.1.0/tests/test_cjk.py +31 -0
- tg_rich_render-0.1.0/tests/test_parser.py +41 -0
- tg_rich_render-0.1.0/tests/test_table.py +51 -0
- tg_rich_render-0.1.0/tg_rich_render/__init__.py +22 -0
- tg_rich_render-0.1.0/tg_rich_render/adapter.py +50 -0
- tg_rich_render-0.1.0/tg_rich_render/cjk.py +51 -0
- tg_rich_render-0.1.0/tg_rich_render/cli.py +44 -0
- tg_rich_render-0.1.0/tg_rich_render/parser.py +115 -0
- tg_rich_render-0.1.0/tg_rich_render/table.py +165 -0
- tg_rich_render-0.1.0/tg_rich_render.egg-info/PKG-INFO +197 -0
- tg_rich_render-0.1.0/tg_rich_render.egg-info/SOURCES.txt +18 -0
- tg_rich_render-0.1.0/tg_rich_render.egg-info/dependency_links.txt +1 -0
- tg_rich_render-0.1.0/tg_rich_render.egg-info/entry_points.txt +2 -0
- tg_rich_render-0.1.0/tg_rich_render.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shali10
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tg-rich-render
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Telegram rich table and format converter with zero-dependency CJK alignment.
|
|
5
|
+
Author-email: shali10 <99240403+shali10@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shali10/tg-rich-render
|
|
8
|
+
Project-URL: Repository, https://github.com/shali10/tg-rich-render
|
|
9
|
+
Project-URL: Issues, https://github.com/shali10/tg-rich-render/issues
|
|
10
|
+
Keywords: telegram,aiogram,telegram-bot,table,markdown,cjk,rich-formatting
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# tg-rich-render 📊✨
|
|
27
|
+
|
|
28
|
+
> **Zero-dependency, CJK-aware Markdown table & rich format converter for Telegram bots.**
|
|
29
|
+
> 专治 Telegram 机器人表格排版错位、中英混排对不齐、手机端横向溢出变难看代码块的痛点。
|
|
30
|
+
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](https://github.com/shali10/tg-rich-render/actions)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 💡 为什么需要 tg-rich-render?
|
|
38
|
+
|
|
39
|
+
Telegram 官方的 MarkdownV2 并不支持标准的 GFM Pipe Table(管道表格)。在日常 Telegram 机器人开发(运维巡检播报、资产统计、行情早报)中,直接发送表格通常会遇到以下问题:
|
|
40
|
+
|
|
41
|
+
1. **直接报错**:Telegram Bot API 无法解析 `| col | col |` 语法;
|
|
42
|
+
2. **粗暴丢进代码块**:直接用 ```` 包裹,但在中英汉字、Emoji 混排时由于字符显示宽度(CJK 宽度为 2)导致竖线完全错位,排版歪歪扭扭;
|
|
43
|
+
3. **窄屏移动端阅读体验差**:宽表格在手机竖屏下横向拉长折行。
|
|
44
|
+
|
|
45
|
+
`tg-rich-render` 提供纯 Python 标准库实现的智能 CJK 宽度对齐与多风格自适应渲染,一行代码即可集成到 **aiogram 3**、**python-telegram-bot** 或任何 HTTP 请求中。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ✨ 核心特性
|
|
50
|
+
|
|
51
|
+
- 🚀 **零外部依赖**:纯 Python 标准库构建(基于 `unicodedata` 模块),即装即用,启动开销 0ms。
|
|
52
|
+
- 📐 **精准 CJK / Emoji 宽度补偿**:完美对齐汉字、日韩文、全角符号与 Emoji 表情,拒绝折线与锯齿。
|
|
53
|
+
- 🎨 **多风格视觉呈现**:
|
|
54
|
+
- `rounded`:现代优雅圆角框线(`╭───┬───╮`),视觉质感拉满。
|
|
55
|
+
- `classic`:经典 ASCII 风格(`+---+---+`)。
|
|
56
|
+
- `clean`:极简流式无竖框风格。
|
|
57
|
+
- `card`:移动端窄屏优先卡片流(键值对展示,杜绝横向滚动)。
|
|
58
|
+
- `html`:Telegram 兼容 HTML `<table>` 格式。
|
|
59
|
+
- 🤖 **主流框架开箱即用**:自带 aiogram 3 与 python-telegram-bot 发送适配器。
|
|
60
|
+
- 💻 **CLI 工具支持**:支持管道输入与终端即时预览。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 快速安装
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install tg-rich-render
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
或直接克隆使用:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/shali10/tg-rich-render.git
|
|
74
|
+
cd tg-rich-render
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🚀 快速上手
|
|
81
|
+
|
|
82
|
+
### 1. 独立使用(纯 Python)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from tg_rich_render import render_telegram
|
|
86
|
+
|
|
87
|
+
raw_markdown = """
|
|
88
|
+
# 节点健康巡检
|
|
89
|
+
|
|
90
|
+
| 节点 | 状态 | 延迟 |
|
|
91
|
+
|:---|:---:|---:|
|
|
92
|
+
| 香港CN2 🚀 | 正常 | 15ms |
|
|
93
|
+
| 美国洛杉矶 | 良好 | 135ms |
|
|
94
|
+
| 日本东京 ⚡ | 正常 | 48ms |
|
|
95
|
+
|
|
96
|
+
巡检完成,无异常节点。
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# 渲染为 Telegram 优雅圆角等宽表格
|
|
100
|
+
message = render_telegram(raw_markdown, style="rounded")
|
|
101
|
+
print(message)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**输出效果:**
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
# 节点健康巡检
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
╭────────────┬──────┬───────╮
|
|
111
|
+
│ 节点 │ 状态 │ 延迟 │
|
|
112
|
+
├────────────┼──────┼───────┤
|
|
113
|
+
│ 香港CN2 🚀 │ 正常 │ 15ms │
|
|
114
|
+
│ 美国洛杉矶 │ 良好 │ 135ms │
|
|
115
|
+
│ 日本东京 ⚡ │ 正常 │ 48ms │
|
|
116
|
+
╰────────────┴──────┴───────╯
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
巡检完成,无异常节点。
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 2. 结合 aiogram 3
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from aiogram import Bot
|
|
128
|
+
from tg_rich_render import send_smart_message
|
|
129
|
+
|
|
130
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
131
|
+
|
|
132
|
+
# 一行代码自适应格式化并发送
|
|
133
|
+
await send_smart_message(
|
|
134
|
+
bot=bot,
|
|
135
|
+
chat_id=12345678,
|
|
136
|
+
text=raw_markdown,
|
|
137
|
+
table_style="rounded"
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 3. 结合 python-telegram-bot
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from telegram import Bot
|
|
147
|
+
from tg_rich_render import send_smart_message
|
|
148
|
+
|
|
149
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
150
|
+
|
|
151
|
+
await send_smart_message(
|
|
152
|
+
bot=bot,
|
|
153
|
+
chat_id=12345678,
|
|
154
|
+
text=raw_markdown,
|
|
155
|
+
table_style="rounded"
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 4. 命令行(CLI)使用
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# 转换 Markdown 文件
|
|
165
|
+
tg-rich-render report.md --style rounded
|
|
166
|
+
|
|
167
|
+
# 从终端管道流式转换
|
|
168
|
+
cat summary.md | tg-rich-render --style card
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 🎨 渲染风格展示
|
|
174
|
+
|
|
175
|
+
| 风格名称 | 预览示意 | 适用场景 |
|
|
176
|
+
|:---|:---|:---|
|
|
177
|
+
| **rounded** (默认) | `╭─┬─╮\n│A│B│\n╰─┴─╯` | PC 端与大屏客户端,视觉质感极高 |
|
|
178
|
+
| **classic** | `+-+-+\n|A|B|\n+-+-+` | 通用等宽终端、标准日志输出 |
|
|
179
|
+
| **clean** | `A B\n─ ─\n1 2` | 极简通知、紧凑监控通知 |
|
|
180
|
+
| **card** | `📌 节点\n • 延迟: 15ms` | 移动端窄屏、列数较多的宽表格 |
|
|
181
|
+
| **html** | `<table>...</table>` | Telegram WebApp 或特定富文本容器 |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🧪 单元测试
|
|
186
|
+
|
|
187
|
+
项目自带完整的自动化测试集(覆盖 CJK 宽度、对齐算法、长文本解析与适配器):
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python3 -m unittest discover -s tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📄 开源许可
|
|
196
|
+
|
|
197
|
+
本项目基于 [MIT License](LICENSE) 开源。
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# tg-rich-render 📊✨
|
|
2
|
+
|
|
3
|
+
> **Zero-dependency, CJK-aware Markdown table & rich format converter for Telegram bots.**
|
|
4
|
+
> 专治 Telegram 机器人表格排版错位、中英混排对不齐、手机端横向溢出变难看代码块的痛点。
|
|
5
|
+
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.python.org/downloads/)
|
|
8
|
+
[](https://github.com/shali10/tg-rich-render/actions)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 💡 为什么需要 tg-rich-render?
|
|
13
|
+
|
|
14
|
+
Telegram 官方的 MarkdownV2 并不支持标准的 GFM Pipe Table(管道表格)。在日常 Telegram 机器人开发(运维巡检播报、资产统计、行情早报)中,直接发送表格通常会遇到以下问题:
|
|
15
|
+
|
|
16
|
+
1. **直接报错**:Telegram Bot API 无法解析 `| col | col |` 语法;
|
|
17
|
+
2. **粗暴丢进代码块**:直接用 ```` 包裹,但在中英汉字、Emoji 混排时由于字符显示宽度(CJK 宽度为 2)导致竖线完全错位,排版歪歪扭扭;
|
|
18
|
+
3. **窄屏移动端阅读体验差**:宽表格在手机竖屏下横向拉长折行。
|
|
19
|
+
|
|
20
|
+
`tg-rich-render` 提供纯 Python 标准库实现的智能 CJK 宽度对齐与多风格自适应渲染,一行代码即可集成到 **aiogram 3**、**python-telegram-bot** 或任何 HTTP 请求中。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ 核心特性
|
|
25
|
+
|
|
26
|
+
- 🚀 **零外部依赖**:纯 Python 标准库构建(基于 `unicodedata` 模块),即装即用,启动开销 0ms。
|
|
27
|
+
- 📐 **精准 CJK / Emoji 宽度补偿**:完美对齐汉字、日韩文、全角符号与 Emoji 表情,拒绝折线与锯齿。
|
|
28
|
+
- 🎨 **多风格视觉呈现**:
|
|
29
|
+
- `rounded`:现代优雅圆角框线(`╭───┬───╮`),视觉质感拉满。
|
|
30
|
+
- `classic`:经典 ASCII 风格(`+---+---+`)。
|
|
31
|
+
- `clean`:极简流式无竖框风格。
|
|
32
|
+
- `card`:移动端窄屏优先卡片流(键值对展示,杜绝横向滚动)。
|
|
33
|
+
- `html`:Telegram 兼容 HTML `<table>` 格式。
|
|
34
|
+
- 🤖 **主流框架开箱即用**:自带 aiogram 3 与 python-telegram-bot 发送适配器。
|
|
35
|
+
- 💻 **CLI 工具支持**:支持管道输入与终端即时预览。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📦 快速安装
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install tg-rich-render
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
或直接克隆使用:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/shali10/tg-rich-render.git
|
|
49
|
+
cd tg-rich-render
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🚀 快速上手
|
|
56
|
+
|
|
57
|
+
### 1. 独立使用(纯 Python)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from tg_rich_render import render_telegram
|
|
61
|
+
|
|
62
|
+
raw_markdown = """
|
|
63
|
+
# 节点健康巡检
|
|
64
|
+
|
|
65
|
+
| 节点 | 状态 | 延迟 |
|
|
66
|
+
|:---|:---:|---:|
|
|
67
|
+
| 香港CN2 🚀 | 正常 | 15ms |
|
|
68
|
+
| 美国洛杉矶 | 良好 | 135ms |
|
|
69
|
+
| 日本东京 ⚡ | 正常 | 48ms |
|
|
70
|
+
|
|
71
|
+
巡检完成,无异常节点。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# 渲染为 Telegram 优雅圆角等宽表格
|
|
75
|
+
message = render_telegram(raw_markdown, style="rounded")
|
|
76
|
+
print(message)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**输出效果:**
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
# 节点健康巡检
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
╭────────────┬──────┬───────╮
|
|
86
|
+
│ 节点 │ 状态 │ 延迟 │
|
|
87
|
+
├────────────┼──────┼───────┤
|
|
88
|
+
│ 香港CN2 🚀 │ 正常 │ 15ms │
|
|
89
|
+
│ 美国洛杉矶 │ 良好 │ 135ms │
|
|
90
|
+
│ 日本东京 ⚡ │ 正常 │ 48ms │
|
|
91
|
+
╰────────────┴──────┴───────╯
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
巡检完成,无异常节点。
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### 2. 结合 aiogram 3
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from aiogram import Bot
|
|
103
|
+
from tg_rich_render import send_smart_message
|
|
104
|
+
|
|
105
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
106
|
+
|
|
107
|
+
# 一行代码自适应格式化并发送
|
|
108
|
+
await send_smart_message(
|
|
109
|
+
bot=bot,
|
|
110
|
+
chat_id=12345678,
|
|
111
|
+
text=raw_markdown,
|
|
112
|
+
table_style="rounded"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 3. 结合 python-telegram-bot
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from telegram import Bot
|
|
122
|
+
from tg_rich_render import send_smart_message
|
|
123
|
+
|
|
124
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
125
|
+
|
|
126
|
+
await send_smart_message(
|
|
127
|
+
bot=bot,
|
|
128
|
+
chat_id=12345678,
|
|
129
|
+
text=raw_markdown,
|
|
130
|
+
table_style="rounded"
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### 4. 命令行(CLI)使用
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# 转换 Markdown 文件
|
|
140
|
+
tg-rich-render report.md --style rounded
|
|
141
|
+
|
|
142
|
+
# 从终端管道流式转换
|
|
143
|
+
cat summary.md | tg-rich-render --style card
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🎨 渲染风格展示
|
|
149
|
+
|
|
150
|
+
| 风格名称 | 预览示意 | 适用场景 |
|
|
151
|
+
|:---|:---|:---|
|
|
152
|
+
| **rounded** (默认) | `╭─┬─╮\n│A│B│\n╰─┴─╯` | PC 端与大屏客户端,视觉质感极高 |
|
|
153
|
+
| **classic** | `+-+-+\n|A|B|\n+-+-+` | 通用等宽终端、标准日志输出 |
|
|
154
|
+
| **clean** | `A B\n─ ─\n1 2` | 极简通知、紧凑监控通知 |
|
|
155
|
+
| **card** | `📌 节点\n • 延迟: 15ms` | 移动端窄屏、列数较多的宽表格 |
|
|
156
|
+
| **html** | `<table>...</table>` | Telegram WebApp 或特定富文本容器 |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 🧪 单元测试
|
|
161
|
+
|
|
162
|
+
项目自带完整的自动化测试集(覆盖 CJK 宽度、对齐算法、长文本解析与适配器):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python3 -m unittest discover -s tests
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 📄 开源许可
|
|
171
|
+
|
|
172
|
+
本项目基于 [MIT License](LICENSE) 开源。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tg-rich-render"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight Telegram rich table and format converter with zero-dependency CJK alignment."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "shali10", email = "99240403+shali10@users.noreply.github.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = ["telegram", "aiogram", "telegram-bot", "table", "markdown", "cjk", "rich-formatting"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Communications :: Chat",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
requires-python = ">=3.8"
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/shali10/tg-rich-render"
|
|
32
|
+
Repository = "https://github.com/shali10/tg-rich-render"
|
|
33
|
+
Issues = "https://github.com/shali10/tg-rich-render/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
tg-rich-render = "tg_rich_render.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["tg_rich_render*"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from tg_rich_render.adapter import smart_format_payload
|
|
3
|
+
|
|
4
|
+
MD_SAMPLE = """
|
|
5
|
+
| 维度 | 得分 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| 速度 | 98 |
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
class TestAdapter(unittest.TestCase):
|
|
11
|
+
def test_smart_format_payload_default():
|
|
12
|
+
payload = smart_format_payload(MD_SAMPLE)
|
|
13
|
+
self.assertIsNone(payload["parse_mode"])
|
|
14
|
+
self.assertIn("╭", payload["text"])
|
|
15
|
+
self.assertIn("```", payload["text"])
|
|
16
|
+
|
|
17
|
+
def test_smart_format_payload_html(self):
|
|
18
|
+
payload = smart_format_payload(MD_SAMPLE, prefer_html=True)
|
|
19
|
+
self.assertEqual(payload["parse_mode"], "HTML")
|
|
20
|
+
self.assertIn("<table>", payload["text"])
|
|
21
|
+
|
|
22
|
+
def test_smart_format_payload_default(self):
|
|
23
|
+
payload = smart_format_payload(MD_SAMPLE)
|
|
24
|
+
self.assertIsNone(payload["parse_mode"])
|
|
25
|
+
self.assertIn("╭", payload["text"])
|
|
26
|
+
self.assertIn("```", payload["text"])
|
|
27
|
+
|
|
28
|
+
if __name__ == '__main__':
|
|
29
|
+
unittest.main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from tg_rich_render.cjk import get_display_width, pad_cjk, strip_ansi
|
|
3
|
+
|
|
4
|
+
class TestCJK(unittest.TestCase):
|
|
5
|
+
def test_strip_ansi(self):
|
|
6
|
+
colored = "\x1b[31mRed Text\x1b[0m"
|
|
7
|
+
self.assertEqual(strip_ansi(colored), "Red Text")
|
|
8
|
+
self.assertEqual(get_display_width(colored), 8)
|
|
9
|
+
|
|
10
|
+
def test_get_display_width(self):
|
|
11
|
+
self.assertEqual(get_display_width("hello"), 5)
|
|
12
|
+
self.assertEqual(get_display_width("你好"), 4)
|
|
13
|
+
self.assertEqual(get_display_width("Hello, 世界!"), 13)
|
|
14
|
+
|
|
15
|
+
def test_pad_cjk(self):
|
|
16
|
+
# Left alignment
|
|
17
|
+
padded = pad_cjk("测试", 6, align='left')
|
|
18
|
+
self.assertEqual(padded, "测试 ")
|
|
19
|
+
self.assertEqual(get_display_width(padded), 6)
|
|
20
|
+
|
|
21
|
+
# Right alignment
|
|
22
|
+
padded_r = pad_cjk("测试", 6, align='right')
|
|
23
|
+
self.assertEqual(padded_r, " 测试")
|
|
24
|
+
self.assertEqual(get_display_width(padded_r), 6)
|
|
25
|
+
|
|
26
|
+
# Center alignment
|
|
27
|
+
padded_c = pad_cjk("A", 5, align='center')
|
|
28
|
+
self.assertEqual(padded_c, " A ")
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
unittest.main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from tg_rich_render.parser import render_telegram, extract_tables_and_text
|
|
3
|
+
|
|
4
|
+
DOC = """# 系统巡检报告
|
|
5
|
+
|
|
6
|
+
以下是当前节点健康度:
|
|
7
|
+
|
|
8
|
+
| 节点 | CPU | 内存 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| US-East | 14% | 42% |
|
|
11
|
+
| HK-01 | 28% | 65% |
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 这里是普通代码块,不应被误判为表格
|
|
15
|
+
| not | a | table |
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
请及时关注高负载节点。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
class TestParser(unittest.TestCase):
|
|
22
|
+
def test_extract_tables_and_text(self):
|
|
23
|
+
blocks = extract_tables_and_text(DOC)
|
|
24
|
+
types = [b[0] for b in blocks]
|
|
25
|
+
self.assertIn('table', types)
|
|
26
|
+
self.assertEqual(types.count('table'), 1)
|
|
27
|
+
|
|
28
|
+
def test_render_telegram_rounded(self):
|
|
29
|
+
out = render_telegram(DOC, style='rounded')
|
|
30
|
+
self.assertIn("```", out)
|
|
31
|
+
self.assertIn("╭", out)
|
|
32
|
+
self.assertIn("系统巡检报告", out)
|
|
33
|
+
self.assertIn("请及时关注高负载节点", out)
|
|
34
|
+
|
|
35
|
+
def test_render_telegram_card(self):
|
|
36
|
+
out = render_telegram(DOC, style='card')
|
|
37
|
+
self.assertIn("📌 US-East", out)
|
|
38
|
+
self.assertIn("• CPU: 14%", out)
|
|
39
|
+
|
|
40
|
+
if __name__ == '__main__':
|
|
41
|
+
unittest.main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from tg_rich_render.table import Table
|
|
3
|
+
|
|
4
|
+
RAW_TABLE = """
|
|
5
|
+
| 服务 | 状态 | 延迟 |
|
|
6
|
+
|:---|:---:|---:|
|
|
7
|
+
| API 网关 | 运行中 | 12ms |
|
|
8
|
+
| 数据库 | 正常 | 2ms |
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
class TestTable(unittest.TestCase):
|
|
12
|
+
def test_table_parsing(self):
|
|
13
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
14
|
+
self.assertIsNotNone(table)
|
|
15
|
+
self.assertEqual(table.headers, ["服务", "状态", "延迟"])
|
|
16
|
+
self.assertEqual(len(table.rows), 2)
|
|
17
|
+
self.assertEqual(table.alignments, ["left", "center", "right"])
|
|
18
|
+
|
|
19
|
+
def test_table_render_rounded(self):
|
|
20
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
21
|
+
rendered = table.render_rounded()
|
|
22
|
+
self.assertIn("╭", rendered)
|
|
23
|
+
self.assertIn("╰", rendered)
|
|
24
|
+
self.assertIn("API 网关", rendered)
|
|
25
|
+
|
|
26
|
+
def test_table_render_classic(self):
|
|
27
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
28
|
+
rendered = table.render_classic()
|
|
29
|
+
self.assertIn("+", rendered)
|
|
30
|
+
self.assertIn("|", rendered)
|
|
31
|
+
|
|
32
|
+
def test_table_render_clean(self):
|
|
33
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
34
|
+
rendered = table.render_clean()
|
|
35
|
+
self.assertIn("─", rendered)
|
|
36
|
+
self.assertIn("API 网关", rendered)
|
|
37
|
+
|
|
38
|
+
def test_table_render_card(self):
|
|
39
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
40
|
+
rendered = table.render_card()
|
|
41
|
+
self.assertIn("📌 API 网关", rendered)
|
|
42
|
+
self.assertIn("• 状态: 运行中", rendered)
|
|
43
|
+
|
|
44
|
+
def test_table_render_html(self):
|
|
45
|
+
table = Table.from_markdown(RAW_TABLE)
|
|
46
|
+
rendered = table.render_html()
|
|
47
|
+
self.assertIn("<table>", rendered)
|
|
48
|
+
self.assertIn("<th>服务</th>", rendered)
|
|
49
|
+
|
|
50
|
+
if __name__ == '__main__':
|
|
51
|
+
unittest.main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""tg-rich-render: Lightweight Telegram rich table and format converter.
|
|
2
|
+
|
|
3
|
+
Solves the common pain point of ugly, misaligned tables in Telegram bots.
|
|
4
|
+
Provides zero-dependency CJK alignment and multi-style table formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tg_rich_render.cjk import get_display_width, pad_cjk
|
|
8
|
+
from tg_rich_render.table import Table
|
|
9
|
+
from tg_rich_render.parser import render_telegram, extract_tables_and_text, TableStyle
|
|
10
|
+
from tg_rich_render.adapter import smart_format_payload, send_smart_message
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Table",
|
|
15
|
+
"TableStyle",
|
|
16
|
+
"get_display_width",
|
|
17
|
+
"pad_cjk",
|
|
18
|
+
"render_telegram",
|
|
19
|
+
"extract_tables_and_text",
|
|
20
|
+
"smart_format_payload",
|
|
21
|
+
"send_smart_message",
|
|
22
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Adapters and helper bindings for aiogram 3 and python-telegram-bot.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, Dict, Literal
|
|
5
|
+
from tg_rich_render.parser import render_telegram, TableStyle
|
|
6
|
+
|
|
7
|
+
def smart_format_payload(
|
|
8
|
+
text: str,
|
|
9
|
+
prefer_html: bool = False,
|
|
10
|
+
table_style: TableStyle = 'rounded'
|
|
11
|
+
) -> Dict[str, Any]:
|
|
12
|
+
"""Prepare payload dictionary with formatted text and appropriate parse_mode."""
|
|
13
|
+
if prefer_html:
|
|
14
|
+
formatted = render_telegram(text, style='html')
|
|
15
|
+
return {
|
|
16
|
+
"text": formatted,
|
|
17
|
+
"parse_mode": "HTML"
|
|
18
|
+
}
|
|
19
|
+
else:
|
|
20
|
+
formatted = render_telegram(text, style=table_style, wrap_in_code_block=True)
|
|
21
|
+
return {
|
|
22
|
+
"text": formatted,
|
|
23
|
+
"parse_mode": None # Plain/Markdown compatible
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async def send_smart_message(
|
|
27
|
+
bot: Any,
|
|
28
|
+
chat_id: Any,
|
|
29
|
+
text: str,
|
|
30
|
+
table_style: TableStyle = 'rounded',
|
|
31
|
+
**kwargs
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""Send a message through aiogram 3 or python-telegram-bot with auto-formatted rich tables.
|
|
34
|
+
|
|
35
|
+
Compatible with:
|
|
36
|
+
- aiogram 3.x (Bot instance)
|
|
37
|
+
- python-telegram-bot 20+ (Bot or ExtBot instance)
|
|
38
|
+
"""
|
|
39
|
+
payload = smart_format_payload(text, table_style=table_style)
|
|
40
|
+
send_kwargs = {**payload, **kwargs}
|
|
41
|
+
|
|
42
|
+
# aiogram 3.x
|
|
43
|
+
if hasattr(bot, 'send_message') and callable(getattr(bot, 'send_message')):
|
|
44
|
+
return await bot.send_message(chat_id=chat_id, **send_kwargs)
|
|
45
|
+
|
|
46
|
+
# python-telegram-bot
|
|
47
|
+
elif hasattr(bot, 'send_message'):
|
|
48
|
+
return await bot.send_message(chat_id=chat_id, **send_kwargs)
|
|
49
|
+
|
|
50
|
+
raise TypeError(f"Unsupported bot instance type: {type(bot)}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CJK and wide-character display width utilities.
|
|
2
|
+
|
|
3
|
+
Ensures perfect column alignment for Chinese, Japanese, Korean,
|
|
4
|
+
full-width punctuation, and emoji in Telegram monospace contexts.
|
|
5
|
+
Also strips ANSI escapes so styled strings calculate accurately.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import unicodedata
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
ANSI_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]')
|
|
13
|
+
|
|
14
|
+
def strip_ansi(text: str) -> str:
|
|
15
|
+
"""Remove ANSI escape sequences from string."""
|
|
16
|
+
return ANSI_RE.sub('', text)
|
|
17
|
+
|
|
18
|
+
def get_char_width(ch: str) -> int:
|
|
19
|
+
"""Return the display column width of a single unicode character."""
|
|
20
|
+
# Control characters
|
|
21
|
+
if unicodedata.category(ch).startswith('C'):
|
|
22
|
+
return 0
|
|
23
|
+
# East Asian Width property
|
|
24
|
+
# W: Wide, F: Fullwidth -> 2 columns
|
|
25
|
+
# A: Ambiguous -> usually 1 column in modern monospace fonts
|
|
26
|
+
# Na: Narrow, N: Neutral, H: Halfwidth -> 1 column
|
|
27
|
+
status = unicodedata.east_asian_width(ch)
|
|
28
|
+
if status in ('W', 'F'):
|
|
29
|
+
return 2
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
def get_display_width(text: str) -> int:
|
|
33
|
+
"""Calculate the total display width of a string in monospace columns."""
|
|
34
|
+
clean_text = strip_ansi(text)
|
|
35
|
+
return sum(get_char_width(ch) for ch in clean_text)
|
|
36
|
+
|
|
37
|
+
def pad_cjk(text: str, width: int, align: Literal['left', 'right', 'center'] = 'left') -> str:
|
|
38
|
+
"""Pad a string containing CJK characters to the specified display width."""
|
|
39
|
+
current_width = get_display_width(text)
|
|
40
|
+
if current_width >= width:
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
pad_needed = width - current_width
|
|
44
|
+
if align == 'right':
|
|
45
|
+
return ' ' * pad_needed + text
|
|
46
|
+
elif align == 'center':
|
|
47
|
+
left_pad = pad_needed // 2
|
|
48
|
+
right_pad = pad_needed - left_pad
|
|
49
|
+
return ' ' * left_pad + text + ' ' * right_pad
|
|
50
|
+
else: # left
|
|
51
|
+
return text + ' ' * pad_needed
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Command Line Interface for tg-rich-render."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tg_rich_render.parser import render_telegram
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="Transform Markdown tables into Telegram-optimized rich formats."
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"file",
|
|
14
|
+
nargs="?",
|
|
15
|
+
help="Markdown file to read (reads stdin if omitted)"
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--style",
|
|
19
|
+
choices=["rounded", "classic", "clean", "card", "html"],
|
|
20
|
+
default="rounded",
|
|
21
|
+
help="Table rendering style (default: rounded)"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--no-wrap",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Do not wrap rendered tables in code blocks"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
if args.file and args.file != "-":
|
|
32
|
+
content = Path(args.file).read_text(encoding="utf-8")
|
|
33
|
+
else:
|
|
34
|
+
content = sys.stdin.read()
|
|
35
|
+
|
|
36
|
+
rendered = render_telegram(
|
|
37
|
+
content,
|
|
38
|
+
style=args.style,
|
|
39
|
+
wrap_in_code_block=not args.no_wrap
|
|
40
|
+
)
|
|
41
|
+
print(rendered)
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Markdown document parser that isolates tables and applies rich Telegram formatting.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Literal, Optional, List
|
|
6
|
+
from tg_rich_render.table import Table
|
|
7
|
+
|
|
8
|
+
TableStyle = Literal['rounded', 'classic', 'clean', 'card', 'html']
|
|
9
|
+
|
|
10
|
+
TABLE_LINE_RE = re.compile(r'^\s*\|.*\|\s*$')
|
|
11
|
+
|
|
12
|
+
def extract_tables_and_text(md_text: str):
|
|
13
|
+
"""Split markdown text into ordinary text blocks and table blocks.
|
|
14
|
+
|
|
15
|
+
Protects existing fenced code blocks from being parsed as tables.
|
|
16
|
+
"""
|
|
17
|
+
lines = md_text.splitlines()
|
|
18
|
+
blocks = []
|
|
19
|
+
|
|
20
|
+
in_code_block = False
|
|
21
|
+
current_text_lines: List[str] = []
|
|
22
|
+
current_table_lines: List[str] = []
|
|
23
|
+
|
|
24
|
+
def flush_text():
|
|
25
|
+
nonlocal current_text_lines
|
|
26
|
+
if current_text_lines:
|
|
27
|
+
blocks.append(('text', '\n'.join(current_text_lines)))
|
|
28
|
+
current_text_lines = []
|
|
29
|
+
|
|
30
|
+
def flush_table():
|
|
31
|
+
nonlocal current_table_lines
|
|
32
|
+
if current_table_lines:
|
|
33
|
+
raw_table = '\n'.join(current_table_lines)
|
|
34
|
+
t = Table.from_markdown(raw_table)
|
|
35
|
+
if t:
|
|
36
|
+
blocks.append(('table', t))
|
|
37
|
+
else:
|
|
38
|
+
blocks.append(('text', raw_table))
|
|
39
|
+
current_table_lines = []
|
|
40
|
+
|
|
41
|
+
for line in lines:
|
|
42
|
+
stripped = line.strip()
|
|
43
|
+
|
|
44
|
+
# Check code fence
|
|
45
|
+
if stripped.startswith('```'):
|
|
46
|
+
in_code_block = not in_code_block
|
|
47
|
+
if current_table_lines:
|
|
48
|
+
flush_table()
|
|
49
|
+
current_text_lines.append(line)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
if in_code_block:
|
|
53
|
+
current_text_lines.append(line)
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# Check if line looks like a markdown table row
|
|
57
|
+
if TABLE_LINE_RE.match(stripped):
|
|
58
|
+
if current_text_lines:
|
|
59
|
+
flush_text()
|
|
60
|
+
current_table_lines.append(line)
|
|
61
|
+
else:
|
|
62
|
+
if current_table_lines:
|
|
63
|
+
flush_table()
|
|
64
|
+
current_text_lines.append(line)
|
|
65
|
+
|
|
66
|
+
if current_table_lines:
|
|
67
|
+
flush_table()
|
|
68
|
+
if current_text_lines:
|
|
69
|
+
flush_text()
|
|
70
|
+
|
|
71
|
+
return blocks
|
|
72
|
+
|
|
73
|
+
def render_telegram(
|
|
74
|
+
md_text: str,
|
|
75
|
+
style: TableStyle = 'rounded',
|
|
76
|
+
wrap_in_code_block: bool = True
|
|
77
|
+
) -> str:
|
|
78
|
+
"""Transform markdown text for optimal Telegram display.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
md_text: Input markdown text containing pipe tables.
|
|
82
|
+
style: Rendering style for tables ('rounded', 'classic', 'clean', 'card', 'html').
|
|
83
|
+
wrap_in_code_block: Wrap monospace tables in ``` for perfect alignment.
|
|
84
|
+
"""
|
|
85
|
+
blocks = extract_tables_and_text(md_text)
|
|
86
|
+
output_parts = []
|
|
87
|
+
|
|
88
|
+
for kind, payload in blocks:
|
|
89
|
+
if kind == 'text':
|
|
90
|
+
output_parts.append(payload)
|
|
91
|
+
elif kind == 'table':
|
|
92
|
+
table: Table = payload
|
|
93
|
+
if style == 'rounded':
|
|
94
|
+
rendered = table.render_rounded()
|
|
95
|
+
if wrap_in_code_block:
|
|
96
|
+
rendered = f"```\n{rendered}\n```"
|
|
97
|
+
elif style == 'classic':
|
|
98
|
+
rendered = table.render_classic()
|
|
99
|
+
if wrap_in_code_block:
|
|
100
|
+
rendered = f"```\n{rendered}\n```"
|
|
101
|
+
elif style == 'clean':
|
|
102
|
+
rendered = table.render_clean()
|
|
103
|
+
if wrap_in_code_block:
|
|
104
|
+
rendered = f"```\n{rendered}\n```"
|
|
105
|
+
elif style == 'card':
|
|
106
|
+
rendered = table.render_card()
|
|
107
|
+
elif style == 'html':
|
|
108
|
+
rendered = table.render_html()
|
|
109
|
+
else:
|
|
110
|
+
rendered = table.render_rounded()
|
|
111
|
+
if wrap_in_code_block:
|
|
112
|
+
rendered = f"```\n{rendered}\n```"
|
|
113
|
+
output_parts.append(rendered)
|
|
114
|
+
|
|
115
|
+
return "\n".join(output_parts)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Markdown Pipe Table Parser and Multi-style Telegram Renderers.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- CJK wide character awareness
|
|
5
|
+
- Column alignment (:---, :---:, ---:)
|
|
6
|
+
- Multiple visual styles: rounded, classic, clean, card, html
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import html
|
|
11
|
+
from typing import List, Dict, Any, Optional, Literal
|
|
12
|
+
from tg_rich_render.cjk import get_display_width, pad_cjk
|
|
13
|
+
|
|
14
|
+
Alignment = Literal['left', 'right', 'center']
|
|
15
|
+
|
|
16
|
+
class Table:
|
|
17
|
+
"""Represents a parsed Markdown table."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, headers: List[str], rows: List[List[str]], alignments: Optional[List[Alignment]] = None):
|
|
20
|
+
self.headers = [h.strip() for h in headers]
|
|
21
|
+
self.rows = [[cell.strip() for cell in row] for row in rows]
|
|
22
|
+
|
|
23
|
+
col_count = len(self.headers)
|
|
24
|
+
if not alignments or len(alignments) < col_count:
|
|
25
|
+
alignments = ['left'] * col_count
|
|
26
|
+
self.alignments = alignments[:col_count]
|
|
27
|
+
|
|
28
|
+
# Calculate max display width per column
|
|
29
|
+
self.col_widths = [get_display_width(h) for h in self.headers]
|
|
30
|
+
for row in self.rows:
|
|
31
|
+
# Pad row if incomplete
|
|
32
|
+
while len(row) < col_count:
|
|
33
|
+
row.append('')
|
|
34
|
+
for idx in range(col_count):
|
|
35
|
+
cell_w = get_display_width(row[idx])
|
|
36
|
+
if cell_w > self.col_widths[idx]:
|
|
37
|
+
self.col_widths[idx] = cell_w
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_markdown(cls, table_str: str) -> Optional['Table']:
|
|
41
|
+
"""Parse a markdown pipe table string into a Table instance."""
|
|
42
|
+
lines = [line.strip() for line in table_str.strip().splitlines() if line.strip()]
|
|
43
|
+
if len(lines) < 2:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
# Clean pipe edges
|
|
47
|
+
def split_row(line: str) -> List[str]:
|
|
48
|
+
if line.startswith('|'):
|
|
49
|
+
line = line[1:]
|
|
50
|
+
if line.endswith('|'):
|
|
51
|
+
line = line[:-1]
|
|
52
|
+
return [cell.strip() for cell in line.split('|')]
|
|
53
|
+
|
|
54
|
+
raw_header = split_row(lines[0])
|
|
55
|
+
separator_line = split_row(lines[1])
|
|
56
|
+
|
|
57
|
+
# Validate separator line has dashes
|
|
58
|
+
alignments: List[Alignment] = []
|
|
59
|
+
for sep in separator_line:
|
|
60
|
+
s = sep.strip()
|
|
61
|
+
if not re.match(r'^:?-+:?$', s):
|
|
62
|
+
return None # Invalid markdown table separator
|
|
63
|
+
if s.startswith(':') and s.endswith(':'):
|
|
64
|
+
alignments.append('center')
|
|
65
|
+
elif s.endswith(':'):
|
|
66
|
+
alignments.append('right')
|
|
67
|
+
else:
|
|
68
|
+
alignments.append('left')
|
|
69
|
+
|
|
70
|
+
rows: List[List[str]] = []
|
|
71
|
+
for line in lines[2:]:
|
|
72
|
+
if '|' in line:
|
|
73
|
+
rows.append(split_row(line))
|
|
74
|
+
|
|
75
|
+
return cls(headers=raw_header, rows=rows, alignments=alignments)
|
|
76
|
+
|
|
77
|
+
def render_rounded(self) -> str:
|
|
78
|
+
"""Render using modern Unicode rounded box characters."""
|
|
79
|
+
widths = self.col_widths
|
|
80
|
+
|
|
81
|
+
# ╭──────┬──────╮
|
|
82
|
+
top = "╭" + "┬".join("─" * (w + 2) for w in widths) + "╮"
|
|
83
|
+
# ├──────┼──────┤
|
|
84
|
+
mid = "├" + "┼".join("─" * (w + 2) for w in widths) + "┤"
|
|
85
|
+
# ╰──────┴──────╯
|
|
86
|
+
bot = "╰" + "┴".join("─" * (w + 2) for w in widths) + "╯"
|
|
87
|
+
|
|
88
|
+
def fmt_row(cells: List[str]) -> str:
|
|
89
|
+
padded = []
|
|
90
|
+
for i, cell in enumerate(cells):
|
|
91
|
+
w = widths[i]
|
|
92
|
+
align = self.alignments[i] if i < len(self.alignments) else 'left'
|
|
93
|
+
padded.append(" " + pad_cjk(cell, w, align=align) + " ")
|
|
94
|
+
return "│" + "│".join(padded) + "│"
|
|
95
|
+
|
|
96
|
+
lines = [top, fmt_row(self.headers), mid]
|
|
97
|
+
for row in self.rows:
|
|
98
|
+
lines.append(fmt_row(row))
|
|
99
|
+
lines.append(bot)
|
|
100
|
+
return "\n".join(lines)
|
|
101
|
+
|
|
102
|
+
def render_classic(self) -> str:
|
|
103
|
+
"""Render using classic ASCII borders."""
|
|
104
|
+
widths = self.col_widths
|
|
105
|
+
sep = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
|
|
106
|
+
|
|
107
|
+
def fmt_row(cells: List[str]) -> str:
|
|
108
|
+
padded = []
|
|
109
|
+
for i, cell in enumerate(cells):
|
|
110
|
+
w = widths[i]
|
|
111
|
+
align = self.alignments[i] if i < len(self.alignments) else 'left'
|
|
112
|
+
padded.append(" " + pad_cjk(cell, w, align=align) + " ")
|
|
113
|
+
return "|" + "|".join(padded) + "|"
|
|
114
|
+
|
|
115
|
+
lines = [sep, fmt_row(self.headers), sep]
|
|
116
|
+
for row in self.rows:
|
|
117
|
+
lines.append(fmt_row(row))
|
|
118
|
+
lines.append(sep)
|
|
119
|
+
return "\n".join(lines)
|
|
120
|
+
|
|
121
|
+
def render_clean(self) -> str:
|
|
122
|
+
"""Render minimalist table without vertical border lines."""
|
|
123
|
+
widths = self.col_widths
|
|
124
|
+
header_pad = [pad_cjk(h, widths[i], align=self.alignments[i]) for i, h in enumerate(self.headers)]
|
|
125
|
+
sep_line = ["─" * widths[i] for i in range(len(widths))]
|
|
126
|
+
|
|
127
|
+
lines = [" ".join(header_pad), " ".join(sep_line)]
|
|
128
|
+
for row in self.rows:
|
|
129
|
+
row_pad = []
|
|
130
|
+
for i, cell in enumerate(row):
|
|
131
|
+
w = widths[i] if i < len(widths) else len(cell)
|
|
132
|
+
align = self.alignments[i] if i < len(self.alignments) else 'left'
|
|
133
|
+
row_pad.append(pad_cjk(cell, w, align=align))
|
|
134
|
+
lines.append(" ".join(row_pad))
|
|
135
|
+
return "\n".join(lines)
|
|
136
|
+
|
|
137
|
+
def render_card(self) -> str:
|
|
138
|
+
"""Render table as mobile-friendly cards, ideal for narrow screens."""
|
|
139
|
+
blocks = []
|
|
140
|
+
for row_idx, row in enumerate(self.rows):
|
|
141
|
+
title = row[0] if row else f"Item {row_idx + 1}"
|
|
142
|
+
details = []
|
|
143
|
+
for col_idx in range(1, len(self.headers)):
|
|
144
|
+
h = self.headers[col_idx]
|
|
145
|
+
val = row[col_idx] if col_idx < len(row) else ""
|
|
146
|
+
if val:
|
|
147
|
+
details.append(f" • {h}: {val}")
|
|
148
|
+
if details:
|
|
149
|
+
blocks.append(f"📌 {title}\n" + "\n".join(details))
|
|
150
|
+
else:
|
|
151
|
+
blocks.append(f"📌 {title}")
|
|
152
|
+
return "\n\n".join(blocks)
|
|
153
|
+
|
|
154
|
+
def render_html(self) -> str:
|
|
155
|
+
"""Render as Telegram-compatible HTML table or styled blocks."""
|
|
156
|
+
out = ["<table>"]
|
|
157
|
+
out.append(" <thead>")
|
|
158
|
+
out.append(" <tr>" + "".join(f"<th>{html.escape(h)}</th>" for h in self.headers) + "</tr>")
|
|
159
|
+
out.append(" </thead>")
|
|
160
|
+
out.append(" <tbody>")
|
|
161
|
+
for row in self.rows:
|
|
162
|
+
out.append(" <tr>" + "".join(f"<td>{html.escape(cell)}</td>" for cell in row) + "</tr>")
|
|
163
|
+
out.append(" </tbody>")
|
|
164
|
+
out.append("</table>")
|
|
165
|
+
return "\n".join(out)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tg-rich-render
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Telegram rich table and format converter with zero-dependency CJK alignment.
|
|
5
|
+
Author-email: shali10 <99240403+shali10@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shali10/tg-rich-render
|
|
8
|
+
Project-URL: Repository, https://github.com/shali10/tg-rich-render
|
|
9
|
+
Project-URL: Issues, https://github.com/shali10/tg-rich-render/issues
|
|
10
|
+
Keywords: telegram,aiogram,telegram-bot,table,markdown,cjk,rich-formatting
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# tg-rich-render 📊✨
|
|
27
|
+
|
|
28
|
+
> **Zero-dependency, CJK-aware Markdown table & rich format converter for Telegram bots.**
|
|
29
|
+
> 专治 Telegram 机器人表格排版错位、中英混排对不齐、手机端横向溢出变难看代码块的痛点。
|
|
30
|
+
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](https://github.com/shali10/tg-rich-render/actions)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 💡 为什么需要 tg-rich-render?
|
|
38
|
+
|
|
39
|
+
Telegram 官方的 MarkdownV2 并不支持标准的 GFM Pipe Table(管道表格)。在日常 Telegram 机器人开发(运维巡检播报、资产统计、行情早报)中,直接发送表格通常会遇到以下问题:
|
|
40
|
+
|
|
41
|
+
1. **直接报错**:Telegram Bot API 无法解析 `| col | col |` 语法;
|
|
42
|
+
2. **粗暴丢进代码块**:直接用 ```` 包裹,但在中英汉字、Emoji 混排时由于字符显示宽度(CJK 宽度为 2)导致竖线完全错位,排版歪歪扭扭;
|
|
43
|
+
3. **窄屏移动端阅读体验差**:宽表格在手机竖屏下横向拉长折行。
|
|
44
|
+
|
|
45
|
+
`tg-rich-render` 提供纯 Python 标准库实现的智能 CJK 宽度对齐与多风格自适应渲染,一行代码即可集成到 **aiogram 3**、**python-telegram-bot** 或任何 HTTP 请求中。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ✨ 核心特性
|
|
50
|
+
|
|
51
|
+
- 🚀 **零外部依赖**:纯 Python 标准库构建(基于 `unicodedata` 模块),即装即用,启动开销 0ms。
|
|
52
|
+
- 📐 **精准 CJK / Emoji 宽度补偿**:完美对齐汉字、日韩文、全角符号与 Emoji 表情,拒绝折线与锯齿。
|
|
53
|
+
- 🎨 **多风格视觉呈现**:
|
|
54
|
+
- `rounded`:现代优雅圆角框线(`╭───┬───╮`),视觉质感拉满。
|
|
55
|
+
- `classic`:经典 ASCII 风格(`+---+---+`)。
|
|
56
|
+
- `clean`:极简流式无竖框风格。
|
|
57
|
+
- `card`:移动端窄屏优先卡片流(键值对展示,杜绝横向滚动)。
|
|
58
|
+
- `html`:Telegram 兼容 HTML `<table>` 格式。
|
|
59
|
+
- 🤖 **主流框架开箱即用**:自带 aiogram 3 与 python-telegram-bot 发送适配器。
|
|
60
|
+
- 💻 **CLI 工具支持**:支持管道输入与终端即时预览。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 快速安装
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install tg-rich-render
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
或直接克隆使用:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/shali10/tg-rich-render.git
|
|
74
|
+
cd tg-rich-render
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🚀 快速上手
|
|
81
|
+
|
|
82
|
+
### 1. 独立使用(纯 Python)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from tg_rich_render import render_telegram
|
|
86
|
+
|
|
87
|
+
raw_markdown = """
|
|
88
|
+
# 节点健康巡检
|
|
89
|
+
|
|
90
|
+
| 节点 | 状态 | 延迟 |
|
|
91
|
+
|:---|:---:|---:|
|
|
92
|
+
| 香港CN2 🚀 | 正常 | 15ms |
|
|
93
|
+
| 美国洛杉矶 | 良好 | 135ms |
|
|
94
|
+
| 日本东京 ⚡ | 正常 | 48ms |
|
|
95
|
+
|
|
96
|
+
巡检完成,无异常节点。
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# 渲染为 Telegram 优雅圆角等宽表格
|
|
100
|
+
message = render_telegram(raw_markdown, style="rounded")
|
|
101
|
+
print(message)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**输出效果:**
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
# 节点健康巡检
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
╭────────────┬──────┬───────╮
|
|
111
|
+
│ 节点 │ 状态 │ 延迟 │
|
|
112
|
+
├────────────┼──────┼───────┤
|
|
113
|
+
│ 香港CN2 🚀 │ 正常 │ 15ms │
|
|
114
|
+
│ 美国洛杉矶 │ 良好 │ 135ms │
|
|
115
|
+
│ 日本东京 ⚡ │ 正常 │ 48ms │
|
|
116
|
+
╰────────────┴──────┴───────╯
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
巡检完成,无异常节点。
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 2. 结合 aiogram 3
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from aiogram import Bot
|
|
128
|
+
from tg_rich_render import send_smart_message
|
|
129
|
+
|
|
130
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
131
|
+
|
|
132
|
+
# 一行代码自适应格式化并发送
|
|
133
|
+
await send_smart_message(
|
|
134
|
+
bot=bot,
|
|
135
|
+
chat_id=12345678,
|
|
136
|
+
text=raw_markdown,
|
|
137
|
+
table_style="rounded"
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 3. 结合 python-telegram-bot
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from telegram import Bot
|
|
147
|
+
from tg_rich_render import send_smart_message
|
|
148
|
+
|
|
149
|
+
bot = Bot(token="YOUR_BOT_TOKEN")
|
|
150
|
+
|
|
151
|
+
await send_smart_message(
|
|
152
|
+
bot=bot,
|
|
153
|
+
chat_id=12345678,
|
|
154
|
+
text=raw_markdown,
|
|
155
|
+
table_style="rounded"
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 4. 命令行(CLI)使用
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# 转换 Markdown 文件
|
|
165
|
+
tg-rich-render report.md --style rounded
|
|
166
|
+
|
|
167
|
+
# 从终端管道流式转换
|
|
168
|
+
cat summary.md | tg-rich-render --style card
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 🎨 渲染风格展示
|
|
174
|
+
|
|
175
|
+
| 风格名称 | 预览示意 | 适用场景 |
|
|
176
|
+
|:---|:---|:---|
|
|
177
|
+
| **rounded** (默认) | `╭─┬─╮\n│A│B│\n╰─┴─╯` | PC 端与大屏客户端,视觉质感极高 |
|
|
178
|
+
| **classic** | `+-+-+\n|A|B|\n+-+-+` | 通用等宽终端、标准日志输出 |
|
|
179
|
+
| **clean** | `A B\n─ ─\n1 2` | 极简通知、紧凑监控通知 |
|
|
180
|
+
| **card** | `📌 节点\n • 延迟: 15ms` | 移动端窄屏、列数较多的宽表格 |
|
|
181
|
+
| **html** | `<table>...</table>` | Telegram WebApp 或特定富文本容器 |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🧪 单元测试
|
|
186
|
+
|
|
187
|
+
项目自带完整的自动化测试集(覆盖 CJK 宽度、对齐算法、长文本解析与适配器):
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python3 -m unittest discover -s tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📄 开源许可
|
|
196
|
+
|
|
197
|
+
本项目基于 [MIT License](LICENSE) 开源。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_adapter.py
|
|
5
|
+
tests/test_cjk.py
|
|
6
|
+
tests/test_parser.py
|
|
7
|
+
tests/test_table.py
|
|
8
|
+
tg_rich_render/__init__.py
|
|
9
|
+
tg_rich_render/adapter.py
|
|
10
|
+
tg_rich_render/cjk.py
|
|
11
|
+
tg_rich_render/cli.py
|
|
12
|
+
tg_rich_render/parser.py
|
|
13
|
+
tg_rich_render/table.py
|
|
14
|
+
tg_rich_render.egg-info/PKG-INFO
|
|
15
|
+
tg_rich_render.egg-info/SOURCES.txt
|
|
16
|
+
tg_rich_render.egg-info/dependency_links.txt
|
|
17
|
+
tg_rich_render.egg-info/entry_points.txt
|
|
18
|
+
tg_rich_render.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tg_rich_render
|