diff --git a/app/services/messages/webhook_content_normalizer.rb b/app/services/messages/webhook_content_normalizer.rb index b45f83ad0..5fa66fe9c 100644 --- a/app/services/messages/webhook_content_normalizer.rb +++ b/app/services/messages/webhook_content_normalizer.rb @@ -1,10 +1,11 @@ # Strips CommonMark hard line breaks from stored markdown source (backslash before newline). # ProseMirror / the dashboard editor emits this form so soft breaks survive as markdown; # webhook consumers expect plain newlines without a visible backslash (e.g. WhatsApp gateways). +# Also strips trailing newlines introduced by TipTap/ProseMirror trailing paragraph nodes. class Messages::WebhookContentNormalizer def self.normalize(text) return text if text.blank? - text.gsub(/\\\r?\n/, "\n") + text.gsub(/\\\r?\n/, "\n").sub(/(\r?\n)+\z/, '') end end diff --git a/spec/services/messages/webhook_content_normalizer_spec.rb b/spec/services/messages/webhook_content_normalizer_spec.rb new file mode 100644 index 000000000..ca5023b9d --- /dev/null +++ b/spec/services/messages/webhook_content_normalizer_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe Messages::WebhookContentNormalizer do + describe '.normalize' do + it 'returns nil unchanged' do + expect(described_class.normalize(nil)).to be_nil + end + + it 'returns blank string unchanged' do + expect(described_class.normalize('')).to eq('') + end + + it 'strips trailing newlines added by TipTap/ProseMirror' do + expect(described_class.normalize("hello\n\n\n")).to eq('hello') + end + + it 'preserves intentional trailing spaces' do + expect(described_class.normalize("hello \n\n")).to eq('hello ') + end + + it 'replaces CommonMark hard line breaks (backslash-newline) with plain newlines' do + expect(described_class.normalize("hello\\\nworld")).to eq("hello\nworld") + end + + it 'replaces CommonMark hard line breaks with CRLF with plain newlines' do + expect(described_class.normalize("hello\\\r\nworld")).to eq("hello\nworld") + end + + it 'preserves intentional internal newlines' do + expect(described_class.normalize("line one\nline two")).to eq("line one\nline two") + end + + it 'strips trailing CRLF newlines without leaving dangling carriage returns' do + expect(described_class.normalize("hello\r\n\r\n")).to eq('hello') + end + + it 'handles both hard line breaks and trailing newlines together' do + expect(described_class.normalize("hello\\\nworld\n\n\n")).to eq("hello\nworld") + end + end +end