From 1bb05d0606b50a5c673db247519fd97f636354b0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 2 Oct 2026 16:19:40 +0200 Subject: [PATCH] fix: separate block elements from the inline content before them An image or link right before a heading or paragraph ended up on the same line, e.g. `![](a.png)### A`. Fixes #259 --- src/HtmlConverter.php | 28 +++++++++++++++++++++++++--- tests/HtmlConverterTest.php | 4 ++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/HtmlConverter.php b/src/HtmlConverter.php index 5ce8e5b..431d98c 100644 --- a/src/HtmlConverter.php +++ b/src/HtmlConverter.php @@ -16,6 +16,11 @@ namespace League\HTMLToMarkdown; */ class HtmlConverter implements HtmlConverterInterface { + /** + * Blocks which start a new paragraph when they come right after inline content + */ + private const SEPARATED_BLOCK_TAGS = ['blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'p', 'pre']; + /** @var Environment */ protected $environment; @@ -173,13 +178,17 @@ class HtmlConverter implements HtmlConverterInterface * * Finds children of each node and convert those to #text nodes containing their Markdown equivalent, * starting with the innermost element and working up to the outermost element. + * + * @param bool $followsInlineContent Whether the element comes right after inline content, like an image, on the same line + * + * @return string The Markdown the element was replaced with */ - private function convertChildren(ElementInterface $element): void + private function convertChildren(ElementInterface $element, bool $followsInlineContent = false): string { // Don't convert HTML code inside and
 blocks to Markdown - that should stay as HTML
         // except if the current node is a code tag, which needs to be converted by the CodeConverter.
         if ($element->isDescendantOf(['pre', 'code']) && $element->getTagName() !== 'code') {
-            return;
+            return '';
         }
 
         // Give converter a chance to inspect/modify the DOM before children are converted
@@ -190,18 +199,31 @@ class HtmlConverter implements HtmlConverterInterface
 
         // If the node has children, convert those to Markdown first
         if ($element->hasChildren()) {
+            $childFollowsInlineContent = false;
             foreach ($element->getChildren() as $child) {
-                $this->convertChildren($child);
+                $childMarkdown = $this->convertChildren($child, $childFollowsInlineContent);
+                if ($child->getTagName() === '#comment' || \trim($childMarkdown, ' ') === '') {
+                    continue;
+                }
+
+                $childFollowsInlineContent = ! $child->isBlock() && \substr(\rtrim($childMarkdown, ' '), -1) !== "\n";
             }
         }
 
         // Now that child nodes have been converted, convert the original node
         $markdown = $this->convertToMarkdown($element);
 
+        // These blocks only add newlines after themselves, so one that follows inline content, like an image, needs them before it too
+        if ($followsInlineContent && \in_array($element->getTagName(), self::SEPARATED_BLOCK_TAGS, true) && \trim($markdown) !== '') {
+            $markdown = "\n\n" . $markdown;
+        }
+
         // Create a DOM text node containing the Markdown equivalent of the original node
 
         // Replace the old $node e.g. '

Title

' with the new $markdown_node e.g. '### Title' $element->setFinalMarkdown($markdown); + + return $markdown; } /** diff --git a/tests/HtmlConverterTest.php b/tests/HtmlConverterTest.php index e62247e..816a491 100644 --- a/tests/HtmlConverterTest.php +++ b/tests/HtmlConverterTest.php @@ -134,6 +134,10 @@ class HtmlConverterTest extends TestCase public function testImage(): void { $this->assertHtmlGivesMarkdown('alt text', '![alt text](/path/img.jpg "Title")'); + $this->assertHtmlGivesMarkdown("\n

A new header

", "![](a.png)\n\n### A new header"); + $this->assertHtmlGivesMarkdown('

Paragraph

', "![](a.png)\n\nParagraph"); + $this->assertHtmlGivesMarkdown('

A new header

', "[![](a.png)](b.html)\n\n### A new header"); + $this->assertHtmlGivesMarkdown('

Paragraph

', "> ![](a.png)\n> \n> Paragraph"); } public function testAnchor(): void