diff --git a/src/Node/Inline/SoftBreakNode.php b/src/Node/Inline/SoftBreakNode.php
new file mode 100644
index 0000000..798bffa
--- /dev/null
+++ b/src/Node/Inline/SoftBreakNode.php
@@ -0,0 +1,11 @@
+ $token) {
- $inlineNodes = $this->inlineParser->parse($token->content, $this->linkRefs, $this->footnoteDefs);
- array_push($result, ...$inlineNodes);
-
- if ($idx < $lastIdx) {
- // Hard break on last token is stripped per CommonMark §6.7.
- $result[] = ($token->meta['hard_break'] ?? false) === true
- ? new HardBreakNode()
- : new TextNode(' ');
+ $content = preg_replace('/^(?:[ ]{1,3}|\t)/', '', $token->content) ?? $token->content;
+ $isHardBreak = ($token->meta['hard_break'] ?? false) === true;
+ $isLast = ($idx === $lastIdx);
+
+ if (!$isLast && !$isHardBreak) {
+ $content = rtrim($content, ' ');
+ }
+
+ $group[] = $content;
+
+ if ($isHardBreak || $isLast) {
+ $nodes = $this->inlineParser->parse(implode("\n", $group), $this->linkRefs, $this->footnoteDefs);
+ array_push($result, ...$nodes);
+ $group = [];
+
+ if ($isHardBreak && !$isLast) {
+ $result[] = new HardBreakNode();
+ }
}
}
diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php
index aa832bb..21309f0 100644
--- a/src/Renderer/HtmlRenderer.php
+++ b/src/Renderer/HtmlRenderer.php
@@ -26,6 +26,7 @@
use PhpMarkdown\Node\Inline\EmphasisNode;
use PhpMarkdown\Node\Inline\FootnoteRefNode;
use PhpMarkdown\Node\Inline\HardBreakNode;
+use PhpMarkdown\Node\Inline\SoftBreakNode;
use PhpMarkdown\Node\Inline\HtmlEntityNode;
use PhpMarkdown\Node\Inline\ImageNode;
use PhpMarkdown\Node\Inline\LinkNode;
@@ -76,6 +77,7 @@ private function renderNode(NodeInterface $node): string
? $this->sanitizer->sanitize($node->content) . "\n"
: $this->esc($node->content) . "\n",
$node instanceof HardBreakNode => "
\n",
+ $node instanceof SoftBreakNode => "\n",
// HTML entities pass through verbatim — validated by InlineParser, no esc() needed.
$node instanceof HtmlEntityNode => $node->entity,
$node instanceof TextNode => $this->esc($node->text),
diff --git a/tests/Integration/MarkdownParserTest.php b/tests/Integration/MarkdownParserTest.php
index 8070614..a4e7f4c 100644
--- a/tests/Integration/MarkdownParserTest.php
+++ b/tests/Integration/MarkdownParserTest.php
@@ -493,9 +493,9 @@ public function testBackslashLineBreakProducesBr(): void
public function testNoTrailingSpacesProducesSoftBreak(): void
{
- // Without trailing spaces the lines are soft-joined with a space.
+ // Without trailing spaces the lines are soft-joined with a newline (CommonMark §6.7).
$html = $this->parser->parse("foo\nbar");
- $this->assertSame("
foo bar
\n", $html); + $this->assertSame("foo\nbar
\n", $html); } public function testHardBreakInsideInlineContent(): void @@ -560,9 +560,9 @@ public function testBlockquoteLineWithTrailingSpacesNoHardBreak(): void public function testSoftBreakBetweenParagraphLinesUnchanged(): void { - // Pre-existing soft-break behavior must not regress. + // Soft-break renders as newline per CommonMark §6.7. $html = $this->parser->parse("line one\nline two\nline three"); - $this->assertSame("line one line two line three
\n", $html); + $this->assertSame("line one\nline two\nline three
\n", $html); } public function testBlankLineSeparatesParagraphs(): void diff --git a/tests/Integration/fixtures/hard-line-breaks.html b/tests/Integration/fixtures/hard-line-breaks.html index 2c6fa4c..8dcefa8 100644 --- a/tests/Integration/fixtures/hard-line-breaks.html +++ b/tests/Integration/fixtures/hard-line-breaks.html @@ -2,7 +2,8 @@ barfoo
bar
foo bar
+foo +bar
bold
text
a
diff --git a/tests/Unit/Renderer/SoftLineBreakTest.php b/tests/Unit/Renderer/SoftLineBreakTest.php
new file mode 100644
index 0000000..6351338
--- /dev/null
+++ b/tests/Unit/Renderer/SoftLineBreakTest.php
@@ -0,0 +1,91 @@
+parser = new MarkdownParser();
+ }
+
+ private function parseAndRender(string $markdown): string
+ {
+ return $this->parser->parse($markdown);
+ }
+
+ public function testSingleNewlineInParagraphPreservedAsSoftBreak(): void
+ {
+ $this->assertSame("
foo\nbaz
\n", $this->parseAndRender("foo\nbaz")); + } + + public function testTrailingSpacesBeforeNewlineStrippedNotHardBreak(): void + { + $this->assertSame("foo
\nbaz
foo\nbaz
\n", $this->parseAndRender("foo \nbaz")); + } + + public function testTwoLineParagraphPreservesInternalNewline(): void + { + $this->assertSame("aaa\nbbb
\nccc\nddd
\n", $this->parseAndRender("aaa\nbbb\n\nccc\nddd")); + } + + public function testLeadingSpacesOnFirstLineStripped(): void + { + $this->assertSame("aaa\nbbb
\n", $this->parseAndRender(" aaa\nbbb")); + } + + public function testLeadingSpacesOnContinuationLineStripped(): void + { + $this->assertSame("aaa\nbbb
\n", $this->parseAndRender(" aaa\n bbb")); + } + + public function testEmphasisSpanningSoftLineBreak(): void + { + $this->assertSame("foo\nbar
\n", $this->parseAndRender("*foo\nbar*")); + } + + public function testStrongEmphasisSpanningSoftLineBreak(): void + { + $this->assertSame("foo\nbar
\n", $this->parseAndRender("**foo\nbar**")); + } + + public function testTabBeforeContinuationTextStripped(): void + { + $this->assertSame("foo\nbaz
\n", $this->parseAndRender("foo\n\tbaz")); + } + + public function testSoftBreakNodeRendersAsNewlineNotSpace(): void + { + $renderer = new HtmlRenderer(); + $document = new DocumentNode([ + new ParagraphNode([new SoftBreakNode()]), + ]); + $this->assertSame("\n
\n", $renderer->render($document)); + } + + public function testHardBreakWithTwoTrailingSpacesUnaffectedBySoftBreak(): void + { + $this->assertSame("foo
\nbar
foo bar
\n", $this->parseAndRender("foo bar")); + } +}