diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dbfad691..da68499df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,37 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi - `autolink/allowed_protocols` - an array of protocols to allow autolinking for - `autolink/default_protocol` - the default protocol to use when none is specified +### Changed + +- Made compatible with CommonMark spec 0.31.0, including: + - Allow closing fence to be followed by tabs + - Remove restrictive limitation on inline comments + - Unicode symbols now treated like punctuation (for purposes of flankingness) + - Trailing tabs on the last line of indented code blocks will be excluded + - Improved HTML comment matching +- `Paragraph`s only containing link reference definitions will be kept in the AST until the `Document` is finalized + - (These were previously removed immediately after parsing the `Paragraph`) + +### Fixed + +- Fixed list tightness not being determined properly in some edge cases +- Fixed incorrect ending line numbers for several block types in various scenarios +- Fixed lowercase inline HTML declarations not being accepted + +## [2.4.4] - 2024-07-22 + +### Fixed + +- Fixed SmartPunct extension changing already-formatted quotation marks (#1030) + +## [2.4.3] - 2024-07-22 + +### Fixed + +- Fixed the Attributes extension not supporting CSS level 3 selectors (#1013) +- Fixed `UrlAutolinkParser` incorrectly parsing text containing `www` anywhere before an autolink (#1025) + + ## [2.4.2] - 2024-02-02 ### Fixed @@ -574,7 +605,9 @@ 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.4.2...main +[unreleased]: https://github.com/thephpleague/commonmark/compare/2.4.4...main +[2.4.4]: https://github.com/thephpleague/commonmark/compare/2.4.3...2.4.4 +[2.4.3]: https://github.com/thephpleague/commonmark/compare/2.4.2...2.4.3 [2.4.2]: https://github.com/thephpleague/commonmark/compare/2.4.1...2.4.2 [2.4.1]: https://github.com/thephpleague/commonmark/compare/2.4.0...2.4.1 [2.4.0]: https://github.com/thephpleague/commonmark/compare/2.3.9...2.4.0 diff --git a/composer.json b/composer.json index 53901ebb7a..184aa96d02 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,8 @@ "require-dev": { "ext-json": "*", "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.3", - "commonmark/commonmark.js": "0.30.0", + "commonmark/cmark": "0.31.0", + "commonmark/commonmark.js": "0.31.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", @@ -56,9 +56,9 @@ "type": "package", "package": { "name": "commonmark/commonmark.js", - "version": "0.30.0", + "version": "0.31.0", "dist": { - "url": "https://github.com/commonmark/commonmark.js/archive/0.30.0.zip", + "url": "https://github.com/commonmark/commonmark.js/archive/0.31.0.zip", "type": "zip" } } @@ -67,9 +67,9 @@ "type": "package", "package": { "name": "commonmark/cmark", - "version": "0.30.3", + "version": "0.31.0", "dist": { - "url": "https://github.com/commonmark/cmark/archive/0.30.3.zip", + "url": "https://github.com/commonmark/cmark/archive/0.31.0.zip", "type": "zip" } } diff --git a/docs/2.4/extensions/embed.md b/docs/2.4/extensions/embed.md index 244e5b5d42..0755d95bc4 100644 --- a/docs/2.4/extensions/embed.md +++ b/docs/2.4/extensions/embed.md @@ -107,7 +107,7 @@ We do provide an adapter for the popular [`embed/embed`](https://github.com/osca because it supports fetching multiple URLs in parallel, which is ideal for performance, and it supports a wide range of embeddable content. -To use that library, you'll need to `composer install embed/embed` and then pass `new OscaroteroEmbedAdapter()` as the `adapter` +To use that library, you'll need to `composer require embed/embed` and then pass `new OscaroteroEmbedAdapter()` as the `adapter` configuration option, as shown in the [**Usage**](#usage) section above. Need to customize the maximum width/height of the embedded content? You can do that by instantiating the service provided by diff --git a/src/Extension/Attributes/Util/AttributesHelper.php b/src/Extension/Attributes/Util/AttributesHelper.php index 8f094fddbd..d74bc00a29 100644 --- a/src/Extension/Attributes/Util/AttributesHelper.php +++ b/src/Extension/Attributes/Util/AttributesHelper.php @@ -23,7 +23,7 @@ */ final class AttributesHelper { - private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . '?)\s*'; + private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z]\S*|[#]\S+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . '?)\s*'; private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i'; /** diff --git a/src/Extension/Autolink/UrlAutolinkParser.php b/src/Extension/Autolink/UrlAutolinkParser.php index 9ed0e70923..1ef270fe85 100644 --- a/src/Extension/Autolink/UrlAutolinkParser.php +++ b/src/Extension/Autolink/UrlAutolinkParser.php @@ -56,7 +56,7 @@ final class UrlAutolinkParser implements InlineParserInterface * * @psalm-readonly */ - private array $prefixes = ['www']; + private array $prefixes = ['www.']; /** * @psalm-var non-empty-string diff --git a/src/Extension/CommonMark/Node/Block/ListBlock.php b/src/Extension/CommonMark/Node/Block/ListBlock.php index 74f9ca84df..504a38a2c6 100644 --- a/src/Extension/CommonMark/Node/Block/ListBlock.php +++ b/src/Extension/CommonMark/Node/Block/ListBlock.php @@ -27,7 +27,7 @@ class ListBlock extends AbstractBlock implements TightBlockInterface public const DELIM_PERIOD = 'period'; public const DELIM_PAREN = 'paren'; - protected bool $tight = false; + protected bool $tight = false; // TODO Make lists tight by default in v3 /** @psalm-readonly */ protected ListData $listData; diff --git a/src/Extension/CommonMark/Parser/Block/FencedCodeParser.php b/src/Extension/CommonMark/Parser/Block/FencedCodeParser.php index 88572c7fbb..96a5baa42e 100644 --- a/src/Extension/CommonMark/Parser/Block/FencedCodeParser.php +++ b/src/Extension/CommonMark/Parser/Block/FencedCodeParser.php @@ -44,7 +44,7 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active { // Check for closing code fence if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) { - $match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition()); + $match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?=[ \t]*$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition()); if ($match !== null && \strlen($match[0]) >= $this->block->getLength()) { // closing fence - we're at end of line, so we can finalize now return BlockContinue::finished(); diff --git a/src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php b/src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php index b7c425aa77..ac6406fb6e 100644 --- a/src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php +++ b/src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php @@ -63,21 +63,14 @@ public function addLine(string $line): void public function closeBlock(): void { - $reversed = \array_reverse($this->strings->toArray(), true); - foreach ($reversed as $index => $line) { - if ($line !== '' && $line !== "\n" && ! \preg_match('/^(\n *)$/', $line)) { - break; - } + $lines = $this->strings->toArray(); - unset($reversed[$index]); + // Note that indented code block cannot be empty, so $lines will always have at least one non-empty element + while (\preg_match('/^[ \t]*$/', \end($lines))) { // @phpstan-ignore-line + \array_pop($lines); } - $fixed = \array_reverse($reversed); - $tmp = \implode("\n", $fixed); - if (\substr($tmp, -1) !== "\n") { - $tmp .= "\n"; - } - - $this->block->setLiteral($tmp); + $this->block->setLiteral(\implode("\n", $lines) . "\n"); + $this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1); } } diff --git a/src/Extension/CommonMark/Parser/Block/ListBlockParser.php b/src/Extension/CommonMark/Parser/Block/ListBlockParser.php index 4dffb7ac65..5a7ee45a4c 100644 --- a/src/Extension/CommonMark/Parser/Block/ListBlockParser.php +++ b/src/Extension/CommonMark/Parser/Block/ListBlockParser.php @@ -27,10 +27,6 @@ final class ListBlockParser extends AbstractBlockContinueParser /** @psalm-readonly */ private ListBlock $block; - private bool $hadBlankLine = false; - - private int $linesAfterBlank = 0; - public function __construct(ListData $listData) { $this->block = new ListBlock($listData); @@ -48,32 +44,50 @@ public function isContainer(): bool public function canContain(AbstractBlock $childBlock): bool { - if (! $childBlock instanceof ListItem) { - return false; - } - - // Another list item is being added to this list block. - // If the previous line was blank, that means this list - // block is "loose" (not tight). - if ($this->hadBlankLine && $this->linesAfterBlank === 1) { - $this->block->setTight(false); - $this->hadBlankLine = false; - } - - return true; + return $childBlock instanceof ListItem; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { - if ($cursor->isBlank()) { - $this->hadBlankLine = true; - $this->linesAfterBlank = 0; - } elseif ($this->hadBlankLine) { - $this->linesAfterBlank++; - } - // List blocks themselves don't have any markers, only list items. So try to stay in the list. // If there is a block start other than list item, canContain makes sure that this list is closed. return BlockContinue::at($cursor); } + + public function closeBlock(): void + { + $item = $this->block->firstChild(); + while ($item instanceof AbstractBlock) { + // check for non-final list item ending with blank line: + if ($item->next() !== null && self::endsWithBlankLine($item)) { + $this->block->setTight(false); + break; + } + + // recurse into children of list item, to see if there are spaces between any of them + $subitem = $item->firstChild(); + while ($subitem instanceof AbstractBlock) { + if ($subitem->next() && self::endsWithBlankLine($subitem)) { + $this->block->setTight(false); + break 2; + } + + $subitem = $subitem->next(); + } + + $item = $item->next(); + } + + $lastChild = $this->block->lastChild(); + if ($lastChild instanceof AbstractBlock) { + $this->block->setEndLine($lastChild->getEndLine()); + } + } + + private static function endsWithBlankLine(AbstractBlock $block): bool + { + $next = $block->next(); + + return $next instanceof AbstractBlock && $block->getEndLine() !== $next->getStartLine() - 1; + } } diff --git a/src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php b/src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php index 65b4535b82..a55f6f9d94 100644 --- a/src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php +++ b/src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php @@ -58,6 +58,7 @@ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserSta if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) { $listBlockParser = new ListBlockParser($listData); // We start out with assuming a list is tight. If we find a blank line, we set it to loose later. + // TODO for 3.0: Just make them tight by default in the block so we can remove this call $listBlockParser->getBlock()->setTight(true); return BlockStart::of($listBlockParser, $listItemParser)->at($cursor); diff --git a/src/Extension/CommonMark/Parser/Block/ListItemParser.php b/src/Extension/CommonMark/Parser/Block/ListItemParser.php index 73b98be706..739eefcbd2 100644 --- a/src/Extension/CommonMark/Parser/Block/ListItemParser.php +++ b/src/Extension/CommonMark/Parser/Block/ListItemParser.php @@ -13,11 +13,9 @@ namespace League\CommonMark\Extension\CommonMark\Parser\Block; -use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListData; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Block\AbstractBlock; -use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; @@ -28,8 +26,6 @@ final class ListItemParser extends AbstractBlockContinueParser /** @psalm-readonly */ private ListItem $block; - private bool $hadBlankLine = false; - public function __construct(ListData $listData) { $this->block = new ListItem($listData); @@ -47,18 +43,7 @@ public function isContainer(): bool public function canContain(AbstractBlock $childBlock): bool { - if ($this->hadBlankLine) { - // We saw a blank line in this list item, that means the list block is loose. - // - // spec: if any of its constituent list items directly contain two block-level elements with a blank line - // between them - $parent = $this->block->parent(); - if ($parent instanceof ListBlock) { - $parent->setTight(false); - } - } - - return true; + return ! $childBlock instanceof ListItem; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue @@ -69,9 +54,6 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active return BlockContinue::none(); } - $activeBlock = $activeBlockParser->getBlock(); - // If the active block is a code block, blank lines in it should not affect if the list is tight. - $this->hadBlankLine = $activeBlock instanceof Paragraph || $activeBlock instanceof ListItem; $cursor->advanceToNextNonSpaceOrTab(); return BlockContinue::at($cursor); @@ -87,4 +69,14 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active // Note: We'll hit this case for lazy continuation lines, they will get added later. return BlockContinue::none(); } + + public function closeBlock(): void + { + if (($lastChild = $this->block->lastChild()) instanceof AbstractBlock) { + $this->block->setEndLine($lastChild->getEndLine()); + } else { + // Empty list item + $this->block->setEndLine($this->block->getStartLine()); + } + } } diff --git a/src/Extension/SmartPunct/QuoteParser.php b/src/Extension/SmartPunct/QuoteParser.php index b77d085518..959930b3ee 100644 --- a/src/Extension/SmartPunct/QuoteParser.php +++ b/src/Extension/SmartPunct/QuoteParser.php @@ -24,12 +24,19 @@ final class QuoteParser implements InlineParserInterface { + /** + * @deprecated This constant is no longer used and will be removed in a future major release + */ public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER]; + + /** + * @deprecated This constant is no longer used and will be removed in a future major release + */ public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER]; public function getMatchDefinition(): InlineParserMatch { - return InlineParserMatch::oneOf(...[...self::DOUBLE_QUOTES, ...self::SINGLE_QUOTES]); + return InlineParserMatch::oneOf(Quote::SINGLE_QUOTE, Quote::DOUBLE_QUOTE); } /** @@ -40,8 +47,6 @@ public function parse(InlineParserContext $inlineContext): bool $char = $inlineContext->getFullMatch(); $cursor = $inlineContext->getCursor(); - $normalizedCharacter = $this->getNormalizedQuoteCharacter($char); - $charBefore = $cursor->peek(-1); if ($charBefore === null) { $charBefore = "\n"; @@ -58,28 +63,15 @@ public function parse(InlineParserContext $inlineContext): bool $canOpen = $leftFlanking && ! $rightFlanking; $canClose = $rightFlanking; - $node = new Quote($normalizedCharacter, ['delim' => true]); + $node = new Quote($char, ['delim' => true]); $inlineContext->getContainer()->appendChild($node); // Add entry to stack to this opener - $inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose)); + $inlineContext->getDelimiterStack()->push(new Delimiter($char, 1, $node, $canOpen, $canClose)); return true; } - private function getNormalizedQuoteCharacter(string $character): string - { - if (\in_array($character, self::DOUBLE_QUOTES, true)) { - return Quote::DOUBLE_QUOTE; - } - - if (\in_array($character, self::SINGLE_QUOTES, true)) { - return Quote::SINGLE_QUOTE; - } - - return $character; - } - /** * @return bool[] */ diff --git a/src/Node/Block/Paragraph.php b/src/Node/Block/Paragraph.php index 5b7d17c27b..d06d84ea13 100644 --- a/src/Node/Block/Paragraph.php +++ b/src/Node/Block/Paragraph.php @@ -18,4 +18,6 @@ class Paragraph extends AbstractBlock { + /** @internal */ + public bool $onlyContainsLinkReferenceDefinitions = false; } diff --git a/src/Parser/Block/DocumentBlockParser.php b/src/Parser/Block/DocumentBlockParser.php index bacb512275..c03c24efc7 100644 --- a/src/Parser/Block/DocumentBlockParser.php +++ b/src/Parser/Block/DocumentBlockParser.php @@ -15,6 +15,7 @@ use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Document; +use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceMapInterface; @@ -50,4 +51,30 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active { return BlockContinue::at($cursor); } + + public function closeBlock(): void + { + $this->removeLinkReferenceDefinitions(); + } + + private function removeLinkReferenceDefinitions(): void + { + $emptyNodes = []; + + $walker = $this->document->walker(); + while ($event = $walker->next()) { + $node = $event->getNode(); + // TODO for v3: It would be great if we could find an alternate way to identify such paragraphs. + // Unfortunately, we can't simply check for empty paragraphs here because inlines haven't been processed yet, + // meaning all paragraphs will appear blank here, and we don't have a way to check the status of the reference parser + // which is attached to the (already-closed) paragraph parser. + if ($event->isEntering() && $node instanceof Paragraph && $node->onlyContainsLinkReferenceDefinitions) { + $emptyNodes[] = $node; + } + } + + foreach ($emptyNodes as $node) { + $node->detach(); + } + } } diff --git a/src/Parser/Block/ParagraphParser.php b/src/Parser/Block/ParagraphParser.php index 1573429fa5..f9312be90f 100644 --- a/src/Parser/Block/ParagraphParser.php +++ b/src/Parser/Block/ParagraphParser.php @@ -59,9 +59,7 @@ public function addLine(string $line): void public function closeBlock(): void { - if ($this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '') { - $this->block->detach(); - } + $this->block->onlyContainsLinkReferenceDefinitions = $this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === ''; } public function parseInlines(InlineParserEngineInterface $inlineParser): void diff --git a/src/Parser/MarkdownParser.php b/src/Parser/MarkdownParser.php index dcf9a42073..9d684dd215 100644 --- a/src/Parser/MarkdownParser.php +++ b/src/Parser/MarkdownParser.php @@ -176,7 +176,7 @@ private function parseLine(string $line): void } else { // finalize any blocks not matched if ($unmatchedBlocks > 0) { - $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber); + $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1); } if (! $blockParser->isContainer()) { diff --git a/src/Util/RegexHelper.php b/src/Util/RegexHelper.php index 7144332a0c..cab073d7f9 100644 --- a/src/Util/RegexHelper.php +++ b/src/Util/RegexHelper.php @@ -53,9 +53,9 @@ final class RegexHelper public const PARTIAL_CLOSETAG = '<\/' . self::PARTIAL_TAGNAME . '\s*[>]'; public const PARTIAL_OPENBLOCKTAG = '<' . self::PARTIAL_BLOCKTAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>'; public const PARTIAL_CLOSEBLOCKTAG = '<\/' . self::PARTIAL_BLOCKTAGNAME . '\s*[>]'; - public const PARTIAL_HTMLCOMMENT = '|'; + public const PARTIAL_HTMLCOMMENT = '||'; public const PARTIAL_PROCESSINGINSTRUCTION = '[<][?][\s\S]*?[?][>]'; - public const PARTIAL_DECLARATION = ']*>'; + public const PARTIAL_DECLARATION = ']*>'; public const PARTIAL_CDATA = ''; public const PARTIAL_HTMLTAG = '(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . '|' . self::PARTIAL_HTMLCOMMENT . '|' . self::PARTIAL_PROCESSINGINSTRUCTION . '|' . self::PARTIAL_DECLARATION . '|' . self::PARTIAL_CDATA . ')'; @@ -65,7 +65,7 @@ final class RegexHelper '|' . '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\'' . '|' . '\((' . self::PARTIAL_ESCAPED_CHAR . '|[^()\x00])*\))'; - public const REGEX_PUNCTUATION = '/^[\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\\\\\'!"#\$%&\(\)\*\+,\-\.\\/:;<=>\?@\[\]\^_`\{\|\}~]/u'; + public const REGEX_PUNCTUATION = '/^[!"#$%&\'()*+,\-.\\/:;<=>?@\\[\\]\\\\^_`{|}~\p{P}\p{S}]/u'; public const REGEX_UNSAFE_PROTOCOL = '/^javascript:|vbscript:|file:|data:/i'; public const REGEX_SAFE_DATA_PROTOCOL = '/^data:image\/(?:png|gif|jpeg|webp)/i'; public const REGEX_NON_SPACE = '/[^ \t\f\v\r\n]/'; diff --git a/tests/functional/CommonMarkJSRegressionTest.php b/tests/functional/CommonMarkJSRegressionTest.php index 5806cac006..64cce72710 100644 --- a/tests/functional/CommonMarkJSRegressionTest.php +++ b/tests/functional/CommonMarkJSRegressionTest.php @@ -27,10 +27,12 @@ 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 example 18 exactly how the upstream library does. We'll likely need to overhaul + // 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;
diff --git a/tests/functional/Extension/Attributes/data/class_names.html b/tests/functional/Extension/Attributes/data/class_names.html
new file mode 100644
index 0000000000..e92bcd308b
--- /dev/null
+++ b/tests/functional/Extension/Attributes/data/class_names.html
@@ -0,0 +1,6 @@
+

