Module: AgentsControl::Channels::Telegram::Markdown

Defined in:
lib/agents_control/channels/telegram/markdown.rb

Overview

Turns plain GFM-like text — the shape Claude Code writes — into Telegram MarkdownV2.

Escaping rules:

- outside code, the 18 characters `_*[]()~`>#+-=|{}.!` must be
backslash-escaped, or sendMessage answers with a 400: Bad
Request: can't parse entities: Character '.' is reserved…
- inside `inline code` and ```blocks``` those same characters
must NOT be escaped — there they're just text;
- a ```lang tag before a block is accepted and syntax-highlighted.

This is Telegram's strict mode: one incorrectly escaped period and the whole message fails to send. So the converter is written conservatively (it never touches code, and escapes prose in full), and the Api side still carries its own separate safety net — falling back to plain text if Telegram rejects it anyway.

Constant Summary collapse

RESERVED =

Escaped outside code. Order matters: \ goes first, or the escaping slashes would themselves get escaped again.

%w[\\ _ * [ ] ( ) ~ ` > # + - = | { } . !].freeze
CODE =

Code is what must never be touched: even the opening triple quote can carry a language tag (```ruby) that has to be kept literal, not escaped.

/(```.*?```|`[^`\n]+`)/m
BOLD =

One of the few GFM constructs worth preserving as actual formatting rather than turning into escaped asterisks: Claude Code often puts important text in headings and summaries.

/\*\*(.+?)\*\*/m

Class Method Summary collapse

Class Method Details

.convert(text) ⇒ Object



40
41
42
43
44
# File 'lib/agents_control/channels/telegram/markdown.rb', line 40

def convert(text)
  text.to_s.split(CODE).each_with_index.map do |chunk, index|
    index.odd? ? chunk : escape_prose(chunk)
  end.join
end

.escape_literal(text) ⇒ Object



52
53
54
55
# File 'lib/agents_control/channels/telegram/markdown.rb', line 52

def escape_literal(text)
  pattern = Regexp.union(RESERVED)
  text.gsub(pattern) { |char| "\\#{char}" }
end

.escape_prose(text) ⇒ Object



46
47
48
49
50
# File 'lib/agents_control/channels/telegram/markdown.rb', line 46

def escape_prose(text)
  text.split(BOLD).each_with_index.map do |chunk, index|
    index.odd? ? "*#{escape_literal(chunk)}*" : escape_literal(chunk)
  end.join
end