diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e12925c1a1..7f5afd7811 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] stable: [true] coverage: [true] composer-flags: [''] @@ -42,7 +42,7 @@ jobs: stable: true coverage: false composer-flags: '--prefer-lowest' - - php: '8.4' + - php: '8.5' stable: false coverage: false composer-flags: '--ignore-platform-req=php' @@ -114,7 +114,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index dd29b5e2dc..e5daacb694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi ## [Unreleased][unreleased] +## [2.6.1] - 2024-12-29 + +### Fixed + +- Rendered list items should only add newlines around block-level children (#1059, #1061) + ## [2.6.0] - 2024-12-07 This is a **security release** to address potential denial of service attacks when parsing specially crafted, @@ -677,7 +683,8 @@ No changes were introduced since the previous release. - Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment - Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself -[unreleased]: https://github.com/thephpleague/commonmark/compare/2.6.0...main +[unreleased]: https://github.com/thephpleague/commonmark/compare/2.6.1...main +[2.6.1]: https://github.com/thephpleague/commonmark/compare/2.6.0...2.6.1 [2.6.0]: https://github.com/thephpleague/commonmark/compare/2.5.3...2.6.0 [2.5.3]: https://github.com/thephpleague/commonmark/compare/2.5.2...2.5.3 [2.5.2]: https://github.com/thephpleague/commonmark/compare/2.5.1...2.5.2 diff --git a/src/Extension/CommonMark/Renderer/Block/ListItemRenderer.php b/src/Extension/CommonMark/Renderer/Block/ListItemRenderer.php index 570f5a7f76..543baad83d 100644 --- a/src/Extension/CommonMark/Renderer/Block/ListItemRenderer.php +++ b/src/Extension/CommonMark/Renderer/Block/ListItemRenderer.php @@ -17,8 +17,9 @@ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; -use League\CommonMark\Extension\TaskList\TaskListItemMarker; +use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Paragraph; +use League\CommonMark\Node\Block\TightBlockInterface; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; @@ -39,11 +40,14 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \ ListItem::assertInstanceOf($node); $contents = $childRenderer->renderNodes($node->children()); - if (\substr($contents, 0, 1) === '<' && ! $this->startsTaskListItem($node)) { + + $inTightList = ($parent = $node->parent()) && $parent instanceof TightBlockInterface && $parent->isTight(); + + if ($this->needsBlockSeparator($node->firstChild(), $inTightList)) { $contents = "\n" . $contents; } - if (\substr($contents, -1, 1) === '>') { + if ($this->needsBlockSeparator($node->lastChild(), $inTightList)) { $contents .= "\n"; } @@ -65,10 +69,12 @@ public function getXmlAttributes(Node $node): array return []; } - private function startsTaskListItem(ListItem $block): bool + private function needsBlockSeparator(?Node $child, bool $inTightList): bool { - $firstChild = $block->firstChild(); + if ($child instanceof Paragraph && $inTightList) { + return false; + } - return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker; + return $child instanceof AbstractBlock; } } diff --git a/tests/functional/CMarkRegressionTest.php b/tests/functional/CMarkRegressionTest.php index 3ebd402fb0..4a5d5f12d8 100644 --- a/tests/functional/CMarkRegressionTest.php +++ b/tests/functional/CMarkRegressionTest.php @@ -27,12 +27,6 @@ public static function dataProvider(): \Generator { $tests = SpecReader::readFile(__DIR__ . '/../../vendor/commonmark/cmark/test/regression.txt'); foreach ($tests as $example) { - // We can't currently render spec example 13 exactly how the upstream library does. We'll likely need to overhaul - // our rendering approach in order to fix that, so we'll use this temporary workaround for now. - if ($example['number'] === 13) { - $example['output'] = \str_replace('', "\n", $example['output']); - } - // The case-fold test from example 21 fails on PHP 8.0.* and below due to the behavior of mb_convert_case(). // See https://3v4l.org/7TeXJ. if (\PHP_VERSION_ID < 81000 && $example['number'] === 21) { diff --git a/tests/functional/CommonMarkJSRegressionTest.php b/tests/functional/CommonMarkJSRegressionTest.php index 64cce72710..ce58efddee 100644 --- a/tests/functional/CommonMarkJSRegressionTest.php +++ b/tests/functional/CommonMarkJSRegressionTest.php @@ -25,17 +25,6 @@ final class CommonMarkJSRegressionTest extends AbstractSpecTestCase { public static function dataProvider(): \Generator { - $tests = SpecReader::readFile(__DIR__ . '/../../vendor/commonmark/commonmark.js/test/regression.txt'); - foreach ($tests as $example) { - // We can't currently render spec examples 18 or 24 exactly how the upstream library does. We'll likely need to overhaul - // our rendering approach in order to fix that, so we'll use this temporary workaround for now. - if ($example['number'] === 18) { - $example['output'] = \str_replace('', "\n", $example['output']); - } elseif ($example['number'] === 24) { - $example['output'] = \str_replace("
The following line is part of HTML block.\n\n", "
The following line is part of HTML block.\n", $example['output']);
-            }
-
-            yield $example;
-        }
+        yield from SpecReader::readFile(__DIR__ . '/../../vendor/commonmark/commonmark.js/test/regression.txt');
     }
 }