Some text

+

Some text

+

Some text

+

Some text

+

Some text

+

Some text

diff --git a/tests/functional/Extension/Attributes/data/class_names.md b/tests/functional/Extension/Attributes/data/class_names.md new file mode 100644 index 0000000000..25ee4f5a5d --- /dev/null +++ b/tests/functional/Extension/Attributes/data/class_names.md @@ -0,0 +1,17 @@ +{class="some-class(value)"} +Some text + +{.some-class(value)} +Some text + +{.-hello} +Some text + +{.smile-😎 .some-class} +Some text + +{id="smile-😎"} +Some text + +{#smile-😎} +Some text diff --git a/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php b/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php index 30d7589947..8be6b9fac3 100644 --- a/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php +++ b/tests/functional/Extension/Autolink/UrlAutolinkParserTest.php @@ -71,6 +71,7 @@ public static function dataProviderForAutolinkTests(): iterable yield ['(www.google.com/search?q=Markup+(business)', '

(www.google.com/search?q=Markup+(business)

']; yield ['www.google.com/search?q=(business))+ok', '

www.google.com/search?q=(business))+ok

']; yield ['(https://www.example.com/test).', '

(https://www.example.com/test).

']; + yield ['WWW text followed by a [link](https://example.com/foo-bar-test)', '

WWW text followed by a link

']; // Tests involving semi-colon endings yield ['www.google.com/search?q=commonmark&hl=en', '

www.google.com/search?q=commonmark&hl=en

']; diff --git a/tests/functional/Extension/SmartPunct/SmartPunctFunctionalTest.php b/tests/functional/Extension/SmartPunct/SmartPunctFunctionalTest.php index 0f372c6ba9..402ca80907 100644 --- a/tests/functional/Extension/SmartPunct/SmartPunctFunctionalTest.php +++ b/tests/functional/Extension/SmartPunct/SmartPunctFunctionalTest.php @@ -40,5 +40,15 @@ protected function setUp(): void public static function dataProvider(): \Generator { yield from SpecReader::readFile(__DIR__ . '/../../../../vendor/commonmark/commonmark.js/test/smart_punct.txt'); + + yield 'Existing formatted quotes should be preserved (issue #1030)' => [ + 'input' => 'In the middle to late ’90s, it was chaos. "We couldn\'t get out of that rut."', + 'output' => "

In the middle to late ’90s, it was chaos. “We couldn’t get out of that rut.”

\n", + ]; + + yield 'already-formatted quotes are kept as-is' => [ + 'input' => '"Plain quotes", “normal quotes”, and ”backwards quotes“', + 'output' => "

“Plain quotes”, “normal quotes”, and ”backwards quotes“

\n", + ]; } } diff --git a/tests/unit/Util/RegexHelperTest.php b/tests/unit/Util/RegexHelperTest.php index 463e23f1da..c71a6e079c 100644 --- a/tests/unit/Util/RegexHelperTest.php +++ b/tests/unit/Util/RegexHelperTest.php @@ -215,9 +215,9 @@ public function testHtmlComment(): void $this->assertRegexMatches($regex, ''); $this->assertRegexMatches($regex, ''); $this->assertRegexMatches($regex, ''); + $this->assertRegexMatches($regex, ''); + $this->assertRegexMatches($regex, ''); $this->assertRegexDoesNotMatch($regex, ''); - $this->assertRegexDoesNotMatch($regex, ''); - $this->assertRegexDoesNotMatch($regex, ''); $this->assertRegexDoesNotMatch($regex, ''); }