fix: Preserve double newlines in text-based messaging channels (#13055)

## Summary

Fixes the issue where double newlines (paragraph breaks) were collapsing
to single newlines in text-based messaging channels (Telegram, WhatsApp,
Instagram, Facebook, LINE, SMS).

### Root Cause

The `preserve_multiple_newlines` method only preserved 3+ consecutive
newlines using the regex `/\n{3,}/`. When users pressed Enter twice
(creating a paragraph break with 2 newlines), CommonMarker would parse
this as separate paragraphs, which then collapsed to a single newline in
the output.

This caused:
-  Normal Enter: Double newlines collapsed to single newline
-  Shift+Enter: Worked (created hard breaks)

### Fix

Changed the regex from `/\n{3,}/` to `/\n{2,}/` to preserve 2+
consecutive newlines. This prevents CommonMarker from collapsing
paragraph breaks.

Now:
-  Single newline (`\n`) → Single newline (handled by softbreak)
-  Double newline (`\n\n`) → Double newline (preserved with
placeholders)
-  Triple+ newlines → Preserved as before

### Test Coverage

Added comprehensive tests for:
- Single newlines preservation
- Double newlines (paragraph breaks) preservation  
- Multiple consecutive newlines
- Newlines with varying amounts of whitespace between them (1 space, 3
spaces, 5 spaces, tabs)

All 66 tests passing.

### Impact

This fix affects all text-based messaging channels that use the markdown
renderer:
- Telegram
- WhatsApp
- Instagram  
- Facebook
- LINE
- SMS
- Twilio SMS (when configured for WhatsApp)

Fixes
https://linear.app/chatwoot/issue/CW-6135/double-newline-is-breaking

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Vinay Keerthi
2025-12-12 16:35:53 +05:30
committed by GitHub
parent 96fe3e146d
commit a8ed074bf0
2 changed files with 31 additions and 4 deletions

View File

@@ -96,10 +96,10 @@ class Messages::MarkdownRendererService
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+
# Preserve multiple consecutive newlines (2+) by replacing them with placeholders
# Standard markdown treats 2 newlines as paragraph break which collapses to 1 newline, we preserve 2+
def preserve_multiple_newlines(content)
content.gsub(/\n{3,}/) do |match|
content.gsub(/\n{2,}/) do |match|
"{{PRESERVE_#{match.length}_NEWLINES}}"
end
end

View File

@@ -241,10 +241,37 @@ RSpec.describe Messages::MarkdownRendererService, type: :service do
expect(result).to include('<a href="https://example.com">link text</a>')
end
it 'preserves newlines' do
it 'preserves single newlines' do
content = "line 1\nline 2"
result = described_class.new(content, channel_type).render
expect(result).to include("\n")
expect(result).to include("line 1\nline 2")
end
it 'preserves double newlines (paragraph breaks)' do
content = "para 1\n\npara 2"
result = described_class.new(content, channel_type).render
expect(result.scan("\n").count).to eq(2)
expect(result).to include("para 1\n\npara 2")
end
it 'preserves multiple consecutive newlines' do
content = "para 1\n\n\n\npara 2"
result = described_class.new(content, channel_type).render
expect(result.scan("\n").count).to eq(4)
expect(result).to include("para 1\n\n\n\npara 2")
end
it 'preserves newlines with varying amounts of whitespace between them' do
# Test with 1 space, 3 spaces, 5 spaces, and tabs to ensure it handles any amount of whitespace
content = "hello\n \n \n \n\t\nworld"
result = described_class.new(content, channel_type).render
# Whitespace-only lines are normalized, so we should have at least 5 newlines preserved
expect(result.scan("\n").count).to be >= 5
expect(result).to include('hello')
expect(result).to include('world')
# Should not collapse to just 1-2 newlines
expect(result.scan("\n").count).to be > 3
end
it 'converts strikethrough to HTML' do