## Description Fixes an issue where multiple newlines with whitespace between them (e.g., `\n \n \n`) were being collapsed to single newlines in text-based messaging channels (Telegram, WhatsApp, Instagram, Facebook, Line, SMS). The frontend was sending messages with spaces/tabs between newlines, and the markdown renderer was treating these as paragraph content, collapsing them during rendering. ### Changes: 1. Added whitespace normalization in `render_telegram_html`, `render_whatsapp`, `render_instagram`, `render_line`, and `render_plain_text` methods 2. Strips whitespace from whitespace-only lines before markdown processing 3. Added comprehensive regression tests for all affected channels ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? 1. **Unit Tests**: Added 7 new specs testing multiple newlines with whitespace between them for all text-based channels 2. **Manual Testing**: Verified with actual frontend payload containing `\n \n \n` patterns 3. **Regression Testing**: All existing 63 specs pass ### Test Results: - ✅ All 63 markdown renderer specs pass (56 original + 7 new) - ✅ All 12 Telegram channel specs pass - ✅ All 27 WhatsApp + Instagram specs pass - ✅ Verified with real-world payload: 18 newlines preserved (previously collapsed to 1) ### Test Command: ```bash RAILS_ENV=test bundle exec rspec spec/services/messages/markdown_renderer_service_spec.rb RAILS_ENV=test bundle exec rspec spec/models/channel/telegram_spec.rb ``` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes
114 lines
4.3 KiB
Ruby
114 lines
4.3 KiB
Ruby
class Messages::MarkdownRendererService
|
|
CHANNEL_RENDERERS = {
|
|
'Channel::Email' => :render_html,
|
|
'Channel::WebWidget' => :render_html,
|
|
'Channel::Telegram' => :render_telegram_html,
|
|
'Channel::Whatsapp' => :render_whatsapp,
|
|
'Channel::FacebookPage' => :render_instagram,
|
|
'Channel::Instagram' => :render_instagram,
|
|
'Channel::Line' => :render_line,
|
|
'Channel::TwitterProfile' => :render_plain_text,
|
|
'Channel::Sms' => :render_plain_text,
|
|
'Channel::TwilioSms' => :render_plain_text
|
|
}.freeze
|
|
|
|
def initialize(content, channel_type, channel = nil)
|
|
@content = content
|
|
@channel_type = channel_type
|
|
@channel = channel
|
|
end
|
|
|
|
def render
|
|
return @content if @content.blank?
|
|
|
|
renderer_method = CHANNEL_RENDERERS[effective_channel_type]
|
|
renderer_method ? send(renderer_method) : @content
|
|
end
|
|
|
|
private
|
|
|
|
def effective_channel_type
|
|
# For Twilio SMS channel, check if it's actually WhatsApp
|
|
if @channel_type == 'Channel::TwilioSms' && @channel&.whatsapp?
|
|
'Channel::Whatsapp'
|
|
else
|
|
@channel_type
|
|
end
|
|
end
|
|
|
|
def commonmarker_doc
|
|
@commonmarker_doc ||= CommonMarker.render_doc(@content, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
|
end
|
|
|
|
def render_html
|
|
markdown_renderer = BaseMarkdownRenderer.new
|
|
doc = CommonMarker.render_doc(@content, :DEFAULT, [:strikethrough])
|
|
markdown_renderer.render(doc)
|
|
end
|
|
|
|
def render_telegram_html
|
|
# Strip whitespace from whitespace-only lines to normalize newlines
|
|
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
|
content_with_preserved_newlines = preserve_multiple_newlines(normalized_content)
|
|
renderer = Messages::MarkdownRenderers::TelegramRenderer.new
|
|
doc = CommonMarker.render_doc(content_with_preserved_newlines, [:STRIKETHROUGH_DOUBLE_TILDE], [:strikethrough])
|
|
result = renderer.render(doc).gsub(/\n+\z/, '')
|
|
restore_multiple_newlines(result)
|
|
end
|
|
|
|
def render_whatsapp
|
|
# Strip whitespace from whitespace-only lines to normalize newlines
|
|
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
|
content_with_preserved_newlines = preserve_multiple_newlines(normalized_content)
|
|
renderer = Messages::MarkdownRenderers::WhatsAppRenderer.new
|
|
doc = CommonMarker.render_doc(content_with_preserved_newlines, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
|
result = renderer.render(doc).gsub(/\n+\z/, '')
|
|
restore_multiple_newlines(result)
|
|
end
|
|
|
|
def render_instagram
|
|
# Strip whitespace from whitespace-only lines to normalize newlines
|
|
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
|
content_with_preserved_newlines = preserve_multiple_newlines(normalized_content)
|
|
renderer = Messages::MarkdownRenderers::InstagramRenderer.new
|
|
doc = CommonMarker.render_doc(content_with_preserved_newlines, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
|
result = renderer.render(doc).gsub(/\n+\z/, '')
|
|
restore_multiple_newlines(result)
|
|
end
|
|
|
|
def render_line
|
|
# Strip whitespace from whitespace-only lines to normalize newlines
|
|
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
|
content_with_preserved_newlines = preserve_multiple_newlines(normalized_content)
|
|
renderer = Messages::MarkdownRenderers::LineRenderer.new
|
|
doc = CommonMarker.render_doc(content_with_preserved_newlines, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
|
result = renderer.render(doc).gsub(/\n+\z/, '')
|
|
restore_multiple_newlines(result)
|
|
end
|
|
|
|
def render_plain_text
|
|
# Strip whitespace from whitespace-only lines to normalize newlines
|
|
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
|
content_with_preserved_newlines = preserve_multiple_newlines(normalized_content)
|
|
renderer = Messages::MarkdownRenderers::PlainTextRenderer.new
|
|
doc = CommonMarker.render_doc(content_with_preserved_newlines, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
|
result = renderer.render(doc).gsub(/\n+\z/, '')
|
|
restore_multiple_newlines(result)
|
|
end
|
|
|
|
# Preserve multiple consecutive newlines (3+) by replacing them with placeholders
|
|
# Standard markdown treats 2 newlines as paragraph break, we preserve 3+
|
|
def preserve_multiple_newlines(content)
|
|
content.gsub(/\n{3,}/) do |match|
|
|
"{{PRESERVE_#{match.length}_NEWLINES}}"
|
|
end
|
|
end
|
|
|
|
# Restore multiple newlines from placeholders
|
|
def restore_multiple_newlines(content)
|
|
content.gsub(/\{\{PRESERVE_(\d+)_NEWLINES\}\}/) do |_match|
|
|
"\n" * Regexp.last_match(1).to_i
|
|
end
|
|
end
|
|
end
|