Skip to content

Commit b81cc1d

Browse files
authored
Merge pull request #1011 from thephpleague/commonmark-spec-0.31
CommonMark spec 0.31 compatibility
2 parents 50bd4dc + a0cd1cd commit b81cc1d

File tree

15 files changed

+120
-74
lines changed

15 files changed

+120
-74
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
66

77
## [Unreleased][unreleased]
88

9+
### Changed
10+
11+
- Made compatible with CommonMark spec 0.31.0, including:
12+
- Allow closing fence to be followed by tabs
13+
- Remove restrictive limitation on inline comments
14+
- Unicode symbols now treated like punctuation (for purposes of flankingness)
15+
- Trailing tabs on the last line of indented code blocks will be excluded
16+
- Improved HTML comment matching
17+
- `Paragraph`s only containing link reference definitions will be kept in the AST until the `Document` is finalized
18+
- (These were previously removed immediately after parsing the `Paragraph`)
19+
20+
### Fixed
21+
22+
- Fixed list tightness not being determined properly in some edge cases
23+
- Fixed incorrect ending line numbers for several block types in various scenarios
24+
- Fixed lowercase inline HTML declarations not being accepted
25+
926
## [2.4.4] - 2024-07-22
1027

1128
### Fixed

composer.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"require-dev": {
3232
"ext-json": "*",
3333
"cebe/markdown": "^1.0",
34-
"commonmark/cmark": "0.30.3",
35-
"commonmark/commonmark.js": "0.30.0",
34+
"commonmark/cmark": "0.31.0",
35+
"commonmark/commonmark.js": "0.31.0",
3636
"composer/package-versions-deprecated": "^1.8",
3737
"embed/embed": "^4.4",
3838
"erusev/parsedown": "^1.0",
@@ -56,9 +56,9 @@
5656
"type": "package",
5757
"package": {
5858
"name": "commonmark/commonmark.js",
59-
"version": "0.30.0",
59+
"version": "0.31.0",
6060
"dist": {
61-
"url": "https://github.com/commonmark/commonmark.js/archive/0.30.0.zip",
61+
"url": "https://github.com/commonmark/commonmark.js/archive/0.31.0.zip",
6262
"type": "zip"
6363
}
6464
}
@@ -67,9 +67,9 @@
6767
"type": "package",
6868
"package": {
6969
"name": "commonmark/cmark",
70-
"version": "0.30.3",
70+
"version": "0.31.0",
7171
"dist": {
72-
"url": "https://github.com/commonmark/cmark/archive/0.30.3.zip",
72+
"url": "https://github.com/commonmark/cmark/archive/0.31.0.zip",
7373
"type": "zip"
7474
}
7575
}

src/Extension/CommonMark/Node/Block/ListBlock.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ListBlock extends AbstractBlock implements TightBlockInterface
2727
public const DELIM_PERIOD = 'period';
2828
public const DELIM_PAREN = 'paren';
2929

30-
protected bool $tight = false;
30+
protected bool $tight = false; // TODO Make lists tight by default in v3
3131

3232
/** @psalm-readonly */
3333
protected ListData $listData;

src/Extension/CommonMark/Parser/Block/FencedCodeParser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
4444
{
4545
// Check for closing code fence
4646
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) {
47-
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
47+
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?=[ \t]*$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
4848
if ($match !== null && \strlen($match[0]) >= $this->block->getLength()) {
4949
// closing fence - we're at end of line, so we can finalize now
5050
return BlockContinue::finished();

src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,14 @@ public function addLine(string $line): void
6363

6464
public function closeBlock(): void
6565
{
66-
$reversed = \array_reverse($this->strings->toArray(), true);
67-
foreach ($reversed as $index => $line) {
68-
if ($line !== '' && $line !== "\n" && ! \preg_match('/^(\n *)$/', $line)) {
69-
break;
70-
}
66+
$lines = $this->strings->toArray();
7167

72-
unset($reversed[$index]);
68+
// Note that indented code block cannot be empty, so $lines will always have at least one non-empty element
69+
while (\preg_match('/^[ \t]*$/', \end($lines))) { // @phpstan-ignore-line
70+
\array_pop($lines);
7371
}
7472

75-
$fixed = \array_reverse($reversed);
76-
$tmp = \implode("\n", $fixed);
77-
if (\substr($tmp, -1) !== "\n") {
78-
$tmp .= "\n";
79-
}
80-
81-
$this->block->setLiteral($tmp);
73+
$this->block->setLiteral(\implode("\n", $lines) . "\n");
74+
$this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1);
8275
}
8376
}

src/Extension/CommonMark/Parser/Block/ListBlockParser.php

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ final class ListBlockParser extends AbstractBlockContinueParser
2727
/** @psalm-readonly */
2828
private ListBlock $block;
2929

30-
private bool $hadBlankLine = false;
31-
32-
private int $linesAfterBlank = 0;
33-
3430
public function __construct(ListData $listData)
3531
{
3632
$this->block = new ListBlock($listData);
@@ -48,32 +44,50 @@ public function isContainer(): bool
4844

4945
public function canContain(AbstractBlock $childBlock): bool
5046
{
51-
if (! $childBlock instanceof ListItem) {
52-
return false;
53-
}
54-
55-
// Another list item is being added to this list block.
56-
// If the previous line was blank, that means this list
57-
// block is "loose" (not tight).
58-
if ($this->hadBlankLine && $this->linesAfterBlank === 1) {
59-
$this->block->setTight(false);
60-
$this->hadBlankLine = false;
61-
}
62-
63-
return true;
47+
return $childBlock instanceof ListItem;
6448
}
6549

6650
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
6751
{
68-
if ($cursor->isBlank()) {
69-
$this->hadBlankLine = true;
70-
$this->linesAfterBlank = 0;
71-
} elseif ($this->hadBlankLine) {
72-
$this->linesAfterBlank++;
73-
}
74-
7552
// List blocks themselves don't have any markers, only list items. So try to stay in the list.
7653
// If there is a block start other than list item, canContain makes sure that this list is closed.
7754
return BlockContinue::at($cursor);
7855
}
56+
57+
public function closeBlock(): void
58+
{
59+
$item = $this->block->firstChild();
60+
while ($item instanceof AbstractBlock) {
61+
// check for non-final list item ending with blank line:
62+
if ($item->next() !== null && self::endsWithBlankLine($item)) {
63+
$this->block->setTight(false);
64+
break;
65+
}
66+
67+
// recurse into children of list item, to see if there are spaces between any of them
68+
$subitem = $item->firstChild();
69+
while ($subitem instanceof AbstractBlock) {
70+
if ($subitem->next() && self::endsWithBlankLine($subitem)) {
71+
$this->block->setTight(false);
72+
break 2;
73+
}
74+
75+
$subitem = $subitem->next();
76+
}
77+
78+
$item = $item->next();
79+
}
80+
81+
$lastChild = $this->block->lastChild();
82+
if ($lastChild instanceof AbstractBlock) {
83+
$this->block->setEndLine($lastChild->getEndLine());
84+
}
85+
}
86+
87+
private static function endsWithBlankLine(AbstractBlock $block): bool
88+
{
89+
$next = $block->next();
90+
91+
return $next instanceof AbstractBlock && $block->getEndLine() !== $next->getStartLine() - 1;
92+
}
7993
}

src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserSta
5858
if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) {
5959
$listBlockParser = new ListBlockParser($listData);
6060
// We start out with assuming a list is tight. If we find a blank line, we set it to loose later.
61+
// TODO for 3.0: Just make them tight by default in the block so we can remove this call
6162
$listBlockParser->getBlock()->setTight(true);
6263

6364
return BlockStart::of($listBlockParser, $listItemParser)->at($cursor);

src/Extension/CommonMark/Parser/Block/ListItemParser.php

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@
1313

1414
namespace League\CommonMark\Extension\CommonMark\Parser\Block;
1515

16-
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
1716
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
1817
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
1918
use League\CommonMark\Node\Block\AbstractBlock;
20-
use League\CommonMark\Node\Block\Paragraph;
2119
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
2220
use League\CommonMark\Parser\Block\BlockContinue;
2321
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
@@ -28,8 +26,6 @@ final class ListItemParser extends AbstractBlockContinueParser
2826
/** @psalm-readonly */
2927
private ListItem $block;
3028

31-
private bool $hadBlankLine = false;
32-
3329
public function __construct(ListData $listData)
3430
{
3531
$this->block = new ListItem($listData);
@@ -47,18 +43,7 @@ public function isContainer(): bool
4743

4844
public function canContain(AbstractBlock $childBlock): bool
4945
{
50-
if ($this->hadBlankLine) {
51-
// We saw a blank line in this list item, that means the list block is loose.
52-
//
53-
// spec: if any of its constituent list items directly contain two block-level elements with a blank line
54-
// between them
55-
$parent = $this->block->parent();
56-
if ($parent instanceof ListBlock) {
57-
$parent->setTight(false);
58-
}
59-
}
60-
61-
return true;
46+
return ! $childBlock instanceof ListItem;
6247
}
6348

6449
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
@@ -69,9 +54,6 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
6954
return BlockContinue::none();
7055
}
7156

72-
$activeBlock = $activeBlockParser->getBlock();
73-
// If the active block is a code block, blank lines in it should not affect if the list is tight.
74-
$this->hadBlankLine = $activeBlock instanceof Paragraph || $activeBlock instanceof ListItem;
7557
$cursor->advanceToNextNonSpaceOrTab();
7658

7759
return BlockContinue::at($cursor);
@@ -87,4 +69,14 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
8769
// Note: We'll hit this case for lazy continuation lines, they will get added later.
8870
return BlockContinue::none();
8971
}
72+
73+
public function closeBlock(): void
74+
{
75+
if (($lastChild = $this->block->lastChild()) instanceof AbstractBlock) {
76+
$this->block->setEndLine($lastChild->getEndLine());
77+
} else {
78+
// Empty list item
79+
$this->block->setEndLine($this->block->getStartLine());
80+
}
81+
}
9082
}

