From 32e7ac4461e51adb82638db299c4c787bf90b4dd Mon Sep 17 00:00:00 2001
From: jmcollin ' . $this->renderChildren($node->children) . ' ' . $this->renderChildren($node->children) . " ' . $this->renderChildren($inlinePart) . ' ' . $this->renderChildren($inlinePart) . " outer inner outer again outer inner outer again triple triple a b c d a b c d line one line two line one line two a c a c quoted plain paragraph quoted plain paragraph first second first second foo foo foo foo foo bar foo bar bold bold a a foo foo foo foo line one line two line three line one line two line three para one para two para one para two foo foo wrapper.
$html = $this->parser->parse(' hello hello This is a quote Another quote This is a quote Another quote Inline Inline <script>alert(1)</script> [xss](javascript:alert(1)) <script>alert(1)</script> [xss](javascript:alert(1)) Text with bold and italic. A link and Text with bold and italic. A link and bold text italic text also bold also italic bold text italic text also bold also italic foo foo foo bar bold a foo foo foo bar bold a A paragraph with bold and italic. blockquote A paragraph with bold and italic. blockquote outer inner outer again outer inner outer again [foo][missing] text [foo][missing] text foo ~~unclosed ~not~ foo ~~unclosed ~not~ <script>alert(1)</script> [xss](javascript:alert(1)) <b>bold attempt</b> <script>alert(1)</script> [xss](javascript:alert(1)) <b>bold attempt</b> ... (.*)<\/p>$/s', '$1', $html) ?? $html;
+ // Strip outer ... (.*)<\/p>\n?$/s', '$1', $html) ?? $html;
}
// ── URL autolinks ─────────────────────────────────────────────────────────
@@ -238,7 +238,7 @@ public function test_url_autolink_inside_emphasis(): void
$document = new DocumentNode([$para]);
$html = $this->renderer->render($document);
$this->assertSame(
- '
,
, ).
---
src/Renderer/HtmlRenderer.php | 31 ++--
tests/Integration/MarkdownParserTest.php | 60 +++----
tests/Integration/fixtures/blockquotes.html | 7 +-
tests/Integration/fixtures/code-blocks.html | 3 +-
tests/Integration/fixtures/columns-xss.html | 4 +-
tests/Integration/fixtures/columns.html | 10 +-
tests/Integration/fixtures/emphasis.html | 5 +-
.../fixtures/hard-line-breaks.html | 11 +-
tests/Integration/fixtures/headings.html | 7 +-
tests/Integration/fixtures/links-images.html | 4 +-
tests/Integration/fixtures/lists.html | 11 +-
tests/Integration/fixtures/mixed.html | 12 +-
.../fixtures/nested-blockquotes.html | 8 +-
.../Integration/fixtures/reference-links.html | 7 +-
.../Integration/fixtures/setext-headings.html | 9 +-
tests/Integration/fixtures/strikethrough.html | 6 +-
tests/Integration/fixtures/xss-injection.html | 4 +-
tests/Unit/AutolinkTest.php | 8 +-
tests/Unit/ColumnsRendererTest.php | 2 +-
tests/Unit/EmphasisDelimiterStackTest.php | 2 +-
tests/Unit/FencedCodeTildeTest.php | 28 ++--
tests/Unit/FootnoteTest.php | 41 ++---
tests/Unit/IndentedCodeBlockTest.php | 10 +-
tests/Unit/LinkAngleBracketUrlTest.php | 14 +-
tests/Unit/LinkRefAngleBracketUrlTest.php | 20 +--
tests/Unit/LinkRefTitleVariantsTest.php | 18 +--
tests/Unit/LinkTitleVariantsTest.php | 20 +--
tests/Unit/LooseTightListTest.php | 26 +--
tests/Unit/Renderer/XhtmlOutputFormatTest.php | 153 ++++++++++++++++++
tests/Unit/RendererEntityTest.php | 8 +-
tests/Unit/RendererTest.php | 62 +++----
tests/Unit/SetextHeadingMultilineTest.php | 2 +-
32 files changed, 424 insertions(+), 189 deletions(-)
create mode 100644 tests/Unit/Renderer/XhtmlOutputFormatTest.php
diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php
index 9b1717d..aa832bb 100644
--- a/src/Renderer/HtmlRenderer.php
+++ b/src/Renderer/HtmlRenderer.php
@@ -62,20 +62,20 @@ private function renderNode(NodeInterface $node): string
{
return match (true) {
$node instanceof HeadingNode => $this->renderHeading($node),
- $node instanceof ParagraphNode => '
' . $this->renderChildren($node->children) . '
',
+ $node instanceof ParagraphNode => '\n" . $this->renderChildren($node->children) . "
\n",
$node instanceof ListNode => $this->renderList($node),
$node instanceof ListItemNode => $this->renderListItem($node),
$node instanceof FencedCodeNode => $this->renderFencedCode($node),
- $node instanceof IndentedCodeNode => '
',
- $node instanceof HorizontalRuleNode => '' . $this->esc($node->content) . '
',
+ $node instanceof IndentedCodeNode => '
\n",
+ $node instanceof HorizontalRuleNode => "' . $this->esc($node->content) . "
\n",
$node instanceof ColumnsNode => $this->renderColumns($node),
// Raw HTML blocks: sanitize if allowRawHtml, else escape (XSS-safe default).
$node instanceof RawHtmlBlockNode =>
$this->allowRawHtml
- ? $this->sanitizer->sanitize($node->content)
- : $this->esc($node->content),
- $node instanceof HardBreakNode => '
',
+ ? $this->sanitizer->sanitize($node->content) . "\n"
+ : $this->esc($node->content) . "\n",
+ $node instanceof HardBreakNode => "
\n",
// HTML entities pass through verbatim — validated by InlineParser, no esc() needed.
$node instanceof HtmlEntityNode => $node->entity,
$node instanceof TextNode => $this->esc($node->text),
@@ -112,7 +112,7 @@ private function renderChildren(array $nodes): string
private function renderHeading(HeadingNode $node): string
{
$tag = 'h' . $node->level;
- return '<' . $tag . '>' . $this->renderChildren($node->children) . '' . $tag . '>';
+ return '<' . $tag . '>' . $this->renderChildren($node->children) . '' . $tag . ">\n";
}
private function renderList(ListNode $node): string
@@ -122,7 +122,7 @@ private function renderList(ListNode $node): string
foreach ($node->children as $item) {
$inner .= $this->renderListItem($item, $node->loose);
}
- return '<' . $tag . '>' . $inner . '' . $tag . '>';
+ return '<' . $tag . ">\n" . $inner . '' . $tag . ">\n";
}
private function renderListItem(ListItemNode $node, bool $loose = false): string
@@ -130,10 +130,13 @@ private function renderListItem(ListItemNode $node, bool $loose = false): string
$content = $this->renderListItemContent($node->children, $loose);
if ($node->checked === null) {
- return '
';
+ return '' . $this->esc($node->content) . '
\n";
}
private function renderLink(LinkNode $node): string
@@ -205,7 +208,7 @@ private function renderImage(ImageNode $node): string
$titleAttr = $node->title !== null
? ' title="' . $this->esc($node->title) . '"'
: '';
- return '' . $this->esc($node->content) . "';
+ return '
';
}
private function renderAutolink(AutolinkNode $node): string
diff --git a/tests/Integration/MarkdownParserTest.php b/tests/Integration/MarkdownParserTest.php
index 7f0cf96..8070614 100644
--- a/tests/Integration/MarkdownParserTest.php
+++ b/tests/Integration/MarkdownParserTest.php
@@ -74,7 +74,7 @@ public function testFullDocumentPipeline(): void
$this->assertStringContainsString('bold', $html);
$this->assertMatchesRegularExpression('/
\s*
', $html);
- $this->assertStringContainsString('
', $html);
+ $this->assertStringContainsString('
', $html);
}
// ── Guard: input validation ───────────────────────────────────────────────
@@ -238,7 +238,7 @@ public function testSimpleNestedList(): void
$md = "- a\n - b\n - c\n- d";
$html = $this->parser->parse($md);
- $this->assertSame('
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testThreeLevelNesting(): void
@@ -246,7 +246,7 @@ public function testThreeLevelNesting(): void
$md = "- a\n - b\n - c";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testMixedNestingTypes(): void
@@ -254,7 +254,7 @@ public function testMixedNestingTypes(): void
$md = "1. first\n - nested\n2. second";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n\n
\n
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testFlatListUnchanged(): void
@@ -262,7 +262,7 @@ public function testFlatListUnchanged(): void
$md = "- a\n- b\n- c";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testInlineContentInNestedItem(): void
@@ -271,7 +271,7 @@ public function testInlineContentInNestedItem(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '
',
+ "\n
\n",
$html
);
}
@@ -282,7 +282,7 @@ public function testLargeDepthGapCollapsesToDirectNesting(): void
$md = "- top\n" . str_repeat(' ', 20) . "- deep";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testUlFollowedByOlAtSameDepthProducesTwoSeparateLists(): void
@@ -292,7 +292,7 @@ public function testUlFollowedByOlAtSameDepthProducesTwoSeparateLists(): void
$md = "- ul item\n1. ol item";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n
', $html);
+ $this->assertSame("\n
\n\n
\n", $html);
}
public function testDepthCapAt32DoesNotCrashAndProducesTwoLists(): void
@@ -325,7 +325,7 @@ public function testNestedBlockquoteMixedLevels(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '
',
+ "\n
\n",
$html,
);
}
@@ -336,7 +336,7 @@ public function testThreeLevelBlockquote(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '\n
\n
',
+ "\n
\n",
$html,
);
}
@@ -347,7 +347,7 @@ public function testLevelDecreaseThenIncrease(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '\n
\n\n
\n
',
+ "\n
\n",
$html,
);
}
@@ -379,7 +379,7 @@ public function testMultiLineSameLevelJoinedWithSpace(): void
$md = "> line one\n> line two";
$html = $this->parser->parse($md);
- $this->assertSame('\n
\n\n
\n
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testLevelSkipOneToThree(): void
@@ -389,7 +389,7 @@ public function testLevelSkipOneToThree(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '
',
+ "\n
\n",
$html,
);
}
@@ -401,7 +401,7 @@ public function testBlockquoteAdjacentToParagraph(): void
$html = $this->parser->parse($md);
$this->assertSame(
- '\n
\n\n
\n\n
\n
',
+ "\n
\n\n
\n",
$html,
);
}
@@ -426,13 +426,13 @@ public function testBlankLineBetweenBlockquoteLevelsSeparatesNodes(): void
public function testCheckedTaskItemRendersCheckbox(): void
{
$html = $this->parser->parse('- [x] Done');
- $this->assertSame('
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testUncheckedTaskItemRendersCheckbox(): void
{
$html = $this->parser->parse('- [ ] Todo');
- $this->assertSame('
', $html);
+ $this->assertSame("\n
\n", $html);
}
public function testMixedTaskList(): void
@@ -481,49 +481,49 @@ public function testTwoTrailingSpacesProducesBr(): void
{
// Two trailing spaces before \n must become
not a space.
$html = $this->parser->parse("foo \nbar");
- $this->assertSame('
bar
\nbar
.
$html = $this->parser->parse("foo\\\nbar");
- $this->assertSame('
bar
\nbar
text
\ntext
b
c
\nb
\nc
.
$html = $this->parser->parse("foo \nbar");
- $this->assertSame('
bar
\nbar
(CommonMark §6.7).
$html = $this->parser->parse("foo ");
- $this->assertSame('
', $html);
+ $this->assertStringContainsString('
', $html);
}
public function testFencedCodeInnerLineWithTrailingSpacesIsNotHardBreak(): void
@@ -562,14 +562,14 @@ public function testSoftBreakBetweenParagraphLinesUnchanged(): void
{
// Pre-existing soft-break behavior must not regress.
$html = $this->parser->parse("line one\nline two\nline three");
- $this->assertSame('
bar
\nbar
\ No newline at end of file
+
+
+
+
diff --git a/tests/Integration/fixtures/code-blocks.html b/tests/Integration/fixtures/code-blocks.html
index 2df855b..985e6be 100644
--- a/tests/Integration/fixtures/code-blocks.html
+++ b/tests/Integration/fixtures/code-blocks.html
@@ -1 +1,2 @@
-echo 'hello';code here.
+echo 'hello';code here.Left column
Right column
inline code.Left column
+
+
+Right column
+inline code.
bar
bar
text
b
c
+bar
+bar
+text
+b
+cHeading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
\ No newline at end of file
+Heading 1
+Heading 2
+Heading 3
+Heading 4
+Heading 5
+Heading 6
diff --git a/tests/Integration/fixtures/links-images.html b/tests/Integration/fixtures/links-images.html
index 7ded60f..fe23e11 100644
--- a/tests/Integration/fixtures/links-images.html
+++ b/tests/Integration/fixtures/links-images.html
@@ -1 +1,3 @@
-

\ No newline at end of file
+
+
+
+
diff --git a/tests/Integration/fixtures/mixed.html b/tests/Integration/fixtures/mixed.html
index d22c149..3d54b88 100644
--- a/tests/Integration/fixtures/mixed.html
+++ b/tests/Integration/fixtures/mixed.html
@@ -1 +1,11 @@
-Title
console.log('hi');
\ No newline at end of file
+Title
+
+
+
+
+
+console.log('hi');
diff --git a/tests/Integration/fixtures/nested-blockquotes.html b/tests/Integration/fixtures/nested-blockquotes.html
index e185b7d..4c1d948 100644
--- a/tests/Integration/fixtures/nested-blockquotes.html
+++ b/tests/Integration/fixtures/nested-blockquotes.html
@@ -1 +1,7 @@
-
\ No newline at end of file
+
+
diff --git a/tests/Integration/fixtures/reference-links.html b/tests/Integration/fixtures/reference-links.html
index 9e5ca0c..4f3908e 100644
--- a/tests/Integration/fixtures/reference-links.html
+++ b/tests/Integration/fixtures/reference-links.html
@@ -1 +1,6 @@
-
+
+My Title
Sub Title
bold title
text
Title
foo bar
line one line two line three
foo bar
\ No newline at end of file
+My Title
+Sub Title
+bold title
+text
+Title
+foo bar
+line one line two line three
+foo bar
diff --git a/tests/Integration/fixtures/strikethrough.html b/tests/Integration/fixtures/strikethrough.html
index 4aa400e..cf099dc 100644
--- a/tests/Integration/fixtures/strikethrough.html
+++ b/tests/Integration/fixtures/strikethrough.html
@@ -1 +1,5 @@
-fooboldbar bazfooboldbar baz
Gauche
Droite
Gauche
' . "\n" . 'Droite
' . "\n" . '(.*)<\/p>$/s', '$1', $html) ?? $html; + return preg_replace('/^
(.*)<\/p>\n?$/s', '$1', $html) ?? $html; } // ── Basic emphasis and strong ───────────────────────────────────────────── diff --git a/tests/Unit/FencedCodeTildeTest.php b/tests/Unit/FencedCodeTildeTest.php index c467f5b..8b8ca78 100644 --- a/tests/Unit/FencedCodeTildeTest.php +++ b/tests/Unit/FencedCodeTildeTest.php @@ -44,7 +44,7 @@ public function testTC01BasicTildeFenceNoInfoString(): void { $html = $this->render("~~~\nfoo\n~~~"); - $this->assertSame('
foo', $html);
+ $this->assertSame("foo\n", $html);
}
/**
@@ -54,7 +54,7 @@ public function testTC02TildeFenceWithInfoString(): void
{
$html = $this->render("~~~php\necho 1;\n~~~");
- $this->assertSame('echo 1;', $html);
+ $this->assertSame("echo 1;\n", $html);
}
/**
@@ -64,7 +64,7 @@ public function testTC03FourTildeCloseThreeTildeOpen(): void
{
$html = $this->render("~~~\nfoo\n~~~~");
- $this->assertSame('foo', $html);
+ $this->assertSame("foo\n", $html);
}
/**
@@ -75,7 +75,7 @@ public function testTC04ThreeTildeCannotCloseFourTildeOpen(): void
// The ~~~ line inside is treated as content, block is unclosed at EOF.
$html = $this->render("~~~~\nfoo\n~~~\nbar");
- $this->assertSame("foo\n~~~\nbar", $html);
+ $this->assertSame("foo\n~~~\nbar\n", $html);
}
/**
@@ -85,7 +85,7 @@ public function testTC05BacktickLineIsContentInsideTildeFence(): void
{
$html = $this->render("~~~\nfoo\n```\n~~~");
- $this->assertSame("foo\n```", $html);
+ $this->assertSame("foo\n```\n", $html);
}
/**
@@ -95,7 +95,7 @@ public function testTC06TildeLineIsContentInsideBacktickFence(): void
{
$html = $this->render("```\nfoo\n~~~\n```");
- $this->assertSame("foo\n~~~", $html);
+ $this->assertSame("foo\n~~~\n", $html);
}
/**
@@ -105,7 +105,7 @@ public function testTC07ThreeSpaceIndentOnFenceLines(): void
{
$html = $this->render(" ~~~\nfoo\n ~~~");
- $this->assertSame('foo', $html);
+ $this->assertSame("foo\n", $html);
}
/**
@@ -116,7 +116,7 @@ public function testTC08FourSpaceIndentProducesIndentedCodeBlock(): void
// " ~~~" is an indented code block containing "~~~" as content.
$html = $this->render(" ~~~\n");
- $this->assertSame("~~~\n", $html);
+ $this->assertSame("~~~\n\n", $html);
$this->assertStringNotContainsString('', $html);
}
@@ -127,7 +127,7 @@ public function testTC09UnclosedTildeFenceEmittedAtEof(): void
{
$html = $this->render("~~~\nfoo\nbar");
- $this->assertSame("foo\nbar
", $html);
+ $this->assertSame("foo\nbar
\n", $html);
}
/**
@@ -137,7 +137,7 @@ public function testTC10FourTildeOpenClosedByFourTildeClose(): void
{
$html = $this->render("~~~~\nfoo\n~~~~");
- $this->assertSame('foo
', $html);
+ $this->assertSame("foo
\n", $html);
}
/**
@@ -148,7 +148,7 @@ public function testTC11HtmlCharactersEscaped(): void
$html = $this->render("~~~\nbold & \"quotes\"\n~~~");
$this->assertSame(
- '<b>bold</b> & "quotes"
',
+ "<b>bold</b> & "quotes"
\n",
$html,
);
}
@@ -162,7 +162,7 @@ public function testTC12BacktickInTildeInfoStringStrippedByRenderer(): void
// Info string is a single backtick; after renderer sanitisation it becomes ''.
$html = $this->render("~~~ `\nconsole.log(1);\n~~~");
- $this->assertSame('console.log(1);
', $html);
+ $this->assertSame("console.log(1);
\n", $html);
}
/**
@@ -184,7 +184,7 @@ public function testTC14AdjacentBacktickThenTildeFence(): void
$html = $this->render("```\nhello\n```\n~~~\nworld\n~~~");
$this->assertSame(
- 'hello
world
',
+ "hello
\nworld
\n",
$html,
);
}
@@ -197,7 +197,7 @@ public function testTC15ParagraphBeforeTildeFence(): void
$html = $this->render("paragraph text\n\n~~~\ncode\n~~~");
$this->assertSame(
- 'paragraph text
code
',
+ "paragraph text
\ncode
\n",
$html,
);
}
diff --git a/tests/Unit/FootnoteTest.php b/tests/Unit/FootnoteTest.php
index 632305e..9db4f93 100644
--- a/tests/Unit/FootnoteTest.php
+++ b/tests/Unit/FootnoteTest.php
@@ -52,7 +52,7 @@ public function testBasicReferenceAndDefinition(): void
$md = "Text[^1].\n\n[^1]: First footnote.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Text1.
'
+ "Text1.
\n"
. ''
. '- First footnote. ↩
'
. '
',
@@ -68,7 +68,7 @@ public function testAppearanceOrderNumbering(): void
$md = "Alpha[^b] and Beta[^a].\n\n[^a]: Definition A.\n[^b]: Definition B.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- ''
+ "\n"
. ''
. '- Definition B. ↩
'
. '- Definition A. ↩
'
@@ -83,8 +83,8 @@ public function testAppearanceOrderNumberingAcrossParagraphs(): void
$md = "First[^a].\n\nSecond[^b] and again[^b].\n\n[^a]: Def A.\n[^b]: Def B.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'First1.
'
- . ''
+ "First1.
\n"
+ . "\n"
. ''
. '- Def A. ↩
'
. '- Def B. ↩ ↩
'
@@ -101,7 +101,7 @@ public function testSameLabelTwiceProducesTwoBackLinks(): void
$md = "First[^x] and second[^x].\n\n[^x]: Shared note.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- ''
+ "\n"
. ' ',
@@ -124,7 +124,7 @@ public function testThirdOccurrenceSuffix(): void
public function testUndefinedReferenceRendersLiteral(): void
{
$html = $this->renderMarkdown("See[^missing] for details.");
- $this->assertSame('See[^missing] for details.
', $html);
+ $this->assertSame("See[^missing] for details.
\n", $html);
$this->assertStringNotContainsString('renderMarkdown("Just a paragraph.\n\n[^unused]: Never referenced.");
- $this->assertSame('Just a paragraph.
', $html);
+ $this->assertSame("Just a paragraph.
\n", $html);
$this->assertStringNotContainsString('section', $html);
$this->assertStringNotContainsString('unused', $html);
}
@@ -147,7 +147,7 @@ public function testMultiLineBodyContinuation(): void
$md = "Text[^ml].\n\n[^ml]: First line.\n Second line, same footnote.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Text1.
'
+ "Text1.
\n"
. ''
. '- First line. Second line, same footnote. ↩
'
. '
',
@@ -163,7 +163,7 @@ public function testFootnoteBodyWithInlineMarkup(): void
$md = "Note[^fmt].\n\n[^fmt]: **Bold** and _italic_.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Note1.
'
+ "Note1.
\n"
. ''
. '- Bold and italic. ↩
'
. '
',
@@ -192,8 +192,8 @@ public function testSectionAtEndOfDocument(): void
$md = "[^early]: Defined first.\n\nParagraph with reference[^early].\n\nAnother paragraph.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Paragraph with reference1.
'
- . 'Another paragraph.
'
+ "Paragraph with reference1.
\n"
+ . "Another paragraph.
\n"
. ''
. '- Defined first. ↩
'
. '
',
@@ -207,7 +207,7 @@ public function testDefinitionBeforeReferencingParagraph(): void
$md = "[^first]: Defined at top.\n\nParagraph with ref[^first].";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Paragraph with ref1.
'
+ "Paragraph with ref1.
\n"
. ''
. '- Defined at top. ↩
'
. '
',
@@ -255,7 +255,7 @@ public function testLabelCaseSensitive(): void
{
$md = "Ref[^Bar].\n\n[^bar]: Definition for bar.";
$html = $this->renderMarkdown($md);
- $this->assertSame('Ref[^Bar].
', $html);
+ $this->assertSame("Ref[^Bar].
\n", $html);
$this->assertStringNotContainsString('section', $html);
}
@@ -276,7 +276,7 @@ public function testLabelWithDigitsOnly(): void
$md = "Ref[^123].\n\n[^123]: All digits.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Ref1.
'
+ "Ref1.
\n"
. ''
. '- All digits. ↩
'
. '
',
@@ -290,7 +290,7 @@ public function testLabelWithHyphen(): void
$md = "Ref[^my-note].\n\n[^my-note]: Hyphenated label.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'Ref1.
'
+ "Ref1.
\n"
. ''
. '- Hyphenated label. ↩
'
. '
',
@@ -302,7 +302,7 @@ public function testLabelWithHyphen(): void
public function testLabelWithSpace_notMatched(): void
{
$html = $this->renderMarkdown("Ref[^bad label].");
- $this->assertSame('Ref[^bad label].
', $html);
+ $this->assertSame("Ref[^bad label].
\n", $html);
}
/** EC-3: Definition with empty body (no text after colon) is rejected; ref renders as literal. */
@@ -323,7 +323,8 @@ public function testEmptyBodyDefinitionNotMatched(): void
public function testListItemNotTokenizedAsDefinition(): void
{
$html = $this->renderMarkdown("- [^1]: This is a list item, not a footnote definition.");
- $this->assertStringContainsString('- ', $html);
+ $this->assertStringContainsString('
', $html);
+ $this->assertStringContainsString('- ', $html);
$this->assertStringContainsString('[^1]: This is a list item, not a footnote definition.', $html);
$this->assertStringNotContainsString('
renderMarkdown("Just text.");
- $this->assertSame('Just text.
', $html);
+ $this->assertSame("Just text.
\n", $html);
$this->assertStringNotContainsString('section', $html);
}
@@ -346,7 +347,7 @@ public function testFootnoteRefInsideEmphasis(): void
$md = "_text[^1]_\n\n[^1]: Inline note.";
$html = $this->renderMarkdown($md);
$this->assertSame(
- 'text1
'
+ "text1
\n"
. ''
. '- Inline note. ↩
'
. '
',
@@ -393,7 +394,7 @@ public function testVeryLongLabel_notMatched(): void
public function testEscapedBracketDoesNotTriggerFootnote(): void
{
$html = $this->renderMarkdown("\\[^1]");
- $this->assertSame('[^1]
', $html);
+ $this->assertSame("[^1]
\n", $html);
$this->assertStringNotContainsString('sup', $html);
}
diff --git a/tests/Unit/IndentedCodeBlockTest.php b/tests/Unit/IndentedCodeBlockTest.php
index 23bd2be..a8401b1 100644
--- a/tests/Unit/IndentedCodeBlockTest.php
+++ b/tests/Unit/IndentedCodeBlockTest.php
@@ -108,7 +108,7 @@ public function testBasicIndentedBlockRendering(): void
{
$html = $this->render(' hello world');
- $this->assertSame('hello world' . "\n" . '
', $html);
+ $this->assertSame('hello world' . "\n" . "
\n", $html);
}
public function testConsecutiveIndentedLinesFormSingleBlock(): void
@@ -124,14 +124,14 @@ public function testBlankLineInsideBlockIsPreserved(): void
{
$html = $this->render(" first\n\n second");
- $this->assertSame('first' . "\n\n" . 'second' . "\n" . '
', $html);
+ $this->assertSame('first' . "\n\n" . 'second' . "\n" . "
\n", $html);
}
public function testTrailingBlankLinesInsideBlockStripped(): void
{
$html = $this->render(" content\n\n\n");
- $this->assertSame('content' . "\n" . '
', $html);
+ $this->assertSame('content' . "\n" . "
\n", $html);
}
public function testHtmlCharactersEscapedInContent(): void
@@ -190,7 +190,7 @@ public function testFiveSpaceIndentPreservesOneLeadingSpace(): void
// AC-04: 5 spaces → 4 stripped, 1 space remains in content.
$html = $this->render(' foo');
- $this->assertSame(' foo' . "\n" . '
', $html);
+ $this->assertSame(' foo' . "\n" . "
\n", $html);
}
public function testNoClassAttributeOnCodeElement(): void
@@ -199,6 +199,6 @@ public function testNoClassAttributeOnCodeElement(): void
$html = $this->render(' php');
$this->assertStringNotContainsString('class=', $html);
- $this->assertSame('php' . "\n" . '
', $html);
+ $this->assertSame('php' . "\n" . "
\n", $html);
}
}
diff --git a/tests/Unit/LinkAngleBracketUrlTest.php b/tests/Unit/LinkAngleBracketUrlTest.php
index 153d2e1..4947211 100644
--- a/tests/Unit/LinkAngleBracketUrlTest.php
+++ b/tests/Unit/LinkAngleBracketUrlTest.php
@@ -32,31 +32,31 @@ private function renderInline(string $input): string
public function test_simple_angle_bracket_url(): void
{
$html = $this->renderInline('[foo]()');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_angle_bracket_url_with_spaces(): void
{
$html = $this->renderInline('[foo]()');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_angle_bracket_empty_url(): void
{
$html = $this->renderInline('[foo](<>)');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_angle_bracket_url_with_title(): void
{
$html = $this->renderInline('[foo]( "title")');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_angle_bracket_url_in_image(): void
{
$html = $this->renderInline('![alt]()');
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
public function test_unclosed_angle_bracket_is_not_a_link(): void
@@ -82,13 +82,13 @@ public function test_backslash_escaped_gt_in_angle_bracket_url(): void
// \> in an angle-bracket URL is a backslash-escape for >; the rendered href
// HTML-escapes the literal > to > per standard attribute encoding.
$html = $this->renderInline('[foo](bar>)');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_ampersand_in_angle_bracket_url_is_escaped_in_href(): void
{
$html = $this->renderInline('[foo]()');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
public function test_cr_in_angle_bracket_url_is_rejected(): void
diff --git a/tests/Unit/LinkRefAngleBracketUrlTest.php b/tests/Unit/LinkRefAngleBracketUrlTest.php
index 2fe823d..8047471 100644
--- a/tests/Unit/LinkRefAngleBracketUrlTest.php
+++ b/tests/Unit/LinkRefAngleBracketUrlTest.php
@@ -38,35 +38,35 @@ private function renderMarkdown(string $markdown): string
public function test_angle_bracket_url_brackets_are_stripped(): void
{
$html = $this->renderMarkdown("[foo]: \n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-2: angle-bracket URL with double-quote title. */
public function test_angle_bracket_url_with_double_quote_title(): void
{
$html = $this->renderMarkdown("[foo]: \"title\"\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-3: angle-bracket URL with single-quote title. */
public function test_angle_bracket_url_with_single_quote_title(): void
{
$html = $this->renderMarkdown("[foo]: 'title'\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-4: empty angle brackets — href is an empty string. */
public function test_empty_angle_brackets_produce_empty_href(): void
{
$html = $this->renderMarkdown("[foo]: <>\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-5: angle-bracket URL used with an image reference. */
public function test_angle_bracket_url_with_image_ref(): void
{
$html = $this->renderMarkdown("[img]: \n\n![img]");
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
/** AC-6: angle brackets stripped — only URL content stored (no brackets in href attr). */
@@ -96,7 +96,7 @@ public function test_backslash_escaped_gt_inside_angle_bracket_url(): void
{
// \> is unescaped to > during lexing; the renderer HTML-escapes > to > in the attribute.
$html = $this->renderMarkdown("[foo]: path>\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Edge: unescaped < inside angle-bracket URL makes the definition invalid. */
@@ -110,27 +110,27 @@ public function test_unescaped_lt_inside_angle_bracket_url_is_invalid(): void
public function test_ampersand_in_angle_bracket_url_is_html_escaped(): void
{
$html = $this->renderMarkdown("[foo]: \n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Edge: parenthesised title with angle-bracket URL (regression guard). */
public function test_angle_bracket_url_with_paren_title(): void
{
$html = $this->renderMarkdown("[foo]: (title)\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Regression: bare URL ref defs still work after the regex change. */
public function test_bare_url_ref_def_still_works(): void
{
$html = $this->renderMarkdown("[foo]: https://example.com\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Regression: bare URL ref def with title still works after the regex change. */
public function test_bare_url_ref_def_with_title_still_works(): void
{
$html = $this->renderMarkdown("[foo]: /url \"a title\"\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
}
diff --git a/tests/Unit/LinkRefTitleVariantsTest.php b/tests/Unit/LinkRefTitleVariantsTest.php
index f09f9a4..58e320a 100644
--- a/tests/Unit/LinkRefTitleVariantsTest.php
+++ b/tests/Unit/LinkRefTitleVariantsTest.php
@@ -37,35 +37,35 @@ private function renderMarkdown(string $markdown): string
public function test_ref_link_with_double_quote_title(): void
{
$html = $this->renderMarkdown("[foo]: /url \"title\"\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-2: single-quote title. */
public function test_ref_link_with_single_quote_title(): void
{
$html = $this->renderMarkdown("[foo]: /url 'title'\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-3: parenthesised title. */
public function test_ref_link_with_parenthesised_title(): void
{
$html = $this->renderMarkdown("[foo]: /url (title)\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-4: image ref with single-quote title. */
public function test_ref_image_with_single_quote_title(): void
{
$html = $this->renderMarkdown("[img]: /img.png 'alt title'\n\n![img]");
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
/** AC-5: title on a separate continuation line (CommonMark §4.7). */
public function test_ref_link_with_title_on_separate_line(): void
{
$html = $this->renderMarkdown("[foo]: /url\n'title'\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/**
@@ -82,7 +82,7 @@ public function test_mismatched_title_delimiters_not_a_valid_def(): void
public function test_ref_link_with_no_title(): void
{
$html = $this->renderMarkdown("[foo]: /url\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Edge: parenthesised title must not contain unescaped ( — treated as invalid. */
@@ -114,20 +114,20 @@ public function test_double_quote_title_may_contain_single_quotes(): void
public function test_ref_link_with_double_quote_title_on_separate_line(): void
{
$html = $this->renderMarkdown("[foo]: /url\n\"title\"\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Edge: title on separate line with parenthesised delimiter. */
public function test_ref_link_with_paren_title_on_separate_line(): void
{
$html = $this->renderMarkdown("[foo]: /url\n(title)\n\n[foo]");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** Edge: orphan title line not preceded by a URL-only ref def must NOT be consumed as a title. */
public function test_orphan_title_line_not_consumed_as_title(): void
{
$html = $this->renderMarkdown("'orphan'\n\n[foo]: /url\n\n[foo]");
- $this->assertSame(''orphan'
', $html);
+ $this->assertSame("'orphan'
\n\n", $html);
}
}
diff --git a/tests/Unit/LinkTitleVariantsTest.php b/tests/Unit/LinkTitleVariantsTest.php
index 97a86be..5494645 100644
--- a/tests/Unit/LinkTitleVariantsTest.php
+++ b/tests/Unit/LinkTitleVariantsTest.php
@@ -33,49 +33,49 @@ private function renderInline(string $input): string
public function test_link_with_double_quote_title(): void
{
$html = $this->renderInline('[foo](url "title")');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-2: single-quote title on a plain link. */
public function test_link_with_single_quote_title(): void
{
$html = $this->renderInline("[foo](url 'title')");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-3: parenthesised title on a plain link. */
public function test_link_with_parenthesised_title(): void
{
$html = $this->renderInline('[foo](url (title))');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-4: single-quote title on an image. */
public function test_image_with_single_quote_title(): void
{
$html = $this->renderInline("");
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
/** AC-5: parenthesised title on an image. */
public function test_image_with_parenthesised_title(): void
{
$html = $this->renderInline(')');
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
/** AC-6: single-quote title with angle-bracket URL. */
public function test_angle_bracket_url_link_with_single_quote_title(): void
{
$html = $this->renderInline("[foo]( 'title')");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/** AC-7: parenthesised title with angle-bracket URL. */
public function test_angle_bracket_url_link_with_parenthesised_title(): void
{
$html = $this->renderInline('[foo]( (title))');
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/**
@@ -93,7 +93,7 @@ public function test_single_quote_title_with_escaped_inner_quote(): void
{
// The renderer applies htmlspecialchars(ENT_QUOTES), so ' is encoded as ' in attributes.
$html = $this->renderInline("[foo](url 'it\\'s')");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/**
@@ -121,7 +121,7 @@ public function test_no_whitespace_before_title_delimiter_not_a_title(): void
public function test_trailing_whitespace_before_close_paren_is_tolerated(): void
{
$html = $this->renderInline("[foo](url 'title' )");
- $this->assertSame('', $html);
+ $this->assertSame("\n", $html);
}
/**
@@ -139,6 +139,6 @@ public function test_non_whitespace_after_title_close_delimiter_invalidates_titl
public function test_image_angle_bracket_url_with_double_quote_title_regression(): void
{
$html = $this->renderInline('');
- $this->assertSame('
', $html);
+ $this->assertSame("
\n", $html);
}
}
diff --git a/tests/Unit/LooseTightListTest.php b/tests/Unit/LooseTightListTest.php
index a060a3d..3e5f502 100644
--- a/tests/Unit/LooseTightListTest.php
+++ b/tests/Unit/LooseTightListTest.php
@@ -56,9 +56,9 @@ public function testLooseUnorderedList(): void
$html = $this->render("- foo\n\n- bar\n\n- baz");
$this->assertStringContainsString('', $html);
- $this->assertStringContainsString('foo
', $html);
- $this->assertStringContainsString('bar
', $html);
- $this->assertStringContainsString('baz
', $html);
+ $this->assertStringContainsString("- \n
foo
\n ", $html);
+ $this->assertStringContainsString("- \n
bar
\n ", $html);
+ $this->assertStringContainsString("- \n
baz
\n ", $html);
}
// -------------------------------------------------------------------------
@@ -71,9 +71,9 @@ public function testOneBlankMakesAllLoose(): void
$html = $this->render("- alpha\n- beta\n\n- gamma");
// All three items must be wrapped in .
- $this->assertStringContainsString('
alpha
', $html);
- $this->assertStringContainsString('beta
', $html);
- $this->assertStringContainsString('gamma
', $html);
+ $this->assertStringContainsString("- \n
alpha
\n ", $html);
+ $this->assertStringContainsString("- \n
beta
\n ", $html);
+ $this->assertStringContainsString("- \n
gamma
\n ", $html);
}
// -------------------------------------------------------------------------
@@ -100,9 +100,9 @@ public function testLooseOrderedList(): void
$html = $this->render("1. one\n\n2. two\n\n3. three");
$this->assertStringContainsString('', $html);
- $this->assertStringContainsString('one
', $html);
- $this->assertStringContainsString('two
', $html);
- $this->assertStringContainsString('three
', $html);
+ $this->assertStringContainsString("- \n
one
\n ", $html);
+ $this->assertStringContainsString("- \n
two
\n ", $html);
+ $this->assertStringContainsString("- \n
three
\n ", $html);
}
// -------------------------------------------------------------------------
@@ -183,9 +183,9 @@ public function testMidListBlankMakesLoose(): void
// the whole list loose, so x must also be wrapped in .
$html = $this->render("- x\n- y\n\n- z");
- $this->assertStringContainsString('
x
', $html);
- $this->assertStringContainsString('y
', $html);
- $this->assertStringContainsString('z
', $html);
+ $this->assertStringContainsString("- \n
x
\n ", $html);
+ $this->assertStringContainsString("- \n
y
\n ", $html);
+ $this->assertStringContainsString("- \n
z
\n ", $html);
}
// -------------------------------------------------------------------------
@@ -239,7 +239,7 @@ public function testRendererWrapsInParaWhenLoose(): void
$html = $this->renderer->render($doc);
- $this->assertStringContainsString('hello
', $html);
+ $this->assertStringContainsString("- \n
hello
\n ", $html);
}
// -------------------------------------------------------------------------
diff --git a/tests/Unit/Renderer/XhtmlOutputFormatTest.php b/tests/Unit/Renderer/XhtmlOutputFormatTest.php
new file mode 100644
index 0000000..cee5694
--- /dev/null
+++ b/tests/Unit/Renderer/XhtmlOutputFormatTest.php
@@ -0,0 +1,153 @@
+,
,
) must use self-closing syntax.
+ * Inline nodes must produce no trailing newline of their own.
+ */
+#[RequiresPhpExtension('intl')]
+final class XhtmlOutputFormatTest extends TestCase
+{
+ private MarkdownParser $parser;
+
+ protected function setUp(): void
+ {
+ $this->parser = new MarkdownParser();
+ }
+
+ private function parseAndRender(string $markdown): string
+ {
+ return $this->parser->parse($markdown);
+ }
+
+ // ── Headings ──────────────────────────────────────────────────────────────
+
+ public function testAtxHeadingLevel1EndsWithNewline(): void
+ {
+ $out = $this->parseAndRender('# Foo');
+ $this->assertSame("Foo
\n", $out);
+ }
+
+ public function testAtxHeadingLevel2EndsWithNewline(): void
+ {
+ $out = $this->parseAndRender('## Bar');
+ $this->assertSame("Bar
\n", $out);
+ }
+
+ // ── Paragraph ─────────────────────────────────────────────────────────────
+
+ public function testParagraphEndsWithNewline(): void
+ {
+ $out = $this->parseAndRender('hello');
+ $this->assertSame("hello
\n", $out);
+ }
+
+ // ── Code blocks ───────────────────────────────────────────────────────────
+
+ public function testIndentedCodeBlockEndsWithNewline(): void
+ {
+ $out = $this->parseAndRender(" a simple\n indented code block");
+ $this->assertSame("a simple\n indented code block\n
\n", $out);
+ }
+
+ public function testFencedCodeBlockEndsWithNewline(): void
+ {
+ $out = $this->parseAndRender("```\n<\n >\n```");
+ // The lexer preserves content as-is; the renderer wraps in ...
\n.
+ $this->assertSame("<\n >
\n", $out);
+ }
+
+ public function testFencedCodeBlockWithLanguageEndsWithNewline(): void
+ {
+ $out = $this->parseAndRender("```php\necho 1;\n```");
+ $this->assertSame("echo 1;
\n", $out);
+ }
+
+ // ── Void elements: self-closing syntax ────────────────────────────────────
+
+ public function testHorizontalRuleRendersAsSelfClosingWithNewline(): void
+ {
+ $out = $this->parseAndRender('***');
+ $this->assertSame("
\n", $out);
+ }
+
+ public function testHardLineBreakRendersAsSelfClosingWithTrailingNewline(): void
+ {
+ $out = $this->parseAndRender("foo \nbar");
+ $this->assertSame("foo
\nbar
\n", $out);
+ }
+
+ public function testImageRendersAsSelfClosingWithTitleAttribute(): void
+ {
+ $out = $this->parseAndRender('');
+ $this->assertSame("
\n", $out);
+ }
+
+ public function testImageWithoutTitleRendersAsSelfClosing(): void
+ {
+ $out = $this->parseAndRender('');
+ $this->assertSame("
\n", $out);
+ }
+
+ // ── Blockquotes ───────────────────────────────────────────────────────────
+
+ public function testBlockquoteHasInternalNewlines(): void
+ {
+ $out = $this->parseAndRender('> bar');
+ $this->assertSame("\nbar
\n
\n", $out);
+ }
+
+ public function testNestedBlockquoteHasNewlinesAtEveryLevel(): void
+ {
+ $out = $this->parseAndRender('> > nested');
+ $this->assertSame("\n\nnested
\n
\n
\n", $out);
+ }
+
+ // ── Lists ─────────────────────────────────────────────────────────────────
+
+ public function testTightUnorderedListHasNewlines(): void
+ {
+ $out = $this->parseAndRender("- foo\n- bar");
+ $this->assertSame("\n- foo
\n- bar
\n
\n", $out);
+ }
+
+ public function testTightOrderedListHasNewlines(): void
+ {
+ $out = $this->parseAndRender("1. foo\n2. bar");
+ $this->assertSame("\n- foo
\n- bar
\n
\n", $out);
+ }
+
+ public function testLooseUnorderedListWrapsItemsInParagraph(): void
+ {
+ $out = $this->parseAndRender("- foo\n\n- bar");
+ $this->assertSame("\n- \n
foo
\n \n- \n
bar
\n \n
\n", $out);
+ }
+
+ // ── Inline nodes: no trailing newline of their own ────────────────────────
+
+ public function testInlineNodesHaveNoTrailingNewline(): void
+ {
+ // Strong, link, and code are all inline — the outer carries the \n,
+ // no inline node should inject a newline between itself and its siblings.
+ $out = $this->parseAndRender('**bold** [link](https://example.com) `code`');
+
+ // Outer paragraph ends with exactly one newline.
+ $this->assertStringEndsWith("
\n", $out);
+
+ // The string between and
must contain no raw newlines
+ // (only the paragraph wrapper provides the trailing \n).
+ preg_match('/(.*?)<\/p>/s', $out, $matches);
+ $this->assertArrayHasKey(1, $matches);
+ $this->assertStringNotContainsString("\n", $matches[1]);
+ }
+}
diff --git a/tests/Unit/RendererEntityTest.php b/tests/Unit/RendererEntityTest.php
index c53ddfb..bfbf18f 100644
--- a/tests/Unit/RendererEntityTest.php
+++ b/tests/Unit/RendererEntityTest.php
@@ -31,21 +31,21 @@ public function testHtmlEntityNodeRenderedVerbatim(): void
$out = $this->render([new ParagraphNode([new HtmlEntityNode('&')])]);
// Must be & exactly — no double-escaping to &
- $this->assertSame('
&
', $out);
+ $this->assertSame("&
\n", $out);
}
public function testHtmlEntityNodeLtRenderedVerbatim(): void
{
$out = $this->render([new ParagraphNode([new HtmlEntityNode('<')])]);
- $this->assertSame('<
', $out);
+ $this->assertSame("<
\n", $out);
}
public function testHtmlEntityNodeHexRenderedVerbatim(): void
{
$out = $this->render([new ParagraphNode([new HtmlEntityNode(' ')])]);
- $this->assertSame('
', $out);
+ $this->assertSame("
\n", $out);
}
public function testTextNodeBareAmpersandIsEscaped(): void
@@ -53,6 +53,6 @@ public function testTextNodeBareAmpersandIsEscaped(): void
// Regression guard: TextNode('a & b') must still produce a & b.
$out = $this->render([new ParagraphNode([new TextNode('a & b')])]);
- $this->assertSame('a & b
', $out);
+ $this->assertSame("a & b
\n", $out);
}
}
diff --git a/tests/Unit/RendererTest.php b/tests/Unit/RendererTest.php
index 2f4ce2a..a6ad6ef 100644
--- a/tests/Unit/RendererTest.php
+++ b/tests/Unit/RendererTest.php
@@ -62,7 +62,7 @@ public static function headingProvider(): array
{
return array_combine(
array_map(fn($l) => "h{$l}", range(1, 6)),
- array_map(fn($l) => [$l, "T "], range(1, 6)),
+ array_map(fn($l) => [$l, "T \n"], range(1, 6)),
);
}
@@ -77,7 +77,7 @@ public function testXssInTextNodeEscaped(): void
public function testAmpersandEscaped(): void
{
$out = $this->render([new ParagraphNode([new TextNode('a & b')])]);
- $this->assertSame('a & b
', $out);
+ $this->assertSame("a & b
\n", $out);
}
public function testQuoteEscapedInAttribute(): void
@@ -91,7 +91,7 @@ public function testLinkRendered(): void
$out = $this->render([new ParagraphNode([
new LinkNode('https://example.com', [new TextNode('click')]),
])]);
- $this->assertSame('', $out);
+ $this->assertSame("\n", $out);
}
public function testLinkHrefEscaped(): void
@@ -106,13 +106,13 @@ public function testLinkHrefEscaped(): void
public function testImageRendered(): void
{
$out = $this->render([new ParagraphNode([new ImageNode('img.png', 'desc')])]);
- $this->assertSame('
', $out);
+ $this->assertSame("
\n", $out);
}
public function testImageWithTitleRendered(): void
{
$out = $this->render([new ParagraphNode([new ImageNode('img.png', 'alt text', 'My tooltip')])]);
- $this->assertSame('
', $out);
+ $this->assertSame("
\n", $out);
}
public function testImageTitleEscaped(): void
@@ -124,7 +124,7 @@ public function testImageTitleEscaped(): void
public function testFencedCodeWithLanguage(): void
{
$out = $this->render([new FencedCodeNode("echo 'x';", 'php')]);
- $this->assertSame('echo 'x';
', $out);
+ $this->assertSame("echo 'x';
\n", $out);
}
public function testFencedCodeContentEscaped(): void
@@ -139,7 +139,7 @@ public function testUnorderedList(): void
new ListItemNode([new TextNode('a')]),
new ListItemNode([new TextNode('b')]),
])]);
- $this->assertSame('- a
- b
', $out);
+ $this->assertSame("\n- a
\n- b
\n
\n", $out);
}
public function testOrderedList(): void
@@ -147,7 +147,7 @@ public function testOrderedList(): void
$out = $this->render([new ListNode(ordered: true, children: [
new ListItemNode([new TextNode('a')]),
])]);
- $this->assertSame('- a
', $out);
+ $this->assertSame("\n- a
\n
\n", $out);
}
public function testBlockquoteRendered(): void
@@ -155,7 +155,7 @@ public function testBlockquoteRendered(): void
$out = $this->render([new BlockquoteNode([
new ParagraphNode([new TextNode('quote')]),
])]);
- $this->assertSame('quote
', $out);
+ $this->assertSame("\nquote
\n
\n", $out);
}
public function testNestedBlockquoteRendered(): void
@@ -167,7 +167,7 @@ public function testNestedBlockquoteRendered(): void
]),
]),
]);
- $this->assertSame('deep
', $out);
+ $this->assertSame("\n\ndeep
\n
\n
\n", $out);
}
public function testBlockquoteMixedChildrenRendered(): void
@@ -182,7 +182,7 @@ public function testBlockquoteMixedChildrenRendered(): void
]),
]);
$this->assertSame(
- 'outer
inner
outer again
',
+ "\nouter
\n\ninner
\n
\nouter again
\n
\n",
$out,
);
}
@@ -194,19 +194,19 @@ public function testBlockquoteWithInlineMarkupRendered(): void
new ParagraphNode([new StrongNode([new TextNode('bold')])]),
]),
]);
- $this->assertSame('bold
', $out);
+ $this->assertSame("\nbold
\n
\n", $out);
}
public function testHorizontalRuleRendered(): void
{
$out = $this->render([new HorizontalRuleNode()]);
- $this->assertSame('
', $out);
+ $this->assertSame("
\n", $out);
}
public function testInlineCodeEscaped(): void
{
$out = $this->render([new ParagraphNode([new CodeNode('')])]);
- $this->assertSame('<b>
', $out);
+ $this->assertSame("<b>
\n", $out);
}
public function testStrongAndEmRendered(): void
@@ -215,7 +215,7 @@ public function testStrongAndEmRendered(): void
new StrongNode([new TextNode('b')]),
new EmphasisNode([new TextNode('i')]),
])]);
- $this->assertSame('bi
', $out);
+ $this->assertSame("bi
\n", $out);
}
public function testTableRendered(): void
@@ -335,13 +335,13 @@ public function testMixedDocumentMermaidAndCodeBlock(): void
public function testStrikethroughRendered(): void
{
$out = $this->render([new ParagraphNode([new StrikethroughNode([new TextNode('foo')])])]);
- $this->assertSame('foo
', $out);
+ $this->assertSame("foo
\n", $out);
}
public function testStrikethroughXssEscaped(): void
{
$out = $this->render([new ParagraphNode([new StrikethroughNode([new TextNode('')])])]);
- $this->assertSame('<b>
', $out);
+ $this->assertSame("<b>
\n", $out);
}
// ── Nested list rendering ────────────────────────────────────────────────
@@ -358,7 +358,7 @@ public function testNestedUnorderedList(): void
]),
]),
]);
- $this->assertSame('- a
- b
', $out);
+ $this->assertSame("\n- a
\n- b
\n
\n \n
\n", $out);
}
public function testMixedOrderedUnorderedNesting(): void
@@ -373,7 +373,7 @@ public function testMixedOrderedUnorderedNesting(): void
]),
]),
]);
- $this->assertSame('- first
- nested
', $out);
+ $this->assertSame("\n- first
\n- nested
\n
\n \n
\n", $out);
}
// ── Task list (checkbox) rendering ───────────────────────────────────────
@@ -383,7 +383,7 @@ public function testCheckedListItemRendersCheckbox(): void
$out = $this->render([new ListNode(ordered: false, children: [
new ListItemNode([new TextNode('Done')], checked: true),
])]);
- $this->assertSame('- Done
', $out);
+ $this->assertSame("\n- Done
\n
\n", $out);
}
public function testUncheckedListItemRendersCheckbox(): void
@@ -391,7 +391,7 @@ public function testUncheckedListItemRendersCheckbox(): void
$out = $this->render([new ListNode(ordered: false, children: [
new ListItemNode([new TextNode('Todo')], checked: false),
])]);
- $this->assertSame('- Todo
', $out);
+ $this->assertSame("\n- Todo
\n
\n", $out);
}
public function testPlainListItemNoCheckbox(): void
@@ -399,7 +399,7 @@ public function testPlainListItemNoCheckbox(): void
$out = $this->render([new ListNode(ordered: false, children: [
new ListItemNode([new TextNode('plain')], checked: null),
])]);
- $this->assertSame('- plain
', $out);
+ $this->assertSame("\n- plain
\n
\n", $out);
}
public function testCheckedListItemWithNoChildrenHasTrailingSpace(): void
@@ -409,7 +409,7 @@ public function testCheckedListItemWithNoChildrenHasTrailingSpace(): void
$out = $this->render([new ListNode(ordered: false, children: [
new ListItemNode([], checked: true),
])]);
- $this->assertSame('-
', $out);
+ $this->assertSame("\n-
\n
\n", $out);
}
// ── HardBreakNode rendering ──────────────────────────────────────────────
@@ -422,7 +422,7 @@ public function testHardBreakNodeRendersAsBr(): void
new HardBreakNode(),
new TextNode('bar'),
])]);
- $this->assertSame('foo
bar
', $out);
+ $this->assertSame("foo
\nbar
\n", $out);
}
public function testHardBreakNodeAloneRendersAsBr(): void
@@ -431,7 +431,7 @@ public function testHardBreakNodeAloneRendersAsBr(): void
$out = $this->render([new ParagraphNode([
new HardBreakNode(),
])]);
- $this->assertSame('
', $out);
+ $this->assertSame("
\n
\n", $out);
}
// ── RawHtmlInlineNode rendering ────────────────────────────────────────────
@@ -440,7 +440,7 @@ public function testRawHtmlInlineNodeContentIsEscaped(): void
{
// S29 escaped-fallback contract: tag chars become HTML entities.
$out = $this->render([new ParagraphNode([new RawHtmlInlineNode('')])]);
- $this->assertSame('<span class="hi">
', $out);
+ $this->assertSame("<span class="hi">
\n", $out);
}
public function testRawHtmlInlineScriptNodeIsEscaped(): void
@@ -457,7 +457,7 @@ public function testRawHtmlInlineNodeInHeadingChildren(): void
new TextNode('1'),
new RawHtmlInlineNode(''),
])]);
- $this->assertSame('<sup>1</sup>
', $out);
+ $this->assertSame("<sup>1</sup>
\n", $out);
}
public function testRawHtmlInlineNodeMixedWithText(): void
@@ -469,7 +469,7 @@ public function testRawHtmlInlineNodeMixedWithText(): void
new RawHtmlInlineNode(''),
new TextNode(' more'),
])]);
- $this->assertSame('text <span>word</span> more
', $out);
+ $this->assertSame("text <span>word</span> more
\n", $out);
}
// ── RawHtmlBlockNode rendering ─────────────────────────────────────────────
@@ -480,7 +480,7 @@ public function testRawHtmlBlockNodeEscapedByDefault(): void
$out = (new HtmlRenderer())->render(new DocumentNode([
new RawHtmlBlockNode('x'),
]));
- $this->assertSame('<div>x</div>', $out);
+ $this->assertSame("<div>x</div>\n", $out);
}
public function testRawHtmlBlockNodeSanitizedWhenAllowed(): void
@@ -555,7 +555,7 @@ public function testHtmlEntityNodeVerbatimByDefault(): void
$out = (new HtmlRenderer())->render(new DocumentNode([
new ParagraphNode([new HtmlEntityNode('&')]),
]));
- $this->assertSame('&
', $out);
+ $this->assertSame("&
\n", $out);
}
public function testHtmlEntityNodeVerbatimWhenAllowRawHtmlEnabled(): void
@@ -564,6 +564,6 @@ public function testHtmlEntityNodeVerbatimWhenAllowRawHtmlEnabled(): void
$out = (new HtmlRenderer(allowRawHtml: true))->render(new DocumentNode([
new ParagraphNode([new HtmlEntityNode('&')]),
]));
- $this->assertSame('&
', $out);
+ $this->assertSame("&
\n", $out);
}
}
diff --git a/tests/Unit/SetextHeadingMultilineTest.php b/tests/Unit/SetextHeadingMultilineTest.php
index ad3f953..c5261af 100644
--- a/tests/Unit/SetextHeadingMultilineTest.php
+++ b/tests/Unit/SetextHeadingMultilineTest.php
@@ -147,7 +147,7 @@ public function testSingleLineRegressionRenders(): void
public function testBlankBreaksAccumulationRenders(): void
{
- self::assertSame('foo
bar
', $this->render("foo\n\nbar\n==="));
+ self::assertSame("foo
\nbar
", $this->render("foo\n\nbar\n==="));
}
public function testInlineMarkupSpansLinesRenders(): void