From 89d02e2c92d42c4e8f531a5ab1983e86679fe5b3 Mon Sep 17 00:00:00 2001 From: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:44:16 +0530 Subject: [PATCH] fix: Preserve multiple newlines with whitespace in text-based messaging channels (#13044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .../messages/markdown_renderer_service.rb | 24 +++++++++---- .../markdown_renderer_service_spec.rb | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app/services/messages/markdown_renderer_service.rb b/app/services/messages/markdown_renderer_service.rb index 8984374d1..a90cfca06 100644 --- a/app/services/messages/markdown_renderer_service.rb +++ b/app/services/messages/markdown_renderer_service.rb @@ -47,13 +47,19 @@ class Messages::MarkdownRendererService 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, [:STRIKETHROUGH_DOUBLE_TILDE], [:strikethrough]) - renderer.render(doc).gsub(/\n+\z/, '') + 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 - content_with_preserved_newlines = preserve_multiple_newlines(@content) + # 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/, '') @@ -61,7 +67,9 @@ class Messages::MarkdownRendererService end def render_instagram - content_with_preserved_newlines = preserve_multiple_newlines(@content) + # 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/, '') @@ -69,7 +77,9 @@ class Messages::MarkdownRendererService end def render_line - content_with_preserved_newlines = preserve_multiple_newlines(@content) + # 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/, '') @@ -77,7 +87,9 @@ class Messages::MarkdownRendererService end def render_plain_text - content_with_preserved_newlines = preserve_multiple_newlines(@content) + # 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/, '') diff --git a/spec/services/messages/markdown_renderer_service_spec.rb b/spec/services/messages/markdown_renderer_service_spec.rb index d589c6ad1..89210dc0e 100644 --- a/spec/services/messages/markdown_renderer_service_spec.rb +++ b/spec/services/messages/markdown_renderer_service_spec.rb @@ -426,5 +426,40 @@ RSpec.describe Messages::MarkdownRendererService, type: :service do expect(result).to eq(content) end end + + # Shared test for all text-based channels that preserve multiple newlines + # This tests the real-world scenario where frontend sends newlines with whitespace between them + context 'when content has multiple newlines with whitespace between them' do + # This mimics what frontends often send: newlines with spaces/tabs between them + let(:content_with_whitespace_newlines) { "hello \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\nhello wow" } + + %w[ + Channel::Telegram + Channel::Whatsapp + Channel::Instagram + Channel::FacebookPage + Channel::Line + Channel::Sms + ].each do |channel_type| + context "when channel is #{channel_type}" do + it 'normalizes whitespace-only lines and preserves multiple newlines' do + result = described_class.new(content_with_whitespace_newlines, channel_type).render + # Should preserve most of the newlines (at least 10+) + # The exact count may vary slightly by renderer, but should be significantly more than 1-2 + expect(result.scan("\n").count).to be >= 10 + # Should not collapse everything to just 1-2 newlines + expect(result.scan("\n").count).to be > 5 + end + end + end + + context 'when channel is Channel::TwilioSms with WhatsApp' do + it 'normalizes whitespace-only lines and preserves multiple newlines' do + channel = instance_double(Channel::TwilioSms, whatsapp?: true) + result = described_class.new(content_with_whitespace_newlines, 'Channel::TwilioSms', channel).render + expect(result.scan("\n").count).to be >= 10 + end + end + end end end