src/Node/Block/Paragraph.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@
1818

1919
class Paragraph extends AbstractBlock
2020
{
21+
/** @internal */
22+
public bool $onlyContainsLinkReferenceDefinitions = false;
2123
}

src/Parser/Block/DocumentBlockParser.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use League\CommonMark\Node\Block\AbstractBlock;
1717
use League\CommonMark\Node\Block\Document;
18+
use League\CommonMark\Node\Block\Paragraph;
1819
use League\CommonMark\Parser\Cursor;
1920
use League\CommonMark\Reference\ReferenceMapInterface;
2021

@@ -50,4 +51,30 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
5051
{
5152
return BlockContinue::at($cursor);
5253
}
54+
55+
public function closeBlock(): void
56+
{
57+
$this->removeLinkReferenceDefinitions();
58+
}
59+
60+
private function removeLinkReferenceDefinitions(): void
61+
{
62+
$emptyNodes = [];
63+
64+
$walker = $this->document->walker();
65+
while ($event = $walker->next()) {
66+
$node = $event->getNode();
67+
// TODO for v3: It would be great if we could find an alternate way to identify such paragraphs.
68+
// Unfortunately, we can't simply check for empty paragraphs here because inlines haven't been processed yet,
69+
// meaning all paragraphs will appear blank here, and we don't have a way to check the status of the reference parser
70+
// which is attached to the (already-closed) paragraph parser.
71+
if ($event->isEntering() && $node instanceof Paragraph && $node->onlyContainsLinkReferenceDefinitions) {
72+
$emptyNodes[] = $node;
73+
}
74+
}
75+
76+
foreach ($emptyNodes as $node) {
77+
$node->detach();
78+
}
79+
}
5380
}

0 commit comments

Comments
 (0)