From 7dd22196db4afa25aea6688baa9e74b578c1eacb Mon Sep 17 00:00:00 2001 From: Johannes Opper Date: Tue, 20 Jan 2026 11:55:30 +0100 Subject: [PATCH] Split emphasis markers at paragraph breaks, merge heading lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When

(or other paragraph breaks) occur inside or , the markdown markers must be split so they don't span across breaks. Otherwise, markdown renderers fail to apply the formatting. Before: hello

world
→ **hello \n \nworld** After: hello

world
→ **hello** \n \n**world** For headings, line breaks are merged into a single line since markdown headings can't span multiple lines. Before:

foo
bar

→ # foo \nbar (bar loses heading) After:

foo
bar

→ # foo bar (all content in heading) Added wrap_with_markers helper in Base class for reuse. Fixes #95 Co-Authored-By: Claude Opus 4.5 --- lib/reverse_markdown/converters/base.rb | 20 +++++++++++++ lib/reverse_markdown/converters/em.rb | 2 +- lib/reverse_markdown/converters/h.rb | 5 +++- lib/reverse_markdown/converters/strong.rb | 2 +- .../reverse_markdown/converters/em_spec.rb | 28 +++++++++++++++++++ .../lib/reverse_markdown/converters/h_spec.rb | 16 +++++++++++ .../converters/strong_spec.rb | 8 ++++++ 7 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 spec/lib/reverse_markdown/converters/em_spec.rb create mode 100644 spec/lib/reverse_markdown/converters/h_spec.rb diff --git a/lib/reverse_markdown/converters/base.rb b/lib/reverse_markdown/converters/base.rb index cfdf7c9..1d1e4fe 100644 --- a/lib/reverse_markdown/converters/base.rb +++ b/lib/reverse_markdown/converters/base.rb @@ -15,6 +15,26 @@ def escape_keychars(string) string.gsub(/(? '\*', '_' => '\_') end + # Wrap content with markers (e.g., ** or _), splitting at paragraph breaks + # so markers don't span across breaks (which breaks markdown rendering) + def wrap_with_markers(content, marker) + # Split on paragraph breaks, preserving the breaks + segments = content.split(/(\s*\n\s*\n\s*)/) + + segments.map.with_index do |segment, i| + if i.odd? # This is a break segment (captured delimiter) + segment + elsif segment.strip.empty? + segment + else + # Wrap with markers, preserving border whitespace + leading = segment[/\A\s*/] + trailing = segment[/\s*\z/] + "#{leading}#{marker}#{segment.strip}#{marker}#{trailing}" + end + end.join + end + def extract_title(node) title = escape_keychars(node['title'].to_s) title.empty? ? '' : %[ "#{title}"] diff --git a/lib/reverse_markdown/converters/em.rb b/lib/reverse_markdown/converters/em.rb index e31582a..5d7167a 100644 --- a/lib/reverse_markdown/converters/em.rb +++ b/lib/reverse_markdown/converters/em.rb @@ -6,7 +6,7 @@ def convert(node, state = {}) if content.strip.empty? || state[:already_italic] content else - "#{content[/^\s*/]}_#{content.strip}_#{content[/\s*$/]}" + wrap_with_markers(content, '_') end end end diff --git a/lib/reverse_markdown/converters/h.rb b/lib/reverse_markdown/converters/h.rb index 1aa50c3..bf929aa 100644 --- a/lib/reverse_markdown/converters/h.rb +++ b/lib/reverse_markdown/converters/h.rb @@ -3,7 +3,10 @@ module Converters class H < Base def convert(node, state = {}) prefix = '#' * node.name[/\d/].to_i - ["\n", prefix, ' ', treat_children(node, state), "\n"].join + content = treat_children(node, state).strip + # Merge lines into one (markdown headings can't span multiple lines) + content = content.split(/\s*\n\s*/).join(' ') + "\n#{prefix} #{content}\n" end end diff --git a/lib/reverse_markdown/converters/strong.rb b/lib/reverse_markdown/converters/strong.rb index b513096..6993939 100644 --- a/lib/reverse_markdown/converters/strong.rb +++ b/lib/reverse_markdown/converters/strong.rb @@ -6,7 +6,7 @@ def convert(node, state = {}) if content.strip.empty? || state[:already_strong] content else - "#{content[/^\s*/]}**#{content.strip}**#{content[/\s*$/]}" + wrap_with_markers(content, '**') end end end diff --git a/spec/lib/reverse_markdown/converters/em_spec.rb b/spec/lib/reverse_markdown/converters/em_spec.rb new file mode 100644 index 0000000..ab98c2e --- /dev/null +++ b/spec/lib/reverse_markdown/converters/em_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ReverseMarkdown::Converters::Em do + let(:converter) { ReverseMarkdown::Converters::Em.new } + + it 'returns an empty string if the node is empty' do + input = node_for('') + expect(converter.convert(input)).to eq '' + end + + it 'returns just the content if the em tag is nested in another em' do + input = node_for('foo') + expect(converter.convert(input.children.first, already_italic: true)).to eq 'foo' + end + + it 'moves border whitespaces outside of the delimiters tag' do + input = node_for(" \n foo ") + expect(converter.convert(input)).to eq " _foo_ " + end + + it 'splits markers at paragraph breaks' do + # Issue #95:

inside em creates a paragraph break + # Markers must be split so markdown renders correctly + result = ReverseMarkdown.convert('hello

world
') + expect(result).to include('_hello_') + expect(result).to include('_world_') + end +end diff --git a/spec/lib/reverse_markdown/converters/h_spec.rb b/spec/lib/reverse_markdown/converters/h_spec.rb new file mode 100644 index 0000000..9f87090 --- /dev/null +++ b/spec/lib/reverse_markdown/converters/h_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ReverseMarkdown::Converters::H do + let(:converter) { ReverseMarkdown::Converters::H.new } + + it 'merges line breaks into single line' do + # Markdown headings can't span multiple lines, so merge them + result = ReverseMarkdown.convert('

foo
bar

') + expect(result.strip).to eq '# foo bar' + end + + it 'handles multiple line breaks' do + result = ReverseMarkdown.convert('

a
b
c

') + expect(result.strip).to eq '## a b c' + end +end diff --git a/spec/lib/reverse_markdown/converters/strong_spec.rb b/spec/lib/reverse_markdown/converters/strong_spec.rb index ecee4e0..6d156ac 100644 --- a/spec/lib/reverse_markdown/converters/strong_spec.rb +++ b/spec/lib/reverse_markdown/converters/strong_spec.rb @@ -17,4 +17,12 @@ input = node_for(" \n foo ") expect(converter.convert(input)).to eq " **foo** " end + + it 'splits markers at paragraph breaks' do + # Issue #95:

inside strong creates a paragraph break + # Markers must be split so markdown renders correctly + result = ReverseMarkdown.convert('hello

world
') + expect(result).to include('**hello**') + expect(result).to include('**world**') + end end