diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ecd73e1..bcfcd1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,24 @@ recovers cleanly from stale processes, and has zero EADDRINUSE crashes. ## [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 + `
` element instead of one per line +- **Indented bullet lists** — ` • ` with leading spaces for better readability +- **Table support** — Markdown tables (`| col | col |`) rendered as `
` blocks
+- **Horizontal rules** — `---` and `***` render as ──── separator lines
+- **Code blocks** — fenced code blocks get `
` with language class attribute
+- Cleaner vertical spacing (excessive blank lines collapsed)
+
+
 ### 🎉 Major Release - Ruflo Integration Complete
 
 Complete integration of Ruflo's multi-agent orchestration system with comprehensive documentation update.
diff --git a/src/bot/message-sender.js b/src/bot/message-sender.js
index 53cbccd6..af147c84 100644
--- a/src/bot/message-sender.js
+++ b/src/bot/message-sender.js
@@ -31,6 +31,16 @@ const CURSOR = ' ▉';
 export function markdownToHtml(text) {
   if (!text) return '';
 
+  // 0. Extract tables → protect, render as 
+  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, '&').replace(//g, '>')).join('\n');
+    tables.push(`
${escaped}
`); + return `\x00TB${idx}\x00`; + }); + // 1. Extract fenced code blocks → protect from escaping const codeBlocks = []; text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { @@ -62,19 +72,34 @@ export function markdownToHtml(text) { .replace(/>/g, '>'); // 4. Convert Markdown patterns → HTML tags + // Headings: visual hierarchy via Unicode markers + bold text = text - .replace(/\*\*([\s\S]+?)\*\*/g, '$1') // **bold** (multiline) - .replace(/(?$1') // *italic* (not inside **) - .replace(/~~(.+?)~~/g, '$1') // ~~strike~~ - .replace(/\[(.+?)\]\((.+?)\)/g, '$1') // [link](url) - .replace(/^####\s+(.+)$/gm, '$1') // h4 - .replace(/^###\s+(.+)$/gm, '$1') // h3 - .replace(/^##\s+(.+)$/gm, '$1') // h2 - .replace(/^#\s+(.+)$/gm, '$1') // h1 - .replace(/^>\s+(.+)$/gm, '
$1
') // > quote - .replace(/^[-*]\s+/gm, '• '); // - or * list → bullet + .replace(/\*\*([\s\S]+?)\*\*/g, '$1') // **bold** + .replace(/(?$1') // *italic* + .replace(/~~(.+?)~~/g, '$1') // ~~strike~~ + .replace(/\[(.+?)\]\((.+?)\)/g, '$1') // [link](url) + // Headings with Unicode visual hierarchy + .replace(/^#\s+(.+)$/gm, '\n\n\u{1F680} $1\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, '\n\u2588 $1') // h2 — block + bold (standout) + .replace(/^###\s+(.+)$/gm, '\n\u25B8 $1') // h3 — triangle + bold + .replace(/^####\s+(.+)$/gm, '\n\u25CF $1') // h4 — dot + bold + // Multi-line blockquotes: merge consecutive > lines into one blockquote + .replace(/(^>\s+(.+)$\n?)+/gm, (match) => { + const content = match.trim().split('\n').map(l => l.replace(/^>\s+/, '')).join('\n'); + return `
${content}
`; + }) + // 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++) { text = text.replace(`\x00CB${i}\x00`, codeBlocks[i]); } @@ -82,7 +107,10 @@ export function markdownToHtml(text) { 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(); } /**