style: enhance Telegram message formatting with visual hierarchy

Improved markdownToHtml converter for richer Telegram messages:
- Heading hierarchy: h1 (🚀+separator), h2 (█), h3 (▸), h4 (●)
- Multi-line blockquote merging
- Indented bullet lists
- Markdown table support (rendered as <pre>)
- Horizontal rule rendering
- Language class on fenced code blocks

Co-Authored-By: zcode <noreply@zcode.dev>
This commit is contained in:
admin
2026-05-06 13:01:45 +00:00
Unverified
parent 98ed33ba8f
commit b216f2b90d
2 changed files with 58 additions and 12 deletions

View File

@@ -55,6 +55,24 @@ recovers cleanly from stale processes, and has zero EADDRINUSE crashes.
## [2.0.0] - 2026-05-06 ## [2.0.0] - 2026-05-06
### 🎨 Improved
#### Telegram Message Formatting Overhaul
Enhanced the `markdownToHtml` converter in `src/bot/message-sender.js` to produce
visually rich, well-structured Telegram messages:
- **Heading hierarchy** — h1 gets 🚀 + separator line, h2 gets █ block marker,
h3 gets ▸ triangle, h4 gets ● dot — all bold, visually distinct
- **Multi-line blockquotes** — consecutive `>` lines now merge into a single
`<blockquote>` element instead of one per line
- **Indented bullet lists** — ` • ` with leading spaces for better readability
- **Table support** — Markdown tables (`| col | col |`) rendered as `<pre>` blocks
- **Horizontal rules** — `---` and `***` render as ──── separator lines
- **Code blocks** — fenced code blocks get `<pre><code>` with language class attribute
- Cleaner vertical spacing (excessive blank lines collapsed)
### 🎉 Major Release - Ruflo Integration Complete ### 🎉 Major Release - Ruflo Integration Complete
Complete integration of Ruflo's multi-agent orchestration system with comprehensive documentation update. Complete integration of Ruflo's multi-agent orchestration system with comprehensive documentation update.

View File

@@ -31,6 +31,16 @@ const CURSOR = ' ▉';
export function markdownToHtml(text) { export function markdownToHtml(text) {
if (!text) return ''; if (!text) return '';
// 0. Extract tables → protect, render as <pre>
const tables = [];
text = text.replace(/^(\|.+\|)\n(\|[-:\s|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, header, sep, body) => {
const idx = tables.length;
const rows = [header, sep, ...body.trim().split('\n').filter(Boolean)];
const escaped = rows.map(r => r.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')).join('\n');
tables.push(`<pre>${escaped}</pre>`);
return `\x00TB${idx}\x00`;
});
// 1. Extract fenced code blocks → protect from escaping // 1. Extract fenced code blocks → protect from escaping
const codeBlocks = []; const codeBlocks = [];
text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
@@ -62,19 +72,34 @@ export function markdownToHtml(text) {
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
// 4. Convert Markdown patterns → HTML tags // 4. Convert Markdown patterns → HTML tags
// Headings: visual hierarchy via Unicode markers + bold
text = text text = text
.replace(/\*\*([\s\S]+?)\*\*/g, '<b>$1</b>') // **bold** (multiline) .replace(/\*\*([\s\S]+?)\*\*/g, '<b>$1</b>') // **bold**
.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<i>$1</i>') // *italic* (not inside **) .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<i>$1</i>') // *italic*
.replace(/~~(.+?)~~/g, '<s>$1</s>') // ~~strike~~ .replace(/~~(.+?)~~/g, '<s>$1</s>') // ~~strike~~
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>') // [link](url) .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>') // [link](url)
.replace(/^####\s+(.+)$/gm, '<b>$1</b>') // h4 // Headings with Unicode visual hierarchy
.replace(/^###\s+(.+)$/gm, '<b>$1</b>') // h3 .replace(/^#\s+(.+)$/gm, '\n\n<b>\u{1F680} $1</b>\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n') // h1 — rocket + line
.replace(/^##\s+(.+)$/gm, '<b>$1</b>') // h2 .replace(/^##\s+(.+)$/gm, '\n<b>\u2588 $1</b>') // h2 — block + bold (standout)
.replace(/^#\s+(.+)$/gm, '<b>$1</b>') // h1 .replace(/^###\s+(.+)$/gm, '\n<b>\u25B8 $1</b>') // h3 — triangle + bold
.replace(/^&gt;\s+(.+)$/gm, '<blockquote>$1</blockquote>') // > quote .replace(/^####\s+(.+)$/gm, '\n<b>\u25CF $1</b>') // h4 — dot + bold
.replace(/^[-*]\s+/gm, '• '); // - or * list → bullet // Multi-line blockquotes: merge consecutive > lines into one blockquote
.replace(/(^&gt;\s+(.+)$\n?)+/gm, (match) => {
const content = match.trim().split('\n').map(l => l.replace(/^&gt;\s+/, '')).join('\n');
return `<blockquote>${content}</blockquote>`;
})
// Unordered lists (bullet with indent)
.replace(/^[-*+]\s+/gm, ' \u2022 ')
// Ordered lists
.replace(/^\d+\.\s/gm, (m) => m)
// Horizontal rules
.replace(/^---+$/gm, '\u2500'.repeat(30))
.replace(/^\*\*\*+$/gm, '\u2500'.repeat(30));
// 5. Restore protected code blocks and inline code // 5. Restore protected elements
for (let i = 0; i < tables.length; i++) {
text = text.replace(`\x00TB${i}\x00`, tables[i]);
}
for (let i = 0; i < codeBlocks.length; i++) { for (let i = 0; i < codeBlocks.length; i++) {
text = text.replace(`\x00CB${i}\x00`, codeBlocks[i]); text = text.replace(`\x00CB${i}\x00`, codeBlocks[i]);
} }
@@ -82,7 +107,10 @@ export function markdownToHtml(text) {
text = text.replace(`\x00IC${i}\x00`, inlineCodes[i]); text = text.replace(`\x00IC${i}\x00`, inlineCodes[i]);
} }
return text; // 6. Clean up excessive blank lines
text = text.replace(/\n{3,}/g, '\n\n');
return text.trim();
} }
/** /**