diff --git a/tests/functional/Delimiter/DelimiterProcessingTest.php b/tests/functional/Delimiter/DelimiterProcessingTest.php
index 87a7b7a83c..72c0b2f438 100644
--- a/tests/functional/Delimiter/DelimiterProcessingTest.php
+++ b/tests/functional/Delimiter/DelimiterProcessingTest.php
@@ -49,7 +49,7 @@ public function testAsymmetricDelimiterProcessing(string $input, string $expecte
 
         $converter = new MarkdownConverter($e);
 
-        $this->assertEquals($expected, $converter->convert($input));
+        $this->assertEquals($expected, $converter->convert($input)->getContent());
     }
 
     /**
diff --git a/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php b/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php
index 8be6b9fac3..e84cbd694f 100644
--- a/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php
+++ b/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php
@@ -49,7 +49,7 @@ public static function dataProviderForAutolinkTests(): iterable
         yield ['www.google.com', '

www.google.com

']; yield [' http://leadingwhitespace.example.com', '

http://leadingwhitespace.example.com

']; yield ['http://trailingwhitespace.example.com ', '

http://trailingwhitespace.example.com

']; - yield ['- https://example.com/list-item', ""]; + yield ['- https://example.com/list-item', ""]; // Tests of "incomplete" URLs yield ['google.com is missing www and/or a protocol', '

google.com is missing www and/or a protocol

']; @@ -60,7 +60,7 @@ public static function dataProviderForAutolinkTests(): iterable yield ['Maybe you\'re interested in https://www.google.com/search?q=php+commonmark!', '

Maybe you\'re interested in https://www.google.com/search?q=php+commonmark!

']; yield ['Or perhaps you\'re looking for my personal website https://www.colinodell.com...?', '

Or perhaps you\'re looking for my personal website https://www.colinodell.com...?

']; yield ['Check https://www.stackoverflow.com: they have all the answers', '

Check https://www.stackoverflow.com: they have all the answers

']; - yield ['- https://example.com/list-item-with-trailing-colon:', ""]; + yield ['- https://example.com/list-item-with-trailing-colon:', ""]; yield ['Visit www.commonmark.org.', '

Visit www.commonmark.org.

']; yield ['Visit www.commonmark.org/a.b.', '

Visit www.commonmark.org/a.b.

']; diff --git a/tests/functional/Extension/Footnote/spec.txt b/tests/functional/Extension/Footnote/spec.txt index 63a090d0f9..0ff84a81f9 100644 --- a/tests/functional/Extension/Footnote/spec.txt +++ b/tests/functional/Extension/Footnote/spec.txt @@ -106,12 +106,9 @@ A [footnote identifier] can come immediately after a word, or have a space betwe [^5]: Not allowed . diff --git a/tests/functional/Extension/Mention/MentionExtensionTest.php b/tests/functional/Extension/Mention/MentionExtensionTest.php index 8563506853..590b641ebb 100644 --- a/tests/functional/Extension/Mention/MentionExtensionTest.php +++ b/tests/functional/Extension/Mention/MentionExtensionTest.php @@ -44,7 +44,7 @@ public function testNoConfig(): void $converter = new MarkdownConverter($environment); - $this->assertEquals($expected, $converter->convert($input)); + $this->assertEquals($expected, $converter->convert($input)->getContent()); } public function testConfigStringGenerator(): void @@ -72,7 +72,7 @@ public function testConfigStringGenerator(): void $converter = new MarkdownConverter($environment); - $this->assertEquals($expected, $converter->convert($input)); + $this->assertEquals($expected, $converter->convert($input)->getContent()); } public function testConfigCallableGenerator(): void @@ -104,7 +104,7 @@ public function testConfigCallableGenerator(): void $converter = new MarkdownConverter($environment); - $this->assertEquals($expected, $converter->convert($input)); + $this->assertEquals($expected, $converter->convert($input)->getContent()); } public function testConfigObjectImplementingMentionGeneratorInterface(): void @@ -139,7 +139,7 @@ public function generateMention(Mention $mention): ?AbstractInline $converter = new MarkdownConverter($environment); - $this->assertEquals($expected, $converter->convert($input)); + $this->assertEquals($expected, $converter->convert($input)->getContent()); } public function testConfigUnknownGenerator(): void diff --git a/tests/functional/Extension/TableOfContents/md/custom-class.html b/tests/functional/Extension/TableOfContents/md/custom-class.html index 9b3f5271df..84e70c9a81 100644 --- a/tests/functional/Extension/TableOfContents/md/custom-class.html +++ b/tests/functional/Extension/TableOfContents/md/custom-class.html @@ -1,10 +1,7 @@ diff --git a/tests/functional/Extension/TableOfContents/md/headings-with-inlines.html b/tests/functional/Extension/TableOfContents/md/headings-with-inlines.html index e89e44a15f..aca2039511 100644 --- a/tests/functional/Extension/TableOfContents/md/headings-with-inlines.html +++ b/tests/functional/Extension/TableOfContents/md/headings-with-inlines.html @@ -1,13 +1,7 @@

My Awesome Blog Post

This heading has a link

diff --git a/tests/functional/Extension/TableOfContents/md/html-in-headings.html b/tests/functional/Extension/TableOfContents/md/html-in-headings.html index 40c0b8a43c..8e5bfe195a 100644 --- a/tests/functional/Extension/TableOfContents/md/html-in-headings.html +++ b/tests/functional/Extension/TableOfContents/md/html-in-headings.html @@ -1,7 +1,5 @@

Here's a sample Markdown document

Hello World!

diff --git a/tests/functional/Extension/TableOfContents/md/min-max.html b/tests/functional/Extension/TableOfContents/md/min-max.html index a87d59025c..bae12ce60b 100644 --- a/tests/functional/Extension/TableOfContents/md/min-max.html +++ b/tests/functional/Extension/TableOfContents/md/min-max.html @@ -1,16 +1,11 @@