Module: Telegrama::Formatter
- Defined in:
- lib/telegrama/formatter.rb
Defined Under Namespace
Classes: MarkdownError, MarkdownTokenizer
Constant Summary collapse
- MARKDOWN_SPECIAL_CHARS =
Characters that need special escaping in Telegram's MarkdownV2 format
%w[_ * [ ] ( ) ~ ` > # + - = | { } . !].freeze
- ALWAYS_ESCAPE_CHARS =
Characters that should always be escaped in Telegram messages, even when Markdown is enabled
%w[. !].freeze
- MARKDOWN_FORMAT_CHARS =
Characters used for Markdown formatting that need special handling
%w[* _].freeze
- LINK_TEXT_ESCAPE_REGEX =
Escapes every MarkdownV2-reserved character (plus backslash) inside link TEXT. Derived from MARKDOWN_SPECIAL_CHARS so the two can never drift: a hand-typed copy of this class was missing '-', which let hyphenated link labels ("GPS-risk") reach Telegram unescaped — and the API rejects the ENTIRE message, not just the link, so delivery fell back to raw unformatted text. (URL parts keep their own narrower class below: per the MarkdownV2 spec, inside the (...) part only ')' and '' MUST be escaped.)
/([#{Regexp.escape((MARKDOWN_SPECIAL_CHARS + [ "\\" ]).join)}])/
Class Method Summary collapse
-
.apply_prefix_suffix(text) ⇒ String
Apply configured prefix and suffix to the message.
-
.escape_html(text) ⇒ String
Escape HTML special characters.
-
.escape_markdown_aggressive(text) ⇒ String
Fall back to an aggressive approach that escapes everything.
-
.escape_markdown_v2(text) ⇒ String
The main entry point for MarkdownV2 escaping.
-
.format(text, options = {}) ⇒ String
Main formatting entry point - processes text according to configuration and options.
-
.html_to_telegram_markdown(html) ⇒ String
Convert HTML to Telegram MarkdownV2 format.
-
.obfuscate_emails(text) ⇒ String
Obfuscate email addresses in text.
-
.strip_markdown(text) ⇒ String
Strip all markdown formatting for plain text delivery.
-
.tokenize_and_format(text) ⇒ String
Tokenize and format the text using a state machine approach.
-
.truncate(text, max_length) ⇒ String
Truncate text to a maximum length.
Class Method Details
.apply_prefix_suffix(text) ⇒ String
Apply configured prefix and suffix to the message
69 70 71 72 73 74 75 76 77 78 |
# File 'lib/telegrama/formatter.rb', line 69 def self.apply_prefix_suffix(text) prefix = Telegrama.configuration. suffix = Telegrama.configuration. result = text.dup result = "#{prefix}#{result}" if prefix result = "#{result}#{suffix}" if suffix result end |
.escape_html(text) ⇒ String
Escape HTML special characters
597 598 599 600 601 602 |
# File 'lib/telegrama/formatter.rb', line 597 def self.escape_html(text) # Precompile HTML escape regex for better performance @@html_regex ||= /[<>&]/ text.gsub(@@html_regex, '<' => '<', '>' => '>', '&' => '&') end |
.escape_markdown_aggressive(text) ⇒ String
Fall back to an aggressive approach that escapes everything
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'lib/telegrama/formatter.rb', line 501 def self.escape_markdown_aggressive(text) # Escape all special characters indiscriminately # This might break formatting but will at least deliver result = text.dup # Escape backslashes first result.gsub!('\\', '\\\\') # Then escape all other special characters MARKDOWN_SPECIAL_CHARS.each do |char| result.gsub!(char, "\\#{char}") end result end |
.escape_markdown_v2(text) ⇒ String
The main entry point for MarkdownV2 escaping
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/telegrama/formatter.rb', line 83 def self.escape_markdown_v2(text) return text if text.nil? || text.empty? # Special handling for messages with suffix like "Sent via Telegrama" if text.include?("\n--\nSent via Telegrama") # For messages with the standard suffix, we need to keep the dashes unchanged parts = text.split("\n--\n") if parts.length == 2 first_part = tokenize_and_format(parts.first) return "#{first_part}\n--\n#{parts.last}" end end # For all other text, use the tokenizing approach tokenize_and_format(text) end |
.format(text, options = {}) ⇒ String
Main formatting entry point - processes text according to configuration and options
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/telegrama/formatter.rb', line 26 def self.format(text, = {}) # Merge defaults with any runtime overrides defaults = Telegrama.configuration. || {} opts = defaults.merge() text = text.to_s # Apply prefix and suffix if configured text = apply_prefix_suffix(text) # Apply HTML escaping first (always safe to do) text = escape_html(text) if opts[:escape_html] # Apply email obfuscation BEFORE markdown escaping to prevent double-escaping text = obfuscate_emails(text) if opts[:obfuscate_emails] # Handle Markdown escaping if opts[:escape_markdown] begin text = escape_markdown_v2(text) rescue MarkdownError => e # Log the error but continue with plain text begin Telegrama.log_error("Markdown formatting failed: #{e.}. Falling back to plain text.") rescue => _log_error # Ignore logging errors in tests end # Strip all markdown syntax to ensure plain text renders text = strip_markdown(text) # Force parse_mode to nil in the parent context Thread.current[:telegrama_parse_mode_override] = nil end end # Apply truncation last text = truncate(text, opts[:truncate]) if opts[:truncate] text end |
.html_to_telegram_markdown(html) ⇒ String
Convert HTML to Telegram MarkdownV2 format
554 555 556 557 558 559 560 561 562 563 564 565 |
# File 'lib/telegrama/formatter.rb', line 554 def self.html_to_telegram_markdown(html) # Convert HTML back to Telegram MarkdownV2 format # This is a simplified implementation - a real one would be more complex text = html.gsub(/<\/?p>/, "\n") .gsub(/<strong>(.*?)<\/strong>/, "*\\1*") .gsub(/<em>(.*?)<\/em>/, "_\\1_") .gsub(/<code>(.*?)<\/code>/, "`\\1`") .gsub(/<a href="(.*?)">(.*?)<\/a>/, "[\\2](\\1)") # Escape special characters outside of formatting tags escape_markdown_v2(text) end |
.obfuscate_emails(text) ⇒ String
Obfuscate email addresses in text
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'lib/telegrama/formatter.rb', line 570 def self.obfuscate_emails(text) # Precompile the email regex for better performance @@email_regex ||= /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ # Extract emails, obfuscate them, and insert them back emails = [] text = text.gsub(@@email_regex) do |email| emails << email "TELEGRAMA_EMAIL_PLACEHOLDER_#{emails.length - 1}" end # Replace placeholders with obfuscated emails emails.each_with_index do |email, index| local, domain = email.split('@') = local.length > 4 ? "#{local[0..2]}...#{local[-1]}" : "#{local[0]}..." = "#{}@#{domain}" # Replace the placeholder with the obfuscated email, ensuring no escapes in the domain text = text.gsub("TELEGRAMA_EMAIL_PLACEHOLDER_#{index}", ) end text end |
.strip_markdown(text) ⇒ String
Strip all markdown formatting for plain text delivery
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/telegrama/formatter.rb', line 520 def self.strip_markdown(text) result = text.dup # Remove markdown links [text](url) -> text result.gsub!(/\[([^\]]*)\]\([^)]*\)/, '\1') # Remove triple backtick code blocks (preserve content) result.gsub!(/```[a-z]*\n?(.*?)```/m, '\1') # Remove inline code backticks (preserve content) result.gsub!(/`([^`]*)`/, '\1') # Remove bold formatting (both ** and *) result.gsub!(/\*\*([^*]*)\*\*/, '\1') result.gsub!(/\*([^*]*)\*/, '\1') # Remove italic formatting (both __ and _) result.gsub!(/__([^_]*)__/, '\1') result.gsub!(/(?<![\\])_([^_]*)_/, '\1') # Remove strikethrough result.gsub!(/~~([^~]*)~~/, '\1') result.gsub!(/~([^~]*)~/, '\1') # Remove any remaining unmatched formatting characters at word boundaries # but preserve them in the middle of words (like file_name) result.gsub!(/(?<=\s)[*_~`]+|[*_~`]+(?=\s|$)/, '') result end |
.tokenize_and_format(text) ⇒ String
Tokenize and format the text using a state machine approach
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/telegrama/formatter.rb', line 103 def self.tokenize_and_format(text) # Special handling for links with the Markdown format [text](url) # Process only complete links to ensure incomplete links are handled by the state machine link_fixed_text = text.gsub(/\[([^\]]+)\]\(([^)]+)\)/) do |match| # Extract link text and URL text_part = $1 url_part = $2 # Handle escaping within link text text_part = text_part.gsub(LINK_TEXT_ESCAPE_REGEX) { |m| "\\#{m}" } # Escape special characters in URL (except parentheses which define URL boundaries) url_part = url_part.gsub(/([_*\[\]~`>#+=|{}.!\\])/) { |m| "\\#{m}" } # Rebuild the link with proper escaping "[#{text_part}](#{url_part})" end # Process the text with fixed links using tokenizer tokenizer = MarkdownTokenizer.new(link_fixed_text) tokenizer.process end |
.truncate(text, max_length) ⇒ String
Truncate text to a maximum length
608 609 610 611 |
# File 'lib/telegrama/formatter.rb', line 608 def self.truncate(text, max_length) return text if !max_length || text.length <= max_length text[0, max_length] end |