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
This commit is contained in:
dualfroz committed 2026-10-02 16:19:40 +02:00
1 parent 6b7a074e8d
commit 1bb05d0606
2 files changed
+29 -3

No files matched your search

+25 -3
View File
@@ -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 <code> and <pre> 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. '<h3>Title</h3>' with the new $markdown_node e.g. '### Title'
$element->setFinalMarkdown($markdown);
return $markdown;
}
/**
+4
View File
@@ -134,6 +134,10 @@ class HtmlConverterTest extends TestCase
public function testImage(): void
{
$this->assertHtmlGivesMarkdown('<img src="/path/img.jpg" alt="alt text" title="Title" />', '![alt text](/path/img.jpg "Title")');
$this->assertHtmlGivesMarkdown("<img src=\"a.png\" />\n<h3>A new header</h3>", "![](a.png)\n\n### A new header");
$this->assertHtmlGivesMarkdown('<img src="a.png" /><p>Paragraph</p>', "![](a.png)\n\nParagraph");
$this->assertHtmlGivesMarkdown('<a href="b.html"><img src="a.png" /></a><h3>A new header</h3>', "[![](a.png)](b.html)\n\n### A new header");
$this->assertHtmlGivesMarkdown('<blockquote><img src="a.png" /><p>Paragraph</p></blockquote>', "> ![](a.png)\n> \n> Paragraph");
}
public function testAnchor(): void